Best bicep structure with CI/CD pipeline - azure

I'm not sure that there is a right answer for this and it will vary per scenario but I am curious because there isn't much documentation that I can find for a code first azure bicep infrastructure. Most examples you find show how to make a resource within a resource group, or using a module to define scope and deploy to another resource group, but what if you're trying to do more?
Let's do the following scenario: using 2 subscriptions(1 for prod, 1 for dev & qa) with 20 resource groups each containing multiple difference resources and you want to manage this within a CI/CD pipeline, plus the 3 environments: prod, qa, and dev. How would you go about this? I can think of a few scenarios but don't necessarily, but nothing sticks out as the best way to do it, maybe I'm missing something.
CI/CD portion:
Let's assume:
az account set --subscription(set our sub)
az group create --name --location (create resource group if it doesn't exist)
az deployment group create --name --resource-group --template-file --parameters(read from our files to deploy to a resource group)
You could pass an array of resource groups to loop through to create the resource group if it doesn't exist.
You could have the resource group list in a parameters file that you read from and do the same thing as above.
You could create a step for every resource group and the resources inside of it.(seems excessive?)
Bicep Portion:
Bicep restrictions: to specify scope(a resource group in our scenario) we'd have to have use modules dealing with multiple resource groups or have a step for each resource group and have a main.bicep file for the different resource groups/resources.
You could create a folder structure for each resource group and the resources inside of it with a main.bicep but that would mean you have a lot of extra deploy steps(seems excessive?).
You could have 1 main.bicep file and have a folder structure that uses a lot of modules to specify your scope while reading the resource group, resource variables etc using an environment parameters.json file.
You could create a folder for each environment, have folders with each environment then create each resource group and resources inside of it not using a parameters.json but using params in each file instead since they would be specific for each environment.
1 final issue:
Lastly let's say you want to add a step before the deployment of resources to use bicep what-if to check what resources will be updated or deleted(this is pretty important!). Last I checked there was an issue where what-if does not work for bicep modules so you wouldn't get the luxury of knowing what changes would be made prior to a deployment with the what-if. That is a pretty big safety net you'd be losing, so would you want to scratch the module strategy all together?
What would be the best way to tackle something like this while keeping it readable for average non experts to be able to hop in and work on it? I would lean towards making a folder structure using modules and reading from an environment parameters.json but I'm not convinced that's the best way, especially if what-if isn't fully working for bicep modules.

IMO this does depend a lot on the scenario, topology, permissions, etc. The way I would start thinking about this is that you want an "environment" that will vary a bit between dev/test and prod. That env has multiple resourceGroups and a dedicated subscription for each env.
In this case, I would use a single bicep "project" (e.g. main.bicep with modules) and change the deployment using parameter files (for dev/test vs. prod). The project would lay down everything needed for the environment (think greenfield). The main.bicep file is a subscription scoped deployment that will create the RGs and all the resources needed. Oversimplified example:
targetScope = 'subscription'
param sqlAdminUsername string'
param keyVaultResourceGroup
param keyVaultName string
param keyVaultSecretName string
param location string = deployment().location
resource kv 'Microsoft.KeyVault/vaults#2021-06-01-preview' existing = {
scope: resourceGroup(subscription().subscriptionId, keyVaultResourceGroup)
name: keyVaultName
}
resource sqlResourceGroup 'Microsoft.Resources/resourceGroups#2021-04-01' = {
name: 'shared-sql'
location: location
}
resource webResourceGroup 'Microsoft.Resources/resourceGroups#2021-04-01' = {
name: 'shared-web'
location: location
}
module sqlDeployment 'modules/shared-sql.bicep' = {
scope: resourceGroup(sqlResourceGroup.name)
name: 'sqlDeployment'
params: {
sqlAdminUsername: sqlAdminUsername
sqlAdminPassword: kv.getSecret(keyVaultSecretName)
location: location
}
}
module webDeployment 'modules/shared-web.bicep' = {
scope: resourceGroup(webResourceGroup.name)
name: 'webDeployment'
params: {
location: location
}
}
A single template + modules that creates the RGs, creates a SQL Server (via module) and an app service plan with an admin website (also via module). You can then parameterize whatever you want to for each environment.
re: what-if - what if will skip evaluation of a module if that module has a parameter that is an output of another module. If you don't pass outputs between modules then the module will be evaluated by what-if. The sample above does not pass outputs - often you don't need to do this because the information output was known by the parent (i.e. main.bicep) but sometimes you can't avoid it - ymmv.
Once you have the template designed in such a way, the pipeline is really straightforward - just deploy the template to the desired subscription.
That help?

Related

Why is 'dependsOn' not recommended in Bicep?

Microsoft documents the purpose of implicit and explicit dependencies. Where the explicit dependency uses 'dependsOn'. But it is mentioned that use cases for this approach are rare.
Could use some clarification on the following:
Example from MS
resource dnsZone 'Microsoft.Network/dnszones#2018-05-01' = {
name: 'demoeZone1'
location: 'global'
}
resource otherZone 'Microsoft.Network/dnszones#2018-05-01' = {
name: 'demoZone2'
location: 'global'
dependsOn: [
dnsZone
]
}
Quote
You can't query which resources were defined in the dependsOn element after deployment.
Assuming 'otherZone' is a dependsOn element, it can still be queried using the symbolic name.
Am I wrong and if so what is meant with the dependsOn element?
As stated in the article you link to:
The following example shows a DNS zone named otherZone that depends on a DNS zone named dnsZone
This means the otherZone is not the dependsOn element, it has a dependance on the dnsZone.
and
While you may be inclined to use dependsOn to map relationships between your resources, it's important to understand why you're doing it. For example, to document how resources are interconnected, dependsOn isn't the right approach. You can't query which resources were defined in the dependsOn element after deployment. Setting unnecessary dependencies slows deployment time because Resource Manager can't deploy those resources in parallel.
Bicep creates dependencies automatically (implicitly)
Azure Resource Manager evaluates the dependencies between resources, and deploys them in their dependent order. When resources aren't dependent on each other, Resource Manager deploys them in parallel.
So setting them explicitly is not needed in most cases.
The way I see it what they mean is that setting those explicit dependsOn elements without a good (deployment related) reason has no benefit because once the deployment has been done you can't see anywhere (in the Azure Portal) that you had this dependency set in your file.
The use case for dependsOn is if you need to make sure that the Resource Manager will create the resource with the dependOn only after the other resource has been created (and not in parallel).
But in most cases where you would need that you will set a parent relationship anyway which has the same effect (Implicit dependency).

How can I adapt an existing resource with Azure Bicep?

I'm currently porting some infrastructure as code scripts from Azure CLI to Azure Bicep. Among many other things, the Bicep files should create a subnet and allow access from this subnet to an existing Azure SQL Server and an existing Storage Account.
For the SQL Server, this is simple - I can reference the existing server resource and declare a child resource representing the VNET rule:
resource azureSqlServer 'Microsoft.Sql/servers#2021-05-01-preview' existing = {
name: azureSqlServerName
resource vnetRule 'virtualNetworkRules' = {
name: azureSqlServerVnetRuleName
properties: {
virtualNetworkSubnetId: subnetId
}
}
}
However, with the Storage Account, the network rules are not child resources, but a property of the Storage Account resource (properties.networkAcls.virtualNetworkRules). I cannot declare all the details of the Storage Account in my Bicep file because that resource is way out of scope from the deployment I'm currently working on. In essence, I want to adapt the existing resource, just ensuring a single rule is present.
The following does not work because existing cannot be combined with properties:
resource storageAccount 'Microsoft.Storage/storageAccounts#2021-06-01' existing = {
name: storageAccountName
properties: {
networkAcls: {
virtualNetworkRules: [
{
id: subnetId
action: 'Allow'
}
]
}
}
}
Is there any way I can adapt just a tiny bit of an existing resource using Bicep?
UPDATE: I just realized you came from Azure CLI and was trying to find a way in bicep - sorry for not answering your actual question - anyway your post made me think about this in another way other than bicep, so my "answer" is what I came up with...
...sounds like we thought about this in the same manner; using bicep to pimp an existing Storage Account, granting a new subnet access. However I ended up using AzureCLI az storage account network-rule add
e.g.
newSubnet='/subscriptions/<subscr-guid>/resourceGroups/<rg-name-where-vnet-resides>/providers/Microsoft.Network/virtualNetworks/<vnet-name>/subnets/<subnet-name>'
az storage account network-rule add -g <rg-name-where-sa-resides> --account-name <storage-account-name> --subnet $newSubnet
run this from a terminal or put it in an AzureCLI task in a devops pipeline (which is what I needed)
The existing keyword in bicep is used to tell bicep that the resource already exists and you just want a symbolic reference to that resource in the code. If the resource doesn't exist it's likely that the the deployment will fail in some way.
Your first snippet is equivalent to:
resource vnetRule 'Microsoft.Sql/servers/virtualNetworkRules#2021-05-01-preview' = {
name: '${azureSqlServerName}/${azureSqlServerVnetRuleName}'
properties: {
virtualNetworkSubnetId: subnetId
}
}
In your second snippet since you want to update properties you have to provide the complete declaration of the resource, IOW you have to define and deploy the storageAccount. This isn't unique to bicep, it's the way the declarative model in Azure works.
That said, if you want to deploy to another scope in bicep, you can use a module with the scope property. E.g.
module updateStorage 'storage.bicep' = {
scope: resourceGroup(storageResourceGroupName)
name: 'updateStorage'
}
The downside is that you need to make sure you define/declare all the properties need for that storageAccount which is not ideal. There are some ways you can author around that, but if the storageAccount doesn't exist, the deployment is guaranteed to fail. For example, you could assert the storageAccount exists, fetch its properties and then union or modify the properties in the module. You might be able to make that work (depending on the extent of your changes) but it's a bit of an anti-pattern in a declarative model.
That help?

Terraform Import - Is the Resource Label critical?

Our terraform remote state file in Azure has been completely destroyed and we're now faced with the challenge of recreating the state file from scratch. My option is to use the Terraform import command, using the following simple syntax:
terraform import <Terraform Resource Name>.<Resource Label> <Azure Resource ID>
To import the existing resource group for example, I will create the following configuration in a main.tf file.
provider "azurerm" {
version="1.39.0"
}
# create resource group
resource "azurerm_resource_group" "rg"{
name = "rg-terraform"
location = "uksouth"
}
Now, the problem I have is as follows:
When the existing Azure resources were originally created, they were assigned names that used an extremely complex naming convention, with some characters even being randomly generated. To further compound matters, they were all unique and there are hundreds of them. All would have been rosy if they were assigned a simplistic name like "main", as is used commonly in a lot of Terraform examples, but unfortunately, that's not the case.
My question therefore, is this:
When putting together my main.tf configuration file to be used for the Import, is it an absolute requirement that my "Resource Label" (given in my Import command) has to match the original "Resource Label" name from when the resource was created?
If it is a mandatory requirement, is there any way I could retrieve the original "Resource Label" from Azure in the same way that I can for instance obtain the "Azure Resource ID" from the Azure Portal or even an Az CLI query?
How can I ensure any child resources such as Subnets are included in the Import, without having to trawl manually through the Azure Portal to identify each one of them?
No, absolutely not. Choose whatever you want.
No, Azure generally does not know about this label, it is something terraform internal.
Unfortunately you need to import each and every resource manually and separately.
Have you made absolutely sure the current state file is lost? The storage location was not versioned? Does no developer still have a local copy of the state file laying around?

Order of resource creation in Terraform template

I have created terraform template (azure) with two modules. One module is for the resource group. the other is for vnet (it handles the creation of NSG and route table as well along with their association with subnets).
When I run terraform apply, it is giving an error for the route table, as the resource group is not created yet. the order of creation is showing as route table is created first and then resource group.
Is there a way to set the order of creation? in the main.tf in the root folder, module resource group is called first and then vnet.
Reconsider the idea of creating RG and resources using two modules. Ask yourself a simple question: why?
If you are 100% sure it is the right approach then use depends_on:
module "rg1" {
source = "./rg_module"
...
}
module "net1" {
source = "./network_module"
....
depends_on = [module.rg1]
}
Another alternative would be to use implicit dependence:
-Have the root module where resource group is actually defined return an output:
output "rg_name" {
value = azurerm_resource_group.root_rg.name
}
-No modifications in resource group module which calls the root module
-While creating route table(module),use the output value from resource group module:
[Assuming the variable assignment in below module is providing input to its root source using the name resource_group_name]
resource_group_name =module.rg_module["<OPTIONAL KEY IF USING FOR EACH IN RG MODULE"].rg_name
This creates an internal dependence on the resource group.
Note that it is not possible to reference arguments(actually variables) from the resource group module unless output values were defined.
You must use -out option to save the plan to a file. Like:
terraform plan -out <plan_file>
It is always recommended to use -out and save the plan file. This will ensure that the order of creation is preserved across subsequent applies.

Azure ARM template nested template deployment won't update resources\fails to start

I have the following ARM Template structure:
Parent Template
|--Nested Template 1
|--...
|--Nested Template 6
So I only have 2 levels of templates, Parent and nested.
Lets say I deploy parent to an empty resource group and everything works well. After that I delete one of the resources and want to deploy the same Parent Template with the same parameters to bring deleted resources back. But the deployment would fail saying that the resource already exists (the other, not the one i'm tried to recreate). I tried both incremental mode and full mode for deployments.
If i directly invoke nested template with the missing resources it works as expected (so specifically creating a deployment with nested template only, not with parent that invokes nested template).
UPD:
After some additional testing I can conclude thats even weirder then before. So I'm starting this deployment with powershell:
New-AzureRmResourceGroupDeployment #parameters
And it deploys just fine, however if I invoke the same command after the first deployment completed I would get an error:
The resource 'gggg-1s-the-wordd' already exists in location
'westeurope' in resource group 'gggg'. A resource with the same name
cannot be created in location 'northeurope'. Please select a new
resource name.
Is this behavior excepted? I can't seem to find anything relevant, thanks!
UPD2: It doesn't really matter if I use portal or powershell, I get the same error.
So with the help from Brian we were able to identify the culprit. The issue was that the WebApp had its location set to resourcegroup().location while the App Service Plan was correctly getting location from parameters. So that lead to a problem where at deployment time WebApp would deploy to the region where its App Service Plan was, but at evaluation time it would consider that this WebApp belongs to the region where the resource group was.
TLDR - copy paste error, which coupled with a bug in evaluation of location in ARM lead to a quite weird behavior.
If you deploy the same resource (intentionally did not use the word "template" there) to the same resource group, Azure should "make it so". IOW, if it's not there, it will create it, if it is there, it should no-op. It's not that black and white there are some nuances (like you can't change certain properties if the resource exsists) but if you deploy the same resource with the same property values to the same resource group you should not get an error.
In general, nesting (or not) shouldn't affect any of this.
If you're deploying to different resource groups, then you could see an error about "already exists" depending on the resource.
All that said, it's really hard to tell in your specific case what's going on without more detail... So if this doesn't help, can you add some detail (what's the exact error message) or a repro (template that we could see the problem with)?
I experienced the same issue. The reason was that, location of App Service was defined as [resourceGroup().location] instead of App service plan (ASP) location, which was creating the problem. I changed it by passing the location of ASP as a parameter to the template.
Getting location of of ASP is as:
internal static string GetASPLocation(TokenCloudCredentials credentials, string resourceGroup, string ASP)
{
Console.WriteLine($"Getting location of App Service Plan {ASP} in Resource Group {resourceGroup}");
var resourceClient = new ResourceManagementClient(credentials);
ResourceExistsResult result = resourceClient.Resources.CheckExistence(resourceGroup, new ResourceIdentity(ASP, "Microsoft.Web/serverfarms", "2015-08-01"));
var appServicePlan = resourceClient.Resources.Get(resourceGroup, new ResourceIdentity(ASP, "Microsoft.Web/serverfarms", "2015-08-01"));
return appServicePlan.Resource.Location;
}
And in ARM template, location can be changed as :
"location": "[parameters('ASPLocation')]"

Resources