🏭

GitHub Actions + Argo CDでプッシュ型のGitOpsを構築してみた

2022/09/06に公開

1. はじめに

CIとしてのGitHub ActionsとCDとしてのArgo CDを利用して、下図のようなプッシュ型のGitOpsを構築してみました。この記事では、特にGitHub Actionsワークフローに焦点を当てて、実装例を紹介していきます。

2. GitHub Actionsのワークフロー

今回構築したプッシュ型のGitOpsでは、以下の2点をGitHub Actionsが実行します。

  1. アプリケーションイメージのビルド&ECRリポジトリへのプッシュ
  2. マニフェストに記述されたイメージタグを書き換えて、コミット&プッシュ

上記を実現するのが以下のYAML定義であり、これをアプリケーションリポジトリ内のワークフローとして配置しています。

.github/workflows/build_and_push_image.yml
name: Build and Push Docker Images to ECR Repositories

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ap-northeast-1
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT }}:role/role-github
          role-duration-seconds: 1800

      - 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 }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
          docker build -t $ECR_REGISTRY/app-repo:$IMAGE_TAG -f ./build/app/Dockerfile.prod .
          docker build -t $ECR_REGISTRY/web-repo:$IMAGE_TAG -f ./build/web/Dockerfile .
          docker build -t $ECR_REGISTRY/migration-repo:$IMAGE_TAG -f ./build/migration/Dockerfile .
          docker push $ECR_REGISTRY/web-repo:$IMAGE_TAG
          docker push $ECR_REGISTRY/app-repo:$IMAGE_TAG
          docker push $ECR_REGISTRY/migration-repo:$IMAGE_TAG
      
      - name: Checkout config repository
        uses: actions/checkout@v3
        with:
          repository: your_account/your_repository
	  ref: branch_name
	  token: ${{ secrets.PAT }}
          path: your_repository

      - name: Update image tag
        id: update-image-tag
        working-directory: your_repository
        continue-on-error: true
        run: |
          yq -i '.front.imageTag = "'`printenv IMAGE_TAG`'"' ./main/values.yaml
          git diff --name-only --exit-code

      - name: Commit and push
        working-directory: your_repository
        if: steps.update-image-tag.outcome == 'failure'
        run: |
          git config user.name github-actions[bot]
          git config user.email 41898282+github-actions[bot]@users.noreply.github.com
          git add .
          git commit --author=. -m "Update image tag to "`printenv IMAGE_TAG`
          git push

3. 解説

3-1. OIDCを利用したAWSへのアクセス[1]

#~ 省略 ~
permissions:
  id-token: write
  contents: read
#~ 省略 ~    
- name: Configure AWS Credentials
  uses: aws-actions/configure-aws-credentials@v1
  with:
    aws-region: ap-northeast-1
    role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT }}:role/role-github
    role-duration-seconds: 1800

GitHub ActionsからOIDCを利用してAWSへアクセスさせる方法は簡単で、Configure AWS Credentialsアクションでaws-access-key-idweb-identity-token-fileを指定せずにrole-to-assumeを指定すれば、これが合図となります[2]。ですので、OIDC認証に成功したGitHubが引き受けるIAMロールを用意してrole-to-assumeに指定するだけで、GitHubからAWSへOIDCを利用したアクセスが可能となります。注意点としては、ワークフローのジョブ自体にid-token: writecontents: readの権限を与えることです。ワークフローのジョブ内でOIDC認証を利用してAWSへアクセスするにはこれらの権限が必要なので、前述した宣言を忘れずに追加しておきましょう。

ちなみに、GitHubが引き受けるIAMロール(role-github)は以下のように定義しています。

role-githubの信頼されたエンティティ
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
              "Federated": "arn:aws:iam::<AWSアカウントID>:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:sub": "repo:アカウント名/*",
                },
	        "StringEquals": {
	            "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
	        },
            }
        }
    ]
}

また、role-githubにアタッチしているポリシーは以下の通りで、DockerイメージをECRリポジトリへプッシュするための権限のみを付与しています。[3]

role-githubにアタッチしているIAMポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:CompleteLayerUpload",
                "ecr:GetAuthorizationToken",
                "ecr:UploadLayerPart",
                "ecr:InitiateLayerUpload",
                "ecr:BatchCheckLayerAvailability",
                "ecr:PutImage"
            ],
            "Resource": "*"
        }
    ]
}

3-2. イメージのビルドと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 }}
    IMAGE_TAG: ${{ github.sha }}
  run: |
    echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
    docker build -t $ECR_REGISTRY/app-repo:$IMAGE_TAG -f ./build/app/Dockerfile.prod .
    docker build -t $ECR_REGISTRY/web-repo:$IMAGE_TAG -f ./build/web/Dockerfile .
    docker build -t $ECR_REGISTRY/migration-repo:$IMAGE_TAG -f ./build/migration/Dockerfile .
    docker push $ECR_REGISTRY/web-repo:$IMAGE_TAG
    docker push $ECR_REGISTRY/app-repo:$IMAGE_TAG
    docker push $ECR_REGISTRY/migration-repo:$IMAGE_TAG

公式ドキュメントのExamples of Usageとほぼ同じ構成ですので、詳しくはそちらをご参照ください。

3-3. マニフェストリポジトリへのチェックアウト[4]

- name: Checkout config repository
  uses: actions/checkout@v3
  with:
    repository: your_account/your_repository
    ref: branch_name
    token: ${{ secrets.PAT }}
    path: your_repository

ここでは、Checkout V3アクションに対してrepositoryrefパラメータを指定し、チェックアウトするk8sマニフェスト用のリポジトリとブランチを指定しています。このとき、tokenパラメータにPersonal Access Token (= PAT)を指定しておくことで、プライベートリポジトリへのアクセスが可能となります。そして、pathパラメータを指定することによって、チェックアウトしたリポジトリを指定したパスに配置しています。

3-4. マニフェストのイメージタグを書き換えてコミット&プッシュ[5]

- name: Update image tag
  id: update-image-tag
  working-directory: your_repository
  continue-on-error: true
  run: |
    yq -i '.front.imageTag = "'`printenv IMAGE_TAG`'"' ./main/values.yaml
    git diff --name-only --exit-code

- name: Commit and push
  working-directory: your_repository
  if: steps.update-image-tag.outcome == 'failure'
  run: |
    git config user.name github-actions[bot]
    git config user.email 41898282+github-actions[bot]@users.noreply.github.com
    git add .
    git commit --author=. -m "Update image tag to "`printenv IMAGE_TAG`
    git push

まず、Update image tagステップでは、working-directory: your_repositoryを指定して、前のステップでチェックアウトしたk8sマニフェスト用リポジトリを作業ディレクトリに指定しています。次に、YAMLファイルを良い感じに編集してくれるyqコマンドを利用して、main/values.yaml.front.imageTagの値を書き換えています。そして、git diffコマンドを実行することで、yqコマンドにより差分が生じているかどうかを確認しています。このとき、--exit-codeオプションを指定しているので、差分がある場合には終了ステータスが1となる一方で、差分がない場合には終了ステータスが0となり、差分の有無が判定可能となります。ただし、このままでは差分が生じたときの終了ステータスが1となってGitHubワークフロー自体がエラー判定となって停止してしまうので、continue-on-error: trueというワークフロー構文を宣言して、エラー判定となった場合でもワークフローを継続するように設定しています。ちなみに、git diffコマンドの--name-onlyオプションはなくても構わないのですが、yqコマンドにより差分が生じたファイル名をログ出力させるために、ここでは指定しています。

次に、Commit and pushステップはUpdate image tagステップの終了ステータスが1となって出力がfailureとなっている場合、つまり、yqコマンドによりファイル差分が発生している場合に実行されます。このステップも、引き続きk8sマニフェスト用リポジトリで作業していくので、working-directory: your_repositoryを指定しています。そして、通常のコミット&プッシュと同様のフローでコマンドを実行して処理を進めていきます。ここで、git config user.name github-actions[bot]git config user.email 41898282+github-actions[bot]@users.noreply.github.comを指定していますが、これはちょっとしたオシャレのためです。これらを指定することでコミットする主体をGitHub Actionsと明記した上で、アイコンまで付けてくれるようになります。こんな感じで。

4. まとめ

ここでは、GitHub Actions + Argo CDでプッシュ型のGitOpsを構築してみたので、GitHub Actionsにフォーカスして構築したワークフローを紹介しました。GitOpsを構築する中で、GitHub Actionsワークフロー全体を紹介する記事はあまり多くないように感じたので、記録として残しておこうと思った次第です。誰かの何かのお役に立てれば幸いです^^

脚注
  1. GitHub ActionsからAWSへの認証にはOIDCを利用することが推奨されています。 ↩︎

  2. 詳しくは公式ドキュメントをご参照ください。 ↩︎

  3. 詳しくはRequired IAM permissions for pushing an imageをご参照ください。 ↩︎

  4. ワークフローを配置しているリポジトリとは異なるプライベートリポジトリへチェックアウトする方法は公式ドキュメントのCheckout multiple repos (private)が参考になります。 ↩︎

  5. この部分を実装するにあたっては、変更があるときだけコミット [GitHub Actions]を参考にさせていただきました。 ↩︎

Discussion