0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DockerHubのイメージをGitHub ActionsからECRでスキャンする

Last updated at Posted at 2025-05-07

DockerHubのコンテナイメージは出自が分からないものも多く、特に企業が利用するには少し不安が残る。
またいつ公開停止になるかも分からないし、自前のリポジトリに移して利用したいケースは多いと思う。
ここではコンテナイメージ利用者はDockerHubのイメージは直接参照せず、脆弱性が見つからなかったイメージのみをECRから使ってもらうようなユースケースを想定し、以下を実現するGitHub Actionsのフローを作成する。

なお、ECRではリポジトリに対してscanOnPush=trueを設定することでPush時に自動的にスキャンさせることも可能だが、設定されていることは期待せずに手動でスキャンを実行する形にする。

前提

以下を前提とする

  • CI/CDはGitHub Actionsを利用し、Runnerは起動済み
  • 以下の環境変数は作業端末内で設定済みとする。
    • export AWS_REGION=us-east-1
    • export AWS_ACCESS_KEY_ID="ASIAV..."
    • export AWS_SECRET_ACCESS_KEY="Fh4E..."
    • export AWS_SESSION_TOKEN="IQoJ..."
  • GitHub CLIをインストール済み

ECRのリポジトリの準備

ECRのリポジトリを作成する。

REPOSITORY_NAME=dockerhub-migration/kong-gateway
aws ecr create-repository --repository-name $REPOSITORY_NAME --region $AWS_REGION

今回はイメージの対象をkong/kong-gatewayとし、tag:3.4.0.0と3.4.3.18で動作検証する。

パイプラインの作成

カレントディレクトリにgitのcloneがあることを前提として進める。
最初にGitリポジトリにAWSを操作するためのSecretを作成する。

gh secret set AWS_ACCESS_KEY_ID --body $AWS_ACCESS_KEY_ID
gh secret set AWS_SECRET_ACCESS_KEY --body $AWS_SECRET_ACCESS_KEY
gh secret set AWS_SESSION_TOKEN --body $AWS_SESSION_TOKEN

また、コンテナリポジトリに関する設定を同様にVariablesとして設定する。

gh variable set AWS_REGION --body "us-east-1"
gh variable set REPO_NAME_DOCKER --body "kong/kong-gateway"
gh variable set IMAGE_TAG_DOCKER --body "3.4.0.0"
gh variable set REPO_NAME_ECR --body "dockerhub-migration/kong-gateway"
gh variable set IMAGE_TAG_ECR --body "latest"

ECR側のTagをlatestにしているが、セキュリティ観点からは同じTag名にした方がいいと思う。
今回は利便性の都合上latestにしている。
これらはパイプラインからECRにアクセスする際に利用される。

次にGitHub Actionsで使うファイルを作成するためにディレクトリを作成する。

mkdir -p .github/workflows

パイプラインを作成してpushする。

cat <<'EOF' > ./.github/workflows/dockerhub2ecr.yaml
name: Push and Scan ECR Image

on:
  workflow_dispatch:

jobs:
  build-and-scan:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }}
          aws-region: ${{ vars.AWS_REGION }}

      - name: Log in to Amazon ECR
        id: login-ecr
        run: |
          set -x
          ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
          echo "ACCOUNT_ID=$ACCOUNT_ID" >> $GITHUB_ENV
          ECR_URI="${ACCOUNT_ID}.dkr.ecr.${{ vars.AWS_REGION }}.amazonaws.com/${{ vars.REPO_NAME_ECR }}"
          echo "ECR_URI=$ECR_URI" >> $GITHUB_ENV
          aws ecr get-login-password --region ${{ vars.AWS_REGION }} | docker login --username AWS --password-stdin "$ECR_URI"

      - name: Pull and Tag Docker image
        run: |
          set -x
          docker pull ${{ vars.REPO_NAME_DOCKER }}:${{ vars.IMAGE_TAG_DOCKER }}
          docker tag ${{ vars.REPO_NAME_DOCKER }}:${{ vars.IMAGE_TAG_DOCKER }} $ECR_URI:${{ vars.IMAGE_TAG_ECR }}

      - name: Push Docker image to ECR
        run: |
          docker push $ECR_URI:${{ vars.IMAGE_TAG_ECR }}

      - name: Start ECR image scan
        run: |
          aws ecr start-image-scan \
            --repository-name ${{ vars.REPO_NAME_ECR }} \
            --image-id imageTag=${{ vars.IMAGE_TAG_ECR }}

      - name: Wait and check scan results
        run: |
          for i in {1..10}; do
            result=$(aws ecr describe-image-scan-findings \
              --repository-name ${{ vars.REPO_NAME_ECR }} \
              --image-id imageTag=${{ vars.IMAGE_TAG_ECR }})

            status=$(echo "$result" | jq -r '.imageScanStatus.status')
            if [[ "$status" == "COMPLETE" ]]; then
              echo "Scan complete."
              echo "$result" > scan-results.json
              break
            fi
            echo "Waiting for scan to complete... ($i)"
            sleep 10
          done

          CRITICALS=$(jq '[.imageScanFindings.findings[] | select(.severity == "CRITICAL")] | length' scan-results.json)
          if [ "$CRITICALS" -gt 0 ]; then
            echo "Vulnerabilities found: CRITICAL=$CRITICALS"
            echo "Deleting image..."
            aws ecr batch-delete-image --repository-name ${{ vars.REPO_NAME_ECR }} \
              --image-ids imageTag=${{ vars.IMAGE_TAG_ECR }}
            exit 1
          else
            echo "No critical vulnerabilities found."
          fi

      - name: Upload scan results as artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: scan-results
          path: scan-results.json
EOF
git add -A
git commit -m "Initial Commit"
git push origin main

以下、GitHub Actionsのフローについて説明する。

on:
  workflow_dispatch:

実行契機について指定する箇所だが、対象コードなどを含まない構造にしているので手動実行であるworkflow_dispatchのみ設定している。

      - name: Checkout code
        uses: actions/checkout@v4

上記はRunnerがソースコードを引っ張る処理である。
通常はビルドのためのコードなどを引っ張るために利用するが、今回に関しては不要。

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }}
          aws-region: ${{ vars.AWS_REGION }}

上記はAWSを操作するための環境変数を設定する箇所となる。
詳細は以下の記事が参考になる。
-  configure-aws-credentialsを読み解く

      - name: Log in to Amazon ECR
        id: login-ecr
        run: |
          set -x
          ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
          echo "ACCOUNT_ID=$ACCOUNT_ID" >> $GITHUB_ENV
          ECR_URI="${ACCOUNT_ID}.dkr.ecr.${{ vars.AWS_REGION }}.amazonaws.com/${{ vars.REPO_NAME_ECR }}"
          echo "ECR_URI=$ECR_URI" >> $GITHUB_ENV
          aws ecr get-login-password --region ${{ vars.AWS_REGION }} | docker login --username AWS --password-stdin "$ECR_URI"

上記はECRへのログインである。最初にECRのURLを組み立てるためにaws sts get-caller-identityでアカウントIDを取得し、ECR_URIという環境変数にECRのアクセス先を定義する。
次に aws ecr get-login-passwordでECRにログインするためのパスワードを取得し、docker loginでログインしてpush出来る状態にしている。
なお、ECRのURIはこの先も利用するが、毎回アカウントIDを取得するのは大変なので、"ECR_URI=$ECR_URI" >> $GITHUB_ENVGITHUB_ENV に環境変数を追加することで別のstepでも参照できるようにしている。

      - name: Push Docker image to ECR
        run: |
          docker push $ECR_URI:${{ vars.IMAGE_TAG_ECR }}

      - name: Start ECR image scan
        run: |
          aws ecr start-image-scan \
            --repository-name ${{ vars.REPO_NAME_ECR }} \
            --image-id imageTag=${{ vars.IMAGE_TAG_ECR }}

上記でイメージをPushし、スキャンを開始している。
aws ecr start-image-scanではスキャンの開始の指示のみを行い、コマンドはスキャンの完了を待たない。
待つオプションもなさそうなので、次のstepで状態を監視する。
なお、AWS的なベストプラクティスとしてはスキャン結果の通知をEventEventBridge経由で受け取り、その後ゴニョゴニョするのが良さそうなのだが、個人的には同一のパイプラインの処理の中で完結させたかったのと、ベンダロックインを多少は避ける意味でも採用しないこととした。

      - name: Wait and check scan results
        run: |
          for i in {1..10}; do
            result=$(aws ecr describe-image-scan-findings \
              --repository-name ${{ vars.REPO_NAME_ECR }} \
              --image-id imageTag=${{ vars.IMAGE_TAG_ECR }})

            status=$(echo "$result" | jq -r '.imageScanStatus.status')
            if [[ "$status" == "COMPLETE" ]]; then
              echo "Scan complete."
              echo "$result" > scan-results.json
              break
            fi
            echo "Waiting for scan to complete... ($i)"
            sleep 10
          done

上記ではaws ecr describe-image-scan-findingsの結果からスキャンの状態をチェックし、COMPLETEなら結果をscan-results.jsonに保存し、そうでない場合は10秒待って再チェック(最大10回)する処理となっている。
ここの待ち時間等はお好みで調整すると良い。

          CRITICALS=$(jq '[.imageScanFindings.findings[] | select(.severity == "CRITICAL")] | length' scan-results.json)
          if [ "$CRITICALS" -gt 0 ]; then
            echo "Vulnerabilities found: CRITICAL=$CRITICALS"
            echo "Deleting image..."
            aws ecr batch-delete-image --repository-name ${{ vars.REPO_NAME_ECR }} \
              --image-ids imageTag=${{ vars.IMAGE_TAG_ECR }}
            exit 1
          else
            echo "No critical vulnerabilities found."
          fi

上記ではCRITICALがあればエラーに倒してパイプラインも終了するようにしている。
またその際にPushしたイメージも誤って利用されるのを避けるために削除するようにしている。
HIGHでも引っ掛けたい場合は同じような書き方でHIGHも引っ掛けてif文の条件に追加すると良い。

      - name: Upload scan results as artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: scan-results
          path: scan-results.json

最後に結果をArtifactとしてアップロードして終了する。

動作確認

今の設定はkong/kong-gateway:3.4.0.0に対してスキャンする設定となっている。
一旦この設定で動かしてみる。
GitHubのActionsからRun workflowをクリックして実行する。
Pasted image 20250507155614.png

実行するとパイプラインは失敗する。
エラーを見ると以下のようになっている。
Pasted image 20250507160926.png

CRITICALの脆弱性が23個検出されたため、パイプラインが停止したことが分かる。
ECR上にもイメージは存在しない。

 $ aws ecr describe-images \
   --repository-name dockerhub-migration/kong-gateway
{
    "imageDetails": []
}

イメージのバージョンが古かったため脆弱性が検出されたと考えられるので、次にイメージのバージョンを最新にして試してみる。
リポジトリ内のVariablesを変更する。

gh variable set IMAGE_TAG_DOCKER --body "3.4.3.18"

変更後、Actionsのジョブを再実行する。
次は問題なく成功する。
Pasted image 20250507161454.png

スキャン結果はArtifactとしてアップロードされたJSONからも確認できるし、ECRのUI上からも確認できる。
Pasted image 20250507162039.png

余談

スキャンを何度か実行すると以下のエラーが出るようになる。

An error occurred (LimitExceededException) when calling the StartImageScan operation (reached max retries: 2): The scan quota per image has been exceeded. Wait and try again.

こちらを見る感じ、1イメージあたり24時間につき1回、全体で24時間につき10万回スキャンが可能であり、1イメージを短時間で何度もスキャンするのは許されていない模様。
Quotaを気にするのが嫌な人は毎回イメージを削除してPushし直すか、イメージのPush前にTrivyでスキャンするようにし、そこで問題なかったらPushするような形に書き換えるとよい。
(ECRはイメージの格納のみでスキャンは利用しない)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?