TECHSTEP

ITインフラ関連の記事を公開してます。

【メモ】AWS CodeCommit / CodeBuild + Flux を使ったGitHub Flowの構築例

今回はAWS CodeCommit/CodeBuildとFluxを使い、GitHub Flowを実現することを目指して構成を考えてみました。

GitHub Flowとは

GitHub Flowは開発ワークフローの1つであり、以下のようなルールが定められた、比較的シンプルなワークフローになります。

  1. master ブランチは常にデプロイ可能である
  2. 作業用ブランチをmasterから作成する
  3. 作業用ブランチをリモートブランチに定期的にプッシュする
  4. フィードバックや助言が欲しい時、ブランチをマージしたいときは、 プルリクエスト を作成する
  5. プルリクエストが承認されたらmasterへマージする
  6. masterへのマージが完了したら直ちにデプロイを行う

f:id:FY0323:20220119170728p:plain

GitHub Flowでは、運用するコードは1つのバージョンだけを持ち、常にデプロイ可能なメインラインに頻繁にインテグレーションすることを目指します。また通常の機能開発と本番環境向けの修正を同じフローで対応します。このためGit Flowと異なり、リリース専用のreleaseブランチや、本番環境のバグ修正に使うHotfixブランチなどは利用しません。

※参考:

パイプラインの全体像

今回用意したパイプラインの全体像は以下の通りです。今回、アプリケーションコードのテストやコンテナイメージのビルドはAWS CodeBuild、EKS環境へのデプロイは Flux を採用しています。

なお今回GitHub Flowを適用しているのはアプリケーションコードの開発フローのみになります。マニフェストの開発には特にフローを設定していません。

f:id:FY0323:20220119170800p:plain

AWS CodeBuildの部分は、以前試した構成を再利用しています。

EKSなどKubernetesクラスターへのマニフェストの自動デプロイは、以下の2つのどちらかのアプローチを採用するかと思います。今回は2つ目の、GitOpsのアプローチで実行しています。

  • CIOps: アプリケーション変更のマージ時に、コンテナイメージビルドと合わせて kubectl 等でデプロイをする
  • GitOps: コンテナイメージの更新を検知してテスト環境への自動デプロイを実行する

※参考:

ここでは Flux というCI/CD用ツールを利用します。Fluxの利用方法については 以前紹介したことがあるので、そちらをご覧ください。

実際の開発フローは、以下のような流れを想定しています。

f:id:FY0323:20220119174128p:plain

  • 開発者はアプリケーションの機能追加・修正のためにmainブランチからfeatureブランチを作成する
  • featureブランチ上で開発を行い、mainブランチに対するPRを作成する
  • PRの作成を検知して自動テストが実行される
  • テストに合格したらレビューを依頼し、修正などを経てmainブランチへマージする
  • mainブランチへマージされると、コンテナイメージのビルドを実行する
  • 新しいコンテナイメージがPushされるのを検知し、マニフェストのイメージタグの書き換えと、テスト環境への自動デプロイが実行される
  • テスト環境で問題がないことを確認したら、対象のコミットに対しGitタグを付与する
  • Gitタグを検知し、本番環境への自動デプロイが実行される

※参考:

開発フローの流れ

開発者はアプリケーションの機能追加・修正のためにmainブランチからfeatureブランチを作成する

AWSではIAMユーザー・グループに対してIAMポリシーを適用し、CodeCommitに対する操作制限をかけることができます。

今回は testuser というIAMユーザーを作成しました。このユーザーは testgroup というIAMグループに属しており、testgroup にはあらかじめ main ブランチへの直接PushができないようIAMロールを付与しています。

ここでは testusercodecommit-ci というCodeCommitリポジトリをCloneし、作業用のブランチを作成します。

なお、IAMユーザー作成後はAWSマネジメントコンソールにアクセスし、CodeCommit用のアクセス情報を取得します。今回はHTTPS認証を利用しました。

※参考:

featureブランチ上で開発を行い、mainブランチに対するPRを作成する

featureブランチ上でソースコードなどに修正を行い、mainブランチへのPRを作成します。

PRの作成を検知して自動テストが実行される

codecommit-ci でPRが作成されると、AWS EventBridgeを経由して codebuild-ci-test というCodeBuildプロジェクトのジョブが起動します。 codebuild-ci-test ではテストが実行され、実行結果はSlackに通知されます。

f:id:FY0323:20220120203750p:plain

テストに合格したらレビューを依頼し、修正などを経てmainブランチへマージする

CodeBuildによるテストが無事通ったら、レビューを依頼してPRのチェックをします。今回は承認ルールテンプレートも作成しており、PRに対して1件の承認がないとマージできないよう設定しています。

※参考:

mainブランチへマージされると、コンテナイメージのビルドを実行する

PRのレビューで問題がなければ承認・マージを行います。EventBridgeがマージを検知すると codebuild-ci-build というCodeBuildプロジェクトがジョブを開始します。このジョブではDockerfileを用いたイメージのビルド、ECRリポジトリへのPushを行います。ジョブの結果はSlackへ通知されます。

f:id:FY0323:20220120204444p:plain

なお今回コンテナイメージのタグは main-<date>-<Commit IDの先頭7文字> という形式にしています。

※参考:

新しいコンテナイメージがPushされるのを検知し、マニフェストのイメージタグの書き換えと、テスト環境への自動デプロイが実行される

ECRに新しいイメージがPushされると、あらかじめEKSにインストールされたFluxがイメージの更新を検知し、codecommit-cd というCodeCommitリポジトリ上のマニフェストファイルを書き換えます。

マニフェストファイルが書き換わると、Fluxがリポジトリ上の変更を検知し、EKSクラスターへ自動的に更新を行います。これにより、コンテナイメージが更新されることで、テスト環境への自動デプロイを実現します。

f:id:FY0323:20220119171002p:plain

※参考:

テスト環境で問題がないことを確認したら、対象のコミットに対しGitタグを付与する

今回は作成しませんでしたが、テスト環境での動作確認が取れたら、特定のGitタグを付与することで本番環境のPodが更新されることを想定しています。ここでもFluxを利用し、以下のようなマニフェストファイルを使うことで、Gitタグを検知してアップデートを行います。

apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
  name: prod
  namespace: flux-system
spec:
  interval: 1m
  url: https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/codecommit-cd
  ref:
    semver: ">=3.1.0-rc.1 <3.2.0" # Gitタグの形式を指定
  gitImplementation: libgit2
  secretRef:
    name: https-credentials

※参考:

Gitタグを検知し、本番環境への自動デプロイが実行される

前項の通り、FluxがGitタグを検知して自動デプロイを実行します。

ここまでで、今回のGitHub Flowの流れは終了です。

環境構築

最後に、今回の環境構築についての紹介です。

CodeCommit / CodeBuildの作成

CodeCommit / CodeBuild関連のリソースは、以下のマニフェストファイルで作成します。ここでは承認ルールテンプレートなども合わせて作成します。承認ルールテンプレートの作成は以前試したこちらの記事をご確認ください。

sample-resources.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: Sample resources for GitHub flow
 
Parameters:
  CodeCommitRepoNameForCI:
    Type: String
    Default: "codecommit-ci"
  CodeBuildProjectForCITest:
    Type: String
    Default: "codebuild-ci-test"
  CodeBuildProjectForCIBuild:
    Type: String
    Default: "codebuild-ci-build"
  RuleForCITest:
    Type: String
    Default: "rule-ci-test"
  RuleForCIBuild:
    Type: String
    Default: "rule-ci-build"
  CodeCommitRepoNameForCD:
    Type: String
    Default: "codecommit-cd"
  ECRRepoName:
    Type: String
    Default: "test-ecr-cicd"
  NotifySlackChannel:
    Type: String
    Description: Slack Channel ID
    Default: "xxxxxxxxxxx"
  NotifyChatbotWorkspaceId:
    Type: String
    Description: Chatbot Workspace ID
    Default: "xxxxxxxxxxx"
  UserName:
    Type: String
    Default: "testuser"
  UserPassword:
    Type: String
    Default: "testuser@1234"
  GroupName:
    Type: String
    Default: "testgroup"
  RuleTemplateName:
    Type: String
    Default: "testrule"
 
Resources:
  ###########################
  # Sample resources for CI #
  ###########################
  # CodeCommit for CI
  CodeCommitForCI:
    Type: AWS::CodeCommit::Repository
    Properties:
      RepositoryName: !Ref CodeCommitRepoNameForCI
      RepositoryDescription: CodeCommit repository for CI
  # CodeBuild + IAM for CI
  CodeBuildForCITest:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Ref CodeBuildProjectForCITest
      Description: CodeBuild project for CI test
      ServiceRole: !GetAtt BuildRoleForCITest.Arn
      Artifacts:
        Type: NO_ARTIFACTS
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
      Source:
        Location: !GetAtt CodeCommitForCI.CloneUrlHttp
        Type: CODECOMMIT
        BuildSpec: |
          version: 0.2
 
          phases:
            install:
              runtime-versions:
                golang: 1.14
            build: 
              commands:
                - echo Test the Go code
                - go test -v $CODEBUILD_SRC_DIR/app
  BuildRoleForCITest:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: "Allow"
            Action: 'sts:AssumeRole'
            Principal:
              Service: codebuild.amazonaws.com
  BuildPoliciesForCITest:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: Policy-for-ci-test
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - logs:*
            Resource: '*'
            Effect: Allow
          - Resource: "*"
            Effect: Allow
            Action:
              - 'codebuild:*'
              - 'codecommit:*'
              - 'events:*'
              - 'logs:CreateLogGroup'
              - 'logs:CreateLogStream'
              - 'logs:PutLogEvents'
              - 'iam:*'
      Roles:
        - Ref: BuildRoleForCITest
 
  CodeBuildForCIBuild:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Ref CodeBuildProjectForCIBuild
      Description: CodeBuild project for CI build
      ServiceRole: !GetAtt BuildRoleForCIBuild.Arn
      Artifacts:
        Type: NO_ARTIFACTS
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/docker:18.09.0
        # please set the variables
        EnvironmentVariables:
        - Name: "ECR_REPO"
          Value: !Ref ECRRepoName
        - Name: "BRANCH"
          Value: "main"
      Source:
        Location: !GetAtt CodeCommitForCI.CloneUrlHttp
        Type: CODECOMMIT
        BuildSpec: |
          version: 0.2
 
          phases:
            pre_build:
              commands:
                - echo Login to ECR
                - AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
                - $(aws ecr get-login --no-include-email --region ap-northeast-1)
                - REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$ECR_REPO
                - echo Set IMAGE_TAG
                - DATE=$(date "+%Y%m%d%H%M")
                - COMMITID=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | head -c 7)
                - IMAGE_TAG=$BRANCH-$DATE-$COMMITID
            build: 
              commands:
                - echo Build Docker image
                - docker build -t $REPOSITORY_URI:$IMAGE_TAG app/
            post_build:
              commands:
                - echo Push to ECR
                - docker push $REPOSITORY_URI:$IMAGE_TAG
  BuildRoleForCIBuild:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: "Allow"
            Action: 'sts:AssumeRole'
            Principal:
              Service: codebuild.amazonaws.com
  BuildPoliciesForCIBuild:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: Policy-for-ci-build
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - logs:*
            Resource: '*'
            Effect: Allow
          - Resource: "*"
            Effect: Allow
            Action:
              - 'ecr:*'
              - 'codebuild:*'
              - 'codecommit:*'
              - 'events:*'
              - 'logs:CreateLogGroup'
              - 'logs:CreateLogStream'
              - 'logs:PutLogEvents'
              - 'iam:*'
      Roles:
        - Ref: "BuildRoleForCIBuild"
  # EventBridge for CI
  EventBridgeForCITest:
    Type: AWS::Events::Rule
    Properties:
      Description: EventBridge for PR test
      EventPattern:
        source:
          - "aws.codecommit"
        resources: 
          - !GetAtt CodeCommitForCI.Arn
        detail-type:
          - "CodeCommit Pull Request State Change"
        detail:
          event:
            - "pullRequestCreated"
          pullRequestStatus:
            - "Open"
          destinationReference:
            - "refs/heads/main"
      Name: !Ref RuleForCITest
      State: "ENABLED"
      Targets:
        - Arn: !GetAtt CodeBuildForCITest.Arn
          Id: AppPRTest
          RoleArn: !GetAtt EventBridgeRoleForCITest.Arn
  EventBridgeRoleForCITest:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: Policy-eventbridge-for-ci-test
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Resource: !GetAtt CodeBuildForCITest.Arn
                Action: 
                  - codebuild:StartBuild
  EventBridgeForCIBuild:
    Type: AWS::Events::Rule
    Properties:
      Description: EventBridge for PR build
      EventPattern:
        source:
          - "aws.codecommit"
        resources: 
          - !GetAtt CodeCommitForCI.Arn
        detail-type:
          - "CodeCommit Pull Request State Change"
        detail:
          event:
            - "pullRequestMergeStatusUpdated"
          pullRequestStatus:
            - "Closed"
          destinationReference:
            - "refs/heads/main"
      Name: !Ref RuleForCIBuild
      State: "ENABLED"
      Targets:
        - Arn: !GetAtt CodeBuildForCIBuild.Arn
          Id: AppPRTest
          RoleArn: !GetAtt EventBridgeRoleForCIBuild.Arn
  EventBridgeRoleForCIBuild:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: Policy-eventbridge-for-ci-build
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Resource: !GetAtt CodeBuildForCIBuild.Arn
                Action: 
                  - codebuild:StartBuild
 
  # CodeCommit for CD
  CodeCommitForCD:
    Type: AWS::CodeCommit::Repository
    Properties:
      RepositoryName: !Ref CodeCommitRepoNameForCD
      RepositoryDescription: CodeCommit repository for CD
 
  ###########################
  # Sample resources common #
  ###########################
  # ECR
  ECRForCICD:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Ref ECRRepoName
      ImageScanningConfiguration: 
        ScanOnPush: "true"
  # Chatbot
  ChatbotSlackConfiguration:
    Type: AWS::Chatbot::SlackChannelConfiguration
    Properties:
      ConfigurationName: chatbot-slack-configuration
      IamRoleArn: !GetAtt ChatbotRole.Arn
      LoggingLevel: ERROR
      SlackChannelId: !Ref NotifySlackChannel
      SlackWorkspaceId: !Ref NotifyChatbotWorkspaceId
  ChatbotRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - chatbot.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess
  # CodeStar Notification Rule
  NotificationRoleForCITest:
    Type: 'AWS::CodeStarNotifications::NotificationRule'
    Properties:
      Name: 'notifications for CI test'
      DetailType: FULL
      Resource: !GetAtt CodeBuildForCITest.Arn
      EventTypeIds: 
        - codebuild-project-build-state-failed
        - codebuild-project-build-state-succeeded
      Targets: 
        - TargetType: AWSChatbotSlack 
          TargetAddress: !GetAtt ChatbotSlackConfiguration.Arn
  NotificationRoleForCIBuild:
    Type: 'AWS::CodeStarNotifications::NotificationRule'
    Properties:
      Name: 'notifications for PR build'
      DetailType: FULL
      Resource: !GetAtt CodeBuildForCIBuild.Arn
      EventTypeIds: 
        - codebuild-project-build-state-failed
        - codebuild-project-build-state-succeeded
      Targets: 
        - TargetType: AWSChatbotSlack 
          TargetAddress: !GetAtt ChatbotSlackConfiguration.Arn
 
  # IAM User
  IAMPolicy:
    Type: 'AWS::IAM::ManagedPolicy'
    Properties: 
      Description: prohibit direct push to codecommit
      Path: /
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Deny
            Action: 'codecommit:GitPush'
            Resource: '*'
            Condition:
              "StringEqualsIfExists":
                "codecommit:References": 
                  - "refs/heads/main"
              "Null":
                "codecommit:References": 
                  - "false"
  IAMUser:
    Type: AWS::IAM::User
    Properties:
      Groups:
        - !Ref IAMGroup
      UserName: !Ref UserName
      LoginProfile:
        Password: !Ref UserPassword
        PasswordResetRequired: "false"
  IAMGroup:
    Type: AWS::IAM::Group
    Properties:
      GroupName: !Ref GroupName
      ManagedPolicyArns:
        - !Ref IAMPolicy
        - "arn:aws:iam::aws:policy/AWSCodeCommitPowerUser"
  # Approval Rule Template
  RuleTemplate:
    Type: Community::CodeCommit::ApprovalRuleTemplate
    Properties:
      Name: !Ref RuleTemplateName
      Description: test rule
      Content:
        Version: "2018-11-08"
        DestinationReferences:
          - "refs/heads/main"
        Statements:
          - Type: "Approvers"
            NumberOfApprovalsNeeded: 1
            ApprovalPoolMembers:
              - "*"
  RepoAssociation:
    Type: Community::CodeCommit::RepositoryAssociation
    Properties:
      ApprovalRuleTemplateArn: !Ref RuleTemplate
      RepositoryNames: 
        - !Ref CodeCommitRepoNameForCI

ソースコード・Dockerfileの配置

CodeCommitを作成したら、各リポジトリREADME.md を適当に配置し、ローカルへクローンします。クローンしたら、ソースコード・Dockerfileは codecommit-ci リポジトリapp ディレクトリに、Kubernetesマニフェストファイルは codecommit-cd リポジトリmanifest ディレクトリに配置します。

今回利用したソースコード・Dockerfileはこちらです。

main.go

package main
 
import (
    "fmt"
    "net/http"
)
 
func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "<html><body>hello AWS CI/CD</body></html>\n")
}
 
func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

main_test.go

package main
 
import (
    "net/http"
    "net/http/httptest"
    "testing"
)
 
func TestHandler(t *testing.T) {
    testserver := httptest.NewServer(http.HandlerFunc(handler))
    defer testserver.Close()
 
    res, err := http.Get(testserver.URL)
    defer res.Body.Close()
 
    if err != nil {
        t.Fatalf("failed test %#v", err)
    }
    if res.StatusCode != 200 {
        t.Error("response code is not 200")
    }
}

Dockerfile

FROM golang:1.15.6 as builder
 
WORKDIR /go/src
 
COPY ./main.go ./
ARG CGO_ENABLED=0
ARG GOOS=linux
ARG GOARCH=amd64
RUN go build -o /go/bin/main
 
 
FROM scratch as runner
 
COPY --from=builder /go/bin/main /app/main
 
ENTRYPOINT ["/app/main"]

マニフェストファイルはこちらです。Fluxがコンテナイメージタグを検索できるよう、 # {"$imagepolicy": "flux-system:test-policy"} という記述を追加しています。

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
  namespace: default
  labels:
    app: sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample
  template:
    metadata:
      labels:
        app: sample
    spec:
      containers:
      - name: app
        image: 000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/test-ecr-cicd:main-000000000000-0123abc # {"$imagepolicy": "flux-system:test-policy"}
        ports:
        - containerPort: 8080
 
---
apiVersion: v1
kind: Service
metadata:
  name: sample-app
  namespace: default
spec:
  selector:
    app: sample
  ports:
  - protocol: TCP
    name: http
    port: 80
    targetPort: 8080

※参考:

EKSクラスターの作成

次にEKSクラスターを作成します。今回は eksctl コマンドで作成しました。

$ eksctl version
0.77.0

$ eksctl create cluster -f eks-clusterconfig.yml 

Fluxのインストール

次にEKSクラスターへFluxをインストールします。今回は flux install コマンドを利用しました。

$ flux --version
flux version 0.24.1

$ flux install --components-extra=image-reflector-controller,image-automation-controller

次にFlux用のリソースを作成します。今回はコンテナイメージリポジトリのモニタリングも行うので、以下のように複数のCustom Resourceを利用しています。

  • GitRepository
  • Kustomization
  • ImageRepository
  • ImagePolicy
  • ImageUpdateAutomation

GitRepository Kustomizationのファイルはこちら。

gitrepo.yaml

---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
  name: codecommit-cd
  namespace: flux-system
spec:
  interval: 1m
  url: https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/codecommit-cd
  ref:
    branch: main
  gitImplementation: libgit2
  secretRef:
    name: https-credentials
---
apiVersion: v1
kind: Secret
metadata:
  name: https-credentials
  namespace: flux-system
type: Opaque
data:
  username: <base64 encoded username>
  password: <base64 encoded password>
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
  name: codecommit-cd
  namespace: flux-system
spec:
  interval: 5m0s
  path: ./manifest
  prune: true
  sourceRef:
    kind: GitRepository
    name: codecommit-cd
  validation: client

今回はソースコードリポジトリにCodeCommitを利用し、アクセスはHTTPS認証を使っています。FluxからCodeCommitを利用する場合は、こちらのリンク先に書かれている通り、Secret リソースにアクセス情報を定義します。また、Fluxがリポジトリからマニフェストファイルを利用するため、 Kustomization リソースも合わせて作成します。

※参考:

ImageRepository ImagePolicy ImageUpdateAutomation のファイルはこちら。

imagerepo.yaml

---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageRepository
metadata:
  name: test-ecr-cicd
  namespace: flux-system
spec:
  interval: 1m0s
  image: 000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/test-ecr-cicd
  secretRef:
    name: ecr-credentials
---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImagePolicy
metadata:
  name: test-policy
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: test-ecr-cicd
  filterTags:
    pattern: '^main-[0-9].*-[a-f0-9].*'
  policy:
    alphabetical:
      order: asc
---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
  name: test-automation
  namespace: flux-system
spec:
  interval: 1m0s
  sourceRef:
    kind: GitRepository
    name: codecommit-cd
  git:
    checkout:
      ref:
        branch: main
    commit:
      author:
        email: fluxcdbot@users.noreply.github.com
        name: fluxcdbot
      messageTemplate: '{{range .Updated.Images}}{{println .}}{{end}}'
    push:
      branch: main
  update:
    path: ./manifest
    strategy: Setters

secret-cronjob.yaml

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: ecr-credentials-sync
  namespace: flux-system
rules:
- apiGroups: [""]
  resources:
  - secrets
  verbs:
  - get
  - create
  - patch
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: ecr-credentials-sync
  namespace: flux-system
subjects:
- kind: ServiceAccount
  name: ecr-credentials-sync
roleRef:
  kind: Role
  name: ecr-credentials-sync
  apiGroup: ""
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ecr-credentials-sync
  namespace: flux-system
---
apiVersion: v1
kind: Secret
metadata:
  name: aws-credentials
  namespace: flux-system
type: Opaque
data:
  AWS_ACCESS_KEY_ID: <base64 encoded AWS Access Key>
  AWS_SECRET_ACCESS_KEY: <base64 encoded AWS Secret Access Key>
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: ecr-credentials-sync
  namespace: flux-system
spec:
  suspend: false
  schedule: 0 */6 * * *
  failedJobsHistoryLimit: 1
  successfulJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: ecr-credentials-sync
          restartPolicy: Never
          volumes:
          - name: token
            emptyDir:
              medium: Memory
          initContainers:
          - image: amazon/aws-cli
            name: get-token
            imagePullPolicy: IfNotPresent
            envFrom:
            - secretRef:
                name: aws-credentials
            env:
            - name: REGION
              value: ap-northeast-1
            volumeMounts:
            - mountPath: /token
              name: token
            command:
            - /bin/sh
            - -ce
            - aws ecr get-login-password --region ${REGION} > /token/ecr-token
          containers:
          - image: bitnami/kubectl
            name: create-secret
            imagePullPolicy: IfNotPresent
            env:
            - name: SECRET_NAME
              value: ecr-credentials
            - name: ECR_REGISTRY
              value: 000000000000.dkr.ecr.ap-northeast-1.amazonaws.com
            volumeMounts:
            - mountPath: /token
              name: token
            command:
            - /bin/bash
            - -ce
            - |-
              kubectl create secret docker-registry $SECRET_NAME \
                --dry-run=client \
                --docker-server="$ECR_REGISTRY" \
                --docker-username=AWS \
                --docker-password="$(</token/ecr-token)" \
                -o yaml | kubectl apply -f -

Amazon ECRを利用する場合は、12時間ごとにアクセストークンが更新されるため、定期的にアクセス情報を更新するための CronJob を用意します。とりあえずアクセス用の Secret リソースを作成したかったので、CronJobからJobを作成する以下のコマンドを実行し、Secretリソースを作成しました。

$ kubectl create job docker-registry –from=cronjob/ecr-credentials-sync
$ kubectl get job -n flux-system
NAME                            COMPLETIONS   DURATION   AGE
docker-registry                 1/1           22s        5h6m

※参考:

パイプラインの実行

ここまでで必要なリソースの作成は完了しました。この状態で codebuild-ci-build のジョブが実行されると、ECRへの新規イメージのPushを検知し、CodeCommit上のマニフェストファイルを書き換えます。

# Image Reflector Controllerのログ
$ kubectl logs image-reflector-controller-6d94666b7d-962x8 -n flux-system

(中略)

{"level":"info","ts":"2022-01-16T06:16:31.391Z","logger":"controller.imagerepository","msg":"reconciliation finished in 43.928032ms, next run in 
1m0s","reconciler group":"image.toolkit.fluxcd.io","reconciler kind":"ImageRepository","name":"test-ecr-cicd","namespace":"flux-system"}

マニフェストファイルの変更を検知すると、FluxはEKSクラスターへのデプロイを実行し、新しいコンテナイメージを持つDeploymentが起動します。

# sample-appのイメージタグが変更されている
$ kubectl get pods
NAME                          READY   STATUS    RESTARTS   AGE
sample-app-7689d7759c-7h56f   1/1     Running   0          83m

$ kubectl describe pod sample-app-7689d7759c-7h56f

(中略)

Containers:
  app:
    Container ID:   docker://b94f50458c881f8b11bbf767dc04b9c12bb5f837392e79651c92e278e745d875
    Image:          000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/test-ecr-cicd:main-202201160619-0ce5ebf