Flux v2 の コンテナイメージの自動更新機能を使ってみる

Fluxは、kubernets クラスターで GitOps を実現するツールです。
類似のツールとしては [Argo CD](https://argo-cd.readthedocs.io/en/stable/) などがありますが、Argo CD のように Web UI のような機能はありませんが、そのため比較的シンプルなので小規模なクラスターに向いていると思います。

* [Flux | GitOps Toolkit](https://toolkit.fluxcd.io/)
* [Argo CD - Declarative GitOps CD for Kubernetes](https://argo-cd.readthedocs.io/en/stable/)


注意: Flux version 2 は、現在開発中の段階で GA になっていません。本番環境などで使うのは、まだ待ったほうがよさそうです。今回検証したコンテナイメージを自動更新する機能についても、ワークアラウンドな対応が必要な箇所があったり、マルチテナントで使う場合に考慮されていない箇所があったりします。

## Flux について
Git でバージョン管理されたものを正しい情報源としてシステムやアプリケーションを管理する仕組みです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/4519509f-90fe-387f-37ad-6ba007ccd06f.png)

Flux では図のように定期的に対象の Git レポジトリの変更を監視し、変更があった場合に自身のKubernetesクラスターに変更を適用します。

## コンテナイメージの自動更新機能について

Flux では更に、コンテナイメージを Docker Hub や ECR などのコンテナレポジトリをスキャンして、更新があった場合に Git の内容を書き換えてコンテナイメージに変更があった場合に自動で更新することができます。
この仕組みを使うことでアプリケーションのデプロイをコンテナイメージをプッシュするだけで実現することができるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/2d377999-020f-cb54-9c37-65f23dc7cd71.png)

コンテナイメージの自動更新は、Flux の追加コンポーネントである image automation components で実現しています。
動作としては上記のように、コンテナレポジトリをスキャンして変更があった場合にコードを書き換える Image automation の処理と Git レポジトリの変更に対して Kubernetes に適用するコンポーネントは分離されて動作しています。

## Flux のレポジトリ構成
Flux では kubernetes のマニフェストファイルを管理する Git のレポジトリ構成をクラスターやアプリケーションの管理を1つのレポジトリで管理する方法と、クラスターの設定とアプリケーションの設定を別々のレポジトリで管理する方法を選ぶことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/cd479278-55a8-4764-64b4-fa2ea2d4cd82.png)

1つの Git レポジトリを採用する場合、1つのクラスターで1つアプリケーションを動かす、開発と運用が同じチームなど、比較的小規模なクラスター構成で採用すると良いと思います。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/0a7a1b6e-5516-7288-4184-1f7a4e83d857.png)

逆に、1つのKubernetesクラスターに複数の開発チームがそれぞれのアプリケーションを動かすような場合（マルチテナント）や、開発チームと運用チームの役割が明確に分かれている場合はレポジトリを分けて管理した方が権限の分離もできるのでこちらを採用した方が良いと思います。

この場合、プラットフォーム側の Git レポジトリは運用チームが管理することになり、クラスター全体で使用する設定や、各テナントの namespace や service account の設定、テナントの Git レポジトリの Flux 設定などの設定を行うことになります。
テナント側の Git レポジトリでは、プラットフォーム側から設定された namespace 内で動かすアプリケーションのみの設定に注力できるようになります。（そのため、基本的には設定された namespace 外を操作することは無いです）

Flux でマルチテナントで管理する場合は以下のレポジトリが参考になると思います。
[GitHub - fluxcd/flux2-multi-tenancy: Manage multi-tenant clusters with Flux](https://github.com/fluxcd/flux2-multi-tenancy)

## Flux のコンテナイメージの自動更新機能のセットアップ
残念ながら、コンテナイメージの自動更新機能については基本的に`flux-system` の namespace 内で設定しないと現状は動かないようです。
後記するイメージをスキャンするコンテナイメージレポジトリの設定や、イメージタグの命名規則から最新版を定義するポリシーの設定など、アプリケーションのnamespaecで定義した方が良さそうなもの `flux-system` の namespace で定義することになるのですが、マルチテナントの場合、テナントのGitレポジトリで管理する場合、アプリケーションが動作する namespace 以外のものを管理することになるし、プラットフォームのGitレポジトリで管理する場合、アプリケーション側の設定が混じることになります。
この辺りは Issue でも上がっているので恐らくGAまでには解消されるんじゃないかなと期待しています。

[ImageUpdateAutomation in different namespace than ImagePolicy · Issue #85 · fluxcd/image-automation-controller · GitHub](https://github.com/fluxcd/image-automation-controller/issues/85)

では、ここでは Flux の基本的なセットアップは完了済みで、コンテナイメージの自動更新機能のセットアップのみを扱います。
Flux のセットアップについては以下から行ってください。
[Installation - Flux | GitOps Toolkit](https://toolkit.fluxcd.io/guides/installation/)

### image automation components を有効化する
デフォルトではコンテナイメージの自動更新に必要なコンポーネントが有効になっていないため、`flux bootstrap` コマンドで有効化します。弊社の環境では [Generic Git Server](https://toolkit.fluxcd.io/guides/installation/#generic-git-server) の設定方法でインストールしたので以下のようになりました。

Flux マニュフェストを再生性

```shell-session
$ flux install \
  --components-extra=image-reflector-controller,image-automation-controller \
  --export > gotk-components.yaml
```

コミットしてプッシュ

```shell-session
$ git add .
$ git commit -m "Add flux image automation components"
$ git push
```

変更を適用させます

```shell-session
$ flux reconcile kustomization flux-system --with-source
```

適用されたことを確認

```shell-session
$ kubectl get deployments -n flux-system | grep image
image-automation-controller   1/1     1            1           2m
image-reflector-controller    1/1     1            1           2m
```

### イメージスキャンの設定
スキャンするコンテナイメージのコンテナイメージレポジトリを設定します。

もし、AWS ECR などのクラウドプロバイダーのイメージレポジトリを使っている場合、認証の設定を行う必要があるのですが、この辺りはまだちゃんとした実装がされていないのでワークアラウンドな対応が必要になります。GAになる頃には解消されていると思います。

弊社では EKS + ECR を使っているため、まず以下のようなIAMロールを作成します。

* ウェブIDプロバイダーに EKS の OpenID Connect プロバイダーを指定
* アタッチするポリシーは `AmazonEC2ContainerRegistryReadOnly`のみ
* 最大セッション時間を `12時間` に設定

あとはマニュアルにあるマニフェストで変更箇所を編集して適用します。

```yaml
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: ecr-credentials-sync
  namespace: flux-system
rules:
- apiGroups: [""]
  resources:
  - secrets
  verbs:
  - delete
  - create
---
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
  # Uncomment and edit if using IRSA
  # annotations:
  #   eks.amazonaws.com/role-arn: <role arn>
---
apiVersion: batch/v1beta1
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
            # You will need to set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables if not using
            # IRSA. It is recommended to store the values in a Secret and load them in the container using envFrom.
            # envFrom:
            # - secretRef:
            #     name: aws-credentials
            env:
            - name: REGION
              value: us-east-1 # change this if ECR repo is in a different region
            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: <account id>.dkr.ecr.<region>.amazonaws.com # fill in the account id and region
            volumeMounts:
            - mountPath: /token
              name: token
            command:
            - /bin/bash
            - -ce
            - |-
              kubectl delete secret --ignore-not-found $SECRET_NAME
              kubectl create secret docker-registry $SECRET_NAME \
                --docker-server="$ECR_REGISTRY" \
                --docker-username=AWS \
                --docker-password="$(</token/ecr-token)"
```

CronJob として定義しているので初回は手動で実行します。

```shell-session
$ kubectl create job --from=cronjob/ecr-credentials-sync -n flux-system ecr-credentials-sync-init
```

完了すると認証情報を持ったシークレットが生成されます

```shell-session
$ kubectl get secrets -n flux-system | grep ecr-credentials
ecr-credentials                           kubernetes.io/dockerconfigjson        1      1m
```

[AWS Elastic Container Registry](https://toolkit.fluxcd.io/guides/image-update/#aws-elastic-container-registry)

次に、`ImageRepository` を定義します。ECRなので認証情報として `spec.secretRef.name` に `ecr-credentails` を指定します。

```yaml
apiVersion: image.toolkit.fluxcd.io/v1alpha1
kind: ImageRepository
metadata:
  name: sample-app
  namespace: flux-system
spec:
  secretRef:
    name: ecr-credentials
  image: <AWS_ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/<IMAGE>
  interval: 1m0s
```

そして イメージのタグから命名規則を定義する `ImagePolicy` を定義します。
タグの命名規則は `${GIT_BRANCH}-${GIT_SHORT_SHA}-$(date +%s)` となっていて、`main` ブランチでタイムスタンプが最も大きい（新しい）ものを最新とします。

```yaml
---
apiVersion: image.toolkit.fluxcd.io/v1alpha1
kind: ImagePolicy
metadata:
  name: sample-app
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: sample-app
  filterTags:
    pattern: '^main-[a-fA-F0-9]+-(?P<ts>.*)'
    extract: '$ts'
  policy:
    numerical:
      order: asc
```

マニフェストを適用後、コマンドで確認します。

```shell-session
$ flux get image repository sample-app
NAME       	READY	MESSAGE                        	LAST SCAN                	SUSPENDED
sample-app	True 	successful scan, found 2 tags	2021-04-02T11:43:15+09:00	False
```

```shell-session
$ flux get image policy sample-app
NAME       	READY	MESSAGE   	LATEST IMAGE
sample-app	True 	Latest image tag for '<イメージURL>' resolved to: main-e2dc7e21-1617154068	<イメージURL>:main-e2dc7e21-1617154068
```

ただし今の状態では最新のイメージは検知できても、Deployment などのコンテナイメージを書き換えることはできないので、書き換えたいイメージの箇所にマーカーを記載します。

Kustomization を使っている場合は以下のようになります。

```yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
  name: sample-app
  namespace: sample-app
spec:
  images:
    - name: <イメージURL>
      newName: <イメージURL>
      newTag: latest # {"$imagepolicy": "flux-system:sample-app"}
```

Git レポジトリに書き込みを行う設定を `ImageUpdateAutomation` で定義します

```yaml
apiVersion: image.toolkit.fluxcd.io/v1alpha1
kind: ImageUpdateAutomation
metadata:
  name: flux-system
  namespace: flux-system
spec:
  checkout:
    branch: main
    gitRepositoryRef:
      name: sample-app
  commit:
    authorEmail: fluxcd@example.com
    authorName: fluxcd
    messageTemplate: '{{range .Updated.Images}}{{println .}}{{end}}'
  interval: 1m0s
  push:
    branch: main
  update:
    path: ./clusters/my-cluster
    strategy: Setters
```

これらを適用の後、新しくコンテナイメージをビルドしてイメージレポジトリにプッシュするとプッシュした最新のコンテナイメージにマニフェストが書き換えられて、最新のコンテナイメージがKubernetsにデプロイされます。

Git のログにコミットされている

```shell-session
$ git log -1

commit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Author: flux <flux@gitlab.rux.jp>
Date:   Wed Mar 31 09:24:46 2021 +0000

    <AWS_ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/sample-app:main-e2dc7e21-1617178407
```

`git pull` してファイルを確認しても `newTag` も最初に設定した `latest` から `main-e2dc7e21-1617178407` に書き換わっているのがわかります。

```yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
  name: sample-app
  namespace: sample-app
spec:
  images:
    - name: <イメージURL>
      newName: <イメージURL>
      newTag: main-e2dc7e21-1617178407 # {"$imagepolicy": "flux-system:sample-app"}
```

## まとめ
残念ながら現状ではもう少しな部分もありますが、GAになる頃にはこの辺りが解消されることを期待しています。
A/B テストや Blue/Green デプロイメインとについては Flux project の中で、[Flagger](https://flagger.app/) という別のツールがあるのでこちらと組み合わせるといいのかなと思います。