Bootstrap the AWS CDK with GitHub Actions

Time to read: 11 minutes

In this guide I will talk about getting started with the aws-cdk while following security best practices.

You will need:

  • A github repository
  • An aws account
  • NodeJS 14

Securing Deployments

Open ID Connect can be used to link GitHub Actions to your target AWS account, without sharing long lived credentials.

I explain it below in this diagram:

  1. Open ID Connect provider is added as an identity provider to IAM in your account.
  2. When we wish to deploy to AWS we exchange the GitHub JWT for temporary AWS credentials.

This is more secure that the common approach of storing a super credential in the GitHub Secrets per environment.

Background Reading

There are a couple of guides on this that I have read, you don't need to read these, use them as references.

These should be considered as fairly raw indigestible information.

I will simplify this here and show how to set it up easily with the AWS-CDK.

Setup an AWS CDK V2 Project

Practical Guide using Infrastructure as Code, lets begin with a new cdk v2 app.

mkdir bootstrap

npx [email protected] init app --language typescript

If you are just getting started with the AWS-CDK you will need to bootstrap it.

yarn cdk bootstrap

The CDK role and te CDK custom resources should be setup by this deployment.

Add Open ID Connect (OIDC)

You can configure the OIDC provider in AWS CDK but I have explained this in a set of snippets that you need to place in bootstrap/lib/<your-stack>.ts

/**
 * Create an Identity provider for GitHub inside your AWS Account. This
 * allows GitHub to present itself to AWS IAM and assume a role.
 */
const provider = new OpenIdConnectProvider(this, 'MyProvider', {
  url: 'https://token.actions.githubusercontent.com',
  clientIds: ['sts.amazonaws.com'],
});

Then establish the trust relationship by defining the conditions for this provider to act as a principal.

I will provide an example that assumes you are https://github.com/simonireilly and you want to deploy from all repository branches of a repo called awesome-project

const githubOrganisation = "simonireilly"
// Change this to the repo you want to push code from
const repoName = "awesome-project"
/**
 * Create a principal for the OpenID; which can allow it to assume
 * deployment roles.
 */
const GitHubPrincipal = new OpenIdConnectPrincipal(provider).withConditions(
  {
    StringLike: {
      'token.actions.githubusercontent.com:sub':
        `repo:${githubOrganisation}/${repoName}:*`,
    },
  }
);

Finally you want to establish the role that can be assumed by the OIDC principal. This will allow GitHub actions to use the AWS Roles, and mutate the AWS Resources you give it access to.

/**
  * Create a deployment role that has short lived credentials. The only
  * principal that can assume this role is the GitHub Open ID provider.
  *
  * This role is granted authority to assume aws cdk roles; which are created
  * by the aws cdk v2.
  */
new Role(this, 'GitHubActionsRole', {
  assumedBy: GitHubPrincipal,
  description:
    'Role assumed by GitHubPrincipal for deploying from CI using aws cdk',
  roleName: 'github-ci-role',
  maxSessionDuration: Duration.hours(1),
  inlinePolicies: {
    CdkDeploymentPolicy: new PolicyDocument({
      assignSids: true,
      statements: [
        new PolicyStatement({
          effect: Effect.ALLOW,
          actions: ['sts:AssumeRole'],
          resources: [`arn:aws:iam::${this.account}:role/cdk-*`],
        }),
      ],
    }),
  },
});

Deploying Bootstrap

In CI/CD we want the bootstrapping process to be more secure, because it can create a trust relationship.

So for this I create a .github/workflows/bootstrap.yml which requires privileged account credentials to perform the deployment.

name: Bootstrap
on:
  workflow_dispatch:
    inputs:
      AWS_ACCESS_KEY_ID:
        description: "Access Key ID with Permissions to deploy IAM, and OIDC"
        required: true
      AWS_SECRET_ACCESS_KEY:
        description: "Secret Access Key with Permissions to deploy IAM, and OIDC"
        required: true
      AWS_REGION:
        description: "Region to deploy to."
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Git clone the repository
        uses: actions/[email protected]

      - name: Configure aws credentials
        uses: aws-actions/[email protected]
        with:
          aws-access-key-id: ${{ github.event.inputs.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ github.event.inputs.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ github.event.inputs.AWS_REGION }}

      - uses: actions/[email protected]
        with:
          node-version: "14"

      - run: yarn install

      - name: Synth stack
        run: yarn --cwd packages/bootstrap cdk synth

      - name: Deploy stack
        run: yarn --cwd packages/bootstrap cdk deploy --require-approval never

This can now be run by someone in your org, that has higher level access.

Post Bootstrap life

With this stack deployed you can now ship any aws-cdk v2 deployments from the trusted repository, to the linked AWS account, without storing long lived credentials.

All you need to do is instruct GitHUb actions to assume the github-ci-role role in your account, and it will get temporary credentials for one hour.

  deploy-infrastructure:
    runs-on: ubuntu-latest
    steps:
      - name: Git clone the repository
        uses: actions/[email protected]

      - name: Assume role using OIDC
        uses: aws-actions/[email protected]
        with:
          role-to-assume: arn:aws:iam::<your-account-id-here>:role/github-ci-role
          aws-region: ${{ env.AWS_REGION }}

      - uses: actions/[email protected]
        with:
          node-version: "14"

      - run: yarn install

      - name: Synth infrastructure stack
        run: yarn --cwd packages/infrastructure cdk synth

      - name: Deploy infrastructure stack
        run: yarn --cwd packages/infrastructure cdk deploy --require-approval never

Next steps might to be:

  • Create a series of roles for different accounts with trust relationships like:
    • Only trust main branch for your production account with condition:
      {
        StringLike: {
          'token.actions.githubusercontent.com:sub':
            `repo:${githubOrganisation}/${repository}:ref:/refs/head/main`,
        },
      }

Follow up

If you are interested in this stuff, you might like microteams!

A guide I am writing for scale ups, that are growing from one person AWS start-ups to multi-team organisations.