| ← Matrix Strategies & Parallel Testing | Next: Creating custom actions → |
|---|
With CI in place, it's time for CD — continuous deployment or continuous delivery. We'll use the Azure Developer CLI (azd), Microsoft's recommended tool for deploying to Azure. azd handles the heavy lifting: generating infrastructure-as-code (Bicep), configuring passwordless authentication (OIDC), and creating the GitHub Actions workflow.
With the prototype built, the shelter is ready to share their application with the world! They want to deploy automatically whenever code is pushed to main — but only after CI passes.
Speaking of secrets and variables... In a prior exercise you utilized GITHUB_TOKEN. GITHUB_TOKEN is a special secret automatically available to every workflow, and provides access to the current repository. You can add your own secrets and variables to your repository for use in workflows.
Secrets are exactly that - secret. These are passwords and other values you don't want the public to be able to see. You can add secrets via the CLI, APIs, and your repository's page on github.com. Secrets are write-only, and are only available to be read by a running workflow. In fact, there's even a filter so if the workflow attempts to write or log a secret it'll automatically be hidden. You can confidently add secrets to a public repository, and the only visible aspect will be its name and not the value.
Variables, on the other hand, are designed to be public values. They're settings like URLs or names, or other values that aren't sensitive. Variables can be both read and written. Use variables whenever you need the ability to configure a value outside a workflow.
There are several strategies for ensuring only validated code reaches production. In a later exercise we'll configure branch rulesets to require CI checks and pull request reviews before code can be merged to main. Since our deploy workflow only triggers on pushes to main, this creates a natural gate: code must pass CI and be reviewed before it can be deployed.
Tip
GitHub also supports environments with deployment protection rules (like manual approval gates). Environments are a powerful option when you need separate staging and production deployments — but for this workshop, branch rulesets give us the same safety with less setup. See the environments documentation to explore that approach on your own.
Let's set up the Azure Developer CLI and scaffold the infrastructure for our project.
-
Open the terminal in your codespace (or press Ctl+` to toggle it).
-
Install azd by running:
curl -fsSL https://aka.ms/install-azd.sh | bash -
Log in to Azure:
azd auth login
Follow the device code flow — open the URL shown, enter the code, and sign in with your Azure account.
-
Initialize the project by running:
azd init --from-code
-
azdwill scan your project and detect the client and server services. When prompted, select Confirm and continue initializing my app to accept the detected services and generate the project configuration. -
By default,
azdgenerates infrastructure in memory at deploy time. To customize the infrastructure, persist it to disk by running:azd infra gen
-
Explore the generated
infra/directory. You'll see Bicep files (.bicep) that define the Azure resources for your application:ls infra/
Tip
Bicep is Azure's domain-specific language for defining infrastructure as code. If you have GitHub Copilot, try asking it to explain the generated Bicep files!
The generated infra/ directory contains several Bicep files that work together:
main.bicep— The entry point. It defines the deployment's parameters (like location and environment name) and orchestrates the other files.main.parameters.json— Default parameter values passed tomain.bicepat deployment time.resources.bicep— The core of the infrastructure. It defines the Azure Container Apps environment and the individual container apps for the client and server, including their Docker images, environment variables, ingress settings, and scaling rules.modules/— Helper modules referenced by the main files (e.g., for fetching container image metadata).abbreviations.json— A lookup tableazduses to generate consistent, short resource names following Azure naming conventions.
The generated Bicep files define the Azure Container Apps that will host the client and server. We need to add an environment variable so the client knows where to find the API server.
-
Open
infra/resources.bicepin your codespace. -
Find the section (around line 109) that reads:
{ name: 'PORT' value: '4321' } -
Create a new line below the closing
}and add the following:{ name: 'API_SERVER_URL' value: 'https://${server.outputs.fqdn}' }
Note
While the syntax resembles JSON, it's not JSON. You'll need to resist the natural urge to add commas between the objects!
By default, azd pipeline config generates a simple workflow that deploys on every push to main. That works for getting started, but we want a workflow that only deploys after CI passes. If you create the workflow file first, azd will detect it and configure credentials around your custom workflow instead of generating the default.
Let's create a workflow that:
- Only deploys after CI passes — using
workflow_run - Can also be triggered manually via
workflow_dispatch - Prevents conflicting deployments with concurrency controls
-
Create a new file at
.github/workflows/azure-dev.yml. -
Add the following content:
name: Deploy App on: workflow_dispatch: workflow_run: workflows: ["Run Tests"] branches: [main] types: [completed] permissions: id-token: write contents: read jobs: deploy: runs-on: ubuntu-latest if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' concurrency: group: deploy-production cancel-in-progress: false steps: - uses: actions/checkout@v4 - name: Install azd uses: Azure/setup-azd@v2 - name: Log in with Azure (Federated Credentials) run: | azd auth login \ --client-id "${{ vars.AZURE_CLIENT_ID }}" \ --federated-credential-provider "github" \ --tenant-id "${{ vars.AZURE_TENANT_ID }}" - name: Provision and deploy run: azd up --no-prompt env: AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} AZURE_LOCATION: ${{ vars.AZURE_LOCATION }}
-
Save the file.
Let's walk through the key parts:
permissions: id-token: write— In the Running Tests module you setcontents: read. Here,id-token: writeis added because the workflow needs to request OIDC tokens from Azure. This is how passwordless authentication works — no stored credentials, just short-lived tokens.vars.*— Variables like${{ vars.AZURE_CLIENT_ID }}reference repository variables thatazd pipeline configwill create for you in the next step.workflow_runtriggers this workflow whenever the Run Tests workflow completes onmain. Theifcondition ensures it only proceeds when tests succeeded — or when triggered manually viaworkflow_dispatch.concurrencyprevents conflicting deployments. Notecancel-in-progress: falseto avoid accidentally cancelling an active deployment.azd upprovisions infrastructure and deploys your application in one command.
Now let's let azd configure the pipeline credentials. Because the workflow file already exists, azd will configure OIDC and variables around it rather than generating a new one.
-
Configure the deployment pipeline:
azd pipeline config
-
Follow the prompts — here's what to expect:
Prompt What to select Select a provider Choose GitHub Enter a unique environment name Enter a short name (e.g., <HANDLE>-pets-workshop) — this names your Azure resource groupSelect an Azure subscription Choose the subscription you want to deploy to Select an Azure location Pick a region close to you (e.g., eastus2)Select how to authenticate the pipeline to Azure Choose Federated Service Principal (SP + OIDC) After you answer these,
azdwill:- Create OIDC credentials in Azure for passwordless authentication
- Store the necessary secrets and variables in your repository automatically
- Detect your existing workflow file and configure it
-
When prompted to commit and push your local changes, say yes.
Tip
After azd pipeline config completes, navigate to Settings > Secrets and variables > Actions > Variables tab to see the repository variables it created (like AZURE_CLIENT_ID, AZURE_TENANT_ID, etc.). These are the vars.* values your workflow references.
When you said yes to azd pipeline config's commit prompt, it pushed your changes — including the workflow file. Let's verify everything is working.
-
Navigate to the Actions tab. The push will trigger the Run Tests workflow first.
-
Once tests complete successfully, the Deploy App workflow will start automatically (via the
workflow_runtrigger). -
Watch the deploy job run — it will provision Azure resources and deploy both the client and server applications.
-
Once the deployment completes return to your codespace.
-
Run the following in the terminal to list the details of your new Azure environment:
azd show
-
Look for the client service endpoint in the output.
-
Open the client URL in your browser — you should see the pet shelter application live!
Congratulations! You've deployed the pet shelter application to Azure with a CI/CD pipeline:
- CI-gated deployment — CD only runs after CI passes, using
workflow_run - OIDC authentication — passwordless, short-lived tokens instead of stored credentials
- Concurrency controls — preventing conflicting deployments
- azd integration —
azd pipeline configconfigured credentials around your custom workflow
In a later exercise, we'll add branch rulesets to ensure code must pass CI and be reviewed before it can reach main — creating a natural production gate.
Next we'll create custom actions to reduce duplication and make our workflows more maintainable.
- What is the Azure Developer CLI?
- Create a custom pipeline definition
- Events that trigger workflows: workflow_run
- About security hardening with OpenID Connect
- Deploying with GitHub Actions
- Using environments for deployment
| ← Matrix Strategies & Parallel Testing | Next: Creating custom actions → |
|---|