Web Development and the Cloud

blog

Connecting GitHub Actions with AWS S3 using CloudFormation

GitHub Actions allows the user to create workflows that can automate tasks that can be triggered on certain actions, such as pull requests to specific branches. Typically a workflow will run tests on a PR before presenting it for merge approval. Another use case, and one that will be discussed here, is using a GitHub Actions workflow to build the static assets for a React project and syncing the resulting build artifacts to an S3 bucket.

For the purpose of this tutorial we will make the following assumptions:

Connecting GitHub with AWS

GitHub Actions needs permissions to run aws cli commands. Previously this was done by creating a new AWS IAM User with the required permissions, generating AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY credentials and storing those in GitHub Actions Secrets. This type of authentication is no longer recommended as a best practice. This is because these credentials are what can be considered as long lived credentials. Long-lived credentials should be avoided since they are more difficult to keep track of, rotate, and overall present a higher security risk.

Instead, it is best practice to create IAM Roles for specific applications/services, and then let these applications/services assume the Roles when required.

GitHub and AWS can authenticate with each other through the use of OIDC Authentication protocol. To achieve this, we will use AWS to create an IODCProvider, whose job will be to authenticate with GitHub Actions and provide it with an IAM Role. GitHub Actions will then Assume this Role when making aws cli calls on our behalf.

To clarify, we will need to create 2 resources on AWS’s side:

IAM Roles have two parts: Permissions and Trust Policies. The permission policy assigns to the role permissions for AWS resources, while the Trust Policy simply indicated who can use (assume) this role.

The OIDCProvider, the GitHub access Role and its Trust and Permission policies can all be created using the CloudFormation template below:

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  WwwBucketName:
    Type: String

Resources:
  GitHubOIDCProvider:
    Type: "AWS::IAM::OIDCProvider"
    Properties:
      ClientIdList:
        - "sts.amazonaws.com"
      ThumbprintList:
        - "6938fd4d98bab03faadb97b34396831e3780aea1"
      Url: https://token.actions.githubusercontent.com

  GitHubActionsRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: "GitHubActionsRole"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Federated: !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com"
            Action: "sts:AssumeRoleWithWebIdentity"
            Condition:
              StringEquals:
                {
                  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
                  "token.actions.githubusercontent.com:sub": "repo:myorg/my-repo:ref:refs/heads/prod"
                }
  SyncToS3BucketPermission:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: SyncToS3BucketPolicy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - "s3:PutObject"
              - "s3:DeleteObject"
              - "s3:ListBucket"
            Resource:
              - !Sub "arn:aws:s3:::${WwwBucketName}"
              - !Sub "arn:aws:s3:::${WwwBucketName}/*"
      Roles:
        - !Ref GitHubActionsRole

This CF template will create an OIDCProvider, a GitHubActionsRole with the required s3 sync permissions, and a trust policy to only allow GitHub to assume this role.

Please note that the condition:

StringEquals: { ? ...
      "token.actions.githubusercontent.com:sub"
    : "repo:myorg/my-repo:ref:refs/heads/prod" }

is required to make sure that only pushes to our repo and the prod branch get authorized.

Creating a GitHub Actions Workflow

Once we can establish authenticated GitHub => AWS connections, we are ready to write our GitHub Actions Workflow.

Workflows are made by creating a .github/workflow directory and placing in it workflow yaml files. It is possible to have many simultaneous workflows running in parallel.

For this tutorial we will create a workflow file called frontend-deploy.yml and place it in .github/workflow

# Build and Deploy the frontend assets to AWS S3

name: Deploy frontend

on:
  push:
    branches: [prod]

env:
  AWS_REGION: "us-east-1"
  BUCKET: "my-website"
  MY_AWS_ACCOUNT: "111111111111"

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-22.04

    steps:
      - uses: actions/checkout@v3

      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18.x"

      - name: Install npm packages
        run: |
          cd my-react-project
          npm i

      - name: Build prod
        run: |
          cd my-react-project
          npm run build

      - name: configure aws credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: "arn:aws:iam::$MY_AWS_ACCOUNT:role/GitHubActionsRole"
          role-session-name: GitHub_OIDC
          aws-region: $

      - name: sync frontend website
        run: |
          aws s3 sync my-react-project/build/ s3://$/

This workflow has a single job called deploy with many steps required to build the react project, setup the aws credentials, and sync the project with the S3 bucket.

The section:

on:
  push:
    branches: [prod]

is required to make sure that this workflow only triggers on pushes to the prod branch.

Finally, the part below is how GitHub sends auth data to AWS:

permissions:
  id-token: write
  contents: read

Stay up to date

Get notified when I publish something new, and unsubscribe at any time.