juusokarlstrom.com // iuuso

Using GitHub Actions to Deploy Resources to Azure with CDKTF

Introduction

Couple of pointers here that hopefully provide some insight to people who are attempting the same sort of route that I've planned to take with this project.

I've been experimenting with GitHub Actions for a while, yet there's still a lot that's new to me. Given the lack of clear instructions for this particular deployment model, I thought it would be beneficial to share an example and some insights I've gained thus far.

Goals simplified:

Steps

Configure OIDC and Service Principal

Following these official guidelines from Microsoft and Hashicorp should provide the information you need for this, but here's the steps I used.

Create new Service Principal in Azure

Microsoft Learn - Create an Azure service principal with Azure CLI

# Bash script
az ad sp create-for-rbac --name myServicePrincipalName1 --role Contributor --scopes /subscriptions/<subscription-id>/

The Contributor role is currently required to allow the Service Principal to create and manage resources within the subscription. I'll likely test this setup by creating new custom roles to determine if this is an appropriate example for that process.

If that example doesn't work check the link provided for official Microsoft guidance.

Use the Service Principal to configure OIDC authentication

Relevant instructions and documentation I followed:

Basically the simplified steps are:

  1. Modify the existing App Registration so that it enables OIDC authentication with GitHub
  2. Insert relevant parameters as secrets in your GitHub repository
  3. Reference those secrets in the Actions template

If you want to check whether the OIDC authentication flow works a'ok, then you can do it with the following stage:

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest

    [..SNIP..]

    - name: Azure login
      uses: azure/login@v2
      with:
          auth-type: SERVICE_PRINCIPAL
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

Full gist of the action-template is here.

After the CDKTF implementation is done you don't need that stage anymore, because the CDKTF stack can handle the authentication for you in the app in itself. More about that later.

Storage Account for Storing Terraform Statefiles

I realized I might have strayed from my original goal of using an Infrastructure-as-Code (IaC) approach. I manually created some resources outside the stack and referenced them in the main.ts file. At some point, I need to explore whether these steps could be integrated into the stack, but I'm uncertain if that approach will work.

To provide some context, Terraform operates differently from CDK when deploying resources. In AWS, CDK synthesizes CloudFormation templates that are deployed, with changes automatically managed by comparing the new stack to the existing one.

In contrast, when using CDKTF in Azure, it generates Terraform configuration files, which are applied using Terraform. These resources are managed individually in the Azure Resource Manager (ARM) and aren’t grouped into 'stacks' like in AWS CloudFormation. Instead, they appear as individual resources or within resource groups based on your deployment structure.

We need a place to store these configuration files, and for purposes of this stack we're using Azure Blop Storage. Just use the example provided in the Microsofts own documentation.

CDKTF Stack - main.ts

In this post, I won't delve into the details of developing with CDK, Terraform, or a combination of both. However, here are a few useful resources to consider:

Once you have the basic setup ready, don't create any additional resources with this deployment stack. We need to do an initial setup run first so that the configuration files are stored properly in the blob container.

Once you have the storage account created, we'll need to import some values from there into our stack:

import { Construct } from 'constructs';
import { 
  App, 
  TerraformStack,
  AzurermBackend,
 } from 'cdktf';
import { AzurermProvider } from '@cdktf/provider-azurerm/lib/provider';

class MyStack extends TerraformStack {
  constructor(scope: Construct, id:string) {
    super(scope, id);

    new AzurermProvider(this, 'AzureRM', {
      features: {},
      useOidc: true,
      clientId: process.env.ARM_CLIENT_ID,
      tenantId: process.env.ARM_TENANT_ID,
      subscriptionId: process.env.ARM_SUBSCRIPTION_ID,
    });

    new AzurermBackend(this, {
      storageAccountName: '<storage-account-name>',
      containerName: '<container-name>',
      key: '<name-of-statefile.tfstate>',
      useOidc: true,
      resourceGroupName: '<rg-where-storageaccount-belongs-to>'
    });
  }
}

const app = new App();
new MyStack(app, 'my-stack');

app.synth();

Notice here that we're using the ARM_CLIENT_ID, ARM_TENANT_ID, and ARM_SUBSCRIPTION_ID as environment variables here for the Terraform to authenticate towards our Azure tenant. That's why those values need to be specifically imported in the GitHub Actions flow specification.

Final Words

It took some time to figure out this flow, but I eventually got there. This process pushed me to learn more about GitHub Actions, Terraform, Azure, and CDKTF. I hope sharing this experience provides value to you as well, especially since there aren't many examples of this specific setup available online.

If you see any errors here or rooms of improvement, you can reach me through the channels I've provided in my About-page.

#azure #cdktf #development #github