-
Notifications
You must be signed in to change notification settings - Fork 66
Add cloud-agnostic CI/CD pipeline deployment guide for Aspire applications #458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
9
commits into
main
Choose a base branch
from
copilot/fix-ci-cd-documentation-gaps
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+361
−0
Open
Changes from 2 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
1752ee3
Initial plan
Copilot 55a4851
Add CI/CD deployment guide and update sidebar
Copilot b3eb860
Update .NET version to 10.x in CI/CD examples
Copilot f6a06e3
Apply suggestions from code review
IEvangelist c1c02b7
Rewrite CI/CD guide to be cloud-agnostic
Copilot 7b5e36e
Add link to app-lifecycle GHCR step in CI/CD guide
Copilot 760718f
Use canonical install script for Aspire CLI in CI/CD examples
Copilot 2466c9b
Wrap CI/CD workflow phases in Steps component
Copilot 5075d7a
Apply editorial review suggestions from @alistairmatthews
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,348 @@ | ||||||
| --- | ||||||
| title: Deploy Aspire apps in CI/CD pipelines | ||||||
| description: Learn how to deploy Aspire applications from CI/CD pipelines using GitHub Actions and Azure DevOps. | ||||||
| --- | ||||||
|
|
||||||
| import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; | ||||||
| import LearnMore from '@components/LearnMore.astro'; | ||||||
|
|
||||||
| Deploying Aspire applications from continuous integration and continuous delivery (CI/CD) pipelines requires a few extra considerations compared to interactive local deployments. This guide covers how to configure the Aspire CLI for non-interactive use, set up GitHub Actions and Azure DevOps pipelines, and handle Azure authentication from service principals. | ||||||
|
|
||||||
| ## Non-interactive deployment | ||||||
|
|
||||||
| The `aspire deploy` command is **interactive by default**—it prompts you to select an Azure tenant, subscription, and resource group. In a CI/CD environment there is no terminal operator, so you must supply those values through environment variables to suppress the prompts: | ||||||
|
|
||||||
| | Environment variable | Description | | ||||||
| |---|---| | ||||||
| | `Azure__SubscriptionId` | The Azure subscription ID to deploy into. | | ||||||
| | `Azure__Location` | The Azure region (for example, `eastus`). | | ||||||
| | `Azure__ResourceGroup` | The resource group name to create or reuse. | | ||||||
|
|
||||||
| When all three variables are set, `aspire deploy` skips the interactive tenant/subscription/resource-group prompts and proceeds automatically. | ||||||
|
|
||||||
| <Aside type="tip"> | ||||||
| Set `Azure__ResourceGroup` to the same value on every run so that subsequent deployments update the existing resources rather than creating new ones. | ||||||
| </Aside> | ||||||
|
|
||||||
| ## Azure authentication in CI/CD | ||||||
|
|
||||||
| ### Service principal vs. interactive user login | ||||||
|
|
||||||
| Locally you typically run `az login` to authenticate as a **user principal**. In CI/CD pipelines the agent runs as a **service principal** (via a federated credential or a client secret), which causes a small but important difference for some Azure resources. | ||||||
|
|
||||||
| For example, Cosmos DB uses `principalType: "User"` for interactive logins but `principalType: "ServicePrincipal"` for headless service-principal logins. If you provision Cosmos DB through Aspire with an interactive login and later deploy from a pipeline with a service principal, the role assignment may fail. | ||||||
|
|
||||||
| To avoid this, use **OpenID Connect (OIDC) / Workload Identity Federation** when possible. This avoids storing long-lived secrets and uses a short-lived token that is automatically associated with the correct principal type. | ||||||
|
|
||||||
| ### Logging in with a service principal | ||||||
|
|
||||||
| <Tabs> | ||||||
| <TabItem label="GitHub Actions"> | ||||||
|
|
||||||
| Use the [`azure/login`](https://github.com/Azure/login) action with OIDC: | ||||||
|
|
||||||
| ```yaml title="GitHub Actions — Azure login with OIDC" | ||||||
| - name: Azure login | ||||||
| uses: azure/login@v2 | ||||||
| with: | ||||||
| client-id: ${{ secrets.AZURE_CLIENT_ID }} | ||||||
| tenant-id: ${{ secrets.AZURE_TENANT_ID }} | ||||||
| subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | ||||||
| ``` | ||||||
|
|
||||||
| Or with a client secret (less preferred): | ||||||
|
|
||||||
| ```yaml title="GitHub Actions — Azure login with client secret" | ||||||
| - name: Azure login | ||||||
| uses: azure/login@v2 | ||||||
| with: | ||||||
| creds: ${{ secrets.AZURE_CREDENTIALS }} | ||||||
| ``` | ||||||
|
|
||||||
| </TabItem> | ||||||
| <TabItem label="Azure DevOps"> | ||||||
|
|
||||||
| Use the [Azure CLI task](https://learn.microsoft.com/azure/devops/pipelines/tasks/reference/azure-cli-v2) with a service connection: | ||||||
|
|
||||||
| ```yaml title="Azure DevOps — Azure CLI task" | ||||||
| - task: AzureCLI@2 | ||||||
| displayName: Deploy with Aspire CLI | ||||||
| inputs: | ||||||
| azureSubscription: '<your-service-connection>' | ||||||
| scriptType: bash | ||||||
| scriptLocation: inlineScript | ||||||
| inlineScript: | | ||||||
| aspire deploy \ | ||||||
| --project src/AppHost/AppHost.csproj | ||||||
| env: | ||||||
| Azure__SubscriptionId: $(AZURE_SUBSCRIPTION_ID) | ||||||
| Azure__Location: $(AZURE_LOCATION) | ||||||
| Azure__ResourceGroup: $(AZURE_RESOURCE_GROUP) | ||||||
| ``` | ||||||
|
|
||||||
| When the `AzureCLI@2` task runs, it automatically authenticates the Azure CLI using the service connection—no separate `az login` step is required. | ||||||
|
|
||||||
| </TabItem> | ||||||
| </Tabs> | ||||||
|
|
||||||
| ## GitHub Actions workflow | ||||||
|
|
||||||
| The following workflow builds and deploys an Aspire application to Azure Container Apps. It triggers on pushes to the `main` branch and uses OIDC for passwordless Azure authentication. | ||||||
|
|
||||||
| ```yaml title="GitHub Actions — .github/workflows/deploy.yml" | ||||||
| name: Deploy to Azure Container Apps | ||||||
|
|
||||||
| on: | ||||||
| push: | ||||||
| branches: [main] | ||||||
| workflow_dispatch: | ||||||
|
|
||||||
| permissions: | ||||||
| id-token: write # Required for OIDC token exchange | ||||||
| contents: read | ||||||
|
|
||||||
| jobs: | ||||||
| deploy: | ||||||
| runs-on: ubuntu-latest | ||||||
|
|
||||||
| steps: | ||||||
| - name: Checkout | ||||||
| uses: actions/checkout@v4 | ||||||
|
|
||||||
| - name: Set up .NET | ||||||
| uses: actions/setup-dotnet@v4 | ||||||
| with: | ||||||
| dotnet-version: '9.x' | ||||||
IEvangelist marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
IEvangelist marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
|
|
||||||
| - name: Install Aspire CLI | ||||||
| run: dotnet tool install -g aspire.cli | ||||||
|
|
||||||
| - name: Azure login | ||||||
| uses: azure/login@v2 | ||||||
| with: | ||||||
| client-id: ${{ secrets.AZURE_CLIENT_ID }} | ||||||
| tenant-id: ${{ secrets.AZURE_TENANT_ID }} | ||||||
| subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | ||||||
|
|
||||||
| - name: Deploy | ||||||
| run: aspire deploy --project src/AppHost/AppHost.csproj | ||||||
| env: | ||||||
| Azure__SubscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | ||||||
| Azure__Location: ${{ vars.AZURE_LOCATION }} | ||||||
| Azure__ResourceGroup: ${{ vars.AZURE_RESOURCE_GROUP }} | ||||||
| ``` | ||||||
|
|
||||||
| ### Setting up OIDC for GitHub Actions | ||||||
|
|
||||||
| <Steps> | ||||||
|
|
||||||
| 1. In the Azure portal, create an **App Registration** (or use an existing one). | ||||||
|
|
||||||
| 1. Add a **Federated credential** to the App Registration: | ||||||
|
|
||||||
| - **Federated credential scenario**: GitHub Actions deploying Azure resources | ||||||
| - **Organization**: your GitHub org or username | ||||||
| - **Repository**: your repository name | ||||||
| - **Entity type**: Branch | ||||||
| - **Branch**: `main` (or the branch you deploy from) | ||||||
|
|
||||||
| 1. Assign the App Registration a role on the target subscription or resource group: | ||||||
|
|
||||||
| ```azurecli title="Azure CLI — Role assignment" | ||||||
| az role assignment create \ | ||||||
| --assignee <app-registration-client-id> \ | ||||||
| --role Contributor \ | ||||||
| --scope /subscriptions/<subscription-id> | ||||||
IEvangelist marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| ``` | ||||||
|
|
||||||
| 1. Add the following secrets to your GitHub repository (**Settings > Secrets and variables > Actions**): | ||||||
|
|
||||||
| | Secret name | Value | | ||||||
| |---|---| | ||||||
| | `AZURE_CLIENT_ID` | The App Registration's Application (client) ID | | ||||||
| | `AZURE_TENANT_ID` | Your Azure tenant ID | | ||||||
| | `AZURE_SUBSCRIPTION_ID` | Your Azure subscription ID | | ||||||
|
|
||||||
| 1. Add the following variables (non-sensitive): | ||||||
|
|
||||||
| | Variable name | Example value | | ||||||
| |---|---| | ||||||
| | `AZURE_LOCATION` | `eastus` | | ||||||
| | `AZURE_RESOURCE_GROUP` | `my-aspire-app-rg` | | ||||||
|
|
||||||
| </Steps> | ||||||
|
|
||||||
| <LearnMore> | ||||||
| For more information, see [GitHub's OIDC documentation](https://docs.github.com/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect). | ||||||
| </LearnMore> | ||||||
|
|
||||||
| ## Azure DevOps pipeline | ||||||
|
|
||||||
| The following pipeline deploys an Aspire application to Azure Container Apps using an Azure DevOps service connection. | ||||||
|
|
||||||
| ```yaml title="Azure DevOps — azure-pipelines.yml" | ||||||
| trigger: | ||||||
| branches: | ||||||
| include: | ||||||
| - main | ||||||
|
|
||||||
| pool: | ||||||
| vmImage: ubuntu-latest | ||||||
|
|
||||||
| variables: | ||||||
| - group: aspire-deploy-vars # Variable group with AZURE_* variables | ||||||
|
|
||||||
| steps: | ||||||
| - task: UseDotNet@2 | ||||||
| displayName: Set up .NET | ||||||
| inputs: | ||||||
| packageType: sdk | ||||||
| version: '9.x' | ||||||
IEvangelist marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
IEvangelist marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
|
|
||||||
| - script: dotnet tool install -g aspire.cli | ||||||
| displayName: Install Aspire CLI | ||||||
|
|
||||||
| - task: AzureCLI@2 | ||||||
| displayName: Deploy with Aspire CLI | ||||||
| inputs: | ||||||
| azureSubscription: '<your-service-connection-name>' | ||||||
| scriptType: bash | ||||||
| scriptLocation: inlineScript | ||||||
| inlineScript: aspire deploy --project src/AppHost/AppHost.csproj | ||||||
| env: | ||||||
| Azure__SubscriptionId: $(AZURE_SUBSCRIPTION_ID) | ||||||
| Azure__Location: $(AZURE_LOCATION) | ||||||
| Azure__ResourceGroup: $(AZURE_RESOURCE_GROUP) | ||||||
| ``` | ||||||
|
|
||||||
| ### Setting up the service connection | ||||||
|
|
||||||
| <Steps> | ||||||
|
|
||||||
| 1. In Azure DevOps, navigate to **Project settings > Service connections**. | ||||||
|
|
||||||
| 1. Select **New service connection** and choose **Azure Resource Manager**. | ||||||
|
|
||||||
| 1. Choose **Workload Identity federation (automatic)** for the authentication method—this creates a federated credential in Azure automatically. | ||||||
|
|
||||||
| 1. Select your subscription and (optionally) a resource group scope. | ||||||
|
|
||||||
| 1. Give the connection a name and save it. Use this name in the `azureSubscription` field of your pipeline task. | ||||||
|
|
||||||
| </Steps> | ||||||
|
|
||||||
| ### Using a variable group | ||||||
|
|
||||||
| <Steps> | ||||||
|
|
||||||
| 1. In Azure DevOps, navigate to **Pipelines > Library** and create a **Variable group** named `aspire-deploy-vars`. | ||||||
|
|
||||||
| 1. Add the following variables: | ||||||
|
|
||||||
| | Variable | Example value | Secret | | ||||||
| |---|---|---| | ||||||
| | `AZURE_SUBSCRIPTION_ID` | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | Yes | | ||||||
| | `AZURE_LOCATION` | `eastus` | No | | ||||||
| | `AZURE_RESOURCE_GROUP` | `my-aspire-app-rg` | No | | ||||||
|
|
||||||
| 1. Link the variable group in your pipeline using the `variables` block shown above. | ||||||
|
|
||||||
| </Steps> | ||||||
|
|
||||||
| ## CI environment tips | ||||||
|
|
||||||
| ### Terminal output and formatting | ||||||
|
|
||||||
| The Aspire CLI detects whether it's running in a CI environment and adjusts its output accordingly (no interactive prompts, plain-text progress). If you see garbled or ANSI escape codes in logs, set the `NO_COLOR` environment variable: | ||||||
|
||||||
| The Aspire CLI detects whether it's running in a CI environment and adjusts its output accordingly (no interactive prompts, plain-text progress). If you see garbled or ANSI escape codes in logs, set the `NO_COLOR` environment variable: | |
| The Aspire CLI detects whether it's running in a CI environment and adjusts its output accordingly, with no interactive prompts and plain-text progress. If you see garbled or ANSI escape codes in logs, set the `NO_COLOR` environment variable: |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.