capsule.adrianhesketh.com

home

Github Actions CI/CD for Go AWS CDK projects

To get CI/CD working, there's a few moving parts to take care of.

Accessing private repositories

There's an extra complication if you're using private Go modules in your project.

For example, if you've got a private repo in your Github organisation (org) that contains the `github.com/org/library` package, and you want to use it from the `github.com/org/api` repo, you have to give Github Actions permission to access that other repository.

At the time of writing, the only way to do this is via a Personal Access Token, which gives anyone with that token access to repositories within any organisation that the user belongs to. Personal Access Tokens are "personal" to a specific Github user, so if I create one and use it for a client, then I leave the organisation, it would be rescinded. They also give access to all of the organisations that you're a member of, which can be a security problem.

To avoid this I recommend that you:

Github Actions - setting up a Docker image

Github Actions can run on various virtual machine configurations, but you can also run your commands inside a Docker container on that machine.

I think this is the best way, because you can ensure that the Docker container contains everything you need to build the software, and you have full control over the versions of dependencies you use.

It's relatively easy to make a Docker image and push it to Github's built-in Docker registry. To reduce problems around authentication against the registry, I try to keep build containers public and open source.

To build up the container that all the CI/CD actions will run in:

Creating a Go/CDK Dockerfile

With Go CDK, we need both Go and Node.js. Since Node is more time consuming to setup, I've based the Docker image on `node:latest`, installed the AWS CDK, then installed Go and update the environment to include the `go` executables on the path.

FROM node:latest

# Install CDK.
RUN npm install -g aws-cdk

# Install Go.
RUN curl -L -o go1.16.6.linux-amd64.tar.gz https://golang.org/dl/go1.16.6.linux-amd64.tar.gz 
RUN rm -rf /usr/local/go && tar -C /usr/local -xzf go1.16.6.linux-amd64.tar.gz
ENV PATH "$PATH:/usr/local/go/bin"

Publishing the image to Github's registry

I setup `.github/workflows/deploy.yaml` to build the Dockerfile and push it to the registry whenever the `main` branch is updated. Even though there's a reference to `secrets.GITHUB_TOKEN`, you don't actually have to create that, it's built-in [0].

[0]
name: Create and publish Docker image

on:
  push:
    branches: ['main']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Log in to the Container registry
        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push Docker image
        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

One thing to note here is that the tag applied to the Docker image is `main` based on the branch name rather than `latest`.

The Dockerfile above is at https://github.com/a-h/aws-go-cdk-action [1]. You can see information about the Docker image at [2]

[1]
[2]

The image is pulled from `ghcr.io/a-h/aws-go-cdk-action:main`

docker pull ghcr.io/a-h/aws-go-cdk-action:main

Using the Docker image

Once you have your build Docker image built and pushed you can set up your CDK project's CI/CD pipeline.

From your CDK project, you'll need to set up a `.github/workflows/deploy.yaml` file to get Github Actions to run the workflow.

Make sure you've added the appropriate secrets:

I'll take the file section by section.

First, the name of the Workflow, and when to trigger. In this case, every push to main will run the workflow.

name: Deploy

on:
  push:
    branches:
      - main

Configure a single job called `Test and deploy`. It runs on a `ubuntu-latest` VM, but inside my `aws-go-cdk-action` container.

jobs:
  deploy:
    runs-on: ubuntu-latest
    container: ghcr.io/a-h/aws-go-cdk-action:main
    name: Test and deploy

I use a lot of DynamoDB in my projects, so they usually contain integration tests that use DynamoDB local for testing. This section runs a DynamoDB local container with the DNS name `dynamodb`, and opens up port 8000 in the container.

    services:
      dynamodb:
        image: amazon/dynamodb-local
        ports:
          - 8000:8000

The job has a number of steps to execute. The first step is to get the code.

    steps:
      - name: Checkout
        uses: actions/checkout@v2

Next, cache Go modules.

      - uses: actions/cache@v2
        with:
          path: |
            ~/.cache/go-build
            ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-

Pull the modules by running `make get` (I usually put all my CI commands into a Makefile in the root of the project).

The `TOKEN` and the `git config` command are only required if you need to access private Go modules in Github repositories. The command sets up `git` to use the `CI_USER_PERSONAL_ACCESS_TOKEN` to use specific credentials to access Github repositories, enabling the pipeline to access private Go modules.

Inside the `make get` command, `go env -w GOPRIVATE=github.com/your-organisation-name` is also set to bypass public Go module proxies.

There's some useful information on Go and Github actions over at [3]

[3]
      - name: Get modules
        env:
          TOKEN: ${{ secrets.CI_USER_PERSONAL_ACCESS_TOKEN }}
        run: |
          git config --global url."https://my-ci-user:${TOKEN}@github.com".insteadOf "https://github.com" 
          make get

The tests access the `dynamodb` container and customise the DynamoDB endpoint to point at the local DynamoDB if the the `DYNAMODB_ENDPOINT` environment variable is set.

      - name: Test
        env:
          DYNAMODB_ENDPOINT: http://dynamodb:8000
        run: make test

If the tests pass, it's time to start the CDK deployment, so the AWS credentials need to be pulled from Github secrets and added to the environment.

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: eu-west-1

Finally, the deployment can be executed.

`make deploy` just runs `cdk deploy` in the appropriate directory. CDK is installed in the build container, so there's no special setup required at this point.

      - name: CDK deploy
        run: make deploy

The complete configuration

For ease of copy/paste, here's the full config:

name: Deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    container: ghcr.io/a-h/aws-go-cdk-action:main
    name: Test and deploy

    services:
      dynamodb:
        image: amazon/dynamodb-local
        ports:
          - 8000:8000

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - uses: actions/cache@v2
        with:
          path: |
            ~/.cache/go-build
            ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Get modules
        env:
          TOKEN: ${{ secrets.CI_USER_PERSONAL_ACCESS_TOKEN }}
        run: |
          git config --global url."https://my-ci-user:${TOKEN}@github.com".insteadOf "https://github.com" 
          make get

      - name: Test
        env:
          DYNAMODB_ENDPOINT: http://dynamodb:8000
        run: make test

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: eu-west-1

      - name: CDK deploy
        run: make deploy

Summary

There's a few things to get right, but using your own Docker image to run your build in simplifies everything a lot.

It's a bit of a pain to get private Go modules running due to Github not having a way to grant Github actions access to specific organisation repositories, but creating a specific user in your Github organisation can make sure you have control of the access, and that your builds don't break when someone leaves the organisation.

In a future post, I'll share some tips on CI/CD user permissions, and how to prevent a common privilege escalation attack vector that I see a lot in AWS CI/CD pipelines.

More

Next

Event Sourced DynamoDB with Go

Previous

Go CDK - building Go Lambda functions

Home

home