Skip to content

Plan: Azure Container Apps Jobs

Status: Planning Roadmap item: aca-jobs Scope: Migrate one-off/batch tasks to Container Apps Jobs


What are Container Apps Jobs?

A Container Apps Job is a container that runs to completion rather than staying alive as a server. It is triggered once, runs its workload, then exits. Azure bills only for the actual execution time (CPU+memory while running).

How it differs from a Container App

Container App Container App Job
Lifecycle Always running (or scaled to 0 idle) Runs once, exits
Trigger HTTP requests / KEDA scale rules Schedule (cron), manual, or event-driven
Billing Per vCPU-second while active Per vCPU-second during execution only
Use case Web servers, APIs One-off tasks, batch, migrations

Beneficial use cases in this project

Task Why a Job fits
Alembic migrations Currently runs on every backend pod startup — wasteful and racy on cold-start. A Job runs once pre-deploy, fails loudly if migration fails, and the app deploy only proceeds after the Job succeeds.
CV generation (on-demand) Long-running pagedjs-cli render (10–30s). Today it blocks an API request thread. A Job could be triggered by the API, run async, and store the PDF in Blob Storage. The API polls for completion.
Database seed / fixture load Run once at environment setup, not on every restart.
Scheduled cost report Already runs as a GitHub Actions workflow — could move to a Container Apps Job on a cron trigger, keeping it closer to the infrastructure.
Smoke test / health check Run a Job post-deploy that pings all endpoints and fails the deployment if any are unhealthy.

Running Alembic in a Job before each app deploy is the highest-value change:

  • Eliminates startup race condition (current source of intermittent crashes)
  • Makes migration failures explicit and deployment-blocking
  • Follows standard Kubernetes init containers / Jobs pattern

Rough implementation

  1. Add portfolio-migrator Job resource to container_apps.bicep (same image as backend, CMD ["alembic", "upgrade", "head"])
  2. In deploy-application.yml, after image push:
az containerapp job start --name portfolio-migrator-job --resource-group <rg>
# poll until succeeded
az containerapp job execution show ... --query "properties.status"
  1. Only proceed to Container App deploy if Job exits 0

Bicep sketch

bicep
resource migrator_job 'Microsoft.App/jobs@2023-05-01' = {
  name: '${org}-${env_short}-${project}-${location_short}-job-migrate'
  location: location
  properties: {
    environmentId: container_app_environment.id
    configuration: {
      triggerType: 'Manual'
      replicaTimeout: 300
      replicaRetryLimit: 1
      manualTriggerConfig: { parallelism: 1, replicaCompletionCount: 1 }
      registries: [{ server: acr_server, identity: backend_identity_id }]
    }
    template: {
      containers: [{
        name: 'migrator'
        image: '${acr_server}/portfolio-backend:${image_tag}'
        command: ['alembic', 'upgrade', 'head']
        resources: { cpu: json('0.25'), memory: '0.5Gi' }
        env: [
          { name: 'DATABASE_URL', secretRef: 'database-url' }
          { name: 'MANAGED_IDENTITY_CLIENT_ID', value: backend_client_id }
        ]
      }]
    }
  }
}

Success Criteria

  • [ ] Migration Job completes before new revision receives traffic
  • [ ] Failed migration blocks the deploy (workflow fails)
  • [ ] Job logs are accessible in Azure portal and GitHub Actions output
  • [ ] No duplicate migrations on cold-start (Alembic idempotent by design)