GitHub Actions

Hi Folks,

Lately, I was experimenting with GitHub Actions (GHA), as it has been a buzzword since General Availability (GA), but I didn’t take the 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 at the following path: .github/workflows with YML file(s) in your GitHub repository.

First example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 the master branch, but it could be a schedule, a pull_request, or an issue creation, or many other events.

More information about the GHA Workflow syntax.

1
2
3
4
5
6
name: "Terraform GitHub Actions"

on:
  push:
    branches:
      - master

Environment variables

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

1
2
3
4
5
6
7
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

In this part, we set the name of our job: terraform, and configure it to run on GitHub virtual-environments using ubuntu-latest.

1
2
3
4
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.

1
2
3
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 in my IAM Role policy and in the trust policy.

1
2
3
4
5
6
7
8
9
- 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.

1
2
- 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 on AWS ECR.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
- 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.

1
2
3
4
5
6
7
8
- 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

1
2
3
4
5
6
7
- 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

1
2
3
4
5
6
7
8
9
- 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 an if conditional statement (only done when there are changes to deploy).

1
2
3
4
5
6
7
8
9
- 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’m voluntarily skipping the first part: name, job definition, env vars, checkout, and AWS credentials, as it’s the same as the previous 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.

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

Build

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

1
2
3
4
- 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 a carriage return, otherwise it fails.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- 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, Email CI/CD notifications
    3. Labeler: https://github.com/actions/labeler
    4. etc.
  3. You can run simple recurring/one-time batch scripts with the 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 filtering capabilities.
  4. Missing global secrets/environment variables: I want to set some secrets or envs at the GitHub account level, not at the repository level.

Advanced use cases

Advanced

That’s all folks!

zoph.