Search code examples
azureazure-devopsazure-container-apps

Azure Container Apps using key vault secrets


I'd like to use Azure Container Apps (ACA) and avoid AKS management

my understanding is that I can create an Azure Container App Environment (ACE) in TF without creating any ACA instances. Then I could use the AzureContainerApps@1 task to deploy my containers to the ACE.

assuming this is all correct, I'm still struggling with how I get secrets from my Key Vault into my ACA instance.

I'm reading store-secret-value-in-container-apps and I noticed they don't even show an option for using the ADO task.

Is what I'm trying to do possible? Where are some decent docs?

TL/DR - I'm really trying to avoid defining my ACA Instance(s) in Terraform. TF is a tool for creating infrastructure.... NOT managing deployments


Solution

  • I got this all working using TF

    in my "shared-infra" module, I create the Container App Environment:

    resource "azurerm_container_app_environment" "cae" {
      name                               = "apps-${var.sub}"
      location                           = local.location
      resource_group_name                = ....
      log_analytics_workspace_id         = azurerm_log_analytics_workspace.law.id
      infrastructure_subnet_id           = azurerm_subnet.apps_subnet.id
      infrastructure_resource_group_name = "${azurerm_resource_group.apps_cae.name}-infra"
      zone_redundancy_enabled            = true
    
      dynamic "workload_profile" {
        for_each = var.app_workload_profiles
        content {
          name                  = workload_profile.value.name
          workload_profile_type = workload_profile.value.type
          minimum_count         = workload_profile.value.min_count
          maximum_count         = workload_profile.value.max_count
        }
      }
    }
    
    • note, if you don't create the CAE with a workload profile, you can't add one later...you have to create an entirely new CAE
    • note, by attaching a profile, you can still use the consumption plan...and get 2x the cpu/memory resources

    in my "aca-app" module, I create a placeholder app using a generic public image

    resource "azurerm_container_app" "app" {
      name                         = "${var.app_name}-${var.sub}"
      container_app_environment_id = data.azurerm_container_app_environment.cae.id
      resource_group_name          = azurerm_resource_group.app.name
      revision_mode                = "Single"
      workload_profile_name        = "Consumption"
    
      identity {
        type = "SystemAssigned"
      }
    
      template {
        min_replicas = 0
        max_replicas = 1
        container {
          name   = "main"
          image  = "nginx:latest" # create the app using this placeholder image
          cpu    = "0.25"
          memory = "0.5Gi"
        }
      }
    
      ingress {
        allow_insecure_connections = false
        external_enabled           = true
        target_port                = 80
        transport                  = "http"
    
        traffic_weight {
          latest_revision = true
          percentage      = 100
        }
      }
    
      lifecycle {
        ignore_changes = all 
        # https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle#ignore_changes
      }
    }
    
    /*
    grant the managed identity assigned to the ACA app permission to pull images from ACR
    */
    resource "azurerm_role_assignment" "acr_pull_assignment" {
      principal_id         = azurerm_container_app.app.identity[0].principal_id
      role_definition_name = "AcrPull"
      scope                = data.azurerm_container_registry.acr.id
      provider             = azurerm.global
    }
    

    each "app" is a root module that call the "aca-app" and "key-vault" child modules:

    module "app" {
      source = "......aca app module ref......"
    
      providers = {
        azurerm        = azurerm
        azurerm.global = azurerm.global
      }
      
      sub       = "dev"
      app_name  = "my-api"
    }
    
    module "vault" {
      source = "...key vault module ref..."
    
      sub                 = "dev"
      name                = "my-api"
      resource_group_name = module.app.resource_group_name
      
      supplemental_role_assignments = [
        {
          name         = "my-api"
          principal_id = module.app.managed_identity_id
          role         = "Key Vault Secrets User"
        },
      ]
    }
    

    once all this TF runs, I can go to my app url and see the NGINX splash page. From this point forward, my ADO pipelines / Github Actions use the az cli to update the app going forward using a command like so:

    az containerapp update -n my-api -g rg-my-api --yaml dev-config.yaml
    

    and dev-config.yaml has

    name: my-api // must match the -n flag of the cli command
    properties:
      managedEnvironmentId: .... your CAE ID ....
      configuration:
        ...other bits omitted...
        registries:
          - server: <your acr>.azurecr.io
            identity: system
        secrets:
          - name: some-api-key
            keyVaultUrl: https://<your-vault>.vault.azure.net/secrets/ld-api-key
            identity: system
      template: 
        containers:
          - image: <your-acr>.azurecr.io/<registry>:{{tag}}
            name: my-api
            resources:
              cpu: 1
              memory: 2Gi
            env:
              - name: IMAGE
                value: <your-acr>.azurecr.io/<registry>:{{tag}}
              - name: SOME_API_KEY
                secretRef: some-api-key
    

    the {{tag}} is replaced with sed in a pipeline step to determine which image to deploy, based on how the pipeline is triggered