今回はTerraformリソースをGitOpsで管理することを可能にする、 Terraform Controllerを試してみます。
Terraform Controllerとは
Terraform Controller (以降 tf-controller) は、Fluxなどを開発するWeave works社が開発するKubernetes Controllerの一つで、名称の通りTerraformリソースを管理するControllerです。tf-controllerはFluxと組み合わせることを前提としており、TerraformリソースをGitOpsのスタイルで管理することを実現します。
Terraform Controllerの機能
tf-controllerは以下のような機能を持ちます。
GitOps Automation for Terraform
tf-controllerは Terraform
というカスタムリソースを管理します。 Terraform
リソースには .spec.approvePlan
というパラメータがあり、このパラメータを変更するとtf-controllerがどのタイミングで terraform apply
を実行するか変更することができます。例えば .spec.approvePlan: auto
にすると自動的にapplyを実行するため、起動中のリソースと定義ファイルに差分 (Drift) を検知すると自動的に適用することができます。
なお、 .spec.approvePlan
で指定できる値は、主に以下の3種類があるようです。
auto
: controllerは自動的にすべてのplanを承認し、applyを実行するdisable
: driftの検知のみするようcontrollerに伝え、plan/apply stageをスキップする空欄
: planのみ実行するようcontrollerに伝え、planの実行とapproveに必要な文字列の生成を行う。plan実行後に指定の文字列を追加すると、再度Sync時にapplyを実行する
Drift detection
前項の繰り返しになりますが、tf-controllerは定義ファイルとリソースの設定に差分があればそれを検知します (Drift detection)。これを制御する方法は主に2つあり、一つは spec.approvePlan: disable
と設定することで、こうするとDrift detectionのみを行い、plan/applyを実行しなくなります。
もう一つは spec.disableDriftDetection
というパラメータを変更します。このパラメータはデフォルトで false
に設定されていますが、これを true
にすることでDrift detectionをdisableにすることができます。
Yaml-based Terraform
tf-controllerは、Tofu-jetというジェネレータを利用して、初めから利用可能なTerraform module (primitive module) を用意しています。これを使用すると、 Terraform
リソースの中でパラメータを指定することで、AWS IAMなどのクラウドリソースを管理することができます。
tfstate management
tf-controllerはTerraformリソースを作成するとき、デフォルトではKubernetes Secretリソースとして tfstateファイルを生成します。ただ、spec.backendConfig.customConfiguration
を設定すると、Amazon S3やGoogle Cloud Storageなどの外部リソースを利用することもできます。
dependency management
tf-controllerで管理する Terraform
リソースは、別のリソースとの間で依存関係を設定することができます。これはFluxのKustomize controllerをベースにしており、 .spec.dependsOn
のパラメータに Terraform
リソースを指定すると、対象のリソースが存在する場合のみリソースが作成されます。
Terraform Controllerを動かす
ここからtf-controllerを動かしてみます。
前提条件
tf-controllerはFluxを前提に動作するcontrollerなので、事前にFluxのインストールされたAmazon EKSクラスターを用意します。
Client Version: version.Info{Major:"1", Minor:"22", GitVersion:"v1.22.0", GitCommit:"c2b5237ccd9c0f1d600d3072634ca66cefdf272f", GitTreeState:"clean", BuildDate:"2021-08-04T18:03:20Z", GoVersion:"go1.16.6", Compiler:"gc", Platform:"linux/amd64"} Server Version: version.Info{Major:"1", Minor:"22+", GitVersion:"v1.22.16-eks-ffeb93d", GitCommit:"52e500d139bdef42fbc4540c357f0565c7867a81", GitTreeState:"clean", BuildDate:"2022-11-29T18:41:42Z", GoVersion:"go1.16.15", Compiler:"gc", Platform:"linux/amd64"} $ kubectl get pods -n flux-system NAME READY STATUS RESTARTS AGE helm-controller-85b89f8958-zrx9j 1/1 Running 0 36s kustomize-controller-59d6498d5-lhskt 1/1 Running 0 36s notification-controller-7cf6f94f46-q5w2k 1/1 Running 0 36s source-controller-b8bf68987-tn69v 1/1 Running 0 36s
そのほかの前提条件は以下の通りです。
- Fluxのバージョンは、一定のバージョンより新しい必要がある (tf-controller 0.13.0の場合、Fluxは
0.32.0
以上) - tf-controllerが各種Podと通信できるよう、一部ポートの通信を許可する必要がある
- Runner Podの起動するNamepsace:
30000
- Flux source-controller:
80
- Flux notification-controller:
80
- Runner Podの起動するNamepsace:
tf-controllerをインストール
tf-controllerのインストーラーはFluxの HelmRepository
HelmRelease
リソースで定義されたものが提供されているので、こちらを使います。
$ kubectl apply -f https://raw.githubusercontent.com/weaveworks/tf-controller/main/docs/release.yaml helmrepository.source.toolkit.fluxcd.io/tf-controller created helmrelease.helm.toolkit.fluxcd.io/tf-controller created $ kubectl get helmrepository -n flux-system NAME URL AGE READY STATUS tf-controller https://weaveworks.github.io/tf-controller/ 24s True stored artifact for revision '483cdb04ff510d91707adfeca19a91a4bdf3a19506074195d300ff6866a6fc5b' $ kubectl get helmrelease -n flux-system NAME AGE READY STATUS tf-controller 43s True Release reconciliation succeeded
しばらくするとtf-controller Podが起動して完了です。
$ kubectl get crd NAME CREATED AT alerts.notification.toolkit.fluxcd.io 2022-12-24T08:01:06Z buckets.source.toolkit.fluxcd.io 2022-12-24T08:01:06Z eniconfigs.crd.k8s.amazonaws.com 2022-12-24T07:39:57Z gitrepositories.source.toolkit.fluxcd.io 2022-12-24T08:01:06Z helmcharts.source.toolkit.fluxcd.io 2022-12-24T08:01:06Z helmreleases.helm.toolkit.fluxcd.io 2022-12-24T08:01:06Z helmrepositories.source.toolkit.fluxcd.io 2022-12-24T08:01:06Z kustomizations.kustomize.toolkit.fluxcd.io 2022-12-24T08:01:06Z ocirepositories.source.toolkit.fluxcd.io 2022-12-24T08:01:07Z providers.notification.toolkit.fluxcd.io 2022-12-24T08:01:07Z receivers.notification.toolkit.fluxcd.io 2022-12-24T08:01:07Z securitygrouppolicies.vpcresources.k8s.aws 2022-12-24T07:40:01Z terraforms.infra.contrib.fluxcd.io 2022-12-24T08:04:19Z # 追加されたCRD $ kubectl get pods -n flux-system NAME READY STATUS RESTARTS AGE helm-controller-85b89f8958-zrx9j 1/1 Running 0 4m47s kustomize-controller-59d6498d5-lhskt 1/1 Running 0 4m47s notification-controller-7cf6f94f46-q5w2k 1/1 Running 0 4m47s source-controller-b8bf68987-tn69v 1/1 Running 0 4m47s tf-controller-5d44bdcbf7-pfnqm 1/1 Running 0 96s # 新規Pod
Hello Worldリソースの作成
ここから実際にTerraformリソースの管理を試します。
まずはTerraformファイルの配置されたGitリポジトリを登録するため、以下のマニフェストファイルをデプロイします。
apiVersion: source.toolkit.fluxcd.io/v1beta1 kind: GitRepository metadata: name: helloworld namespace: flux-system spec: interval: 30s url: https://github.com/tf-controller/helloworld ref: branch: main
$ kubectl apply -f gitrepo-helloworld.yaml gitrepository.source.toolkit.fluxcd.io/helloworld created $ kubectl get gitrepository -n flux-system NAME URL AGE READY STATUS helloworld https://github.com/tf-controller/helloworld 12s True stored artifact for revision 'main/d9c5cc348e555526ea563fb82fc901e37de4d732'
Hello worldのチュートリアルでは、以下のような Terraform
ファイルを使用します。
apiVersion: infra.contrib.fluxcd.io/v1alpha1 kind: Terraform metadata: name: helloworld namespace: flux-system spec: interval: 1m approvePlan: auto path: ./ sourceRef: kind: GitRepository name: helloworld namespace: flux-system
上記ファイルをデプロイします。
$ kubectl apply -f tf-helloworld.yaml terraform.infra.contrib.fluxcd.io/helloworld created # 作成中 $ kubectl get terraform -n flux-system NAME READY STATUS AGE helloworld Unknown Reconciliation in progress 11s # 作成完了 $ kubectl get terraform -n flux-system NAME READY STATUS AGE helloworld True Applied successfully: main/d9c5cc348e555526ea563fb82fc901e37de4d732 23s
tf-controllerは、管理するTerraformリソースの状態 (tfstate) や planなどをSecretリソースに管理しています。Secretリソースを見ると、新たなリソースが作成されていることを確認できます。
$ kubectl get secret -n flux-system NAME TYPE DATA AGE default-token-wm64p kubernetes.io/service-account-token 3 13m helm-controller-token-vsw84 kubernetes.io/service-account-token 3 13m kustomize-controller-token-czctc kubernetes.io/service-account-token 3 13m notification-controller-token-7btz4 kubernetes.io/service-account-token 3 13m sh.helm.release.v1.tf-controller.v1 helm.sh/release.v1 1 10m source-controller-token-tt56n kubernetes.io/service-account-token 3 13m terraform-runner.tls-1671955464 kubernetes.io/tls 4 2m56s tf-controller-token-b4lsh kubernetes.io/service-account-token 3 10m tf-runner-token-tfvsh kubernetes.io/service-account-token 3 10m tfplan-default-helloworld Opaque 1 2m40s # 新規作成 tfstate-default-helloworld Opaque 1 2m40s # 新規作成
これらSecretリソースの中を見ると、tfstate/planの情報が取得できます。tfplan-default-helloworld
はなぜか文字化けして読めなかったのですが、 tfstate-default-helloworld
のほうは以下のように確認することができました。
$ kubectl get secret -n flux-system tfstate-default-helloworld -o jsonpath='{.dat a.tfstate}' | base64 -d | gzip -d { "version": 4, "terraform_version": "1.3.1", "serial": 1, "lineage": "4d62fa7c-68ed-f488-b2c7-e699d0b97e98", "outputs": { "hello_world": { "value": "Hello, World!", "type": "string" } }, "resources": [], "check_results": [] }
primitive moduleによるS3バケットの作成
ここからはAmazon S3バケットをtf-controllerから作成し、手動で設定を変更したのちにDrift Detectionをトリガーに設定が修正されるかを見てみます。
今回は primitive moduleを利用し、 Terraform
リソースの中で必要なパラメータを指定する形で新規S3バケットを作成します。
まず、tf-controllerがAWSリソースを操作できるよう、AWS環境へのアクセス権限を付与します。Amazon EKSの場合はIAM Role for Service Account (IRSA) を利用することも可能ですが、今回はドキュメントに記載のある、Access Key/Secret Access KeyをSecretリソースに定義する方法で行いました。
以下のようなマニフェストファイルを用意し、あらかじめクラスター上に作成しておきます。
apiVersion: v1 kind: Secret metadata: name: aws-credentials namespace: flux-system type: Opaque stringData: AWS_ACCESS_KEY_ID: XXXXXXXXXXXXXXXXXXXX AWS_SECRET_ACCESS_KEY: xxxxxxxxxxxxxxxxxxxx AWS_REGION: ap-northeast-1
$ kubectl apply -f aws-credentials.yml
secret/aws-credentials created
続いて、primitive moduleを格納するOCI リポジトリを追加し、primitive moduleを利用可能にします。以下のファイルをデプロイすることで、primitive moduleを利用可能になります。
apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: OCIRepository metadata: name: aws-package namespace: flux-system spec: interval: 30s url: oci://ghcr.io/tf-controller/aws-primitive-modules ref: tag: v4.38.0-v1alpha9
$ kubectl apply -f aws-package.yaml Warning: resource ocirepositories/aws-package is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically. ocirepository.source.toolkit.fluxcd.io/aws-package configured $ kubectl get ocirepository -n flux-system NAME URL READY STATUS AGE aws-package oci://ghcr.io/tf-controller/aws-primitive-modules True stored artifact for digest 'v4.38.0-v1alpha9/b9e0b7aefd72bec52f1ee922ca76fda112263f037a239ac969e042fde8810332' 37m
最後にS3バケットを作成するため、以下のファイルを使用します。 .spec.values
にバケット名やタグなどの情報を指定します。また先ほど作成した aws-credentials
Secretを使用するため、spec.runnerPodTemplate
でSecretを指定します。
apiVersion: infra.contrib.fluxcd.io/v1alpha1 kind: Terraform metadata: name: aws-s3-bucket namespace: flux-system spec: path: aws_s3_bucket values: bucket: my-tf-controller-test-bucket-20221224 tags: Environment: Dev Name: My bucket sourceRef: kind: OCIRepository name: aws-package approvePlan: auto interval: 1h0m destroyResourcesOnDeletion: true runnerPodTemplate: spec: envFrom: - secretRef: name: aws-credentials
$ kubectl apply -f aws-s3.yaml terraform.infra.contrib.fluxcd.io/aws-s3-bucket created $ kubectl get terraform -n flux-system NAME READY STATUS AGE aws-s3-bucket Unknown Applying 25s helloworld True No drift: main/d9c5cc348e555526ea563fb82fc901e37de4d732 32m
aws s3 ls
コマンドでバケット一覧を表示すると、確かにS3バケットが作成されていることを確認できます。
$ aws s3 ls
(抜粋)
2022-12-24 17:43:38 my-tf-controller-test-bucket-20221224
Drift detectionのテスト
先ほど作成したS3バケットを使って、Drift detectionのテストをしてみます。S3バケットのタグをAWSマネジメントコンソールから変更し、以下のような設定にします。
# 変更前のタグ $ aws s3api get-bucket-tagging --bucket my-tf-controller-test-bucket-20221224 { "TagSet": [ { "Key": "Environment", "Value": "Dev" }, { "Key": "Name", "Value": "My bucket" } ] } # 変更後のタグ $ date && aws s3api get-bucket-tagging --bucket my-tf-controller-test-bucket-20221224 Sat Dec 24 17:48:31 JST 2022 { "TagSet": [ { "Key": "Environment", "Value": "Dev modified" }, { "Key": "Name", "Value": "My bucket modified" } ] }
しばらくするとtf-controllerがリソースの状態を確認し、Driftを検知します。 ここでtf-controllerがリソースの状態を確認する頻度は、 Terraform
リソースの .spec.interval
に指定された時間ごとになります。今回使用した定義ファイルでは、1時間ごとにリソースの状態を確認します。
kubectl get terraform
コマンドを実行すると、以下のようにDriftの検知と再デプロイの様子を確認できます。
$ kubectl get terraform -n flux-system -w NAME READY STATUS AGE aws-s3-bucket True Applied successfully: v4.38.0-v1alpha9/b9e0b7aefd72bec52f1ee922ca76fda112263f037a239ac969e042fde8810332 59m helloworld True No drift: main/d9c5cc348e555526ea563fb82fc901e37de4d732 91m aws-s3-bucket Unknown Reconciliation in progress 60m aws-s3-bucket Unknown Initializing 60m aws-s3-bucket False ... 60m aws-s3-bucket Unknown Terraform Planning 60m aws-s3-bucket Unknown Plan generated 61m aws-s3-bucket Unknown Applying 61m aws-s3-bucket Unknown Applying 61m aws-s3-bucket Unknown Applied successfully: v4.38.0-v1alpha9/b9e0b7aefd72bec52f1ee922ca76fda112263f037a239ac969e042fde8810332 61m aws-s3-bucket True Applied successfully: v4.38.0-v1alpha9/b9e0b7aefd72bec52f1ee922ca76fda112263f037a239ac969e042fde8810332 61m
S3バケットを見ると、元のタグの値に修正されていることもわかります。
$ date && aws s3api get-bucket-tagging --bucket my-tf-controller-test-bucket-20221224 Sat Dec 24 18:45:08 JST 2022 { "TagSet": [ { "Key": "Environment", "Value": "Dev" }, { "Key": "Name", "Value": "My bucket" } ] }