TECHSTEP

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

Terraform Controllerを試す

今回は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 S3Google 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

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"
        }
    ]
}