In this chapter, we will learn how to use GitHub Action to build and deploy our application to Azure. It’s often overlooked by developers that CI/CD usually have a power to access any systems in the enterprise, and it becomes a big liability which requires close attention. Zero trust security for CI/CD pipelines and infrastructure becomes one of the most important topics in DevOps these days. Zero trust security for CI/CD pipelines encompasses a number of techniques. For example, authenticating with your cloud provider via federated identity mechanisms like OIDC instead of using credentials, using the least privilege by minimizing the access of individual user or runner accounts, using self-hosted runners in an ephemeral way instead of reusing them, etc. In this chapter, we will implement some of those techniques to secure our CI/CD pipeline.

1. Accessing Azure in GitHub Actions without credentials using OIDC

Federated identity credentials are a new type of credential that enables workload identity federation for software workloads. Workload identity federation allows you to access Azure Active Directory (Azure AD) protected resources without needing to manage secrets. Details are at https://learn.microsoft.com/en-us/graph/api/resources/federatedidentitycredentials-overview?view=graph-rest-1.0

1.1. Create a Service Principal to access Azure

Create a new service principal to set up with federated identity. We will give Contributor access for the demo, but you should use least privilege principle to minimize the access in real world.

$ az ad sp create-for-rbac \
  --name github-action-sp-fi \
  --scope /subscriptions/[SUBSCRIPTION_ID]/resourceGroups/[RESOURCE_GROUP_NAME] \
  --role Contributor

1.2. Set up Federated Identity Credentials for the Service Principal

Now, Go to Azure portal → Azure AD → App registrations. Look up your service principal created by the command above, and click on Certificates & secrets → Federated credentials. + Add credential.

fi 1

Click on + Add credential. Federated credentials support various scenarios like CMK, Kubernetes, GitHub Actions, etc. If you have ever used AKS workload identity, you must have seen this.

fi 2

Chose GitHub Actions deploying Azure resources . Setup requires a bit of information regarding the GitHub repository where you run your Actions. The entity type controls which situation to issue the OIDC token, for example, pull request, specific tag, specific branch, etc. In my setup below, the OIDC token will be issued inside the GitHub Action run by main branch then used by azure/login@v1.

Challenge 1 : Modify the GitHub Actions workflow to run command az group list successfully. You can refer to https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-azure

2. Creating Container Image and Pushing to Azure Container Registry(ACR) using GitHub Actions

Creating a container image normally requires writing a Dockerfile which could be cumbersome job for the most of the developers. In order to simplify this process, we will be using Cloud Native Buildpack. Cloud Native Buildpack is a CNCF project, and it’s a part of buildpacks.io project. It’s a new way of building container images and it’s getting more and more popular these days. In this chapter, we will first create an image using Cloud Native Buildpack(CNB) and then push the container image to ACR.

2.1. Creating Container Image with CNB

Here is the basic steps to create a container image using CNB.

    steps:
      - uses: actions/checkout@v3

      - name: 'Az CLI Login'
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.CLIENT_ID }}
          tenant-id: ${{ secrets.TENANT_ID }}
          subscription-id: ${{ secrets.SUBSCRIPTION_ID }}

      - name: Install pack CLIs including pack and yq
        uses: buildpacks/github-actions/setup-pack@v5.0.0
        with:
          pack-version: '0.29.0'

      - name: Pack build
        run: |
          pack build ${ACR_URL}/${IMAGE_NAME} --builder paketobuildpacks/builder:base --buildpack paketo-buildpacks/java-azure --env BP_JVM_VERSION=17 (1)
1 ACR_URL is URL of the Azure Container Registry.(ex: https://acrjay.azurecr.io) IMAGE_NAME is the name and tag of the image to be created. (ex: cicd-java:0.0.1)

2.2. Pushing Container Image to ACR

In order to push the created container image, we need to login to the Azure Container Registry. One thing to remember is that authentication to container registry is always HTTP Basic auth which requires username and password. Azure provides more secure way by generating short-lived token for the authentication. Here is the steps to generate a token and authenticate against ACR.

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

permissions:
  id-token: write
  contents: read

jobs:
 ...
  container:
    name: Build container with CNB and push to ACR
    needs: scan
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: 'Az CLI Login'
      uses: azure/login@v1
      with:
        client-id: ${{ secrets.CLIENT_ID }}
        tenant-id: ${{ secrets.TENANT_ID }}
        subscription-id: ${{ secrets.SUBSCRIPTION_ID }}

    - name: ACR Login with AZ CLI
      run: |
        ACR_JSON=$(az acr login --name [ACR_NAME] --expose-token) (1)
        TOKEN=$(echo $ACR_JSON | jq -r .accessToken)
        LOGINSERVER=$(echo $ACR_JSON | jq -r .loginServer)

        docker login ${LOGINSERVER} --username 00000000-0000-0000-0000-000000000000 --password-stdin <<< $TOKEN (2)
1 az acr login --name [ACR_NAME] --expose-token generates short-lived token for specified ACR.
2 docker login command uses the token generated by az acr login command to authenticate against ACR. Note that username is pre-defined as 00000000-0000-0000-0000-000000000000.

One last remaining step at this moment is to push container image to ACR which can be easily done by docker push command.

Challenge 2 : Create one more step to push docker image to ACR. CNB has an option, --publish which can be used to push the image to the registry. You can refer to https://buildpacks.io/docs/app-developer-guide/publish-applications/ Choose one of the option and complete the image pushing.

Congratulations!! You have completed the third challenge!!

3. Complete Example

Here is the complete example of the GitHub Actions workflow implementing the above steps.

name: CI/CD Spring Boot to Azure Kubernetes Service

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

permissions:
  id-token: write
  contents: read

jobs:
  test:
    name: Unit Test and SpotBugs
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'microsoft'
          cache: 'maven'
      - name: Build with Maven
        run: mvn -B clean package site
      - name: Upload SBOM(Cyclonedx)
        uses: actions/upload-artifact@v3
        with:
          name: bom.json
          path: './target/bom.json'

  scan:
    name: Scan dependencies with Trivy
    needs: test
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Install latest Trivy CLI
        run: |
          wget https://github.com/aquasecurity/trivy/releases/download/v0.41.0/trivy_0.41.0_Linux-64bit.deb
          sudo dpkg -i trivy_0.41.0_Linux-64bit.deb
      - uses: actions/download-artifact@v3
        with:
          name: bom.json
      - name: Run Trivy with SBOM
        run: trivy sbom ./bom.json

  container:
    name: Build container with CNB and push to ACR
    needs: scan
    runs-on: ubuntu-latest

    outputs:
      LOGINSERVER: ${{ steps.image.outputs.LOGINSERVER }}
      IMAGE: ${{ steps.versioning.outputs.IMAGE }}

    steps:
      - uses: actions/checkout@v3

      - name: 'Az CLI Login'
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.CLIENT_ID }}
          tenant-id: ${{ secrets.TENANT_ID }}
          subscription-id: ${{ secrets.SUBSCRIPTION_ID }}

      - name: ACR Login with AZ CLI
        id: image
        run: |
          ACR_JSON=$(az acr login --name acrjay --expose-token)
          TOKEN=$(echo $ACR_JSON | jq -r .accessToken)
          LOGINSERVER=$(echo $ACR_JSON | jq -r .loginServer)
          echo "LOGINSERVER=$LOGINSERVER" >> $GITHUB_ENV
          echo "LOGINSERVER=$LOGINSERVER" >> $GITHUB_OUTPUT

          docker login ${LOGINSERVER} --username 00000000-0000-0000-0000-000000000000 --password-stdin <<< $TOKEN

      - name: Install pack CLIs including pack and yq
        uses: buildpacks/github-actions/setup-pack@v5.0.0
        with:
          pack-version: '0.29.0'

      - name: Set the image name and version
        id: versioning
        run: |
          VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
          REPO_NAME=${{ github.event.repository.name }}
          echo "IMAGE=$REPO_NAME:$VERSION" >> $GITHUB_ENV
          echo "IMAGE=$REPO_NAME:$VERSION" >> $GITHUB_OUTPUT

      - name: Pack build
        run: |
          pack build ${LOGINSERVER}/${IMAGE} --builder paketobuildpacks/builder:base --buildpack paketo-buildpacks/java-azure --env BP_JVM_VERSION=17 --publish