CDK and GitHub Actions

Using the AWS Cloud Development Kit for building and deploying workloads in AWS has been a positive developer experience for the most part. No tool is perfect, but there is plenty to be excited about with this approach to provisioning infrastructure.

Whenever I consider automating a task, I find it helpful to first go through the steps manually in the console to ensure I haven't missed any pieces in the process; then write the code to replicate those steps. I wanted to share a minimal configuration for setting up GitHub Actions for your CDK project.

Getting to a point where there are zero manual steps is the gold standard in automation; however, there are often chicken-and-egg type problems where a circular dependency must be broken up by performing a manual step of some kind. In order to use CDK in an account, we must bootstrap it beforehand to build the necessary resources that support the functionality of CDK. Bootstrap resources in an account include the following:

  1. An S3 bucket to store project files and assets. For instance, if you create a lambda function in your project this is where the zip archive is stored.
  2. An ECR repository to store Docker images that your project creates.
  3. Several IAM roles configured to grant permissions for performing CloudFormation deployments.

The formal definition for these resources is outlined in this CloudFormation template. When executed in your account the output in the console is a stack named CDKToolkit.

Bootstrapping your account

The following sections will go through a series of one-time steps required for enabling CDK deployments via GitHub Actions. To get started we will use the CDK CLI. Fill in the appropriate region and profile information for your setup.

# install cdk packages
npm install -g aws-cdk
# set appropriate variables, assumes aws cli is installed and named profiles are specified in your config file
PROFILE=development
REGION=us-east-1
ACCOUNT=$(aws sts get-caller-identity --profile ${PROFILE} --query Account --output text)
# run bootstrap command
cdk bootstrap aws://${ACCOUNT}/${REGION} --profile ${PROFILE}

The benefit of this approach is that any updates to the bootstrap template by the CDK team can be applied by repeating these steps in your account. It is also safe to run these steps multiple times in an account without there being any side effects. The downside of this approach is that it doesn't grant the minimum set of privileges required to perform deployments in your account. For instance, the default execution policy for CDK deployments in your account is arn:aws:iam::aws:policy/AdministratorAccess with this approach. We will improve upon this in later posts.

My account is bootstrapped, now what?

The first order of business is authenticating our GitHub repository with AWS. To do so, GitHub workflows will obtain a short-lived access token from AWS via OpenID Connect (OIDC). Take a moment to understand this diagram as well as the contents supplied within the OIDC token created by GitHub's OIDC provider. Within this token there are three noteworthy key/value pairings, referred to as claims.

  1. Audience, which uses the key aud in the token. When using the official action for configuring AWS credentials this value needs to be set to sts.amazonaws.com.
  2. Issuer, which uses the key iss in the token. This is the URL of GitHub's OIDC provider as they are the entity issuing the token. The value will always be https://token.actions.githubusercontent.com. IAM uses its library of trusted CAs to authenticate tokens issued by this URL.
  3. Subject, which uses the key sub in the token. This value is validated by IAM and needs to be designed with care to ensure that only workflows originating from your repositories can access your AWS account(s).

The first step is to create an Identity Provider within IAM. Rather than do these actions in the console let's use CDK express our dependencies.

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as iam from "aws-cdk-lib/aws-iam";

export interface ActionStackProps extends cdk.StackProps {
    subjectClaim: string;
}

export class ActionStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: ActionStackProps) {    
    super(scope, id, props);
    
    const oidcProvider = new iam.OpenIdConnectProvider(
      this,
      "GithubOidcProvider",
      {
        url: "https://token.actions.githubusercontent.com",
        clientIds: ["sts.amazonaws.com"],
      }
    );
  }
}

The above code establishes GitHub as a trusted Identity Provider in IAM. When GitHub authenticates with AWS, the workflow needs to assume a role in your account that grants it the necessary permissions for CDK deployments.

This requires the creation of a role. We can achieve this by adding the following code to our Stack.

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as iam from "aws-cdk-lib/aws-iam";

export interface ActionStackProps extends cdk.StackProps {
    subjectClaim: string;
}

export class ActionStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: ActionStackProps) {    
    super(scope, id, props);
    
    const oidcProvider = new iam.OpenIdConnectProvider(
      this,
      "github-oidc-provider",
      {
        url: "https://token.actions.githubusercontent.com",
        clientIds: ["sts.amazonaws.com"],
      }
    );

    const gitHubActionsRole = new iam.Role(this, "github-actions-role", {
      assumedBy: new iam.WebIdentityPrincipal(
        oidcProvider.openIdConnectProviderArn,
        {
          StringEquals: {
            "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
            "token.actions.githubusercontent.com:sub": props.subjectClaim,
          },
        }
      ),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"),
      ],
      roleName: "github-actions",
    });
  }
}

With a role created the only configuration item remaining for you to determine is the value of the subject claim to filter on. At the moment, I'm personally using the "repo:GitHubOrg/GitHubRepo:environment:EnvName" pattern in conjunction with GitHub environments and protected deployment branches. I highly recommend checking out the official AWS documentation for guidance. The docs emphasize the subject claim and the potential security implications of misconfiguration with the below warning.

If you do not limit the condition key token.actions.githubusercontent.com:sub to a specific organization or repository, then GitHub Actions from organizations or repositories outside of your control are able to assume roles associated with the GitHub IAM IdP in your AWS account.

You can deploy your stack by running cdk deploy from the command line specifying the same profile you bootstrapped your account with. With these resources in place we can now start building workflows in our GitHub repositories to facilitate automated deployments. That will be the focus of the next post, until then, stay soapy.