GitHub Environments

A few months ago, I gave a talk at an All Around Azure event about safely delivering change to the cloud. I also put together a demo that showcased:

  1. ChatOps with Microsoft Teams and GitHub integration
  2. GitHub Actions
  3. GitHub Environments
  4. Azure App Service Deployment Slots

I thought it was about time I wrote up how to connect all the moving parts, so you can try it out for yourself! Head over to the repo for all the code and steps to set up and run the demo. For this blog post, I’ll review the architecture and some of the steps I took to automate the provisioning of the infrastructure.

The goal of the demo is to show how a change can be made to an application, in a safe, repeatable way, with zero downtime. I also wanted to show how easy it is to integrate Microsoft Teams (or Slack) with GitHub for better visibility and communication.

Architecture

Azure Web App with two slots - Production and Staging

The application itself is a sample .NET app - Tailwind Traders. I’m utilising a pre-created backend that is managed by Microsoft, and the app itself will be hosted on a Web App for Containers (Linux) app service plan.

The web app has two deployment slots - one for staging, where I can deploy and observe the change, and one for production. When I’m happy with the change, I can initiate a swap slot, which is seamless to users of the website.

Azure Container Registry

I need to package my application and build it into a container image using Docker - so I need somewhere to host it - Azure Container Registry! As I will be pushing to the registry my newly created container image, I obviously need somewhere to securely store the password to the registry once it has been created.

Azure Key Vault to store secrets

Azure Key Vault is the place where I will put the secret - I can securely access it later during deployments.

Azure Bicep

Azure Bicep is used to declare what our resources should look like, including dependencies and configuration. I love Bicep! I’m still learning it, but I am finding it much easier to work with than native ARM. I actually got started by decompiling the original ARM template for this demo, and it got me a fair bit of the way there.

I did a lot of work with Terraform recently, and actually I see a lot of similarities - but the biggest difference is not having to worry about maintaining a state file somewhere.

I improved on the demo since April. For example - I wanted to automatically create a secret in Key Vault which would store the password for the newly created Container Registry. I did this with the listCredentials function:

resource kv_secret 'Microsoft.KeyVault/vaults/secrets@2016-10-01' = {
  name: '${keyvault_name_var}/${acr_password_var}'
  properties: {
    value: listCredentials(acr_name.id, acr_name.apiVersion).passwords[0].value
  }
  dependsOn: [
    acr_name
  ]
}

Teams for Collaboration

It is really very easy to set up integration between Teams and GitHub. In the demo, I used two prebuilt connectors:

  1. Incoming Webhook
  2. GitHub Enterprise

The first allows me to pass custom information from pipeline about deployments to my Teams channel with rich information about deployments using cards - with clickable links off to the newly created site! The second allows me to utilise GitHub webhooks for information like commits, pull requests, issues etc. Here is a screenshot of what those look like:

Teams

GitHub for source control and CI/CD

Tying everything together is GitHub! GitHub is used for:

  1. Hosting source code
  2. CI/CD with GitHub workflows and Actions
  3. Environment protection with GitHub Environments

GitHub Environments

GitHub Environments are a way to represent my Azure environments in GitHub. I can:

  1. Give them different approvers
  2. Limit what branches can be deployed to those environments
  3. Assign environment specific secrets only accessible by that environment

At a glance, you can see a history of deployments to an environment, and easily see the commit that is currently deployed to that environment:

Environment log

You can also see the status of deployments, as well as a url to go and visit!

Environments

Workflow

  1. A developer creates a feature branch off main to work on a change
  2. She pushes the changes to the remote feature branch and opens a pull request into main.
  3. The app is built and tested by a pipeline, and a notification is sent to Teams to let the team know that a PR has been opened and is ready for review.
  4. Once the pipeline is successful, the PR can be approved and merged to main.
  5. This will trigger another pipeline to package and deploy the application to the staging environment and eventually the production environment.

GitHub Actions - output variables

A key part of this workflow is that I wanted to automate the creation of the Azure resources, and have key information about those resources passed to the rest of the jobs in the pipeline.

Bicep has a handy output function which I used to return the names (in JSON formatted key-value pairs) of the Container Registry, Key Vault and Web App. Even if Bicep detects no changes to the underlying infrastructure - it will still return the resource names, which is great.

We can set variables and make them accessible to later jobs in the pipeline - this is particularly useful in our scenario, because we want to separate staging and production deployments by using environment deployments. A few key things are needed:

On the job that deploys the Azure resources, we need to define some outputs:

jobs:
  deployInfra:
    name: deployInfra
    runs-on: ubuntu-18.04
    outputs:
      web: ${{ steps.createInfra.outputs.web }}
      acr: ${{ steps.createInfra.outputs.acr }}
      kv: ${{ steps.createInfra.outputs.kv }}

We reference a step in this job called createInfra that will be outputting these variables:

- name: Azure CLI Action
    id: createInfra
    uses: Azure/cli@1.0.4
    with:
    inlineScript: |
        az group create -l westeurope -n TailwindTraders
        WEB=$(az deployment group create -g TailwindTraders -f ./Deploy/main.bicep)
        SITE=$(echo $WEB | jq -re .properties.outputs.web.value)
        ACR=$(echo $WEB | jq -re .properties.outputs.acr.value)
        KV=$(echo $WEB | jq -re .properties.outputs.kv.value)
        echo "::set-output name=web::$SITE"
        echo "::set-output name=acr::$ACR"
        echo "::set-output name=kv::$KV"

This step is running the Bicep file to deploy the resources, then it simply captures the name values of the three resources we are interested in as local variables. The last set of 3 commands is how we convert these local variables into output variables, for use downstream.

To use these in a subsequent job, we need to tell that subsequent job that it depends on the first. We do it like this, with a needs declaration:

  deployStaging:
    environment:
      name: Staging
      url: 'http://${{ needs.deployInfra.outputs.web }}-staging.azurewebsites.net'
    needs: deployInfra
    runs-on: ubuntu-18.04
    steps:

You will notice also that this is a special type of job - an environment deployment job. By declaring this the ‘Staging’ environment, we can now ensure that any deployments to this environment will follow any of the rules set up for that environment.

You can also define a url - and by using those output variables, we don’t need to hardcode it!

Environments

That’s it for the write up - head over to the repo to get started and try it out for yourself.

Note: You will need to use a public repo to access GitHub Environments, unless you are using GitHub Enterprise Cloud.


See Also

comments powered by Disqus