Own Your Dependencies: Customizing GitHub Actions with Container Images

Published on July 17, 2024 | Written by Andreas

Own your dependencies. Do you have full control over all the tools used to build and ship your software? A CI/CD pipeline relies on many tools. In my projects, that’s Node.js, npm, eslint, OpenTofu, tflint, and AWS CLI, for example. It’s crucial, to take full control over those dependencies. Don’t rely on pre-installed software on a GitHub-hosted or self-hosted runner, there is a high risk that changes to the pre-installed tools will break your jobs. Also, do not use GitHub Actions to install dependencies, there is a high risk for malicious code being injected into your deployment pipeline.

Build a container image including all the tools needed to build and ship a project. Then, configure your GitHub workflow to run the jobs in a container from the image.

Own Your Dependencies: Customizing GitHub Actions with Container Images

In the following you will learn how to use GitHub Actions to build a customized container image with all the tools required for your CI/CD jobs pre-installed. After that, you will learn how to configure your GitHub workflow to use the customized image for executing jobs.

The example assumes, that you installed HyperEnv before, as GitHub-hosted runners do not support fetching container images from ECR.

Building a container image for CI/CD

First, create a Dockerfile to install all tools needed to build and ship your project.

For example, the following Dockerfile uses Amazon Linux 2023 as the base image and installs Node.js, OpenTofu, and the AWS CLI.

FROM amazonlinux:2023
RUN curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
RUN yum install nodejs -y
RUN yum update -y
RUN yum install -y awscli
RUN if [ "$(uname -m)" == "x86_64" ]; then CPU_ARCH="amd64"; else CPU_ARCH="arm64"; fi && \
    cd /tmp && \
    curl -L -o tofu.zip "https://github.com/opentofu/opentofu/releases/download/v1.6.2/tofu_1.6.2_linux_${CPU_ARCH}.zip" && unzip tofu.zip && chmod +x tofu && mv tofu /usr/local/bin/ && rm -f *

Next, create a private ECR repository named cicd-tools.

Then, create a GitHub repository and add the Dockerfile as well as the following GitHub Workflow .github/workflows/build-cicd-image.yml. Don’t forget to replace <AWS_ACCOUNT_ID> and <AWS_REGION> with your AWS account ID and region.

  1. Checkout the repository, to fetch the Dockerfile.
  2. Assume an IAM role with write-access to the ECR repository.
  3. Login to ECR.
  4. Enable Docker multi-arch.
  5. Build a multi-arch image by using the Dockerfile and push it to ECR.

Why building a multi-arch image? Doing so allows you to run your jobs on runners executed on AMD64 or ARM64 machines.

The following example uses an OpenID Connect to authenticate with AWS. Check out our CloudFormation template to enable OpenID Connect for GitHub in your AWS account as well.

---
name: build-cicd-image

on:
  push:
    tags: ['*']

permissions:
  id-token: write
  contents: read

jobs:
  build:
    runs-on: ['hyperenv']
    steps:
    - uses: actions/checkout@v3

    - name: 'Assuming IAM role'
      uses: aws-actions/configure-aws-credentials@v1
      with:
        role-to-assume: arn:aws:iam::<AWS_ACCOUNT_ID>:role/github-oidc
        role-session-name: deploy
        aws-region: <AWS_REGION>

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1

    - name: 'Enable Multi Arch'
      run: docker buildx create --use

    - name: 'Build container image'
      run: docker buildx build --platform linux/arm64,linux/amd64 --push -t <AWS_ACCOUNT_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/cicd-tools:${{github.ref_name}} .
      working-directory: build-cicd-image

The GitHub workflow will build and push a new version of the CI/CD container image to ECR whenever you push a new tag to the GitHub repository.

Executing CI/CD jobs by using customized container image

Now modify your exsisting GitHub workflow. For each job, define the container (see Running jobs in a container) that should be used to execute each step.

The following listing demonstrates how to execute tofu apply by using a customized container image. Don’t forget to replace <AWS_ACCOUNT_ID> and <AWS_REGION> with your AWS account ID and region.

---
name: build-deploy

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ['hyperenv']
    container:
      image: <AWS_ACCOUNT_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/cicd-tools:main
    concurrency:
      group: 'build-deploy'
      cancel-in-progress: false

    steps:

    - uses: actions/checkout@v3
    
    - name: 'Assuming IAM role'
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::<AWS_ACCOUNT_ID>:role/github-oidc
        role-session-name: build-deploy
        aws-region: us-east-1

    - name: 'Deploy'
      run: 'tofu apply'
      working-directory: build-cicd-image

Do you want to dive deeper? Check out the full code example on GitHub [widdix/hyperenv-examples)(https://github.com/widdix/hyperenv-examples).

Summary

In conclusion, owning your dependencies through custom container images in GitHub Actions offers a powerful way to enhance the reliability and security of your CI/CD pipeline. By building and using your own containers with pre-installed tools, you gain full control over your build and deployment environment, reducing risks associated with external dependencies. This approach not only streamlines your workflow but also ensures consistency across different runners and architectures. Remember, in the world of CI/CD, independence and control over your tools are key to maintaining a robust and efficient development process.