【GithubActions】Dockerイメージの脆弱性を診断してECRへpushする

# 概要
GithubActionsのワークフローで、Dockerイメージのセキュリティチェックを行う設定をメモとして残しておきます。
設定全容は以下となります。

<details><summary>ソースコード全容</summary><div>

```yaml:ci.yaml
name: Check / Build / Push Docker Image

on:
  push:
    branches:
      - master
  pull_request:

jobs:
  scan:
    name: Scan Image By Azure/container-scan
    runs-on: ubuntu-18.04
    steps:
      - name: Checkout code
        id: checkout
        uses: actions/checkout@v2

      - name: Build image from Dockerfile
        id: build
        run: |
          TAG=test:ci
          docker build -f ${YOUR_DOCKER_FILE} -t ${TAG} .
          echo "::set-output name=tag::${TAG}"

      - name: Run Vulnerability/Security Scanner
        id: run-scanner
        uses: Azure/container-scan@v0
        with:
          image-name: ${{ steps.build.outputs.tag }}
  build:
    name: Build And Push Docker Image
    runs-on: ubuntu-18.04
    needs: [scan]
    if: github.event_name == 'push'

    steps:
      - name: Checkout code
        id: checkout
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        id: aws-cred
        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: ap-northeast-1

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

      - name: Build and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${YOUR_REPOSITORY}
          DOCKER_FILE: ${YOUR_DOCKERFILE}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -f ${DOCKER_FILE} -t ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG} .
          docker push ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}

      - name: Logout From Amazon ECR
        id: logout-ecr
        if: always()
        run: docker logout ${{ steps.login-ecr.outputs.registry }}
```
</div></details>

# pushする前にチェックすること
ここでチェックしたい内容は、コンテナイメージの脆弱性およびセキュリティ診断です。
脆弱性の診断には[Trivy](https://github.com/aquasecurity/trivy)を、セキュリティ診断には[Dockle](https://github.com/goodwithtech/dockle)をそれぞれツールとして利用します。
実際のところ、ユニットテストも通すでしょうが、本記事では省略します。


## β版（Pre-release）で構わない
という場合は、[Azure/container-scan](https://github.com/Azure/container-scan)を利用すると非常に簡単に設定することができます。
Azure/container-scanは内部的にTrivyとDockleを利用してコンテナイメージのチェックを行っており、今回チェックしたい内容と合致しています。
ただし、2020/08時点ではPre-release版のリリースタグが1つあるのみですので、実務でPre-release版の利用を禁止するなどのポリシーがある場合には利用することができません。その場合は後述の内容で設定します。

### 設定の一例

```yaml:ci.yaml
jobs:
  scan:
    name: Scan Image By Azure/container-scan
    runs-on: ubuntu-18.04
    steps:
      # 1
      - name: Checkout code
        id: checkout
        uses: actions/checkout@v2
      # 2
      - name: Build image from Dockerfile
        id: build
        run: |
          TAG=test:ci
          docker build -f ${YOUR_DOCKER_FILE} -t ${TAG} .
          echo "::set-output name=tag::${TAG}"
      # 3
      - name: Run Vulnerability/Security Scanner
        id: run-scanner
        uses: Azure/container-scan@v0
        with:
          image-name: ${{ steps.build.outputs.tag }}
```

### 解説

1. ソースコードのチェックアウトを実行
2. Dockerfileを指定して、イメージのビルドを実行<br>TAGの部分は適当な文字列で構いません。`echo "::set-output"`で、以降のstepで`outputs`として利用可能な変数の設定を行っています。（[こちらのページ](https://docs.github.com/ja/actions/reference/workflow-syntax-for-github-actions)の`jobs.<jobs_id>.outputs`を参照）
3. `Azure/container-scan`で脆弱性とセキュリティのチェックを行います。`image-name`にステップ2のビルド時に設定したイメージタグを指定します。そのほかの引数は任意設定で、以下が指定可能。

|変数|概要|デフォルト|
|:---|:---|:---|
|severity-threshold|脆弱性として検知するレベルの設定。UNKNOWN, LOW, MEDIUM, HIGH, CRITICALの5つを設定でき、設定したレベルとそれより高いレベルの脆弱性を報告する。例えば`MEDIUM`を設定した場合、MEDIUM, HIGH, CRITICALが対象となる。|HIGH
|run-quality-check|イメージがベストプラクティス/CIS標準に従っているかチェックする。具体的な内容は[こちら](https://github.com/goodwithtech/dockle/blob/master/CHECKPOINT.md)|true


### 実行結果
コンソールにも結果は出ていますが、下図のような形で結果報告用のジョブが作成されており、一覧で表示されます。詳細リンク等は貼られないので、そこは自身で調べ直す必要があります。

![スクリーンショット 2020-08-14 21.07.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552343/1267a601-2c9a-4269-2f2e-0644d0030411.png)


## β版（Pre-release）は避けたい
TrivyおよびDockleによるチェックステップをそれぞれ定義します。
`job`単位で分けても良いですが、今回示す例は同一`job`内で別`step`として定義します。

### 設定の一例

```yaml:ci.yaml
jobs:
  scan:
    name: Scan Image
    runs-on: ubuntu-18.04

    steps:
      # 1
      - name: Checkout code
        id: checkout
        uses: actions/checkout@v2
      # 2
      - name: Build image from Dockerfile
        id: build
        run: |
          TAG="${{ matrix.service }}:ci"
          docker build -f deployment/dockerfile/${{ matrix.service }}/Dockerfile -t ${TAG} .
          echo "::set-output name=tag::${TAG}"
      # 3-1
      - name: Install Trivy
        env:
          TRIVY_VERSION: 0.10.2
        run: |
          sudo apt-get install rpm
          wget https://github.com/aquasecurity/trivy/releases/download/v{TRIVY_VERSION}/trivy_{TRIVY_VERSION}_Linux-64bit.deb
          sudo dpkg -i trivy_{TRIVY_VERSION}_Linux-64bit.deb
      # 3-2
      - name: Run Vulnerability Scanner
        id: scan-vulnerability
        run: trivy image --severity "CRITICAL,HIGH" --exit-code 1 ${{ steps.build.outputs.tag }}
      # 4-1
      - name: Install Dockle
        env:
          DOCKLE_VERSION: 0.2.4
        run: |
          curl -L -o dockle.deb https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb
          sudo dpkg -i dockle.deb
          rm dockle.deb
      # 4-2
      - name: Run Security Scanner
        id: scan-security
        run: |
          dockle --exit-level warn --exit-code 1 ${{ steps.build.outputs.tag }}
```

### 解説

1. ソースコードのチェックアウトを実行
2. Dockerfileを指定して、イメージのビルドを実行<br>TAGの部分は適当な文字列で構いません。`echo "::set-output name=変数名::値"`で、以降のstepで`outputs`として利用可能な変数の設定を行っています。（[こちらのページ](https://docs.github.com/ja/actions/reference/workflow-syntax-for-github-actions)の`jobs.<jobs_id>.outputs`を参照）
3. `Trivy`のインストールおよび実行<br>3-1. `Trivy`をインストールします。[READMEのバージョン指定版](https://github.com/aquasecurity/trivy#debianubuntu)をコピペしていますが、`download/`のあとに`v`が必要でしたので付け足してあります。<br>3-2. trivyコマンドを実行します。<br>`--severity "HIGH,CRITICAL"`部分で、検知する脆弱性のレベルをHIGHとCIRITICALに限定し、<br>`--exit-code 1`部分で、脆弱性を検知したときにCIが失敗となるように設定しています。<br>その他のオプション詳細は[こちら](https://github.com/aquasecurity/trivy#image-1)を参照ください。
4. `Dockle`のインストールおよび実行<br>4-1. `Dockle`をインストールします。[README](https://github.com/goodwithtech/dockle#debianubuntu)の`VERSION`をワークフロー定義ファイル内で指定する形としています。<br>4-2. dockleコマンドを実行します。<br>`--exit-level warn --exit-code 1`部分で、`warn`レベル以上の問題を検知したときCIが失敗となるように設定しています。


なお、`Trivy`についてはEXPERIMENTALですがactions用の設定も用意されています。([ソース](https://github.com/aquasecurity/trivy-action))
こちらを利用した場合は、3を以下のように記載します。

```yaml:ci.yaml
      # GoのInstallが必要らしい
      - name: Setup Go
        id: setup
        uses: actions/setup-go@v1
        with:
          go-version: 1.14
      # 実行
      - name: Run Vulnerability Scanner
        id: scan-vulnerability
        uses: aquasecurity/trivy-action@0.0.8
        with:
          image-ref: ${{ steps.build.outputs.tag }}
          format: table
          exit-code: 1
          severity: "CRITICAL,HIGH"
```

# ECRへpush
GithubActionsでは`needs`を利用することでジョブ間の依存関係を定義することが可能です。
これを利用して、脆弱性診断で問題がなければイメージをプッシュするというワークフローを作成できます。

あらかじめ、対象リポジトリのシークレットにAWSへの認証情報を登録しておきます。

- `AWS_ACCESS_KEY_ID`：AWSのアクセスキーID
- `AWS_SECRET_ACCESS_KEY`：AWSのシークレットアクセスキー

### 設定の一例

```yaml:ci.yaml
jobs:
  build:
    name: Build And Push Docker Image
    runs-on: ubuntu-18.04
    # 1
    needs: [scan]
    if: github.event_name == 'push'

    steps:
      # 2
      - name: Checkout code
        id: checkout
        uses: actions/checkout@v2
      # 3
      - name: Configure AWS credentials
        id: aws-cred
        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: ap-northeast-1
      # 4
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
      # 5
      - name: Build and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${YOUR_REPOSITORY}
          DOCKER_FILE: ${YOUR_DOCKERFILE}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -f ${DOCKER_FILE} -t ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG} .
          docker push ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}
      # 6
      - name: Logout From Amazon ECR
        id: logout-ecr
        if: always()
        run: docker logout ${{ steps.login-ecr.outputs.registry }}
```

### 解説

1. `needs`でジョブ間の依存性を指定<br>今回は`scan`ジョブ、すなわち脆弱性チェックのジョブが正常に完了している場合に限り、本ジョブが稼働するような設定です。<br>また、`if`を用いることで`push`イベントのときだけこのジョブが動くように制限しています。これは好き好きですが、個人的には脆弱性チェックはプルリクエストで実行し、イメージプッシュは`develop`や`master`ブランチへのpushをトリガーにするイメージがあるためです。`if`で分けるのではなく、ファイルごと分けてしまうというのも一案かと思います。
2. ソースコードのチェックアウトを実行
3. AWSへのログインを行います。`SECRET`に登録しているキーはここで利用します。
4. ECRへのログインを行います。この`step`が必要な理由は、`push`する`step`にて対象リポジトリを指定する必要があるためです。`amazon-ecr-login`では、`outputs`の`registry`に設定されるためこれを利用していますが、対象のレジストリを取得できればこのactionを指定する必要性はありません。
5. イメージのbuildとpushを行います。ステップ3、4にて認証処理は済んでいるため、ここでは`build`→`push`を行うのみとなります。<br>`env`で環境変数にリポジトリやイメージタグの値を設定していますが、単に見やすいかなと思って設定しただけで、環境変数にする必然性はありません。`run`内のコマンド部分で直接指定で問題ないです。
6. ECRからログアウトします。`if: always()`を指定することでstep4が失敗した場合にも実行されるような設定となっています。

# Tips
## 特定の`Docker Image Checkpoints`を無視したい
例えば、`CIS-DI-0006`（Healthチェックを設定してね）というチェック内容がありますが、ヘルスチェックはk8sに任せるからDockerfileでは書かなくてよい、ということでこのチェック内容は無視したいというケースが発生したとします。
その場合は、リポジトリに`.dockleignore`というファイルを作成し内容にCheckpointsのIDを記載することでチェック対象から意図的に外すことができます。

```git:.dockleignore
# k8sで任せるので無視
CIS-DI-0006
```

## イメージタグにブランチ名またはタグ名を指定したい

上記例ではワークフローをトリガーしたコミットSHAを指定していましたが、タグ名としてブランチ名またはタグ名を指定したいことはあると思います。
`GithubActions`ではワークフロー実行元となったブランチやタグの名称をうまく取る手段は現状ないようですが、`GITHUB_REF`（`github.ref`）をうまく加工することで取得することができます。

```yaml:ci.yaml
- name: Get Version
  id: version
  run: |
    echo ::set-output name=source_branch::${GITHUB_REF#refs/heads/} # ブランチの場合
    echo ::set-output name=source_tag::${GITHUB_REF#refs/tags/}     # タグの場合
```


# References
- [GithubActionsワークフロー構文](https://docs.github.com/ja/actions/reference/workflow-syntax-for-github-actions)
- [Trivy](https://github.com/aquasecurity/trivy)
- [Dockle](https://github.com/goodwithtech/dockle)
- [Azure/container-scan](https://github.com/Azure/container-scan)
- [amazon-ecr-login](https://github.com/aws-actions/amazon-ecr-login)