Terraform Cloud Businessを利用し、Tanzu上のKubernetesリソースを管理する

HashiCorp JapanでSolutions Engineerをしております、伊藤忠司と申します。
TUNA-JP (Tanzu User kNowledge Assembly Japan)のアドベントカレンダーの20日目という事で、HashiCorpが提供しているTerraform Cloud Businessで利用出来るTerraform Cloud AgentをTanzu上で動かして、Tanzu上のKubernetesリソースをTerraformのフレームワークで管理するという内容を紹介したいと思います。

以下の様な流れで本記事は記載しています。

## 目次

- [Terraform Cloud Businessとは](#terraform-cloud-businessとは)
- [Terraform Cloud Agentとは](#terraform-cloud-agentとは)
- [Terraform Cloud Agentのセットアップ](#terraform-cloud-agentのセットアップ)
- [Terraform Cloud AgentのKubernetesへのデプロイ](#terraform-cloud-agentのkubernetesへのデプロイ)
- [Terraformを使ったKubernetesリソースのプロビジョニング](#terraformを使ったkubernetesリソースのプロビジョニング)
- [まとめ](#まとめ)

## Terraform Cloud Businessとは
先ず、Terraform Cloud Business(以下TFCB)についてです。TFCBは、HashiCorpがマネージド・サービスとして提供している[Terraform Cloud(以下TFC)](https://www.terraform.io/cloud)の有償版になります。

[Terraform](https://www.terraform.io/)はIaCの一つでして、プロビジョニングに関わる手動での運用作業をコード化し、自働化する事が出来ます。

ヒトがクラウドのGUIやAPIを直接叩いて実行する代わりに、TerraformがクラウドのAPIを実行し、コードに従ってインフラのプロビジョニングを行います。これによって、ヒトが実施しないといけない作業を削減する事が出来ます。

また、Terraformはマルチクラウド環境で同じ仕組みが利用出来るようになっておりますので、プロビジョニング先の環境が、AWS、Azure、GCP、vSphereだろうと、同じワークフロー、同一の構成要素、同一の文法を用いて、プロビジョニングを実施して頂く事が可能になりますので、汎用性が非常に高いという特徴を持っています。

Terraformですが、実際に扱っていく際の典型的なワークフローは以下の様な形になります。
![tf-workflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2274064/312ff11d-ff90-c3e4-efb5-5eeaab1c8cc3.png)

Hashicorp Configuration Language(以下HCL)で記述されたソースコード、あとはクラウドにインフラをプロビジョニングする際のシークレット情報などの変数を用いて、`terraform plan` というコマンドを実行してあげます。

そうすると、TerraformがHCLを実行する場合に、どういった変更が行われるか？という、プラン内容を出力してくれます。そのあとは、プラン内容のレビューを実施します。

その内容が社内のポリシーに沿っているかどうか等をレビューし、時には修正依頼を掛けたりし、内容に問題がなければ、`terraform apply` を実行し、プロビジョニングを行っていくというワークフローになります。

プロビジョニングが無事成功すると、インフラの状態を記述したファイルであるステートファイルが生成されるといった形になります。

インフラを更新する様な際は、更新したHCL、最新のステートファイル、必要な変数とともに、`terraform plan` を実行し、レビューをし、問題無ければ、`terraform apply` というワークフローを回していく事になります。

ただ、Terraformを安全に利用していくためには、以下のような考慮事項が出てきます。

![tf-workflow-considerations.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2274064/e9a5d241-6219-5b6f-7d52-2ff9cb945af9.png)

この様な考慮事項に関して、HashiCorpが考えるTerraformを扱う上でのベストプラクティスに沿って、OOTBでTerraformをお使い頂ける様になっているのが、有償版Terraformになります。[TFCB](https://www.hashicorp.com/products/terraform/pricing)も、HashiCorpが提供している有償版Terraformの一つになります。

![tf-workflow-with-enterprise.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2274064/146783f8-330b-04ca-b3d1-fd093d6c203f.png)

## Terraform Cloud Agentとは

有償版Terraformをご利用頂く事で、OOTBで色々な機能が使える様になりますが、TFCのネットワーク構成を検討していく際の課題として挙げられるのが、TFCからプロビジョニング対象へのインバウンドでのリクエストがあります。

[TFCが利用するIPレンジ](https://www.terraform.io/docs/cloud/api/ip-ranges.html)は公開されておりますので、お使いの環境のファイアーウォール等に対する設定変更して頂く事で利用する事が出来ますが、作業負荷の高い変更作業かと思います。
また、そもそも、自社環境に対してインバウンドのアクセスを許可するというのはセキュリティポリシー上難しいというのが、エンタープライズのお客様では特に多いのでは無いかと思います。

![tfc-network-issue.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2274064/a2840011-e80e-8f6d-45c0-01f6c90b4759.png)

この様なTFC利用時のネットワーク構成課題を解決するソリューションとして、TFCBで利用出来る[Terraform Cloud Agents](https://www.terraform.io/docs/cloud/agents/index.html)(以下TFC Agent)という機能が存在します。

![tfcb-network.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2274064/3e2cb92a-429a-1504-23e4-cbcf9dfe3da9.png)

この機能をご利用頂くと、TFC上の[Workerインスタンス](https://www.terraform.io/docs/cloud/run/run-environment.html#the-terraform-worker-vms)が対象となる環境に対して、プロビジョニングを行っていくのではなく、TFC Agentがプロビジョニングを行っていく形になります。

自社のデータセンターやクラウド環境にデプロイしたTFC AgentとTFCが連携し、TFCに発生した様々なイベント、Version Control System(VCS)の更新情報とかを、TFC Agentがアウトバウンドで取得していく形になります。
TFC内の更新をポーリングし、何かしら変更があったら、TFC Agentから、その更新を取得し、プロビジョニング対象となる環境に対して、TFC Agent上で`terraform` CLIを実行し、プロビジョニングを行っていくという形になります。

この構成を取る場合、[HTTPSのエンドポイント](https://www.terraform.io/docs/cloud/agents/index.html#networking-requirements)が唯一のエンドポイントになりますので、TFC Agentからこのエンドポイントに対して、アウトバウンドのリクエストを出来るようにしてあげれば、他の設定は必要なく、かつエンドポイントのURLが変わる事は無いので、設定も永続的にお使い頂く事が可能になります。

TFC Agentは、[Linuxのバイナリー、もしくはDockerコンテナとして利用可能](https://www.terraform.io/docs/cloud/agents/index.html#supported-operating-systems)ですので、プラットフォームフリーでお使い頂ける様になっております。

この後は、このTFC AgentをTanzu Kubernetes Grid multicloud(以下TKGm)上にデプロイし、TKGｍ上にデプロイしたTFC Agentを利用し、TKGｍ上にKubernetesリソースをTerraformのワークフローに従って、プロビジョニングを行っていくという内容をご紹介したいと思います。

## Terraform Cloud Agentのセットアップ

TFCBをご利用出来る事が前提になりますが、こちらの[Managing Agent Pools](https://www.terraform.io/docs/cloud/agents/index.html#managing-agent-pools)の手順に従って、TFC上でAgent Poolを作成します。

Agent Poolを作成し、新しいトークンを作成するという手順を実施すると、以下のような形でDockerコンテナとして、TFC Agentを動かす際のインストラクションが表示されます。

```console
# Connect to your Docker host and set the following environment variables. TFC_AGENT_NAME is optional.
$ export TFC_AGENT_TOKEN=...
$ export TFC_AGENT_NAME=<my_agent_name>

# Once the environment is configured, run the Docker container with the following command or download the agent file.
$ docker run -e TFC_AGENT_TOKEN -e TFC_AGENT_NAME hashicorp/tfc-agent:latest
```

今回はTKGmを用いて、TFC Agentの管理はKubernetesに任せる形にしますので、`TFC_AGENT_TOKEN`のみコピーしておきます。

## Terraform Cloud AgentのKubernetesへのデプロイ

Kubernetes環境にTFC Agentをデプロイしていきます。

ここで利用するKubernetesクラスタとしては、[TKGｍ on vSphere](https://blogchobichobi.blogspot.com/2021/10/posts-45.html)を利用していますが、[Tanzu Community Editions](https://tanzucommunityedition.io/)でも同じ様なステップでKubernetesクラスタ環境をインストール出来ると思います。

TFC Agent用のネームスペースを作成し、先程取得した`TFC_AGENT_TOKEN`を作成したネームスペースにsecretリソースとして作成します。

```bash
export KUBECONFIG=~/kubeconfig-lab-fender
k create ns tfc-agent
# 先程取得したTFC_AGENT_TOKENをBase64でエンコードします
echo -n "t..." |base64
```

Base64で出力された値をkey`token` に対するvalueとして、以下のようなマニフェスト`tfc-agent-secrets.yml` を作成します。

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: agent
type: Opaque
data:
  token: t...
```

TFC Agentトークンを定義したSecretリソースを作成します。

```console
k -n tfc-agent apply -f tfc-agent-secrets.yml
```

次に、TKGmのWorkloadクラスタにTFC AgentをデプロイさせるDeploymentリソースを`deployment-tfc-agent.yml`というファイル名で作成します。以下の様な形でマニフェストファイルを作成し、環境変数`TFC_AGENT_TOKEN`、`TFC_AGENT_NAME`を設定します。

ここでは指定していませんが、`TF_VAR_xxx`という変数名で変数を指定しておく事で、Terraformを実行する際のTerraform変数として、TFC Agentに持たせてあげる事も可能です。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: tfc-agent
  name: tfc-agent
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tfc-agent
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: tfc-agent
    spec:
      containers:
      - image: hashicorp/tfc-agent:1.0.2
        name: tfc-agent
        resources:
          requests:
            memory: "64Mi"
            cpu: "250m"
          limits:
            memory: "128Mi"
            cpu: "500m"
        env:
        - name: "TFC_AGENT_TOKEN"
          valueFrom:
            secretKeyRef:
              name: agent
              key: token
        - name: "TFC_AGENT_NAME"
          value: "tfc-agent-on-fender"
status: {}
```

このマニフェストファイルを使って、TKGmのWorkloadクラスタ上に、TFC Agentをデプロイします。

```console
k -n tfc-agent apply -f deployment-tfc-agent-tanzu.yml 
```
```console
$ k -n tfc-agent get all
NAME                                       READY   STATUS    RESTARTS   AGE
pod/tfc-agent-69cd86fb9f-mxt8r             1/1     Running   0          12m

NAME                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/tfc-agent             1/1     1            1           45d

NAME                                             DESIRED   CURRENT   READY   AGE
replicaset.apps/tfc-agent-69cd86fb9f             1         1         1       12m
```

無事、TKGm上へTFC Agentのデプロイが完了したので、このTFC Agentを利用し、TKGm上にTerraformのワークフローを用いて、Kubernetesリソースをプロビジョニングしていきます。

## Terraformを使ったKubernetesリソースのプロビジョニング

環境としては、以下のような形で準備しています。Terraformが[Kubernetes provider](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs)を利用する際に必要なシークレット情報は、[Vault](https://www.vaultproject.io/)のK/Vストアに暗号化し、保管してあります。

![pic-tfc-agent-on-tanzu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2274064/bae034a2-05c2-583c-3967-315f9c041182.png)

また、今回利用したTerraformのコードは以下の通りです。

<details>
<summary>main.tf</summary><div>

```hcl
provider "vault" {
  address         = var.vault_address
  skip_tls_verify = true
  auth_login {
    path = "auth/approle/login"
    parameters = {
      role_id   = var.login_approle_id
      secret_id = var.login_approle_secret_id
    }
  }
}

data "vault_generic_secret" "vsphere" {
  path = "secret/lab_1t0chu_dev/tkg_fender"
}

provider "kubernetes" {
  host                   = data.vault_generic_secret.vsphere.data["host"]
  client_certificate     = data.vault_generic_secret.vsphere.data["client_certificate"]
  client_key             = data.vault_generic_secret.vsphere.data["client_key"]
  cluster_ca_certificate = data.vault_generic_secret.vsphere.data["cluster_ca_certificate"]
}

resource "kubernetes_namespace" "example" {
  metadata {
    labels = {
      mylabel = "tanzu-test"
    }

    name = "terraform-example-namespace"
  }
}

resource "kubernetes_deployment" "nginx" {
  metadata {
    name = "nginx-example-on-tanzu"
    labels = {
      App = "ScalableNginxExample"
    }
    namespace = kubernetes_namespace.example.id
  }

  spec {
    replicas = 4
    selector {
      match_labels = {
        App = "ScalableNginxExample"
      }
    }
    template {
      metadata {
        labels = {
          App = "ScalableNginxExample"
        }
      }
      spec {
        container {
          image = "bitnami/nginx:1.20.2"
          name  = "example"

          port {
            container_port = 80
          }

          resources {
            limits = {
              cpu    = "0.5"
              memory = "512Mi"
            }
            requests = {
              cpu    = "250m"
              memory = "50Mi"
            }
          }
        }
      }
    }
  }
}

resource "kubernetes_service" "nginx" {
  metadata {
    name      = "nginx-example"
    namespace = kubernetes_namespace.example.id
  }
  spec {
    selector = {
      App = kubernetes_deployment.nginx.spec.0.template.0.metadata[0].labels.App
    }
    port {
      node_port   = 30201
      port        = 80
      target_port = 80
    }

    type = "NodePort"
  }
}

```
</div></details>

TFC上の[ワークスペース](https://www.terraform.io/docs/cloud/workspaces/index.html)の設定を行い、TKGｍ上にデプロイした[TFC Agent経由でプロビジョニングが行われる様に設定し](https://www.terraform.io/docs/cloud/agents/index.html#configuring-workspaces-to-use-the-agent)、Terraform Runを実行します。

このワークスペースに関しては、[VCS連携](https://www.terraform.io/docs/cloud/workspaces/vcs.html)していますので、`git commit`などワークスペースが連携しているレポジトリの更新をトリガーにTerraform Runが実行される様になっております。Terraform Runが実行されると、先ずは`terraform plan`が実行され、実際にプロビジョニングが実行されると、どのような変更がなされるか出力されます。 

![pic-tfc-plan-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2274064/68ed5eb0-4e75-5c48-81f4-7d5a6d4db708.png)

プロビジョニングされるネームスペースリソースを見てみますと、以下の様にどの様な変更がされるか、視覚的に確認する事が出来ます。

![pic-tfc-plan-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2274064/39d2c842-2ce3-1ab7-757e-e97596cd4a4d.png)

KubernetesリソースはYAMLのマニフェストで管理される事が一般的かと思いますが、Terraform/HCLのフレームワークを使って、Kubernetesリソースを管理することで、Dry-Run、モジュラリティ、プロバイダやリソース間での値の参照などを行えるメリットがあります。

コードが問題無く通ったので、`terraform apply`を実行し、TKGm上にKubernetesリソースをプロビジョニングします。

![pic-tf-apply.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2274064/61217e7c-f8cb-7870-4fd6-041d09718fa1.png)

TKGmのWorkloadクラスタに、コードで宣言したKubernetesリソースがプロビジョニングされているかどうか確認してみます。

```console
$ k get ns terraform-example-namespace
NAME                          STATUS   AGE
terraform-example-namespace   Active   99s
```
```console
$ k -n terraform-example-namespace get all
NAME                                          READY   STATUS    RESTARTS   AGE
pod/nginx-example-on-tanzu-6f4477ddf5-77727   1/1     Running   0          106s
pod/nginx-example-on-tanzu-6f4477ddf5-d2gpv   1/1     Running   0          106s
pod/nginx-example-on-tanzu-6f4477ddf5-r4bdf   1/1     Running   0          106s
pod/nginx-example-on-tanzu-6f4477ddf5-wcsq8   1/1     Running   0          106s

NAME                    TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/nginx-example   NodePort   100.66.204.68   <none>        80:30201/TCP   80s

NAME                                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-example-on-tanzu   4/4     4            4           106s

NAME                                                DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-example-on-tanzu-6f4477ddf5   4         4         4       106s
```
```console
$ k -n terraform-example-namespace describe deployment.apps/nginx-example-on-tanzu
Name:                   nginx-example-on-tanzu
Namespace:              terraform-example-namespace
CreationTimestamp:      Tue, 07 Dec 2021 22:27:42 +0900
Labels:                 App=ScalableNginxExample
Annotations:            deployment.kubernetes.io/revision: 1
Selector:               App=ScalableNginxExample
Replicas:               4 desired | 4 updated | 4 total | 4 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:  App=ScalableNginxExample
  Containers:
   example:
    Image:      bitnami/nginx:1.20.2
    Port:       80/TCP
    Host Port:  0/TCP
    Limits:
      cpu:     500m
      memory:  512Mi
    Requests:
      cpu:        250m
      memory:     50Mi
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   nginx-example-on-tanzu-6f4477ddf5 (4/4 replicas created)
Events:          <none>
```

問題なくKubernetesリソースがTKGmのWorkloadクラスタ上にプロビジョニング出来ている事を確認出来ました。

この様に、インフラリソースだけではなく、Kubernetesリソースに関しても、TerraformのKubernete providerを使って、Terraformのフレームワークに則った形で管理する事が出来ます。

## まとめ

Terrformはインフラのプロビジョニングだけではなく、Kubernetesリソースも同一のフレームワークで管理出来ます。また、有償版TerraformはOOTBでTerraformのワークフローにおけるベストプラクティスがお使い頂ける様になっておりますので、ご興味あれば[こちら](https://local.hashicorp.com/ja/contact-sales)からお問い合わせ下さい。