GitHub Actions with AWS: Hands-On

GitHub Actions

Hi Folks,

Lately, I was experimenting with GitHub Actions (GHA), as it is a buzzword since General Availability (GA), but I didn’t take time to try it before. I’ve done it for you folks. 🙌

Context

GHA was released on GA in November 2019, the main features are:

  1. Automate development workflows (CI/CD): build, test, deploy
  2. Hosted runners / self-hosted runners
  3. Automate the management of your GH Community: PR, Code Reviews, or Issue Tracking
  4. Built-in secrets store

Build

My Objectives

  • Replace my manually (Makefile) deployed pet projects.
    • Use built-in CI/CD capabilities of GitHub.
    • Use AWS IAM Assume Role.
    • Cover CloudFormation SAM and Terraform Infrastructure As Code (IaC) use-cases.

Getting Started

First of all, you will need to create your workflow in the following path : .github/workflows with YML file(s) in your GitHub repository.

First example

name: Greetings

on: [pull_request, issues]

jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/first-interaction@v1
      with:
        repo-token: ${{ secrets.GITHUB_TOKEN }}
        issue-message: 'Message that will be displayed on users'' first issue'
        pr-message: 'Message that will be displayed on users'' first pr'

Now, let’s focus on my use-case using SAM or Terraform

AWS Credentials

One major step is to authenticate to your Cloud Service Provider (CSP), in my case: AWS. For security purposes, I’m using Assume Role to get least privilege authorizations on my AWS Account.

Fortunately, AWS is sharing a well-maintained GHA to get credentials tokens in your GHA Jobs.

Using Terraform

In this Terraform example, I’m using GHA to replace my manually deployed (Makefile) MAMIP bot.

Name and Event Trigger

In this section you will define the name of the GHA Workflow and the event that will trigger this workflow, in this example, it’s the git push on master branch, but it could be a schedule, a pull_request, or an issue creation or many many other

More information about the GHA Workflow syntax

name: 'Terraform GitHub Actions'

on:
  push:
    branches:
    - master

Environment variables

Optionally, you can set environment variables for your next following steps.

env:
  tf_version: 'latest'
  tf_working_dir: './automation/tf-fargate'
  env: 'dev'
  project: 'mamip'
  aws_region: 'eu-west-1'
  artifacts_bucket: 'mamip-artifacts'

Job definition

On this part, we set the name of our job: terraform and setup to runs-on GitHub virtual-environments using ubuntu-latest.

jobs:
  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest

Checkout of Git repository

With this common GHA, we are doing a git checkout of the entire git repository.

    steps:
      - name: 'Checkout'
        uses: actions/checkout@master

Get AWS Credentials

To follow the least privilege mantra, I’m using a simple assume role and not directly IAM User credentials. This is possible thanks to this official AWS GHA: configure-aws-credentials.

Nb: AWS IAM action sts:TagSession was missing on my IAM Role policy and in the trust policy

      - name: AWS IAM Assume Role
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.REGION }}
          role-to-assume: ${{ secrets.ROLE_TO_ASSUME }}
          role-duration-seconds: 1200
          role-session-name: GH-Actions-${{ env.project }}

As you can see, the majority of arguments will retrieve secrets from the GitHub Action secrets store. (In the settings of your GitHub repository)

Run standard shell command

In this step, I’m running basically AWS CLI commands.

      - name: Update runbook artifact
        run: aws s3 cp ./automation/runbook.sh 's3://${{ env.project }}-artifacts/'

Use standard GitHub Actions for ECR

Using the official AWS ECR Actions, you will be able to login, build, push & logout your Docker images to AWS ECR.

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

      - name: Build, tag, and push image to Amazon ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: mamip-ecr-dev
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t mamip-image ./automation/
          docker tag mamip-image $ECR_REGISTRY/$ECR_REPOSITORY
          docker push $ECR_REGISTRY/$ECR_REPOSITORY

      - name: Logout of Amazon ECR
        if: always()
        run: docker logout ${{ steps.login-ecr.outputs.registry }}

Terraform Init

To properly use Terraform commands, I’m using this official GHA from Hashicorp.

Using args: you can pass arguments at the end of init command.

      - name: 'Terraform Init'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.tf_version }}
          tf_actions_subcommand: 'init'
          tf_actions_working_dir: ${{ env.tf_working_dir }}
          tf_actions_comment: false
          args: '-backend-config="bucket=${{ secrets.TF_STATE_S3_BUCKET }}" -backend-config="key=mamip/terraform.tfstate"'

Terraform Validate

      - name: 'Terraform Validate'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.tf_version }}
          tf_actions_subcommand: 'validate'
          tf_actions_working_dir: ${{ env.tf_working_dir }}
          tf_actions_comment: false

Terraform Plan

      - name: 'Terraform Plan'
        id: plan
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.tf_version }}
          tf_actions_subcommand: 'plan'
          tf_actions_working_dir: ${{ env.tf_working_dir }}
          tf_actions_comment: false
          args: '-var="env=dev" -var="artifacts_bucket=${{ env.artifacts_bucket }}"'

Terraform Apply

For the apply step, I’m using if conditional statement (only done when there is changes to deploy)

      - name: 'Terraform Apply'
        if: steps.plan.outputs.tf_actions_plan_has_changes == 'true'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.tf_version }}
          tf_actions_subcommand: 'apply'
          tf_actions_working_dir: ${{ env.tf_working_dir }}
          tf_actions_comment: false
          args: '-var="env=dev"'

The final result is available here

The overall maturity of the official Hashicorp GitHub Actions is pretty nice, with great documentation and examples. 👏

Using CloudFormation (SAM)

I voluntary skipping the first part: name, job definition, env vars, checkout, and AWS credentials as it’s the same way as before with Terraform example.

Test your credential

I’m using this simple command, equivalent to whoami on AWS CLI to check which IAM Role I’m using.

      - name: Test credentials - whoami
        run: |
          aws sts get-caller-identity

Build

For my sam use-case, I’m using this Action found on Marketplace from TractorZoom, as AWS does not provide any equivalent official GHA yet.

      - name: sam build
        uses: TractorZoom/sam-cli-action@master
        with:
          sam_command: build -b ./build

Deploy

In the deploy step, I need to pass some args, some need to be set at the end without carriage return otherwise it’s failing.

      - name: sam deploy
        uses: TractorZoom/sam-cli-action@master
        with:
          sam_command: |
            deploy \
              --template-file build/template.yaml \
              --stack-name ${{ secrets.PROJECT }}-${{ secrets.ENV }} \
              --s3-bucket ${{ secrets.PROJECT }}-artifacts \
              --region ${{ secrets.REGION }} \
              --no-fail-on-empty-changeset \
              --parameter-overrides ENV=${{ secrets.ENV }} MONITORINGBUCKET=${{ secrets.MONITORINGBUCKET }} S3PREFIX=${{ secrets.S3PREFIX }} PROJECT=${{ secrets.PROJECT }} RECIPIENTS=${{ secrets.RECIPIENTS }} SENDER=${{ secrets.SENDER }} AWSREGION=${{ secrets.REGION }} --capabilities CAPABILITY_IAM

💵 How much ?

GHA minutes are free for public repositories. With a maximum of 20 concurrent jobs for the free tier.

For GitHub private repository:

  • Free for 2,000 minutes per month (nearly 1 hour per day)
  • After free tier: $0.008 per minute (Linux - 2 cores, 7GB)

Conclusion

✅ Good Points

  1. Large community; lots of examples available; See inspiration on many .github/workflow folders on public repositories.
  2. Nearly infinite possibilities (2938 actions available on GH Marketplace),
    1. Caching Artifacts
    2. Slack, Discord, Telegram, Emails CI/CD Notifications
    3. Labeler : https://github.com/actions/labeler
  3. You can run simple recurring/on-time batch scripts with on-schedule event.

❌ Improvement Points

  1. Sometimes, error messages are awful:

    Technical information (for administrator):
    SQL Server Error: 4011
    
  2. Lack of official GitHub Actions from different vendors

  3. Too many duplicated Actions on Marketplace, can’t sort by star ⭐️ - we need more filters capabilities

  4. Missing global secrets/environment variables: I want to set some of secrets or envs at Github Account Level, not at repository level.

Advanced use-cases

Advanced

That’s all folks!

zoph.


comments powered by Disqus