TerraformでHelmのReleaseを管理して、JenkinsをMinikube上に構築してみる

2022/10/01に公開

概要

  • TerraformでHelmのReleaseを管理できるようなので、やってみる

gist

御託はいいので、terraform applyだけさせろ!派の方はgistに最終的な構成を置いてるので、こちらをご参照ください。
ただし、前準備が必要なのは、変わらないのでご注意ください。

https://gist.github.com/oniku-2929/63eb8e54c9d88e5af0de9fdf1dbefd53

こういう用途で参考になるかも?

  • 複数のHelm Releaseの構成管理をterraformでまとめて or 分けて管理できる
  • Minikube,EKSやGKE等のKubernetesクラスタ自体の構成とHelmの構成をまとめて or 分けて管理できる

Helm Providerについて

Hashicorp公式のHelm Providerが存在します。

https://registry.terraform.io/providers/hashicorp/helm/latest/docs

また、公式のチュートリアルページも用意されています。

https://learn.hashicorp.com/tutorials/terraform/helm-provider?in=terraform/kubernetes&_ga=2.228686748.1671055028.1664089494-643606297.1655735501

今回はJenkins用のHelm chartを使わせてもらい, Minikube上にJenkinsを構築してみます。
https://github.com/jenkinsci/helm-charts

動作環境構築

動作の前提条件としてHelmとMinikubeの動作環境構築及び、JenkinsのHelmリポジトリの追加作業が必要なのでそれぞれ準備します

Minikube

https://minikube.sigs.k8s.io/docs/start/

Helm

https://helm.sh/docs/intro/install/

Jenkinsのリポジトリを追加

https://github.com/jenkinsci/helm-charts#usage
下記コマンドを実行

helm repo add
helm repo add jenkins https://charts.jenkins.io

.tfファイルを書いていく

まずは簡単なtfファイルを書いて、動作確認してみます。

providerの設定

providerの設定を記載していきます。

https://registry.terraform.io/providers/hashicorp/helm/latest/docs

backendやproviderのバージョン等の情報はお手元で変更の必要があれば、適正変更してください。

helm.tf
terraform {
  backend "local" {}
  required_providers {
    helm{
      source  = "hashicorp/helm"
      version = "~= 2.6.0"
    }
  }
}

provider "helm" {
  kubernetes {
    config_path = "~/.kube/config"
  }
}

もし、すでに複数のconfigがあるような場合(すでにいくつもクラスタを作成してるようなケース)は以下が参考になります。
https://registry.terraform.io/providers/hashicorp/helm/latest/docs#file-config

helm_releaseの構成を作成して、動作確認してみる

「resource helm_release」を作成する事で対応するHelmのReleaseを作成する事ができます。
https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release

今回使用させて頂くJenkins Chartのレンダリングに使用される変数の種類,用途,デフォルト値等の情報は以下ページに詳細にまとめられているので、一旦これを確認してみます。
https://github.com/jenkinsci/helm-charts/blob/main/charts/jenkins/VALUES_SUMMARY.md

とりあえず、一旦動かしてminikube内で動作するJenkinsのコンソールにログインできることを確認したいので、
下記の設定のみ変更します

  • 「controller.serviceType」
    今回はminikubeで動かす都合上、デフォルトの「ClusterIP」で動かれても外部(この場合はminikubeを動作させているホストPC)
    からアクセスできないので、これを「NodePort」に指定します。
    SUMMARY.mdの方には記載されていないですが、values.yamlの方にはminikubeに関する言及が記載されています
    https://github.com/jenkinsci/helm-charts/blob/main/charts/jenkins/values.yaml

    For minikube, set this to NodePort, elsewhere use LoadBalancer
    Use ClusterIP if your setup includes ingress controller
    serviceType: ClusterIP

Releaseを作成する

「variables.tf_varsに切り分けた方がいい」、「Helmのvalues.yamlに切り分けた方がいい」等の考えは一旦置いておき、直接必要な情報を書いて動かしてみます。

helm.tf
resource "helm_release" "jenkins" {
  name             = "jenkins-k8s-example"
  chart            = "jenkins/jenkins"
  namespace        = "jenkins"
  version          = "4.2.5"
  create_namespace = true

  set {
    name  = "controller.serviceType"
    value = "NodePort"
  }
}

この状態でterraform applyしてみます。
特に問題がなければ、NamespaceやPod,Serviceが作成されているはずなので、確認してみます。

諸々確認するコマンド
>kubectl get ns jenkins
NAME      STATUS   AGE
jenkins   Active   6m35s

>kubectl get pod -n jenkins
NAME                    READY   STATUS    RESTARTS   AGE
jenkins-k8s-example-0   2/2     Running   0          7m3s

>kubectl get svc -n jenkins
NAME                        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
jenkins-k8s-example         NodePort    10.107.0.117   <none>        8080:32488/TCP   7m23s
jenkins-k8s-example-agent   ClusterIP   10.101.219.9   <none>        50000/TCP        7m23s

問題なさそうなので、次は直接アクセスJenkinsのダッシュボードにログインして確認してみます。

Jenkinsのダッシュボードにログインする

  • adminアカウントのパスワードを確認する
    Jenkinsのadminアカウントはデフォルトではランダムにパスワードが設定されおり
    Helm Relaseと同名のSecretにパスワードが保存されているので、ログインする為にこれを確認します
secretがあるかどうか確認
kubectl get secrets -n jenkins
NAME                                        TYPE                 DATA   AGE
jenkins-k8s-example                         Opaque               2      16m
sh.helm.release.v1.jenkins-k8s-example.v1   helm.sh/release.v1   1      16m

中身を確認します。

secretの詳細確認
kubectl describe secrets jenkins-k8s-example -n jenkins
Name:         jenkins-k8s-example
Namespace:    jenkins
Labels:       ~~~~
Annotations:  ~~~~
Type:  Opaque
Data
====
jenkins-admin-password:  22 bytes
jenkins-admin-user:      5 bytes

dataの部分だけ抜き出します。

中身のdataを取得
kubectl get secrets jenkins-k8s-example -o jsonpath='{.data}'
{"jenkins-admin-password":"V3Y2NExWS1FnTEZUMUZ6VXZzazdYdw==","jenkins-admin-user":"YWRtaW4="}

base64エンコードされているのでデコードします。

base64デコード
echo 'V3Y2NExWS1FnTEZUMUZ6VXZzazdYdw==' | base64 -d
->
Wv64LVKQgLFT1FzUvsk7Xw

これが設定されているadminアカウントのパスワードです。
これを使用してログインしてみます。

ダッシュボードにアクセスしてログイン

kubectl get svcでも確認できましたが、NodePortで解放されているServiceを経由して
Jenkinsにアクセスする為のURLを取得します。
今回の構成だと以下のコマンドで確認する事ができます。

minikube service jenkins-k8s-example --url -n jenkins
http://192.168.118.146:32488

出力されたURLにブラウザからアクセスする事で、Jenkinsのダッシュボードにアクセスする事ができます。
あとはadminアカウントを使用して、上記で得たパスワードを入力する事でJenkinsダッシュボードにログインする事ができます。
ログイン画面
ログイン後

set をまとめる

パスワード固定したいからcontroller.adminPasswordも固定で設定しておくか~
みたいな感じで構築する環境毎にsetする値が増えていく事が一般的かなと思います。

そういうケースでは「Terraformの層でsetをまとめる」「Helmの層でvaluesとしてまとめる」の方法が使えるかと思うので、両方書いてみます。

helm.tf
resource "helm_release" "jenkins" {
  name             = "jenkins-k8s-example"
  ~~~~

  set {
    name  = "controller.serviceType"
    value = "NodePort"
  }

  set {
    name  = "controller.adminPassword"
    value = "Wv64LVKQgLFT1FzUvsk7Xw"
  }

  set {
    ~~~~
  }
  ~~~
}

Terraformの層でsetをまとめる

setがだらだらresourceブロックの中に連なると、長ったらしくなるのでdynamic Blocksでまとめてみると以下のような形になります。
https://www.terraform.io/language/expressions/dynamic-blocks
helm_valuesにlistが増えて行く形です。
もちろん、variable自体を**.tfvarsに分けて、別ファイルに切り出す事も可能です。

helm.tf
variable "helm_values" {
  type = list(list(string))
  default = [
    ["controller.serviceType", "NodePort"],
    ["controller.adminSecret", "true"],
    ["controller.adminPassword", "Wv64LVKQgLFT1FzUvsk7Xw"],
  ]
}

resource "helm_release" "jenkins" {
  name             = "jenkins-k8s-example"
  chart            = "jenkins/jenkins"
  namespace        = "jenkins"
  version          = "4.2.5"
  create_namespace = true

  dynamic "set" {
    for_each = var.helm_values
    content {
      name  = set.value[0]
      value = set.value[1]
    }
  }
}

Helmの層でvaluesとしてまとめる

こんな感じのyamlを用意して

jenkins_minikube_values.yaml
controller:
  serviceType: ClusterIP
  adminSecret: true
  adminPassword: Wv64LVKQgLFT1FzUvsk7Xw

helm.tf
variable "helm_values_file" {
  type    = string
  default = "jenkins_minikube_values.yaml"
}

resource "helm_release" "jenkins" {
  name             = "jenkins-k8s-example"
  chart            = "jenkins/jenkins"
  namespace        = "jenkins"
  version          = "4.2.5"
  create_namespace = true
  values = [
    file(var.helm_values_file)
  ]
}

という定義でも、同様の構成が構築されます。

両方併用すると?

以下のようなケースで「terraform apply」すると
setとvaluesで同じ変数を指定した場合「set」で指定した値の方が優先されます
https://helm.sh/ja/docs/intro/using_helm/

両方が使用される場合、--set 値はより高い優先度で --values にマージされます。

↓の例でいうとcontroller.serviceTypeはNodePortになります

jenkins_minikube_values.yaml
controller:
  serviceType: ClusterIP
helm.tf
variable "helm_values" {
  type = list(list(string))
  default = [
    ["controller.serviceType", "NodePort"],
  ]
}

variable "helm_values_file" {
  type    = string
  default = "jenkins_minikube_values.yaml"
}

resource "helm_release" "jenkins" {
  name             = "jenkins-k8s-example"
  chart            = "jenkins/jenkins"
  namespace        = "jenkins"
  version          = "4.2.5"
  create_namespace = true
  values = [
    file(var.helm_values_file)
  ]
  dynamic "set" {
    for_each = var.helm_values
    content {
      name  = set.value[0]
      value = set.value[1]
    }
  }
}

helm_templateの出力について

setにしろvaluesにしろ、業務や開発で利用していくと扱うChartの数も増え、渡す設定値も増加する事が懸念されます。
その為、Helmが最終的にレンダリングしたyamlを確認する手段、つまり「helm template」コマンド相当の出力を、terraform applyする前に確認したいという要望があるかなと思います。

上記の確認にはdata helm_templateを利用できます。
https://registry.terraform.io/providers/hashicorp/helm/latest/docs/data-sources/template

helm_releaseに渡す値と変数を渡して置くことで、dataリソースとして利用できるため
「output定義追加しておいて、terraform planでレンダリングされたマニフェスト(yaml)を確認する」
「ほかの定義、たとえばresourceブロックで参照する」
といった用途に利用する事ができます

helm.tf
variable "helm_values" {
  type = list(list(string))
  default = [
    ["controller.serviceType", "NodePort"],
    ["controller.adminSecret", "true"],
    ["controller.adminPassword", "Wv64LVKQgLFT1FzUvsk7Xw"],
  ]
}

data "helm_template" "jenkins" {
  name      = "jenkins-k8s-example"
  chart     = "jenkins/jenkins"
  namespace = "jenkins"
  version   = "4.2.5"
  dynamic "set" {
    for_each = var.helm_values
    content {
      name  = set.value[0]
      value = set.value[1]
    }
  }
}

output "jenkins_manifests" {
  value = data.helm_template.jenkins.manifests
}

resource "helm_release" "jenkins" {
  name             = data.helm_template.jenkins.name
  chart            = data.helm_template.jenkins.chart
  namespace        = data.helm_template.jenkins.namespace
  version          = data.helm_template.jenkins.version
  create_namespace = true
  dynamic "set" {
    for_each = var.helm_values
    content {
      name  = set.value[0]
      value = set.value[1]
    }
  }
}
出力例

terraform plan

data.helm_template.jenkins: Reading...
data.helm_template.jenkins: Read complete after 2s [id=jenkins-k8s-example]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated
with the following symbols:

  • create

Terraform will perform the following actions:
~~省略

Changes to Outputs:

  • jenkins_manifests = {
    • "templates/config.yaml" = <<-EOT
      ---
      # Source: jenkins/templates/config.yaml
      apiVersion: v1
      kind: ConfigMap
      metadata:
      name: jenkins-k8s-example
      namespace: jenkins
      labels:
      "app.kubernetes.io/name": 'jenkins'
      "app.kubernetes.io/managed-by": "Helm"
      "app.kubernetes.io/instance": "jenkins-k8s-example"
      "app.kubernetes.io/component": "jenkins-controller"
      data:
      ~~~~~~~~~~~~~~~~~~~~
      EOT
    • "templates/home-pvc.yaml" = <<-EOT
      ---
      # Source: jenkins/templates/home-pvc.yaml
      kind: PersistentVolumeClaim
      apiVersion: v1
      ~~~~~~~~~~~~~~~~~~~~

所感

今回はMinikubeで環境構築してますが、本来この構成は他のインフラ構成もまとめて構成管理に含めてしまえる事が
最大のメリットなので、そのうちEKSやGKEでも試してみるかもしれません。

💪😀🗒️⚓「イッツマイyaaaaml」!
💪😁「HA!」
😶「適用するnamespace間違えました!」
😇

Discussion