Exercise - Deploy a multi-container solution to a Kubernetes cluster

Completed

The release pipeline provided with your project is designed to build the solution as a Docker container and deploy it to Azure App Service. To support the deployment of multiple containers to a Kubernetes cluster, you need to modify this pipeline.

In this unit, you'll learn how to:

  • Update the pipeline to trigger on a commit to the main branch.
  • Define variables to be shared across the pipeline.
  • Build and publish Docker images.
  • Publish Kubernetes manifests.
  • Add a task to create an image pull secret for use between your Kubernetes and container registry instances.
  • Deploy updated images to a Kubernetes cluster.

Update the pipeline to support triggers

  1. Sign in to your Azure DevOps organization, and then navigate to your project.

  2. Select Pipelines, and then select your pipeline.

  3. Select Edit to edit your azure-pipelines.yml.

    Andy: This was the build stage we had in place for the previous single-container solution. I knew it wasn't going to run properly, so I disabled it. We can start off by re-enabling triggers on commits to the main branch.

  4. Replace the existing trigger line at the top of the file with the following snippet. This triggers a pipeline run every time a commit is made to the main branch.

    trigger:
    - 'main'
    

Define variables accessible across pipeline

Andy: We're going to need to add two pipeline variables. One for specifying the name of the leaderboard repository, which is leaderboard. The other is for the name of the image pull secret used for sharing between AKS and ACR instances during deployment.

  1. Add the following highlighted code to the variables section.

    variables:
      buildConfiguration: 'Release'
      leaderboardRepository: 'leaderboard'
      webRepository: 'web'
      tag: '$(Build.BuildId)'
      imagePullSecret: 'secret'
    

Build and publish Docker image to Azure Container Registry

Andy: We already have a task for building the web app as a Docker container, which we publish to our container registry. We can just use a second task to do the same for our leaderboard.

  1. Add a second Docker@2 task to build and publish the leaderboard container using the following highlighted snippet. Add this task right after the web container task.

    - task: Docker@2
      displayName: 'Build and push the web image to container registry'
      inputs:
        command: buildAndPush
        buildContext: $(Build.Repository.LocalPath)
        repository: $(webRepository)
        dockerfile: '$(Build.SourcesDirectory)/Tailspin.SpaceGame.Web/Dockerfile'
        containerRegistry: 'Container Registry Connection'
        tags: |
          $(tag)
    
    - task: Docker@2
      displayName: 'Build and push the leaderboard image to container registry'
      inputs:
        command: buildAndPush
        buildContext: $(Build.Repository.LocalPath)
        repository: $(leaderboardRepository)
        dockerfile: '$(Build.SourcesDirectory)/Tailspin.SpaceGame.LeaderboardContainer/Dockerfile'
        containerRegistry: 'Container Registry Connection'
        tags: |
          $(tag)
    

Tip

Make sure that the task you add here uses consistent indentation with the previous task as whitespace is important in a YAML file.

Publish the Kubernetes manifests

Andy: I think we can move on to the next stage. Do you see anything missing?

Mara: You mentioned that there were some manifest files in the source project that define the deployment, and services Kubernetes needs when we deploy. We should publish those before we finish this stage.

Andy: Do we need to? Won't they still be on the local disk?

Mara: They would be if we were adding the deployment tasks within the same stage as the build. However, since our deployment tasks happen in their own Deploy stage, it runs on a fresh environment, probably even on a different agent. We should be sure to publish anything this stage produces that the other stage needs.

Andy: That's a great point. Is it easy to do? We just need to ensure the manifests folder is copied to the new agent.

Mara: That's what the PublishBuildArtifacts@1 task is for. It's so common that there's even a shorthand for it, publish.

  1. Add a publish task that stores the manifests folder for a future stage as shown in the following code snippet. Make sure that the indentation of this task matches that of the previous task.

    - task: Docker@2
      displayName: 'Build and push the leaderboard image to container registry'
      inputs:
        command: buildAndPush
        buildContext: $(Build.Repository.LocalPath)
        repository: $(leaderboardRepository)
        dockerfile: '$(Build.SourcesDirectory)/Tailspin.SpaceGame.LeaderboardContainer/Dockerfile'
        containerRegistry: 'Container Registry Connection'
        tags: |
          $(tag)
    
    - publish: '$(Build.SourcesDirectory)/manifests'
      artifact: manifests
    

Replace the deploy stage

Mara: I'm going to replace our existing Deploy stage with one that uses a deployment job. A deployment job is a special kind of job that allows us to associate our deployment with the Azure DevOps environment created earlier. This makes it easier to track deployment history, which will be especially useful as our solutions get more sophisticated.

  1. Remove the existing Deploy stage (everything after the build stage) and replace it with the following snippet. Take note of the highlighted line that indicates the deployment environment to be utilized.

    - stage: 'Deploy'
      displayName: 'Deploy the containers'
      dependsOn: Build
      jobs:
      - deployment: Deploy
        displayName: Deploy
        pool:
          vmImage: 'ubuntu-20.04'
        environment: 'Dev'
        variables:
        - group: Release
        strategy:
          runOnce:
            deploy:
              steps:
    

    Mara: The first step we'll add in the deployment stage is to download the manifest artifacts published earlier using the DownloadBuildArtifacts@0 task.

    Andy: Let me guess, is there a download shorthand for that task?

    Mara: Exactly correct! We can use the current specifier to indicate that we want the artifact from the current run of the pipeline.

  2. Add the highlighted lines as the first step of the Deploy stage.

    - stage: 'Deploy'
      displayName: 'Deploy the containers'
      dependsOn: Build
      jobs:
      - deployment: Deploy
        displayName: Deploy
        pool:
          vmImage: 'ubuntu-20.04'
        environment: 'spike.default'
        variables:
        - group: Release
        strategy:
          runOnce:
            deploy:
              steps:
              - download: current
                artifact: manifests
    

    Andy: Now we need to create an image pull secret that will be shared between our ACR and AKS instances. Do you know if there's a task we can use?

    Mara: I was just looking that up, and we're in luck. The KubernetesManifest@0 task supports an action to create the secret needed.

Kubernetes manifest task

The Kubernetes manifest task is designed to manage all of the mainstream deployment operations required for Kubernetes. It supports multiple action options that range from creating secrets to deploying images. In this case, the createSecret action is used, along with the following parameters:

  • action indicates the feature to run. In this case, createSecret creates the shared secret.
  • connectionType specifies the type of service connection to use. Options: azureResourceManager or kubernetesServiceConnection.
  • secretName specifies the name of the secret to create.
  • dockerRegistryEndpoint specifies the name of the Azure Container Registry Services connection.
  • azureSubscriptionConnection specifies the name of the ARM Services connection.
  • azureResourceGroup specifies the name of your resource group.
  • kubernetesCluster specifies the name of your AKS cluster.
  • namespace specifies the Kubernetes namespace this action applies to.
  1. Add the following snippet to the end of your pipeline. Make sure that both the resource group name and the cluster name match the names of the ones you created earlier. Ensure that the indentation of this task matches that of the download task.

    - task: KubernetesManifest@1
      displayName: Create imagePullSecret
      inputs:
        action: createSecret
        connectionType: azureResourceManager
        secretName: $(imagePullSecret)
        dockerRegistryEndpoint: 'Container Registry Connection'
        azureSubscriptionConnection: 'Kubernetes Cluster Connection'
        azureResourceGroup: 'tailspin-space-game-rg'
        kubernetesCluster: 'tailspinspacegame-24591'
        namespace: 'default'
    

    Andy: The final step is to trigger the deployment of our images to the Kubernetes cluster. Based on the documentation, it looks like we can use the same task but with a different action and parameters.

    • action indicates the feature to run. In this case, deploy to deploy to AKS cluster.
    • connectionType specifies the type of service connection to use. Options: azureResourceManager or kubernetesServiceConnection.
    • azureSubscriptionConnection specifies the name of the ARM Services connection.
    • azureResourceGroup specifies the name of your resource group.
    • kubernetesCluster specifies the name of your AKS cluster.
    • namespace specifies the Kubernetes namespace this action applies to.
    • imagePullSecrets specifies the list of secrets needed to pull from the container registry.
    • containers specifies the list of container images to deploy.
  2. Add the following snippet to the end of the pipeline. Make sure that both the resource group name and the cluster name match the names of the ones you created earlier. Ensure that the indentation of this task matches that of the previous task.

    - task: KubernetesManifest@1
      displayName: Deploy to Kubernetes cluster
      inputs:
        action: deploy
        connectionType: azureResourceManager
        azureSubscriptionConnection: 'Kubernetes Cluster Connection'
        azureResourceGroup: 'tailspin-space-game-rg'
        kubernetesCluster: 'tailspinspacegame-24591'
        namespace: 'default'
        manifests: |
          $(Pipeline.Workspace)/manifests/deployment.yml
          $(Pipeline.Workspace)/manifests/service.yml
        imagePullSecrets: |
          $(imagePullSecret)
        containers: |
          $(RegistryName)/$(webRepository):$(tag)
          $(RegistryName)/$(leaderboardRepository):$(tag)
    

Run your pipeline

  1. Select Save from the upper-right corner of the page. Select Save to confirm your commit message.

  2. Select Run, confirm your branch name and then select Run to trigger a pipeline run.

  3. Select Pipelines, and then select your pipeline to view the logs as your pipeline runs.

  4. After the pipeline run is complete, select Environments from the left pane, and then select the Dev environment to view your deployment jobs.

  5. Now let's go check out our deployed web app and API endpoint. To do so, we need to get the external IP addresses for both the web and leaderboard services.

  6. Navigate to Azure portal, select your AKS cluster, and then select Services and ingresses.

    Screenshot of how to find the external IPs for your web and leaderboard services.

  7. Select the External IP for your web service to view your site on AKS.

    Screenshot of the Space Game web site.

  8. Go back to your Azure portal window where you left off and then copy the External IP for your leaderboard service. This IP address is where the leaderboard API is publicly hosted.

  9. Replace the placeholder in the following link with the external IP you copied. You can also add a pageSize=10 query parameter to make it easier to view the JSON response in your browser. Use a URL like the following one in a new browser tab.

    http://[IP]/api/Leaderboard?pageSize=10
    
  10. You can view the raw JSON response from the leaderboard API hosted in your AKS cluster. You now have a REST API that you can call from other applications.

    Screenshot of a web browser showing the JSON response from the leaderboard service.

Andy: This turned out great! I think using Kubernetes would be a great way for us to adopt a broader microservices strategy.