See how Adaptiv can transform your business. Schedule a kickoff call today

Creating Azure Automation Accounts using Bicep

  • Technical

Automated deployment

In my previous blog post (Creating Azure Automation Accounts in the Portal), I discussed how to build automation accounts in the Azure portal. This blog will focus on how to deploy the Automation Account and related components using Infrastructure as Code (IaC) language Bicep.

The goal here is to have an easily deployable automation account and script that can be maintained through code repositories and used to deploy to any environment with configuration changes.

I was also interested in using the newly GA feature of runtime environments to maintain packages which are not provided as part of the baseline Automation Account deployment. Updating them previously required manual input to update each individual package and can take a long time depending on the complexity of runbook.

This blog post will focus on the IaC (Infrastructure as Code) deployment of Automation Accounts in Azure using Bicep modules and parameter files.

For the full list of Bicep parameters that can be used please check the Microsoft documentation here.

Automation Account

I deploy the Automation Account with system-assigned identities and public network access for ease of access in this code snippet below. I recommend having tight security controls on Automation Accounts and lock down permissions to least privileged where possible. For ease of understanding, in this example I will not be adding the security controls as part of this deployment.

//Deploys the automation account resource

resource automationAccount 'Microsoft.Automation/automationAccounts@2024-10-23' = {
  name: automationAccountName
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    publicNetworkAccess: publicNetworkAccess
    sku: {
      name: 'Basic'
    }
  }
}

Runbook (Script)

The following code snippet enables reading from the Azure DevOps repository and retrieving a script. This script can be added to the Runbook during our Bicep deployment, ensuring it is available for execution once the deployment is finished.

You will need to provide a full Uniform Resource Identifier (URI) to the resource which I have parameterized to allow the user to configure where to pick-up based on their folder structure.

resource runbook 'Microsoft.Automation/automationAccounts/runbooks@2024-10-23' = {
  name: 'DeployRunbook'
  location: location
  parent: automationAccount
  properties: {
    runbookType: 'PowerShell'
    logVerbose: false
    logProgress: true
    description: 'Test deploy Runbook'
    publishContentLink: {
      uri: '${devopsOrg}/_git/${repository}?version=GB${scriptBranch}&path=${runBookFolderName}/${scriptName}'
    }
  }
}

Runtime Environments

Next, new Runtime Environments can be added to facilitate the management of different environment dependency requirements for each script executed within your Automation Accounts. They will come with some default packages such as AZ CLI and you can specify the type of language the runtime environment will be using.

In the code snippet below, PowerShell 7.2 is used along with custom modules from the PowerShell Gallery that can be used to power your scripts.

//Resource to deploy the Runtime environment for automation accounts
//This allows a more automated way of managing non-standard packages required for runbook cmdlets

resource RuntimeEnvironment 'Microsoft.Automation/automationAccounts/runtimeEnvironments@2024-10-23' = {
  name: runtimeName
  location: location
  parent: automationAccount
  properties: {
    defaultPackages: {
      az: '11.2.0'
    }
    runtime: {
      language: 'PowerShell'
      version: '7.2'
    }
  }
}

In the code snippet below, I use a Bicep module to iterate over an array provided by a parameter file. This will create custom modules from PowerShell Gallery within the runtime environment.

For the example below I use some Microsoft.Graph modules to populate the Runtime environment (see the Appendix for the parameter file, specifically the parameter named runtimeEnvironmentPackageList).

The following information is provided in each array element: environment name, URI link, and package version. Note: there is a dependsOn to ensure the Runtime Environment is created before attempting to write to it.

//Resource for non-standard packages to grab as part of the deployment in the Runtime Environment
//Non-standard modules may be specified to import e.g., MS Graph functionality
//Versions can be updated and add additional packages added as required.
//See the sample param file to see the format

module RuntimeEnvironmentPackages 'automation-account-runtime-package-module-v1.bicep' = [
  for package in runtimeEnvironmentPackageList: {
    name: package.name
    scope: resourceGroup(automationAccountRgName)
    params: {
      automationAccountName: automationAccountName
      automationAccountRuntimeEnv: runtimeName
      contentLinkUri: package.contentLinkUri
      packageVersion: package.packageVersion
      runtimeEnvPackageName: package.runtimeEnvPackageName
    }

    dependsOn:[
      RuntimeEnvironment
    ]
  }
]

The parameters are passed into sub-module “automation-account-runtime-package-module-v1.bicep” to allow iteration over all the different module objects that defined in the parameter file. In this case, 3 Graph API modules are added:

  • “Microsoft.Graph.Authentication”
  • “Microsoft.Graph.Users.Actions”
  • “Microsoft.Graph.Users”

The name, version, and URI are set in the parameter file and passed into the module below to be added into the Runtime Environment.

Runtime-package-module:
/*********************************
  parameters
*********************************/

@description('The name of the automation account package to add.')
param runtimeEnvPackageName string
@description('The link to PS Gallery URI to get package')
param contentLinkUri string
@sys.description('The package version to install. Update this value to import a new/old version.')
param packageVersion string
@sys.description('Name of the target automation account.')
param automationAccountName string
@sys.description('The name of the Runtime Environment to be created in the Automation account.')
param automationAccountRuntimeEnv string

/*********************************
  optional parameters
*********************************/

/*********************************
  resources
*********************************/

resource AutomationAccount 'Microsoft.Automation/automationAccounts@2024-10-23' existing = {
  name: automationAccountName
}

resource RuntimeEnvironment 'Microsoft.Automation/automationAccounts/runtimeEnvironments@2024-10-23' existing = {
  name: automationAccountRuntimeEnv
  parent: AutomationAccount
}

//Get packages based from provided inputs
resource RuntimeEnvironmentPackages 'Microsoft.Automation/automationAccounts/runtimeEnvironments/packages@2024-10-23' = {
  name: runtimeEnvPackageName
  parent: RuntimeEnvironment
  properties:{
    contentLink: {
      version: packageVersion
      uri: contentLinkUri
    }
  }
}

Automation Account Variables

Optionally, you can add variables that can be used within runbooks either by invoking through code or by using out-of-the-box graphic designing. I will not be covering the full contents of runbooks in this document but you can check the documentation for that here.

While Strings are the most common data type for these variables, you can store complex objects by adding them in through parameter files and casting them as an Object using the String(value). They will be immutable in the portal but can still be referenced as the other variable types. In my example I worked with, I added a list of Logic Apps that could be enabled and disabled by the Script referencing the Automation Account variable.

resource automationAccountVariables 'Microsoft.Automation/automationAccounts/variables@2024-10-23' = {
  name: aaVariableName
  parent: automationAccount
  properties: {
    value: string(aaVariablesList)
  }
}

Source Control (Optional)

We can also use the Source Control component of the Automation account to create a webhook to listen for changes to the Azure DevOps repository.  This is so when a change is committed to the repository it will pick up the change once we use a manual sync in the portal.

In the code snippet below I am using a Personal Access Token (PAT) and a manual sync to pick up changes made in a repository.

For this implementation, I would recommend setting up a service account with the PAT under that user. This will protect against a situation where someone who was maintaining the account was to leave the organisation. A service account can easily be regenerated by an administrator if required.

Once the PAT is created under the service account, I highly recommend storing the PAT in a secure place such as Azure Key Vault. Retrieve the PAT from the key vault secret at deploy time using a YAML pipeline deployment of the Automation Account resource.

Note:  To use source control, you will need to provide contributor access to the Automation Account managed identity.

//Resource deployment for Source Control sync for runbooks
//It can use an input branch to listen for any changes in a specified path in an ADO repository
//Use PAT as auth currently but can also be configured to use OAuth2

resource sourceControl 'Microsoft.Automation/automationAccounts/sourceControls@2024-10-23' = {
  name: sourceControlName
  parent: automationAccount
  properties: {
    autoSync: autoSync
    branch: sourceControlBranch
    description: description
    folderPath: sourceControlFolderPath
    publishRunbook: publishRunbook
    repoUrl: sourceControlRepoUrl
    securityToken: {
      accessToken: accessToken
      refreshToken: refreshToken
      tokenType: tokenType
    }
    sourceType: sourceType
  }
}

Role Assignments

Since I am using System-Assigned identities, I can add some role assignments to the Automation Account to give runbooks permission to perform actions on Azure resources. Role assignments required will depend on the workload and you can see the full list of roles available in the documentation here. When assigning these roles, always consider the principle of least privileged access so only assign the minimum necessary permissions required to perform the task the runbook is trying to achieve.

The module loops over the role object set in the parameter file and assigns them to the Automation Account in this example. In the code snippet below, I have assigned “Automation Contributor” and “Logic app Contributor”.

//Role assignments for the Automation account

resource roleAssignmentsAutomationAccount 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
  for role in automationAccountRoles: {
    name: guid('${role.roleDefinitionName}-${role.roleDefinitionId}-${automationAccountName}')
    scope: automationAccount
    properties: {
      principalId: automationAccount.identity.principalId
      roleDefinitionId: role.roleDefinitionId
      principalType: 'ServicePrincipal'
    }
  }
]

Lastly, a call is made to another module to perform role assignments on a different resource group than the current scope in the code snippet below.

role-assignment-module
/********************************* 
  parameters 
*********************************/ 

@description('The object that we want to assign the roles to in a different scope to initial deployment') 
param assignedResource object 
@description('The role definition id for the corresponding role we want to apply') 
param roleDefinitionArray object[] 
@description('The scope of the different resource group to assign roles to') 
param resourceGroupName string 
@description('Name of the Azure Resource being assigned the role') 
param azureResourceName string 

/********************************* 
  optional parameters 
*********************************/ 
 
/********************************* 
  resources 
*********************************/ 

//Assign roles based on provided inputs 
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [  
  for roles in roleDefinitionArray: {  
  name: 
guid('${resourceGroupName}-${roles.roleDefinitionName}-${azureResourceName}') 
  properties:{ 
    principalId: assignedResource.identity.principalId 
    roleDefinitionId: roles.roleDefinitionId 
    principalType: 'ServicePrincipal' 
  } 
} 
]

What’s next?

While I have used a PAT to create the source control webhook to my Azure DevOps repository, it is better to use OAuth to authenticate. This is something that I am currently looking into and will post an update to this blog once I have a working implementation. 

Conclusion

The Automation Account and Runbook combination serves as a powerful tool that can easily execute commonly performed tasks which could include; disable/enable logic app workflows, mange virtual machines or any script that your workflows may require in an automated manner. I have shown how you can easily deploy and adjust the Automation Account to allow for custom modules and how you can deploy between different environments by adjusting configurations.

Appendix

I have attached the files used in the above blog post.

I have split into the

  • Main Bicep file – The template to pass into the deployment task
    • The Bicep module files are found in the snippets in each section
  • Parameters file used to configure and complete the deployment

NOTE: you will need to fill out the details for your own environment based on your own organisation.

Bicep and Paremeter Files – Download Zip

Ready to elevate your data transit security and enjoy peace of mind?

Click here to schedule a free, no-obligation consultation with our Adaptiv experts. Let us guide you through a tailored solution that's just right for your unique needs.

Your journey to robust, reliable, and rapid application security begins now!

Talk To Us