-
Notifications
You must be signed in to change notification settings - Fork 57
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 all 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,339 @@ | ||
| --- | ||
| title: Deploy Aspire apps in CI/CD pipelines | ||
| description: Learn how to use aspire publish with GitHub Actions and Azure DevOps to build container images and generate deployment artifacts for any target platform. | ||
| --- | ||
|
|
||
| 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 follows a consistent pattern regardless of your target platform: generate deployment artifacts, push container images to a registry, then apply the artifacts using your preferred tooling. In this article, you learn how to configure the Aspire CLI for non-interactive use, push container images to a registry, and set up complete pipelines for GitHub Actions and Azure DevOps. | ||
|
|
||
| ## CI/CD workflow overview | ||
|
|
||
| The recommended CI/CD pattern for Aspire applications uses `aspire publish` to produce deployment artifacts and then pushes the built images to a container registry. The artifacts (Docker Compose files, Kubernetes manifests, or other formats) reference your images by a placeholder that you resolve at deploy time. | ||
|
|
||
| ```mermaid | ||
| architecture-beta | ||
| service ci(logos:github-actions)[CI runner] | ||
| service reg(logos:docker)[Container registry] | ||
| service target(logos:kubernetes)[Deployment target] | ||
|
|
||
| ci:R --> L:reg | ||
| reg:R --> L:target | ||
| ``` | ||
|
|
||
| The workflow breaks down into three phases: | ||
|
|
||
| <Steps> | ||
|
|
||
| 1. **Build and publish**: `aspire publish` builds your .NET project container images and generates deployment artifacts with image name placeholders. | ||
|
|
||
| 1. **Push**: Your pipeline tags the locally-built images and pushes them to a container registry such as GitHub Container Registry, Docker Hub, Amazon ECR, or Azure Container Registry. | ||
|
|
||
| 1. **Deploy**: Your pipeline applies the generated artifacts using the appropriate tooling (`kubectl apply`, `docker compose up`, and so on), substituting the image placeholders with the pushed registry addresses. | ||
|
|
||
| </Steps> | ||
|
|
||
| <Aside type="note"> | ||
| Some integrations also support `aspire deploy`, which handles image building, pushing, and provisioning in a single command. See the platform-specific deployment documentation for details. | ||
| </Aside> | ||
|
|
||
| <LearnMore> | ||
| Learn more: [Publishing and deployment overview](/deployment/overview/) | ||
| </LearnMore> | ||
|
|
||
| ## Running aspire publish in CI | ||
|
|
||
| The `aspire publish` command is **non-interactive by default** — it does not prompt for input and writes all artifacts to the output path you specify. This makes it straightforward to use in automated pipelines. | ||
|
|
||
| ```bash title="Aspire CLI — Generate deployment artifacts" | ||
| aspire publish --project src/AppHost/AppHost.csproj -o ./artifacts | ||
| ``` | ||
|
|
||
| The output directory contains the deployment manifests and the container images are built into the local Docker daemon during the publish step. You push those images to a registry as a separate step. | ||
|
|
||
| <Aside type="tip"> | ||
| Use a consistent, versioned image tag (for example, the Git commit SHA) so that each pipeline run produces a unique, traceable image. Avoid overwriting a fixed `latest` tag in production pipelines. | ||
| </Aside> | ||
|
|
||
| ## Pushing container images to a registry | ||
|
|
||
| After `aspire publish` builds your images locally, tag and push them to your registry using standard Docker CLI commands. | ||
|
|
||
| ### GitHub Container Registry | ||
|
|
||
| [GitHub Container Registry (GHCR)](https://docs.github.com/packages/working-with-a-github-packages-registry/working-with-the-container-registry) is built into every GitHub repository and requires no additional setup. Authenticate with the built-in `GITHUB_TOKEN`: | ||
|
|
||
| ```bash title="Bash — Authenticate and push to GHCR" | ||
| echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin | ||
| docker tag myapp-api:latest ghcr.io/$GITHUB_REPOSITORY/myapp-api:$GITHUB_SHA | ||
| docker push ghcr.io/$GITHUB_REPOSITORY/myapp-api:$GITHUB_SHA | ||
| ``` | ||
|
|
||
| <LearnMore> | ||
| For a complete end-to-end example including GHCR login and `aspire do push`, see [Step 3: Build app, create & push image to GHCR](/fundamentals/app-lifecycle/#step-3-build-app-create--push-image-to-ghcr). | ||
| </LearnMore> | ||
|
|
||
| ### Docker Hub | ||
|
|
||
| Use a script like this to push your images to Docker Hub: | ||
|
|
||
| ```bash title="Bash — Authenticate and push to Docker Hub" | ||
| echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin | ||
| docker tag myapp-api:latest $DOCKERHUB_USERNAME/myapp-api:$IMAGE_TAG | ||
| docker push $DOCKERHUB_USERNAME/myapp-api:$IMAGE_TAG | ||
| ``` | ||
|
|
||
| Protect the sensitive `DOCKERHUB_TOKEN` and `DOCKERHUB_USERNAME` values by storing them as secrets in your pipeline. Generate the token from [hub.docker.com](https://hub.docker.com) under **Account settings > Personal access tokens**. | ||
|
|
||
| ### Other registries | ||
|
|
||
| Other container registries, such as Amazon ECR, Azure Container Registry, Google Artifact Registry, JFrog Artifactory, and self-hosted registries, follow the same pattern: | ||
|
|
||
| - Authenticate with `docker login`. | ||
| - Tag your image with the registry hostname. | ||
| - Push your images. | ||
|
|
||
| Refer to your registry's authentication documentation for the specific login command. | ||
|
|
||
| ## GitHub Actions workflow | ||
|
|
||
| The following complete workflow generates artifacts with `aspire publish`, pushes images to GitHub Container Registry, and applies a Docker Compose deployment. Customize the deploy step for your own target (Kubernetes, a cloud provider, or something else). | ||
|
|
||
| ```yaml title="GitHub Actions — .github/workflows/deploy.yml" | ||
| name: Build and deploy Aspire app | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| packages: write # Required to push to GHCR | ||
|
|
||
| jobs: | ||
| deploy: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
|
|
||
| env: | ||
| IMAGE_TAG: ${{ github.sha }} | ||
| REGISTRY: ghcr.io/${{ github.repository_owner }} | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up .NET | ||
| uses: actions/setup-dotnet@v4 | ||
| with: | ||
| dotnet-version: '10.x' | ||
|
|
||
| - name: Install Aspire CLI | ||
| run: | | ||
| curl -sSL https://aspire.dev/install.sh | bash | ||
| echo "$HOME/.aspire/bin" >> $GITHUB_PATH | ||
|
|
||
| - name: Generate deployment artifacts | ||
| # Adjust the --project path to match your AppHost project location | ||
| run: aspire publish --project src/AppHost/AppHost.csproj -o ./artifacts | ||
|
|
||
| - name: Log in to GHCR | ||
| run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin | ||
|
|
||
| - name: Push container images | ||
| run: | | ||
| # Replace 'myapp-api' and 'myapp-web' with the resource names from your AppHost. | ||
| # The image names produced by aspire publish match your AppHost resource names. | ||
| for image in myapp-api myapp-web; do | ||
| docker tag ${image}:latest ${REGISTRY}/${image}:${IMAGE_TAG} | ||
| docker push ${REGISTRY}/${image}:${IMAGE_TAG} | ||
| done | ||
|
|
||
| - name: Deploy | ||
| run: | | ||
| # This example uses Docker Compose. Replace with kubectl apply, helm upgrade, | ||
| # or your cloud provider's CLI as needed for your deployment target. | ||
| export MYAPP_API_IMAGE=${REGISTRY}/myapp-api:${IMAGE_TAG} | ||
| export MYAPP_WEB_IMAGE=${REGISTRY}/myapp-web:${IMAGE_TAG} | ||
| docker compose -f artifacts/docker-compose.yml up -d | ||
| ``` | ||
|
|
||
| <Aside type="tip"> | ||
| Replace the service image names (`myapp-api`, `myapp-web`) and the deploy step with those specific to your application. The image names used by `aspire publish` match the resource names defined in your AppHost. | ||
| </Aside> | ||
|
|
||
| ## Azure DevOps pipeline | ||
|
|
||
| The following pipeline publishes and deploys an Aspire application using the same pattern. It uses generic Docker CLI commands to push to any container registry. | ||
|
|
||
| ```yaml title="Azure DevOps — azure-pipelines.yml" | ||
| trigger: | ||
| branches: | ||
| include: | ||
| - main | ||
|
|
||
| pool: | ||
| vmImage: ubuntu-latest | ||
|
|
||
| variables: | ||
| imageTag: $(Build.SourceVersion) | ||
| registry: $(REGISTRY_HOST)/$(REGISTRY_NAMESPACE) | ||
|
|
||
| steps: | ||
| - task: UseDotNet@2 | ||
| displayName: Set up .NET | ||
| inputs: | ||
| packageType: sdk | ||
| version: '10.x' | ||
|
|
||
| - script: | | ||
| curl -sSL https://aspire.dev/install.sh | bash | ||
| echo "##vso[task.prependpath]$HOME/.aspire/bin" | ||
| displayName: Install Aspire CLI | ||
|
|
||
| - script: | | ||
| # Adjust the --project path to match your AppHost project location | ||
| aspire publish --project src/AppHost/AppHost.csproj -o $(Build.ArtifactStagingDirectory)/artifacts | ||
| displayName: Generate deployment artifacts | ||
|
|
||
| - script: | | ||
| echo "$(REGISTRY_PASSWORD)" | docker login $(REGISTRY_HOST) \ | ||
| -u $(REGISTRY_USERNAME) --password-stdin | ||
| displayName: Log in to container registry | ||
|
|
||
| - script: | | ||
| # Replace 'myapp-api' and 'myapp-web' with the resource names from your AppHost. | ||
| for image in myapp-api myapp-web; do | ||
| docker tag ${image}:latest $(registry)/${image}:$(imageTag) | ||
| docker push $(registry)/${image}:$(imageTag) | ||
| done | ||
| displayName: Push container images | ||
|
|
||
| - script: | | ||
| # This example uses Docker Compose. Replace with kubectl apply, helm upgrade, | ||
| # or your cloud provider's CLI as needed for your deployment target. | ||
| export MYAPP_API_IMAGE=$(registry)/myapp-api:$(imageTag) | ||
| export MYAPP_WEB_IMAGE=$(registry)/myapp-web:$(imageTag) | ||
| docker compose -f $(Build.ArtifactStagingDirectory)/artifacts/docker-compose.yml up -d | ||
| displayName: Deploy | ||
|
|
||
| - task: PublishBuildArtifacts@1 | ||
| displayName: Publish artifacts | ||
| inputs: | ||
| PathtoPublish: $(Build.ArtifactStagingDirectory)/artifacts | ||
| ArtifactName: deployment-artifacts | ||
| ``` | ||
|
|
||
| Set `REGISTRY_HOST`, `REGISTRY_NAMESPACE`, `REGISTRY_USERNAME`, and `REGISTRY_PASSWORD` as pipeline variables or a variable group. Mark credentials as secret so they're not logged. | ||
|
|
||
| ## CI environment tips | ||
|
|
||
| ### Terminal output and formatting | ||
|
|
||
| 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: | ||
|
|
||
| <Tabs> | ||
| <TabItem label="GitHub Actions"> | ||
|
|
||
| ```yaml title="GitHub Actions — Disable color output" | ||
| env: | ||
| NO_COLOR: '1' | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| <TabItem label="Azure DevOps"> | ||
|
|
||
| ```yaml title="Azure DevOps — Disable color output" | ||
| variables: | ||
| NO_COLOR: '1' | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| ### Timeouts | ||
|
|
||
| Publishing and deploying Aspire apps can take several minutes. Set your pipeline's timeout high enough to allow for: | ||
|
|
||
| - .NET container image builds. | ||
| - Container registry image pushes. These times can vary with image size and network speed. | ||
| - Provisioning and startup of deployment targets. | ||
|
|
||
| <Tabs> | ||
| <TabItem label="GitHub Actions"> | ||
|
|
||
| ```yaml title="GitHub Actions — job timeout" | ||
| jobs: | ||
| deploy: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| <TabItem label="Azure DevOps"> | ||
|
|
||
| ```yaml title="Azure DevOps — job timeout" | ||
| jobs: | ||
| - job: deploy | ||
| timeoutInMinutes: 30 | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| ### Docker availability | ||
|
|
||
| The Aspire CLI builds container images using the local Docker daemon during `aspire publish`. Ensure Docker is available on the build agent: | ||
|
|
||
| - **GitHub Actions**: Docker is pre-installed on `ubuntu-latest` and `windows-latest` runners. | ||
| - **Azure DevOps**: Docker is pre-installed on Microsoft-hosted `ubuntu-latest` agents. Self-hosted agents may need Docker installed separately. | ||
|
|
||
| <Aside type="note"> | ||
| If your pipeline uses Podman or another container runtime, ensure it is configured to be compatible with Docker CLI commands (for example, by aliasing `docker` to `podman`). | ||
| </Aside> | ||
|
|
||
| ### Caching deployment state | ||
|
|
||
| The Aspire CLI caches deployment state (provisioned resource IDs, resolved parameter values) to speed up subsequent runs. In CI/CD you typically want one of two behaviors: | ||
|
|
||
| - **Ephemeral (fresh deploy every run)**: Use `--clear-cache` to discard saved state and provision from scratch. | ||
| - **Incremental (update existing resources)**: Persist the `.aspire/` directory between runs using your pipeline's cache mechanism. | ||
|
|
||
| <Tabs> | ||
| <TabItem label="GitHub Actions"> | ||
|
|
||
| ```yaml title="GitHub Actions — cache .aspire directory" | ||
| - name: Restore deployment cache | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: .aspire | ||
| key: aspire-deploy-${{ github.ref_name }} | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| <TabItem label="Azure DevOps"> | ||
|
|
||
| ```yaml title="Azure DevOps — cache .aspire directory" | ||
| - task: Cache@2 | ||
| inputs: | ||
| key: aspire-deploy | $(Build.SourceBranchName) | ||
| path: .aspire | ||
| displayName: Restore deployment cache | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| <LearnMore> | ||
| For more information about deployment state caching, see [Deployment state caching](/deployment/deployment-state-caching/). | ||
| </LearnMore> | ||
|
|
||
| ## See also | ||
|
|
||
| - [Publishing and deployment overview](/deployment/overview/) | ||
| - [Deploy using the Aspire CLI (Azure Container Apps)](/deployment/azure/aca-deployment-aspire-cli/) | ||
| - [Deployment state caching](/deployment/deployment-state-caching/) | ||
| - [`aspire publish` command reference](/reference/cli/commands/aspire-publish/) | ||
| - [`aspire deploy` command reference](/reference/cli/commands/aspire-deploy/) | ||
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.