Continuous integration and deployment (CI/CD) is a key feature of rapid software development. That phrase was a mouthful, so let's digest its meaning one term at a time:

  • Continuous: the automation of some manual operation
  • Integration: the building and testing of new code
  • Deployment: the release of new code to production

That is, CI/CD tools focus on the automation of building, testing, and releasing new code to production. And while the CI/CD ecosystem is vast – full of exotic names like Bamboo, CircleCI, Jenkins, Spinnaker, and Travis CI – today's post will focus on the setup of GitHub Actions to connect code that is hosted on GitHub to applications that are hosted on AWS.

For this CI/CD pipeline, my preferred method of authentication is the OpenID Connect (OIDC) protocol. OIDC is built on top of the OAuth 2.0 framework, and it's more lightweight than SAML. Most importantly, it allows individuals to build GitHub Actions workflows that access AWS resources without storing any long-lived AWS credentials on GitHub.

Step 1: Create an AWS identity provider

In the AWS IAM menu, select "Identity providers" and add a new identity provider that uses the following settings:

  • Provider type: OpenID Connect
  • Provider url: https://token.actions.githubusercontent.com
  • Audience: sts.amazonaws.com

Select the "Get thumbprint" button, and then "Add provider."

The AWS IAM menu.
The AWS wizard for adding an identity provider.

Step 2: Create an AWS role

In the AWS IAM menu, select "Roles" and add a new role that uses the following settings for the trusted entity:

  • Trusted entity type: web identity
  • Identity provider: token.actions.githubusercontent.com (in dropdown menu)
  • Audience: sts.amazonaws.com (in dropdown menu)
Step 1 of the AWS wizard for creating a role.

Add policies that give the minimal necessary permissions that GitHub will need to test or deploy code on AWS (e.g. some permissions to CodeDeploy or EC2 services). Create the role.

Afterwards, navigate to the AWS page for the newly-created role and modify the trust relationship to restrict which GitHub repositories can use those policies. For example, a GitHub organization might add the following StringLike condition to the default OIDC trust relationship:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::<AWS_ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:<GITHUB_ORGANIZATION>/*"
                }
            }
        }
    ]
}

Save the role's ARN to use in the next step.

Step 3: Create a GitHub Actions workflow

Create a YAML file inside the .github/workflows directory of the GitHub repo. In the file, include a step to configure AWS credentials for the runner, and then add any desired AWS commands in the steps that follow. An example workflow file might look like this:

name: Run AWS command

env:
  AWS_REGION: <AWS_REGION>
  AWS_ROLE: <AWS_ROLE_ARN>

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1-node16
      with:
        role-to-assume: ${{ env.AWS_ROLE }}
        aws-region: ${{ env.AWS_REGION }}
    - name: Deploy via AWS
      run: <AWS_COMMAND>

If desired, the ARN for the AWS role can be kept secret by using a GitHub secret instead of the env variable above.

Step 4: Profit!

Push new code up to the remote repository and check out the Actions tab on GitHub to see the new workflow get kicked off!