progrhyme's tech blog

主にIT関連の技術メモ

TerraformのKubernetes ProviderでK8sのリソース管理にトライ

はじめに

昨日の記事↓で、Kubernetesのリソース設定をTerraformで管理できそうな件について触れた。

TerraformにはKubernetesのProviderもあるので、できるというわけだ。

ググる先行者がいるので、できること自体はわかっているが、オブジェクトの削除や再作成時の動きなど気になるので、自分でも試しておくことにした。

環境構成

さっきこのブログに記事を上げたけど、昨日UbuntuデスクトップにMicrok8sを入れたので、この環境を使う。

  • Ubuntu 18.04 LTS
  • microk8s v1.18.2
  • terraformer v0.8.7
  • terraform v0.12.24
  • terraform-provider-kubernetes v1.11.2

いつもterraformのHCLを書くのにはIntelliJ IDEAを使っていたのだけど、最近Visual Studio Code拡張機能もTerraform v0.12に対応したので、今回はVS Codeを使ってみた。*1

参考: VSCodeでTerraformを書くときの設定(2019/11/07追記: HCL2対応) - Qiita

手順

Microk8sを入れたときに default namespaceに、kubernetes-bootcampというDeploymentを作ったので、これをTerraform管理にして弄ってみる。

下準備

terraformからmicrok8s用のクラスタに接続したいのだが、 ~/.kube/config にcontextが見当たらないので、下のコマンドでkubeconfigファイルを作っておいた。

microk8s kubectl config view > ~/tmp/microk8s/kubeconfig 

また、カレントディレクトリに下の内容で providers.tf を用意する:

provider "kubernetes" {
  config_path = "~/tmp/microk8s/kubeconfig"
  # ちゃんとした証明書を用意してないからか、これをつけてないとterraformコマンドがコケる
  insecure    = true
}

terraformerによるimport→失敗

terraformer自体をまだ入れてなかったので、Linuxbrewでインストール。

brew install terraformer

(実際にはHomebrew Bundleでbundle installしている。)

terraform init によって .terraform/plugins に取得されたプロバイダーの実行ファイルを ~/.terraform.d/plugins にコピーする。

その後、 terraformer import を実行したが、なぜか下のようなメッセージが出て失敗する。

% env KUBECONFIG=~/tmp/microk8s/kubeconfig terraformer import \
  kubernetes --resources=configmaps,deployments,namespaces,pods,secrets,services
2020/05/10 09:22:18 kubernetes importing... configmaps
2020/05/10 09:22:18 kubernetes: configmaps not supported resource

他のリソースを先頭にしても同様。

microk8s特有の問題だろうか?
よくわからないけど、ここはやりたいことの本質部分じゃないので、諦めて手でHCLを書くことにする。

HCLを書いてterraform import

microk8s kubectl get deployment -o yaml で出力したYAMLを見ながらHCLを書き起こす:

resource "kubernetes_deployment" "bootcamp" {
  metadata {
    name = "kubernetes-bootcamp"
    labels = {
      app = "kubernetes-bootcamp"
    }
  }

  spec {
    replicas = 1

    selector {
      match_labels = {
        app = "kubernetes-bootcamp"
      }
    }

    template {
      metadata {
        labels = {
          app = "kubernetes-bootcamp"
        }
      }

      spec {
        container {
          image = "gcr.io/google-samples/kubernetes-bootcamp:v1"
          name  = "kubernetes-bootcamp"
        }
      }
    }
  }
}

下のように terraform import してリソースをTerraformの管理下に置く。
TerraformのBackendは設定してないので、ローカルのカレントディレクトリに terraform.tfstate ファイルが作られる。

% terraform import kubernetes_deployment.bootcamp default/kubernetes-bootcamp
kubernetes_deployment.bootcamp: Importing from ID "default/kubernetes-bootcamp"...
kubernetes_deployment.bootcamp: Import prepared!
  Prepared kubernetes_deployment for import
kubernetes_deployment.bootcamp: Refreshing state... [id=default/kubernetes-bootcamp]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

HCLを編集してリソースを更新してみる

まずは、さっきの kubernetes_deployment.bootcampspec.template.spec.container に下のようなresources設定を足してみる

resources {
  limits {
    memory = "64Mi"
  }
}

terraform apply したログは下の通り:

% terraform apply
kubernetes_deployment.bootcamp: Refreshing state... [id=default/kubernetes-bootcamp]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # kubernetes_deployment.bootcamp will be updated in-place
  ~ resource "kubernetes_deployment" "bootcamp" {
        id = "default/kubernetes-bootcamp"
        :
      ~ spec {
            :
          ~ template {
                :
              ~ spec {
                    :
                  ~ container {
                        :
                      ~ resources {
                          + limits {
                              + memory = "64Mi"
                            }
                        }
                    }
                }
            }
        }

        timeouts {}
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

kubernetes_deployment.bootcamp: Modifying... [id=default/kubernetes-bootcamp]
kubernetes_deployment.bootcamp: Modifications complete after 1s [id=default/kubernetes-bootcamp]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

PodsでDeploymentsの様子をwatchしていたが、下のような動きだった。

  1. 新しいPodが立ち上がる
  2. 古いPodがTerminateされる

ふつうにDeploymentを更新するときと同じで、RollingUpdateなんだろうと思う。(未検証だがdeployment.spec.strategyに従ってくれると信じる)

リソースの名前を変えて、再作成してみる

次に、下のように metadata.name を変えてみる:

  resource "kubernetes_deployment" "bootcamp" {
    metadata {
-     name = "kubernetes-bootcamp"
+     name = "kubernetes-bootcamp2"
      labels = {
        app     = "kubernetes-bootcamp"

terraform apply のログは下の通り(長いので折りたたみにした):

% terraform apply
kubernetes_deployment.bootcamp: Refreshing state... [id=default/kubernetes-bootcamp]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # kubernetes_deployment.bootcamp must be replaced
-/+ resource "kubernetes_deployment" "bootcamp" {
      ~ id = "default/kubernetes-bootcamp" -> (known after apply)

      ~ metadata {
          - annotations      = {} -> null
          ~ generation       = 2 -> (known after apply)
            labels           = {
                "app" = "kubernetes-bootcamp"
            }
          ~ name             = "kubernetes-bootcamp" -> "kubernetes-bootcamp2" # forces replacement
            namespace        = "default"
          ~ resource_version = "44171" -> (known after apply)
          ~ self_link        = "/apis/apps/v1/namespaces/default/deployments/kubernetes-bootcamp" -> (known after
apply)
          ~ uid              = "91420400-2545-4c7a-bc23-2fd6769d41a7" -> (known after apply)
        }

      ~ spec {
            :
          ~ strategy {
              ~ type = "RollingUpdate" -> (known after apply)

              ~ rolling_update {
                  ~ max_surge       = "25%" -> (known after apply)
                  ~ max_unavailable = "25%" -> (known after apply)
                }
            }

          ~ template {
              ~ metadata {
                  - annotations      = {} -> null
                  ~ generation       = 0 -> (known after apply)
                    labels           = {
                        "app" = "kubernetes-bootcamp"
                    }
                  + name             = (known after apply)
                  + resource_version = (known after apply)
                  + self_link        = (known after apply)
                  + uid              = (known after apply)
                }

              ~ spec {
                  - active_deadline_seconds          = 0 -> null
                  - automount_service_account_token  = false -> null
                    dns_policy                       = "ClusterFirst"
                    host_ipc                         = false
                    host_network                     = false
                    host_pid                         = false
                  + hostname                         = (known after apply)
                  + node_name                        = (known after apply)
                  - node_selector                    = {} -> null
                    restart_policy                   = "Always"
                  + service_account_name             = (known after apply)
                    share_process_namespace          = false
                    termination_grace_period_seconds = 30

                  ~ container {
                      - args                     = [] -> null
                      - command                  = [] -> null
                        image                    = "gcr.io/google-samples/kubernetes-bootcamp:v1"
                      ~ image_pull_policy        = "IfNotPresent" -> (known after apply)
                        name                     = "kubernetes-bootcamp"
                        stdin                    = false
                        stdin_once               = false
                        termination_message_path = "/dev/termination-log"
                        tty                      = false

                      ~ resources {
                          ~ limits {
                              + cpu    = (known after apply)
                                memory = "64Mi"
                            }

                          + requests {
                              + cpu    = (known after apply)
                              + memory = (known after apply)
                            }
                        }

                      + volume_mount {
                          :
                        }
                    }

                  + image_pull_secrets {
                      + name = (known after apply)
                    }

                  + volume {
                      + name = (known after apply)

                      + aws_elastic_block_store {
                          + fs_type   = (known after apply)
                          + partition = (known after apply)
                          + read_only = (known after apply)
                          + volume_id = (known after apply)
                        }
                      :
                    }
                }
            }
        }

      - timeouts {}
    }

Plan: 1 to add, 0 to change, 1 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

kubernetes_deployment.bootcamp: Destroying... [id=default/kubernetes-bootcamp]
kubernetes_deployment.bootcamp: Destruction complete after 0s
kubernetes_deployment.bootcamp: Creating...
kubernetes_deployment.bootcamp: Creation complete after 8s [id=default/kubernetes-bootcamp2]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

この場合、Kubernetes的に別々のリソースとして認識されるからだろうけど、リソースの再作成が行われる。
再作成だからか、変えてない差分もたくさん出てくる。ログではいくらか間引いたが、それでも長くなった。

大事なのは実行ログの最後のところで、先にDestroyが走ってから、Createされている。

この挙動については、HCLのresource設定で lifecycle ブロックを設定することで変えることができる。

  resource "kubernetes_deployment" "bootcamp" {
    :
+   lifecycle {
+     create_before_destroy = true
+   }
  }

これをつけて metadata.name を変えて terraform apply を再実行すると、期待した動きになった。 実行ログの最後のところだけ下に貼る:

% terraform apply
:
  Enter a value: yes

kubernetes_deployment.bootcamp: Creating...
kubernetes_deployment.bootcamp: Creation complete after 8s [id=default/kubernetes-bootcamp3]
kubernetes_deployment.bootcamp: Destroying... [id=default/kubernetes-bootcamp2]
kubernetes_deployment.bootcamp: Destruction complete after 0s

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

所感

普通に使えそうなので、これでマニフェスト管理するのも全然アリではないだろうか。

リソースの削除も反映できるし、HCLの機能が使えるので、素のYAML管理やKustomizeを使うより、運用の体感としては良さそうに思う。

ただ、Kubernetesから見ると非公式ツールだし、対応してないリソースがあったり、K8sの最新版に追従できていなかったりする可能性は考慮が必要だろう。
見たところ、よく使いそうなリソースは一通り揃っていると思うけど。

参考記事では、Secretsの管理で使うと便利という例があったので、ピンポイントで使うのもありかもしれない。

参考記事