Azure, IaC

Create Management Groups and Subscriptions with Bicep

Recently I had the opportunity to do something that I had never done before and setup Management Groups and Subscriptions for a new Tenant in Azure. As there was going to be quite a few subscriptions and groups I wondered about using Bicep to do this.

Getting Started

In order to programmatically create Subscriptions, according to the docs, you must have an owner, contributor, or Azure subscription creator role on an invoice section or owner or contributor role on a billing profile or a billing account to create subscriptions. 

So I added Contributor rights on the Billing Account for my user but I still had some permission issues and found I needed elevate permissions using this command.

az role assignment create  --scope '/' --role 'Owner' --assignee-object-id $(az ad signed-in-user show --query objectId)

Note: My user has the Global Admin role

In order to find the billing scope you can use the Azure Portal by navigating to Cost Management + Billing

  1. Select Properties and then select the ID
  2. Then go to Billing Profiles and select the profile then select Properties and then the ID
  3. Then go to Invoice Sections select the invoice section then Properties then the ID

You can then use that information to build up a valid Billing Scope id “/providers/Microsoft.Billing/billingAccounts/[1. Billing Account ID]/billingProfiles/[2. Profile ID]/invoiceSections/[3. Invoice ID]”

I like the idea of getting the ids using the Azure CLI and after some experimentation I was able to build a single Azure CLI command  to get the appropriate full id needed to create subscriptions by supplying the name of the billing account and the name of the invoice section.

az billing profile list --account-name (az billing account list  --query "[?displayName == '<Name of Billing account>'].name" -o tsv) --expand "InvoiceSections" --query "[*].invoiceSections[].value[?displayName == '<Name of Invoice Section>'].id" -o tsv

So, let’s assume I want to create this structure (I’ve substituted the real names with generic ones e.g. Main, Project1):

Note: This diagram was created using Diagrams, see my previous post for details

Bicep

I wanted to be able to specify the main management group and also sub management groups with subscription names. Then I saw this as building the Subscriptions, Management Groups and then moving the Subscription to the specified Management Group. The end result is the below bicep file and modules.

Main/Deploy File

targetScope = 'tenant'

@description('Provide the full resource ID of billing scope to use for subscription creation.')
param billingScope string
@description('The name of the main group')
param mainManagementGroupName string = 'mg-main'
@description('The display name for the main group')
param mainMangementGroupDisplayName string = 'Main Management Group'
param managementGroups array = [
  {
    name: 'mg-project1-non-prod'
    displayName: 'Project1 Non-Prod Management Group'
    subscriptions: [
      {
        name: 'project1-dev'
        workload: 'Production'
      }
      {
        name: 'project1-test'
        workload: 'Production'
      }
    ]
  }
  {
    name: 'mg-project1-prod'
    displayName: 'Project1 Prod Management Group'
    subscriptions: [
      {
        name: 'project1-prod'
        workload: 'Production'
      }
    ]
  }  
  {
    name: 'mg-project2-non-prod'
    displayName: 'Project2 Non-Prod Management Group'
    subscriptions: [
      {
        name: 'project2-dev'
        workload: 'Production'
      }
      {
        name: 'project2-test'
        workload: 'Production'
      }
    ]
  }
  { 
    name: 'mg-project2-prod' 
    displayName: 'Project2 Prod Management Group' 
    subscriptions: [ 
      { 
        name: 'project2-prod' 
        workload: 'Production' 
      } 
    ] 
  }
  {
    name: 'mg-infrastructure'
    displayName: 'Infrastructure Management Group'    
    subscriptions: [    
      {
        name: 'infrastructure'
        workload: 'Production'
      }
    ]
  }
]

resource mainManagementGroup 'Microsoft.Management/managementGroups@2020-02-01' = {
  name: mainManagementGroupName
  scope: tenant()
  properties: {
    displayName: mainMangementGroupDisplayName
  }
}

module subsModule './modules/subs.bicep' = [for group in managementGroups: {
  name: 'subscriptionDeploy-${group.name}'  
  params: {
    subscriptions: group.subscriptions
    billingScope: billingScope
  }
}]

module mgSubModule './modules/mg.bicep' = [for (group, i) in managementGroups: {
  name: 'managementGroupDeploy-${group.name}'
  scope: managementGroup(mainManagementGroupName)
  params: {
    groupName: group.name
    groupDisplayName: group.displayName
    parentId: mainManagementGroup.id
    subscriptionIds: subsModule[i].outputs.subscriptionIds
  }
  dependsOn: [
    subsModule
  ]
}]

Management Group Module

targetScope = 'managementGroup'
param groupName string
param groupDisplayName string
param subscriptionIds array
param parentId string
resource mainManagementGroup 'Microsoft.Management/managementGroups@2020-02-01' = {
  name: groupName
  scope: tenant()
  properties: {
    displayName: groupDisplayName
    details: {      
      parent: {
        id: parentId
      }
    }
  }
}
resource subscriptionResources 'Microsoft.Management/managementGroups/subscriptions@2020-05-01' = [for sub in subscriptionIds: {
  parent: mainManagementGroup
  name: sub.id
  dependsOn: [
    mainManagementGroup
  ]
}]
output groupId string = mainManagementGroup.id

Subscription Module

targetScope = 'tenant'
@description('Provide the full resource ID of billing scope to use for subscription creation.')
param billingScope string
param subscriptions array = []
resource subscriptionAlias 'Microsoft.Subscription/aliases@2020-09-01' = [for sub in subscriptions: {
  scope: tenant()
  name: sub.name
  properties: {
    workload: sub.workload
    displayName: sub.name
    billingScope: billingScope
  }  
}]
 output subscriptionIds array = [for (subs, i) in subscriptions: {
    id: subscriptionAlias[i].properties.subscriptionId
 }]

Running

Running the bicep, using the script from earlier we can get the scope and update the parameter to run it with a what-if to see what will be built.

$scope = az billing profile list --account-name (az billing account list  --query "[?displayName == '<Name of Billing account>'].name" -o tsv) --expand "InvoiceSections" --query "[*].invoiceSections[].value[?displayName == '<Name of Invoice Section>'].id" -o tsv
az deployment tenant what-if --name "subscriptions-deploy" --location uksouth --template-file main.bicep --parameters billingScope=$scope

We can then run using create to actually run in the script.

az deployment tenant create --name "subscriptions-deploy" --location uksouth --template-file main.bicep --parameters billingScope=$scope

Once all this ran in to took a bit of time for the Subscriptions to move under the Management Groups in the Azure Portal.

It’s great that something like this can be done using Bicep and so much easier than using ARM templates. Bicep is really becoming my go to for Infrastructure as Code.

Azure, Security

Create Issues in Azure DevOps via Snyk API

Snyk is a great tool for scanning your code and containers for vulnerabilities. Snyk is constantly evolving and adding new features and integrations so if you haven’t checked out the Snyk website, I highly recommend you do so. There also is a free tier for you to get started.

One of the features is Jira Integration, this allows you to create a Jira Issue from within Snyk. If you use Jira then I can see a benefit for this but what if you use Azure DevOps or you want to automate the issue creation.

This post goes though using an Azure Logic App to create an issue in Azure DevOps when a new issue is discovered (Note: the process works for Jira too, just use the Azure Logic App Jira connector).

To use the Snyk API you will need to be on the Business Plan or above (at the time of writing), this then allows the ability to add a webhook to receive events.


So the flow of the Logic App is something like this

On enable of the Logic App it will register as a webhook with your Snyk account and on disable will unregister the webhook.

Let’s build the logic app, step one create a new Logic App in Azure

Once that has been created, select a blank logic app and find the HTTP Webhook trigger

As detailed in the Snyk API Documentation set the subscribe method to POST and the URI for web hooks with your organization Id (this can be found on your Snyk account under Org Settings -> General)

Add the subscribe body that includes a secret defined by you and the url of the logic app (for the URL select the Callback Url in dynamic content)

Now set the unsubscribe method to DELETE and use an expression for the URI and leave the Body as blank

concat('https://snyk.io/api/v1/org/<your org id>/webhooks/',triggerOutputs().subscribe.body.id)

Now add new parameters for Subscribe – Headers and Unsubscribe – Headers

Authorization in both headers should be set to your API token (this can be found on your Snyk account Account Settings -> API Token)

When registering the application Snyk sends a Ping event which is determined by the X-Snyk-Event header, we really don’t want to run the rest of the workflow when this happens so we can add a condition to terminate

Select New Step then find Control and select it

Then select Condition

For the value use an expression to get the X-Snyk-Event header

triggerOutputs()?['headers']?['X-Snyk-Event']

and then make the condition check it contains the word ping (the actual value is ping and the version e.g. ping/v0)

Now in the True side add an action to Terminate and Set it to Cancelled

Now if the message is anything other than a ping then we want to continue processing the response. We will want to validate that the message coming in is from Snyk and is intended for us as it will have created a signature using our custom secret and added to the header under X-Hub-Signature. To perform this validation we can use an Azure Function.

You can create an Azure Function via the Portal, I suggest you use Linux as the OS

Using Visual Studio Code with the Functions Runtime installed you can create and deploy the following function. If you are not sure how to do this take a look at the Microsoft Docs they are really helpful

I named the function ValidateRequest and used some code from the Snyk API documentation to perform the validation and return either OK (200) or Bad Request (400)

const crypto = require('crypto');
module.exports = async function (context, req) {
     
    context.log('JavaScript HTTP trigger function processed a request.');
    const secret = req.headers['x-logicapp-secret'];
    const hubsignature = req.headers['x-hub-signature'];
    const hmac = crypto.createHmac('sha256', secret);
    const buffer = JSON.stringify(req.body);
    hmac.update(buffer, 'utf8');
    const signature = `sha256=${hmac.digest('hex')}`;    
    const isValid = signature === hubsignature;
   
    context.res = {
        status: isValid ? 200 : 400,
        body: isValid
    };    
}

Now the Function is deployed we can add the next step to the Logic App.

Select the function app we created previously

And select the function we deployed previously

Now we need to pass the payload from the webhook into our ValidateRequest function

Add additional parameters for method and header

Set the method to POST and switch the headers to text mode

Then add the following expression to add the request headers and one with your secret

addProperty(triggerOutputs()['headers'], 'X-LogicApp-Secret', '<your secret>')

If the check is successful then the next step is to parse the json and loop through the new issues

For the Content add the payload as you did previously for the validate functionand the Schema add the following

{
    "properties": {
        "group": {
            "properties": {},
            "type": "object"
        },
        "newIssues": {
            "type": "array"
        },
        "org": {
            "properties": {},
            "type": "object"
        },
        "project": {
            "properties": {},
            "type": "object"
        },
        "removedIssues": {
            "type": "array"
        }
    },
    "type": "object"
}

Next we need to loop through the new issues, add a new action using the control For each and add newIssues

For this I am only interested in the high severity issues so we need to add another condition using an expression for the value and is equal to high (Note: I renamed the condition to Severity)

items('For_each')?['issueData']?['severity']

Now if the severity is high then create work item using the built-in Azure DevOps connector

This will ask you to sign in

Once signed in you can set the details of the Azure DevOps organization, project and work item type

To add the details from the issue as the title and description use the following expressions

items('For_each')?['issueData']?['title']
items('For_each')?['issueData']?['description']

And that is the Logic App complete, a created work item with the title and description fields from the payload looks like this

Perhaps the formatting could do with some work but the information is there and the workflow works.

Although I used Azure DevOps as the output, there is a Jira connector that will allow creation of an issue in Jira

Once you are happy everything is running there is one last step, securing the secrets inside the logic app so they can only be seen by the designer

For the webhook select the settings and turn on Secure Inputs and Secure Outputs

and for the ValidateRequest function turn on at least Secure Inputs.

I find Azure Logic Apps a great way to connect systems together for these types of workflows because it has so many connectors.

I hope it helps others integrate Snyk with their workflows and can’t wait to see what other features the API will provide in the future.