【GitHub】新GitHub Actionsを使ってAWS ECRにイメージをプッシュするCIを作ってみた

# 前置き
GitHub Actionsが刷新されてHCLからYAMLでCIを作成できるようになりました。
前まではAction毎にDockerfileを必ず用意する必要がありましたが、Ubuntu・Windows・MacOS上でCIを回すことが可能となり、Dockerfileのメンテナンスが要らなくなりました。<- まじ感謝:pray:

では、新しくなったGitHub Actionsを使用してどのようにCIを作ったか説明していきたいと思います。

# 概要
今回作成してみたCIは、Pythonで開発したアプリケーションをDockerizeしてAWS ECRへプッシュするというものです。
Pushする前にソースコードのLintチェック、テストの実行、Trivyを使用した脆弱性診断、Dockleを用いたセキュリテイ診断を並列に実施します。並列に実行できるJobは２０個までとなっているので、今回の規模であれば余裕で並列実行できます。
Jobを並列実行する際に注意すべきは、Job毎に仮想インスタンスが異なるのでファイルシステム上で情報が共有されないことです。情報を共有したい場合は同一のJobで全てのStepを実行する必要があります。そのため、脆弱性診断・セキュリティ診断・ECRへPushする際にそれぞれでDockerイメージをビルドしています。まぁ、トレードオフですね。
オンラインでCIを回す時に気をつけたいのが、クレデンシャルデータの扱いです。GitHubではSecretsという機能があり、データを暗号化して保存してくれます。GitHub Actionsでも、Secretsに設定されている値にアクセスすることができるので、YAMLにクレデンシャルデータを直接書かなくて済みます。

# 処理の流れ

![github_actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173064/20abe30d-b1a5-214b-c7f8-31330c168c01.png)


# ソースコード
サクッと、完成形だけ知りたい方のためにソースコードを書いておきます。
※[追記] trivyのリポジトリが[aquasecurity/trivy](https://github.com/aquasecurity/trivy)に変更しました

```..github/workflows/main.yml
on:
  push:
    branches:
      - master
      - develop
name: Build and Push Image to ECR
jobs:
  lint:
    name: Lint check
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@master
      - name: Set up Python 3.7
        uses: actions/setup-python@v1
        with:
          version: '3.7'
          architecture: 'x64'
      - name: Install dependencies
        run: |
          pip install --upgrade pip pipenv flake8
          pipenv install --system
      - name: Lint check with flake8
        run: flake8 src tests

  test:
    name: Python Test
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@master
      - name: Set up Python 3.7
        uses: actions/setup-python@v1
        with:
          version: '3.7'
          architecture: 'x64'
      - name: Install dependencies
        run: |
          pip install --upgrade pip pipenv pytest
          pipenv install --system
      - name: Test with pytest
        run: python -m pytest -v

  trivy:
    name: Trivy Scan Vulnerability
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@master
      - name: Build docker image
        run:
          docker build -t ${IMAGE_NAME} . --file Dockerfile
        env:
          IMAGE_NAME: app
      - name: Install trivy
        run: |
          sudo apt-get install apt-transport-https gnupg
          wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
          echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -cs) main | sudo tee -a /etc/apt/sources.list.d/trivy.list
          sudo apt-get update
          sudo apt-get install trivy
      - name: Vulnerability Scan with Trivy
        run: trivy --only-update alpine -q --severity HIGH,CRITICAL --exit-code 1 ${IMAGE_NAME}
        env:
          IMAGE_NAME: app

  dockle:
    name: Dockle
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@master
      - name: Build docker image
        run: docker build -t ${IMAGE_NAME} . --file Dockerfile
        env:
          IMAGE_NAME: app
      - name: Install dockle
        run: |
          VERSION=$(curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | \
          grep '"tag_name":' | \
          sed -E 's/.*"v([^"]+)".*/\1/' \
          )
          curl -L -o dockle.deb https://github.com/goodwithtech/dockle/releases/download/v${VERSION}/dockle_${VERSION}_Linux-64bit.deb
          sudo dpkg -i dockle.deb
          rm dockle.deb
      - name: Check image with dockle
        run: dockle ${IMAGE_NAME}
        env:
          IMAGE_NAME: app

  build:
    name: Build and Push Docker Image
    runs-on: ubuntu-18.04
    needs: [lint, test, trivy, dockle]
    steps:
      - uses: actions/checkout@master
      - name: Setup Python 3.7 for awscli
        uses: actions/setup-python@v1
        with:
          version: '3.7'
          architecture: 'x64'
      - name: Install awscli
        run: pip install --upgrade pip awscli
      - name: Login to ECR
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: $(aws ecr get-login --no-include-email --region ap-northeast-1)
      - name: Build docker image
        env:
          CONTAINER_REGISTRY_PATH: ${{ secrets.CONTAINER_REGISTRY_PATH }}
          IMAGE_NAME: app
        run: |
          TAG=`echo $GITHUB_REF | sed 's/refs\/heads\///g'`
          docker build -t ${CONTAINER_REGISTRY_PATH}/${IMAGE_NAME}:${TAG} .
      - name: Notify push start to Slack
        run: curl -X POST -H 'Content-type:application/json' --data '{"text":"'"${GITHUB_REPOSITORY}"'\n【START】Push Image to ECR!!"}' ${{ secrets.SLACK_WEBHOOK }}
      - name: Push image to ECR
        env:
          CONTAINER_REGISTRY_PATH: ${{ secrets.CONTAINER_REGISTRY_PATH }}
          IMAGE_NAME: app
        run: |
          TAG=`echo $GITHUB_REF | sed 's/refs\/heads\///g'`
          docker push ${CONTAINER_REGISTRY_PATH}/${IMAGE_NAME}:${TAG}
      - name: Notify push finish to Slack
        run: curl -X POST -H 'Content-type:application/json' --data '{"text":"'"${GITHUB_REPOSITORY}"'\n【FINISH】Pushed Image to ECR!!"}' ${{ secrets.SLACK_WEBHOOK }}
```

# 解説
ソースコードに書かれていることを分割して解説していきます。

## イベントトリガー

```yaml
on:
  push:
    branches:
      - master
      - develop
```
`on`によってトリガーとなるイベントの設定をします。
設定できるイベントはpush、issues、pull_requestなどがあります。全てのイベントを知りたい方は[Events that trigger workflows - GitHub Help](https://help.github.com/en/articles/events-that-trigger-workflows)を参照してください。
ここではpushをトリガーとして、かつmasterとdevelopブランチのみを対象としています。イベント対象を制限するためにbranches以外にtagsとpathsがあります。tagsは名前の通りですが、pathsは対象となるファイルやディレクトリを指定します。

## Jobs

```yaml
jobs:
  lint:
    name: Lint check
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@master
```

`jobs`で実行したいCIのフローを宣言します。複数のjobがある場合はデフォルトで並列実行されます。<-すごい（小並感）
シーケンシャルにjobを実行したい場合は`needs`を使います。これによって、指定されたjobが完了するまで待ちます。
`lint`はjobのIDです。jobsにおいて一意である必要がありますが、割り当てるIDはお好みでOKです。
`runs-on`で仮想インスタンスを指定します。指定できるOSはUbuntu、MacOS、Windowsです。また、バージョンの指定もできます。
これでjobが実行できる環境が整ったので、`stpes`で実際の処理内容を宣言します。

## Steps

```yaml
steps:
  - uses: actions/checkout@master
  - name: Set up Python 3.7
    uses: actions/setup-python@v1
    with:
      version: '3.7'
      architecture: 'x64'
  - name: Install dependencies
    run: |
      pip install --upgrade pip pipenv flake8
      pipenv install --system
  - name: Lint check with flake8
    run: flake8 src tests
```

`steps`では処理内容を記述していきます。全てのsteps内の処理は同一仮想インスタンス上で実行されます。情報共有したい場合は同一steps内に処理を書きましょう。
`uses`は利用したいアクションを指定します。同一リポジトリにあるアクション、パブリックリポジトリのアクション、公開されているDockerイメージを指定することができます。stepsの一番最初に宣言しているアクション`actions/checkout@master`は、裏では`git fetch`と`git checkout $GITHUB_SHA`と同様のことを実行しています。つまり、イベントトリガーとなったコミットへチェックアウトしてくれます。このアクションは必ず指定しましょう。ちなみにactions/setup-python@v1は裏ではTypeScriptが動いています。
`with`はアクションの入力値を指定します。何を指定可能かはアクションごとのYAMLに書かれているので、そちらを確認してください。
実行したいコマンドは`run`で指定します。実行したいコマンドが長くなりすぎる場合は可読性が落ちますのでアクションを作成することをお勧めします。

## 環境変数

```yaml
env:
  IMAGE_NAME: app
```

環境変数は`env`で指定します。「environment」とフルで書く必要がないので楽です。

## Secrets

```
${{ secrets.AWS_ACCESS_KEY_ID }}
```

Secretsに設定した値にアクセスしたい場合は、上記のように書きます。secretsの後に取得したい値のキー(AWS_ACCESS_KEY_ID)を指定します。

# 余談
余談ですが、GitHub Actionsの実行画面がかなりリッチになりました。

<img width="1271" alt="スクリーンショット 2019-08-15 17.09.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173064/477260cc-a6ec-f88c-e8cf-0c359aa63e09.png">


# おわりに
GitHub ActionsでCIを書いてみましたが、他のCIツールと遜色がないぐらいのクオリティになったと感じました。ただ、CIの結果を通知する手段がメールしかサポートされていないのは痛いです。今回のCIでは、とりあえずcurlでSlackへ通知する処理を組み込みましたが、アクションを作った方がいいと思いました。

本記事が、これからGitHub ActionsでCIを書こうと思っている方の参考になれば幸いです:smile:

# Reference
- [About GitHub Actions](https://help.github.com/en/articles/about-github-actions)
- [Workflow syntax for GitHub Actions](https://help.github.com/en/articles/workflow-syntax-for-github-actions)
- [Virtual environments for GitHub Actions](https://help.github.com/en/articles/virtual-environments-for-github-actions#filesystems-on-github-hosted-machines)