Deploy Your Book Catalog App on AKS

Build a CI/CD pipeline in Azure DevOps to deploy a Spring Boot app to AKS.

Introduction

30 Second Summary

Every time a developer changes their code, someone has to build it, package it, and ship it to a running server. Doing that by hand every single time is slow and error-prone.

In this project, you will build a fully automated CI/CD pipeline in Azure DevOps. The pipeline takes your existing Spring Boot book catalog app from code commit to live deployment on Azure Kubernetes Service.

What You'll Build

Push a commit to Azure Repos and watch your pipeline automatically build, push, and deploy your bookcatalog app to a live Kubernetes cluster you can open in any browser.

By the end of this project, you'll have:

  • A multi-stage YAML pipeline in Azure DevOps that automatically builds your Docker image and pushes it to Azure Container Registry on every commit.
  • Your bookcatalog app running live on AKS with a public IP address you can open in your browser and share.
  • First-hand experience hitting the classic ErrImagePull failure and fixing it by understanding ACR-to-AKS authentication.
  • Secret Mission: Add a staging environment with a manual approval gate before production deployment.

Are there any prerequisites?

This project picks up from an existing Spring Boot bookcatalog app that has a Dockerfile and Kubernetes manifests (deployment.yml and service.yml). You should be comfortable with Docker and Kubernetes concepts.

You will also need Homebrew installed on your macOS machine for installing the Azure CLI in Step 1.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. You already have a working Spring Boot app with a Dockerfile and Kubernetes manifests. Now it's time to think about the automated pipeline that will take your app from a code commit all the way to a live deployment on Azure Kubernetes Service.

Set Up Azure and Azure DevOps

Your bookcatalog app is ready to go, but it only runs on your local machine. To automate building and deploying it to the cloud, you need the right tools and accounts in place first.

In this step, you will install the Azure CLI, set up your Azure and Azure DevOps accounts, and provision the cloud infrastructure that your pipeline will target.

In this step, get ready to:
  • Install Azure CLI and create your Azure free account.
  • Create your Azure DevOps organization and project.
  • Provision Azure Container Registry and an AKS cluster.
Install Azure CLI and create your Azure free account

The Azure CLI is the command-line tool you will use throughout this project to create and manage Azure resources from your terminal. Let's install it and connect it to a new Azure account.

  • Install Azure CLI by running this command to add the Azure CLI tap to Homebrew:
brew tap azure/azure-cli
  • Install the Azure CLI package by running:
brew install --cask azure-cli-preview

Why two commands?

Azure CLI on macOS currently installs through a custom Homebrew Cask. The brew tap command adds the Azure CLI repository to your Homebrew sources. Then brew install --cask azure-cli-preview installs the CLI itself as a macOS application from that repository.

  • Verify the installation succeeded by running:
az --version

✔️ I see a version number

You should see azure-cli followed by a version number (such as 2.89.1). Azure CLI is installed and ready.

ⓧ Command not found

If your terminal does not recognise the az command, try closing and reopening your terminal so the new PATH entries take effect. Then run az --version again.

If it still fails, re-run the install commands above and check for errors in the Homebrew output.

Now you need an Azure account so the CLI has a subscription to work with.

  • Open https://azure.microsoft.com/free in your browser.
  • Click Start free and follow the sign-up process using your Microsoft account. Create a Microsoft account if you do not have one.

What does the free account include?

The Azure free account gives you $200 in credit for 30 days. That is more than enough to cover all the resources in this project.

  • Connect Azure CLI to your new account by running:
az login

What does this command do?

This command opens your default browser and asks you to sign in. Use the same Microsoft account you just created your Azure free account with. After authentication, the terminal prints your subscription details in JSON format.

Browser did not open?

If a browser window does not appear, look for a URL in the terminal output and paste it into your browser manually. Sign in and return to the terminal.

help me troubleshoot az login

Create your Azure DevOps organization and project

Azure DevOps is the platform where your CI/CD pipeline will live. You need an organization (your team's top-level container) and a project (where your code and pipelines go).

  • Open https://dev.azure.com in your browser.
  • Sign in with the same Microsoft account you used for your Azure free account.
  • Click New organization.
  • Enter a name for your organization.
  • Select your nearest geography from the dropdown.
  • Select your Azure subscription for billing.
  • Click Continue.

Why does it ask for an Azure subscription?

Azure DevOps now requires an active Azure subscription when creating a new organization. Your free account subscription covers this. The free tier gives you one Microsoft-hosted CI/CD parallel job with up to 1,800 minutes per month.

You should now see your organization's home page. Time to create your project.

  • Click New project in the top right corner.
  • Enter bookcatalog as the project name.
  • Select Git for version control.
  • Leave other settings as defaults and click Create.

You should see the bookcatalog project dashboard with sections for Boards, Repos, Pipelines, and more.

Provision Azure Container Registry and AKS cluster

Your pipeline needs two targets: a container registry to store your Docker images and a Kubernetes cluster to run them. Let's create both using the Azure CLI.

  • Create a resource group to hold all your Azure resources by running:
az group create --name bookcatalog-rg --location eastus

What is a resource group?

A resource group is a logical container in Azure that groups related resources together. Everything you create in this project will live in bookcatalog-rg. When you are done, you can delete the entire group to clean up all resources at once.

You should see JSON output with "provisioningState": "Succeeded" confirming the resource group was created.

  • Create an Azure Container Registry instance by running:
az acr create --resource-group bookcatalog-rg --name [[ACR_NAME="your unique ACR name, e.g. bookcatalogacrjane"]] --sku Basic

What are the ACR naming rules?

The ACR name must be globally unique across all of Azure. Use lowercase alphanumeric characters only, between 5 and 50 characters. A good pattern is bookcatalogacr followed by your name (for example, bookcatalogacrjane). The Basic SKU includes 10 GB of storage, which is plenty for this project.

You should see JSON output showing your new ACR resource. Note the loginServer value (it looks like yourname.azurecr.io). You will need it in the next step.

  • Create an Azure Kubernetes Service cluster by running:
az aks create --resource-group bookcatalog-rg --name bookcatalog-aks --node-count 1 --generate-ssh-keys

Why one node and no ACR attachment?

A single-node cluster keeps costs low within your free credit. One node is enough to run your bookcatalog deployment.

You might notice we are not attaching the ACR to the cluster yet. This is intentional. You will see why in a later step.

This command takes several minutes

Creating an AKS cluster typically takes 5 to 10 minutes. Wait for the command to finish before continuing. You will see a large JSON response when it completes.

  • Download the cluster credentials to your local machine by running:
az aks get-credentials --resource-group bookcatalog-rg --name bookcatalog-aks

What does this command do?

This command merges the AKS cluster's connection details into your local kubeconfig file. After running it, kubectl commands on your terminal will target your new AKS cluster.

Before you run the next command, what do you expect to see if your AKS cluster is set up correctly?

  • Confirm your cluster is reachable by running:
kubectl get nodes

You should see one node listed with a STATUS of Ready. That confirms your AKS cluster is running and your terminal can communicate with it.

Node not showing Ready?

If you see zero nodes or a NotReady status, the cluster may still be provisioning. Wait a minute and run kubectl get nodes again.

If you see a connection error, re-run the az aks get-credentials command to refresh your kubeconfig.

help me troubleshoot my AKS cluster connection

Your Azure infrastructure is in place. Next up, you will push your bookcatalog app to Azure Repos so your pipeline has source code to build from.

Push Your App to Azure Repos

Your Azure environment is ready. You have an Azure DevOps project, a container registry, and a Kubernetes cluster all waiting for code.

The missing piece is the code itself. Azure Repos is where Azure DevOps looks for changes to trigger pipelines. Without your bookcatalog source code in Azure Repos, there is no trigger and no pipeline. In this step, you will connect your local project, update your Kubernetes manifests for ACR, and push everything up.

In this step, get ready to:
  • Add your Azure Repos repository as a Git remote.
  • Update your Kubernetes manifests so the image reference points to your ACR instance.
  • Push your full project to Azure Repos and verify it appears.
Add the Azure Repos remote to your project

Your Azure DevOps project already has an empty Git repository. You need the HTTPS clone URL so your local Git can push to it.

  • Switch back to your Azure DevOps project at dev.azure.com.
  • In the left sidebar, click Repos.
  • Click the Clone button in the top right of the empty repository page.
  • Copy the HTTPS clone URL and save it here: https://dev.azure.com/yourorg/bookcatalog/_git/bookcatalog

Now connect your local bookcatalog project to this remote.

  • In your terminal, navigate to your local bookcatalog project directory.
  • Add Azure Repos as a new remote named azure by running this command:
git remote add azure [[AZURE_CLONE_URL="https://dev.azure.com/yourorg/bookcatalog/_git/bookcatalog"]]
  • Confirm the remote was added by running:
git remote -v

You should see your new azure remote listed alongside any existing remotes (like origin).

Not seeing the azure remote?

Double-check that you ran the command from inside your bookcatalog project folder, not a parent directory.

If you see an error about the remote already existing, you may have run the command twice. Run git remote remove azure and then try again.

Help me troubleshoot my Azure Repos remote setup.

Update your Kubernetes manifests for ACR

Your existing manifests reference a local Docker image. Before pushing, you need to update them so the image reference points to your Azure Container Registry instead.

Do I need to split my manifest files?

Your pipeline will reference manifests/deployment.yml and manifests/service.yml as separate files. If your Deployment and Service definitions are combined in a single YAML file (separated by ---), split the Service block into its own service.yml file inside manifests/.

If they are already separate files, you are good to go.

  • Confirm your project has a manifests/ directory containing both deployment.yml and service.yml by running:
ls manifests/

You should see deployment.yml and service.yml listed.

Your service.yml should contain the Service definition for your bookcatalog app. Verify it looks like this:

apiVersion: v1
kind: Service
metadata:
  name: bookcatalog-service
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: bookcatalog

What does this Service do?

  • type: LoadBalancer tells Azure to provision a public IP so you can reach your app from a browser.
  • port: 80 is the external port visitors use. targetPort: 8080 maps traffic to the container port your Spring Boot app listens on.
  • The selector matches pods labeled app: bookcatalog, connecting the Service to your Deployment.

Now update the image reference in your deployment.yml. Your current file points to a local image that will not exist in the cloud.

  • Open manifests/deployment.yml in your code editor.
  • Find the image line under containers. It currently looks like this:
          image: local-bookcatalog:latest
  • Replace that line with your ACR image reference. Use the ACR name you created in Step 1:
          image: [[ACRNAME="bookcatalogacryourname"]].azurecr.io/bookcatalog:latest
  • Next, find the imagePullPolicy line directly below the old image reference:
          imagePullPolicy: IfNotPresent
  • Delete the entire imagePullPolicy: IfNotPresent line. That policy was only useful for local development.
  • Save manifests/deployment.yml.

Why remove imagePullPolicy?

IfNotPresent tells Kubernetes to skip pulling the image if it already exists locally. That made sense on your local machine, but on AKS the image has never been pulled. Removing it lets Kubernetes use the default pull behavior, which is Always for :latest tags and IfNotPresent for specific tags like a build ID.

Double check that your deployment.yml matches the expected state after your edits.

✔️ Awesome, I've got everything!

Great. Make sure you have saved manifests/deployment.yml and manifests/service.yml.

ⓧ I'd like to double check the full code

Your manifests/deployment.yml should look like this (with your own ACR name in the image line):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: bookcatalog-deployment
  labels:
    app: bookcatalog
spec:
  replicas: 2
  selector:
    matchLabels:
      app: bookcatalog
  template:
    metadata:
      labels:
        app: bookcatalog
    spec:
      containers:
        - name: bookcatalog-app
          image: [[ACRNAME="bookcatalogacryourname"]].azurecr.io/bookcatalog:latest
          ports:
            - containerPort: 8080
          resources:
            limits:
              memory: "512Mi"
              cpu: "500m"
            requests:
              memory: "256Mi"
              cpu: "250m"

Your manifests/service.yml should look like this:

apiVersion: v1
kind: Service
metadata:
  name: bookcatalog-service
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: bookcatalog
Push your code and verify in Azure Repos

Your manifests are updated and your remote is connected. Time to push your project to Azure Repos.

  • Stage all your changes by running:
git add .
  • Commit with a descriptive message by running:
git commit -m "Update manifests for ACR deployment"
  • Push your code to the Azure Repos remote by running:
git push azure main

Using a different default branch?

If your default branch is master instead of main, replace main with master in the push command. You can check your branch name with git branch.

Push failing with authentication errors?

Azure DevOps may prompt you to authenticate when you push over HTTPS. If you see a credential prompt, enter your Microsoft account credentials.

If you see a "remote rejected" error, check that you are signed in to Azure DevOps with the same Microsoft account you used for your Azure subscription.

Help me fix my Azure Repos push authentication error.

Before you check Azure DevOps, predict what you expect to see in the Repos section. What files and folders should appear?

  • Switch back to your Azure DevOps project at dev.azure.com.
  • Click Repos in the left sidebar.
  • Click Files to browse the repository contents.

You should see your bookcatalog source code, your Dockerfile, and the manifests/ directory containing deployment.yml and service.yml.

Repos page still showing an empty repository?

Make sure your push command completed without errors in the terminal. Look for a line like Branch 'main' set up to track remote branch 'main' from 'azure'.

If Repos is empty, check that you pushed to the azure remote (not origin). Run git remote -v to confirm the remote URL points to your Azure DevOps project.

Help me figure out why my Azure Repos is empty after pushing.

Your code is in the cloud and ready for automation. Next up, you will create the CI pipeline that automatically builds your Docker image and pushes it to ACR every time you commit.

Build Your CI Pipeline

Your bookcatalog code is sitting in Azure Repos, but nothing happens when you push a new commit. To get the app onto your AKS cluster right now, you would need to manually build the Docker image, push it to ACR, and apply the Kubernetes manifests every single time.

A CI pipeline automates that manual work. In this step, you will create an Azure DevOps pipeline that builds your Docker image and pushes it to Azure Container Registry on every commit.

In this step, get ready to:
  • Create service connections so your pipeline can authenticate with ACR and AKS.
  • Write a YAML pipeline that builds and pushes your Docker image automatically.
  • Run the pipeline and verify your image appears in ACR.
Create service connections

Service connections are how Azure DevOps authenticates with external resources like your container registry and Kubernetes cluster. Your pipeline needs two connections: one to push Docker images to ACR, and one to deploy to AKS in the next step.

Start by creating the Docker Registry connection for ACR.

  • In your Azure DevOps project, click the gear icon at the bottom left to open Project settings.
  • In the left sidebar under Pipelines, click Service connections.
  • Click New service connection.
  • Select Docker Registry and click Next.
  • Select Azure Container Registry as the registry type.
  • Select Service Principal as the Authentication Type.
  • Select your Azure subscription from the Subscription dropdown.
  • Select your ACR instance from the Azure container registry list.

Which ACR instance?

Look for the registry name you created in Step 1. It starts with bookcatalogacr followed by your unique suffix.

  • Enter ACR-Connection as the connection name.
  • Check the Grant access permission to all pipelines checkbox.
  • Click Save.

You should see ACR-Connection listed on the Service connections page. Now create the second connection for Azure Resource Manager.

  • Click New service connection again.
  • Select Azure Resource Manager and click Next.
  • Select Service principal (automatic) and click Next.
  • For Scope level, select Subscription.
  • Select your Azure subscription from the dropdown.
  • Select bookcatalog-rg from the Resource group dropdown.
  • Enter AKS-Connection as the Service connection name.
  • Check Grant access permission to all pipelines.
  • Click Save.

You should now see both ACR-Connection and AKS-Connection listed on the Service connections page.

What are service connections?

Service connections store credentials that let your pipeline interact with external services securely. The ACR-Connection lets the pipeline push Docker images to your registry. The AKS-Connection lets the pipeline deploy manifests to your Kubernetes cluster.

By naming them explicitly, you can reference them in your pipeline YAML without embedding any credentials in your code.

Subscription dropdown is empty?

Make sure you are signed in to Azure DevOps with the same Microsoft account you used for your Azure subscription. If the dropdown still does not load, try opening the page in an incognito or private browser window.

help me fix my Azure DevOps service connection subscription dropdown

Create the pipeline YAML file

With the service connections in place, you can now write the pipeline definition. Azure DevOps pipelines are defined in a YAML file at the root of your repository. The pipeline will trigger automatically on every push to main.

  • In your local bookcatalog project directory, create a new file named azure-pipelines.yml at the repository root.
  • Add the trigger and variables section by pasting the following code:
# Trigger the pipeline on every push to main
trigger:
  - main

# Variables shared across all stages
variables:
  imageRepository: 'bookcatalog'
  containerRegistry: 'ACR-Connection'
  dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile'
  tag: '$(Build.BuildId)'
  vmImageName: 'ubuntu-latest'

What does this code do?

The trigger block tells Azure DevOps to start a pipeline run whenever code is pushed to the main branch.

The variables block defines reusable values. containerRegistry points to the ACR-Connection service connection you just created. tag: '$(Build.BuildId)' gives each image a unique tag based on the pipeline run number, so you always know which build produced which image.

  • Below the variables block in the same file, add the Build stage by pasting this code:
# Define the Build stage
stages:
  - stage: Build
    displayName: Build and push image
    jobs:
      - job: Build
        displayName: Build
        pool:
          vmImage: $(vmImageName)
        steps:
          # Build the Docker image and push it to ACR
          - task: Docker@2
            displayName: Build and push image to ACR
            inputs:
              command: buildAndPush
              repository: $(imageRepository)
              dockerfile: $(dockerfilePath)
              containerRegistry: $(containerRegistry)
              tags: |
                $(tag)

          # Publish manifests so the Deploy stage can use them
          - task: PublishPipelineArtifact@1
            inputs:
              artifactName: 'manifests'
              path: '$(Build.SourcesDirectory)/manifests'

What does this code do?

The Docker@2 task with command: buildAndPush builds your Docker image from your Dockerfile and pushes it to ACR in a single step. The image gets tagged with the build ID for traceability.

The PublishPipelineArtifact@1 task saves your manifests/ directory as a pipeline artifact. This makes your Kubernetes manifests available to a Deploy stage you will add in the next step.

  • Save azure-pipelines.yml.

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

Your azure-pipelines.yml has two sections you added above. Scroll up and confirm both are present in this order:

  • The trigger and variables block (starting with trigger:).
  • The stages block containing the Build stage (starting with stages:).

Make sure there is no duplicate trigger: or variables: block, and that the stages: block sits directly below variables: with no extra indentation.

Run the pipeline and verify the image in ACR

Your pipeline file is ready. Now you need to commit it and push it to Azure Repos so the pipeline can trigger.

  • Commit and push the new file to Azure Repos by running these commands:
git add azure-pipelines.yml
git commit -m "Add CI pipeline for Docker build and push"
git push azure main

What does this command do?

This stages your new pipeline file, creates a commit, and pushes it to the azure remote (your Azure Repos repository). Pushing to main triggers the pipeline because your YAML defines trigger: - main.

  • Return to your Azure DevOps project in the browser.
  • Click Pipelines in the left sidebar.

Azure DevOps auto-detects azure-pipelines.yml and triggers a run. You should see your pipeline listed with a run in progress.

First pipeline run may need permission

Azure DevOps may ask you to permit the pipeline to use your service connections. If you see a permission prompt, click Permit to allow the pipeline to access ACR-Connection.

  • Click on the pipeline run to open its details.
  • Click on the Build stage to watch the logs in real time.

Before you check, take a moment to predict: do you think the Build stage will succeed? You set up the service connection and the Dockerfile already exists in your repository.

  • Wait for the Build stage to finish. You should see a green checkmark next to the stage name.

Now verify the image made it to your container registry.

  • Open the Azure portal at https://portal.azure.com.
  • Search for Container registries in the top search bar and select your ACR instance.
  • Click Repositories in the left sidebar under Services.
  • You should see a bookcatalog repository listed. Click on it.
  • You should see a tagged image. The tag is a number matching your pipeline's build ID.

Build stage failed?

Click on the failed step in the pipeline logs to see the error details. Common causes:

  • Your Dockerfile is not at the repository root. Check that dockerfilePath in your YAML matches the actual file location.
  • The containerRegistry value in your YAML does not match the service connection name. It must be exactly ACR-Connection.
  • Indentation errors in azure-pipelines.yml. YAML is whitespace-sensitive. Check that task: lines are indented with spaces, not tabs.

help me debug my Azure DevOps pipeline build failure

Your CI pipeline is live. Every time you push to main, Azure DevOps builds a fresh Docker image and pushes it to ACR. Next up, you will add a Deploy stage that takes this image and deploys it to your AKS cluster.

Deploy to AKS and Fix the Pull Failure

Your pipeline builds and pushes the Docker image to ACR on every commit. But the app is not actually running anywhere yet. To get it live on your AKS cluster, you need a Deploy stage in your pipeline that applies your Kubernetes manifests.

There is a catch. Your AKS cluster does not have permission to pull images from your private ACR. You will hit this wall firsthand, diagnose it, and fix it.

In this step, get ready to:
  • Add a Deploy stage to your pipeline YAML that uses the KubernetesManifest@1 task.
  • Diagnose an ErrImagePull failure caused by missing ACR-to-AKS authentication.
  • Fix the authentication and verify your bookcatalog app is running live on AKS.
Add the Deploy stage to your pipeline

Your pipeline currently has a single Build stage. You need a second stage that takes the published manifests artifact and applies it to your AKS cluster.

  • Open azure-pipelines.yml in your editor.
  • Find the end of the Build stage (after the PublishPipelineArtifact@1 task).
  • Add the following Deploy stage directly below the Build stage, at the same indentation level as - stage: Build:
  - stage: Deploy
    displayName: Deploy to AKS
    dependsOn: Build
    jobs:
      - deployment: Deploy
        displayName: Deploy to AKS
        pool:
          vmImage: $(vmImageName)
        # Links this deployment to Azure DevOps Environments
        environment: 'bookcatalog.default'
        strategy:
          runOnce:
            deploy:
              steps:
                # Apply Kubernetes manifests to AKS cluster
                - task: KubernetesManifest@1
                  displayName: Deploy to Kubernetes cluster
                  inputs:
                    action: deploy
                    connectionType: azureResourceManager
                    azureSubscriptionConnection: 'AKS-Connection'
                    azureResourceGroup: 'bookcatalog-rg'
                    kubernetesCluster: 'bookcatalog-aks'
                    namespace: 'default'
                    manifests: |
                      $(Pipeline.Workspace)/manifests/deployment.yml
                      $(Pipeline.Workspace)/manifests/service.yml
                    # Replaces image refs in manifests with the tagged build
                    containers: |
                      [[ACRNAME="your ACR name e.g. bookcatalogacrjane"]].azurecr.io/bookcatalog:$(tag)

What does this code do?

This Deploy stage runs only after the Build stage succeeds, thanks to dependsOn: Build.

  • deployment is a special job type that records release history in Azure DevOps Environments, giving you traceability of what was deployed and when.
  • environment: 'bookcatalog.default' links this deployment to the bookcatalog environment targeting the default Kubernetes namespace. Azure DevOps creates this environment automatically on first use.
  • The KubernetesManifest@1 task applies your deployment.yml and service.yml to AKS using the AKS-Connection service connection you created earlier.
  • The containers input performs artifact substitution. It replaces the image reference in your manifests with the exact tagged image built by the pipeline.
  • Save azure-pipelines.yml.
  • Commit and push the updated pipeline to Azure Repos by running these commands:
git add azure-pipelines.yml
git commit -m "Add Deploy stage"
git push azure main

Pushing to Azure Repos automatically triggers a new pipeline run.

First-time permission prompt

Because the Deploy stage uses the AKS-Connection service connection and the bookcatalog environment for the first time, the pipeline may pause and ask for permission to access these resources.

If you see a message saying the pipeline needs permission, click View next to the message, then click Permit for each resource.

  • Switch back to your Azure DevOps project in the browser.
  • Click Pipelines in the left sidebar.
  • Click on the latest pipeline run to open it.
  • Watch both the Build and Deploy stages complete. Both should show green checkmarks.

Pipeline stuck or failed?

  • If the pipeline is waiting for permission, look for the yellow banner at the top of the run. Click View then Permit for each listed resource.
  • If the Deploy stage fails, click the stage to see the logs. Verify that your azureSubscriptionConnection is set to AKS-Connection (not ACR-Connection).
  • Check that your YAML indentation is correct. YAML is sensitive to spaces, and a misaligned key will cause the pipeline to fail to parse.

Why is my Deploy stage failing in Azure DevOps?

✔️ Awesome, I've got everything!

Double check you have saved azure-pipelines.yml and pushed it to Azure Repos.

ⓧ I'd like to double check the full code

Your azure-pipelines.yml is over 30 lines now, so scroll through and confirm these sections are present in order:

  • trigger block at the top, triggering on main.
  • variables block with imageRepository, containerRegistry, dockerfilePath, tag, and vmImageName.
  • stages: containing a Build stage with Docker@2 and PublishPipelineArtifact@1 tasks.
  • A Deploy stage with dependsOn: Build, a deployment job, and a KubernetesManifest@1 task pointing to your manifests and ACR image.
Observe the ErrImagePull failure and fix ACR-to-AKS authentication

Both pipeline stages show green checkmarks. That means your manifests were applied to AKS successfully. But "applied" does not mean "running."

Before you check the pods, what do you think you will see? Will the pods be running successfully, or will something go wrong?

  • Check the status of your pods by running this command:
kubectl get pods

You should see your pods stuck in ErrImagePull or ImagePullBackOff status. This is expected and intentional.

Why are the pods failing?

Your ACR is a private registry. By default, AKS does not have permission to pull images from it. When Kubernetes tries to pull the image, it receives a 401 Unauthorized response and marks the pod as ErrImagePull.

This is the most common real-world issue practitioners face when connecting ACR and AKS. You intentionally skipped attaching ACR during cluster creation so you could see this failure firsthand.

The fix is to grant AKS permission to pull from your ACR. The az aks update --attach-acr command assigns the AcrPull role to your AKS cluster's kubelet managed identity.

  • Attach your ACR to AKS by running this command:
az aks update --name bookcatalog-aks --resource-group bookcatalog-rg --attach-acr [[ACRNAME="your ACR name e.g. bookcatalogacrjane"]]

What does this command do?

This command grants the AcrPull role to the AKS kubelet managed identity. Once this role is assigned, your AKS nodes can authenticate with your private ACR and pull container images without needing an image pull secret.

This command may take a minute to complete. Once it finishes, you need a fresh deployment to pick up the new permissions.

  • Switch back to your Azure DevOps project in the browser.
  • Click Pipelines in the left sidebar.
  • Click on your pipeline name to see the list of runs.
  • Click Run pipeline in the top right corner.
  • Leave the defaults and click Run.
  • Watch both the Build and Deploy stages complete with green checkmarks.

Still seeing ErrImagePull after re-running?

  • Double check that the az aks update --attach-acr command completed successfully. Look for confirmation output in your terminal.
  • Make sure the ACR name you passed to --attach-acr matches the ACR name in your containers line in azure-pipelines.yml.
  • If the old pods are still stuck, delete them to force Kubernetes to recreate them with the new permissions by running kubectl delete pods --all.

Help me fix ErrImagePull on my AKS pods after attaching ACR.

Verify the live deployment

The pipeline reports success and ACR is attached. Time to confirm your app is actually running on AKS.

  • Check the status of your pods by running this command:
kubectl get pods

You should see two pods with Running status. Your deployment manifest requests 2 replicas, so both should be up.

  • Get the external IP address of your service by running this command:
kubectl get service bookcatalog-service

What should I see?

Look at the EXTERNAL-IP column. It may show <pending> for a minute or two while Azure provisions the load balancer. Re-run the command until you see a real IP address.

  • Record the external IP address from the output: your external IP address

Before you open the URL, what do you expect to see in the browser? Think about what your bookcatalog Spring Boot app serves on its default route.

  • Open your browser and navigate to:
http://[[EXTERNAL_IP="your external IP address"]]

You should see your bookcatalog Spring Boot app running live on AKS. This is your app, deployed automatically through a pipeline you built from scratch.

Page not loading?

  • Make sure you are using http:// (not https://). Your service does not have TLS configured.
  • If EXTERNAL-IP still shows <pending>, wait another minute and re-run kubectl get service bookcatalog-service.
  • Verify your pods are still in Running status with kubectl get pods. If a pod restarted, check its logs with kubectl logs <pod-name>.

My bookcatalog app is not loading in the browser even though pods are running.

Your full CI/CD pipeline is working end to end. Every time you push code to Azure Repos, your pipeline automatically builds the Docker image, pushes it to ACR, and deploys it to AKS. Next up, there is an optional Secret Mission where you can take this pipeline further.

Secret mission

Add a Staging Environment with Manual Approval

In production teams, code never goes straight to production. Extend your pipeline with a staging namespace, a manual approval gate, and a three-stage workflow so every change is verified before it reaches users.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses Azure resources covered by the $200 free credit for 30 days, but your AKS cluster will consume that credit while running.

Cost warning

Your 1-node AKS cluster costs roughly $2-4 per day against your Azure free credit. If you leave it running and do not delete it, you will exhaust your $200 credit within weeks.

Choose Pause or Delete below if you are not actively working on this project.

Resources you used:

  • Azure Resource Group bookcatalog-rg containing your AKS cluster (bookcatalog-aks) and ACR instance (Basic SKU).
  • Azure DevOps project bookcatalog with pipelines, service connections, environments, and Azure Repos.
  • Local kubeconfig context for bookcatalog-aks.

Keep everything running

No action needed. Choose this if you are still actively building on this project.

  • Your AKS cluster continues to consume your Azure free credit at roughly $2-4 per day.
  • Your ACR instance (Basic SKU) incurs a small ongoing storage charge against your free credit.
  • Your Azure DevOps project remains accessible at no additional cost on the free tier.

Pause - I'll come back to this later

You cannot fully pause AKS, but you can scale your node pool to zero nodes. This stops the compute charges while keeping your cluster configuration intact.

  • Scale your AKS node pool to 0 nodes by running this command:
az aks nodepool scale --resource-group bookcatalog-rg --cluster-name bookcatalog-aks --name nodepool1 --node-count 0
  • Your ACR instance and Azure DevOps project remain in place at minimal or no cost.
  • When you are ready to resume, scale back to 1 node by running:
az aks nodepool scale --resource-group bookcatalog-rg --cluster-name bookcatalog-aks --name nodepool1 --node-count 1

Delete - I don't want to use this again

Remove all project resources and start fresh. This is permanent.

  • Delete the Azure resource group (this removes your AKS cluster, ACR instance, and everything inside it) by running:
az group delete --name bookcatalog-rg --yes --no-wait
  • Delete your Azure DevOps project by navigating to your project in dev.azure.com.
  • Click Project Settings (gear icon at the bottom left).
  • Scroll to the bottom of the Overview page and click Delete.
  • Type the project name bookcatalog to confirm, then click Delete.
  • Remove the local kubeconfig context by running:
kubectl config delete-context bookcatalog-aks

Nice Work!

Nice Work!

Nice work! You just built a production-style CI/CD pipeline in Azure DevOps that takes your Spring Boot bookcatalog app from code commit to live deployment on Azure Kubernetes Service.

You've learned how to:

  • Create a multi-stage YAML pipeline with Docker@2 and KubernetesManifest@1 tasks that automatically builds Docker images, pushes them to Azure Container Registry, and deploys to AKS on every commit.
  • Diagnose and fix the classic ErrImagePull failure by attaching ACR to AKS, understanding how container registry authentication works between Azure services.
  • Push your Spring Boot app to Azure Repos and access it live on AKS via a public LoadBalancer IP that anyone can visit.
  • Secret Mission: Added a staging environment with a manual approval gate before production deployment.

Ready to quiz yourself?