🛫

Highway to Helm

2023/08/08に公開

初めての方は、初めまして。そうでない方も、初めまして。クラウドエースのシステム開発部 SRE DivisionでProfessional Cooking Architectをしているzetaです。

INTRODUCTION

以前弊社の案件でHelmのカスタムチャートを使ったGKE(Google Kubernetes Engine)上のKubernetes(以後k8s)リソースの構築を行いました。今後Helmを初めて使うという方が筆者と同じところで詰まったりハマったりしないように知見を共有していきます。

INTENDED AUDIENCE

CKAD[1]レベルの知識があるとわかり良いと思います。

WHAT THIS ARTICLE DOESN'T PRESENT

  • Kustomize等の他のミドルウェアとの比較
  • インターネットで公開されているチャートをDLして使う方法
  • Helm CLIのインストールの方法
  • チャートの公開の方法
    後ろ3つの項目については、Helmの公式ドキュメント[2]にチュートリアルがありますのでそちらを参考にしてください。

WHAT IS Helm ?

公式サイトの説明文がこちら

Helm は、Kubernetes アプリケーションの管理を支援します。Helm チャートは、最も複雑な Kubernetes アプリケーションの定義、インストール、およびアップグレードを支援します。




🤔

ようするにKubernetesの実装とか管理とかを楽にするためのミドルウェアです。

ちなみに英単語的には「舵」という意味で、Kubernetesが航海長だとか操舵手だとかいう意味らしいのでそういう人たちの使う道具というつもりで名付けていそうですね。

WHAT IS HARD ABOUT RAW k8s ?

そもそもなぜHelmのようなミドルウェアを使おうとするのでしょうか?生のk8s[3]ではどこに課題があるのでしょうか?

  • 生のk8sはマニフェストファイルの数だけkubectl applyを繰り返す必要があり、マニフェストファイルの数だけkubectl deleteを繰り返さなければならない

    • デプロイに手間がかかる
    • オペミスの温床
  • 生のk8sは95%設定が同じだが5%だけパラメータが違うというようなマニフェストファイル群が存在し得る

    • dev / prd / stg環境があるとすると、ほとんど同じマニフェストファイル群を3セット書くことになり、冗長
    • オペミスの温床
      • パラメータ変更が発生した時に変更漏れを起こす等

こういった問題を解消しようという切なる願いがあって、ミドルウェアの導入を検討することになるわけです。

WHAT IS THE MOTIVATION FOR USING Helm ?

前項の問題群を解決する目的でHelmというミドルウェアに使うわけですが、Helmによって解決が見込めるのは以下のような要件です。

  • あるひとまとまりのマニフェストファイル群に記述された一連のk8sリソースを、コマンド一発でデプロイし、コマンド一発で消し去りたい
  • 変更を加えたマニフェストファイル群の差分をk8sリソースに適用する更新をコマンド一発でやりたい
  • 環境毎に別々のマニフェストファイル群を作りたくない

では、具体的な使い方の解説に入っていきます。

WHAT IS THE Helm CHART ?

Helmではひとまとまりにした一連のk8sマニフェストファイル群・それらで使う変数を記したファイル・Helm版のREADMEのようなファイル等で構成されるチャートという単位を使ってk8sリソースをまとめます。このファイル群が格納されているディレクトリが、チャートとニアリーイコールと考えていいと思います。

HOW DIVEDE THE Helm CHARTS ?

小規模なシステムであれば1つのチャートで作ってしまってもいいですが、規模が大きくなるとコンポーネント毎にチャートを分けたくなってくることがあると思います。マイクロサービスごとに分けるといった具合に、システムの特性によって分け方は変わってくると思います。Helmのチャートはデプロイ・削除・更新をひとまとめにやる単位ですから、更新頻度が高いリソース群と低いリソース群という分け方をするのは一つの戦略です。

HOW CONFIGURE THE FILE STRUCTURE IN THE CHART ?

helm create <チャート名>というコマンドを実行すると以下のようなディレクトリ構造が生成されます。templates/の中にサンプルのマニフェストファイルなんかが生成されて、これらを改造して開発していくことも一応できますが、実際の開発現場において現実的な方法ではないと思うので消してしまっていいと思います。values.yamlの中身も同様です。NOTE.txtはチャートの備考を書いておくと後述のinstall / upgradeをするときに内容を表示してくれるものです(使わなかったので解説は略)。

sample_chart/               // ディレクトリ名がチャート名
    ┣ charts/               // サブチャートが入るディレクトリ。使わなかったので本記事では扱わない。
    ┣ templates/            // 本体。生のk8sで動くマニフェストファイルをここに突っ込んだらとりあえずは動くはず。多分。
        ┣ deployment.yaml
        ┣ service.yaml
        ...
    ┣ .helmignore           // .gitignore のような役割のファイル
    ┣ Chart.yaml            // README のような役割のファイル。チャート配布するときは書くほうがいいらしい。
    ┣ values.yaml           // 変数を書く。環境毎に差が出る部分は env/ の中で書く。
sample_chart-0.1.0.tgz      // **helm packageで生成される** これを使ってデプロイをする。 .gitignore に入れておこう
┣ env/                      // **自分で作る** 環境毎に異なる値の入る変数はこの中のファイルに記述
    ┣ dev.yaml
    ┣ prd.yaml

/env以下に書く環境変数は、後述のデプロイ手順のオプションでvalues.yamlの値を上書きする使い方ができます。しかしながら、環境変数の扱いについてはHelmが公式に用意しているものではないので、このやり方がベストプラクティスかどうかは議論の余地がありそうです。

WHAT IS THE Helm IMPREMENTATION ?

そろそろ生のk8sとHelmの実装で実際の作業としてどういう差分があるのかということが気になってこのあたりまでスクロールしてきてると思うので、その辺りの解説やTipsを書いていきます。

WHAT IS ToDo BEFORE IMPREMENTATION ?

Helmのtemplate化をしたマニフェストファイルは、「このようなものはyaml構文の文法ではない」といった具合にエディタに怒られてエラーの赤線塗れにされます。シンタックスエラーと区別がつかないのでHelmのtemplate構文に対応したlinterを入れておきたいところ。
VSCodeの場合、Microsoftが出しているKubernetes[4]という拡張機能をインストールして、setting.jsonに以下の記述を追加すると解決します。

setting.json
{
    "files.associations": {
        "*helmfile*.yaml": "helm"
    }
}

values.yaml: VARIAVLE DESCRIPTION

values.yamlには変数を書きます。何をどこまで変数にするのかはシステムの特性にもよると思いますが、

  • 後々変更が起きそうな値
  • 繰り返し複数作られるリソースで使用する値

は変数化しておいた方が効率的です。

文章で説明されるよりコードを見る方が理解が早いでしょうからサンプルを貼ります。

values.yaml
environment: dev

serviceAccountName: sample-sa

namespace: sample

deployment:
  labels: 
    app: sample
  image: nginx
  imageTag: latest
  containerPort : 8080
  env: 
    name: PORT
    value: 8080
  volumeMounts:
    mountPath: /vol
    name: sample_vol
service: 
  selector: 
    app: sample
  ports: 
    port: 80
    protocol: TCP
    targetPort: 8080
backendconfig:
  iap: 
    enabled: true
    secretName: iap-secret
  healthCheck: 
    type: HTTP
    port: 8080
    requestPath: /api/v1/health

後述のマニフェストファイルのtemplate化の工程で呼び出しが可能であればそれでいいので、values.yamlは任意の構造で好きな変数名を名付けて作ることができます。しかしながら、わかりやすさ・使いやすさの観点でこういう構造にしておくと綺麗というのはあると思うので、いくつかポイントを挙げておきます。

  • 基本的にリソースkind毎に変数群をまとめる
    • 後述のtemplate化するときに、ややこしいことを考えなくていいのでミスしにくい
    • リソースkindのネスト内でも、実際のマニフェストのブロックに合わせたネストをしておくほうが視覚的にわかりやすい
  • namespaceのようなリソースkindを超越して使うような値は、リソースkindのネストの外に置いてグローバル変数のように使うと良い
  • カスタムマニフェストなんかに登場するnode.attr.zoneのようなkeyは.がエラー扱いになるので代わりの名前を考える必要がある

似たようなリソースを複数回作るときもポイントがありますが、それは後述のtemplate化のパートで紹介します。

TEMPLATIZATION

templates/以下のマニフェストファイルはtemplate化を行うことで、前述のvalues.yamlに記述した変数を代入して使用できます。また、値が一つだけ違うというような設定値がほぼ同じのリソースを、ループを使って定義するということもできます。

ここからはいろんなパターンで該当部分のvalues.yamlとマニフェストファイルの抜粋を例示するという構成で紹介していきます。

基本的な変数の呼び方

values.yamlが以下のようになっているとき

values.yaml
deployment:
  image: nginx

templates/以下のマニフェストファイルでの呼び出し方は以下のようになります。

templates/deployment.yaml(ファイル名は任意)
apiVersion: apps/v1
kind: Deployment
...
spec:
  ...
  template:
    spec:
      containers:
        - name: sample
          image: {{ .Values.deployment.image }}

また、変数を複数つなげて使うこともできて、

values.yaml
image: nginx
imageTag: latest
templates/deployment.yaml
image: {{ .Values.deployment.image }}:{{ .Values.deployment.imageTag }} //nginx:latest

といった感じでシンプルで直感的な方法で接続できます。

ローカル変数を使って環境毎の値を設定

templates/以下のマニフェストファイル内で呼び出した変数をローカル変数として扱うことができます。これを活用して環境毎に異なる値に対応する書き方ができます。

env/dev.yaml
environment: dev
values.yaml
serviceAccountName: sample-sa
templates/deployment.yaml
{{ $env := .Values.environment }}
{{ $serviceAccountName := .Values.serviceAccountName }}

serviceAccountName: {{ $serviceAccountName }}-{{ $env }} //sample-sa-dev

ループを使って値がわずかに違うほとんど同じリソースを定義

マルチゾーンで冗長化してるリソースのような、値が一つ違うだけで他のスペックは同じリソースを作るといったことはよくあると思います。これに対してはループを使ってコードの冗長化を防ぎます。

values.yaml
es:
  nodeSets:
  - name: node-a
    zone: asia-northeast1-a
  - name: node-b
    zone: asia-northeast1-b
  - name: node-c
    zone: asia-northeast1-c
  ...

このようなvalues.yamlがあった場合、ループにしたい部分をrangeとendで囲んでループで使いたい変数ブロックをセットします。

templates/es.yaml
kind: ElasticSearch
...
spec:
  nodeSets:
  {{- range .Values.es.nodeSets }}
  name: {{ .name }}
  ...
  config:
    node.attr.zone: {{ .zone }}
  ...
  {{- end }}

ループ毎に変わる値と不変の値が混在する状態でループを回したい

実際開発をしているとこういった複雑なことをやらなければならない場面は出てくると思います。こういうパターンはドキュメントやブログなんかでも載ってなくて、かゆいところに手が届かないということもあると思います。

values.yaml
es:
  nodeSets: //変動値
  - name: node-a
    zone: asia-northeast1-a
  - name: node-b
    zone: asia-northeast1-b
  - name: node-c
    zone: asia-northeast1-c
  podSpec:  //不変動値
    requests:
      memory: 2.0Gi
      cpu: 1
    limits:
      memory: 2.0Gi
      cpu: 1

この場合、podSpec以下の値は各ループで不変です。この部分をマニフェストファイルでローカル変数にします。

templates/es.yaml
{{ $podSpec := .Values.es.podSpec }}

kind: ElasticSearch
...
spec:
  nodeSets:
  {{- range .Values.es.nodeSets }}
  name: {{ .name }}
  ...
  config:
    node.attr.zone: {{ .zone }}
  ...
  podTemplate:
      spec:
      ...
      containers:
        - name: sample_container
          resources:
            requests:
              memory: {{ $podSpec.requests.memory }}
              cpu: {{ $podSpec.requests.cpu }}
            limits:
              memory: {{ $podSpec.limits.memory }}
              cpu: {{ $podSpec.limits.cpu }}
      ...
  {{- end }}

環境毎に作ったり作らなかったりするリソース

これは開発環境では作らないが本番環境では必要なのだ(ギュッ)というリソースはあると思います。こういう場合は条件分岐をします。

env/prd.yaml
ingress:
  enabled: true
  ...
env/dev.yaml
ingress:
  enabled: false
templates/ingress.yaml
{{- if .Values.ingress.enabled -}}

apiVersion: networking.k8s.io/v1
kind: Ingress
...

{{- end }}

環境ファイルの選択の方法は後述のデプロイ時に実行するコマンドの引数のパートで説明します。

一つの設定項目に複数値を設定するとき

これもループで書きます。

values.yaml
ingress:
  annotations:
    kubernetes.io/ingress.global-static-ip-name: sample-ip-name
    kubernetes.io/ingress.class: "gce-internal"
templates/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  ...
  annotations:
    {{- toYaml .Values.ingress.annotations | nindent 4 }}

toYamlとかnindentとか突然出てきてなんだこれは?となると思いますが、これを書かないと2行目以降が左詰めになりyamlの構造が壊れてしまってちゃんと設定できないのでこの呪文を書いておく必要があるのです。ここはあんまりスマートじゃないところですね。

HOW TO DEPLOY Helm ?

マニフェストファイルのtemplate化が済んだらデプロイをします。デプロイはチャート単位で行います。なお、チャートのデプロイの方法には新規デプロイで使うインストールと、既存のデプロイを更新するアップグレードがあります。
ここではkubectlのコンテキスト取得や切り替えは済んでいる前提で省略します。

CREATE COMPRESSED CHART FILE

helm package <チャート名>

チャート名はhelm createの際に名付けた名前です。チャートのディレクトリ名と一致しているはずです。

INSTALL CHART

先程の手順でチャートの圧縮ファイルが<チャート名>-0.1.0.tgzというような名前でできているはずです。これを使ってデプロイを行います。新規デプロイの場合はインストールという方法を使います。基本的な構文は以下の形です。

helm install <release名> <Helmチャートの圧縮ファイル名>

環境ファイルの選択

環境毎に差がある値が書かれている設定ファイルを指定する場合の書式は以下の形です。

helm install <release名> <Helmチャートの圧縮ファイル名> -f <env設定ファイルのパス>

namespaceを指定する場合

namespaceはマニフェストファイル側で指定しているとは思いますが、Helmのチャート自体にもnamespaceを指定したいという場合に使うオプションです。基本的にkubectlの--namespaceないし-nと同じ使い方です。

helm install <release名> <Helmチャートの圧縮ファイル名> --namespace <namespace名>

デプロイ前にデプロイされるリソース設定が正しいかチェックしたい

kubectlでおなじみの--dry-runオプションが使えます。kubectlと違って=clientとかつけなくてもいいです。

helm install <release名> <Helmチャートの圧縮ファイル名> --dry-run

別にパッケージ化しなくてもデプロイできるらしい

あとから知った......

helm install <release名> --values <envファイルとか> <チャートディレクトリ名>

UPGRADE CHART

既存のreleaseで使用したチャートに加えた修正を環境に反映したい場合、アップグレードというデプロイの方法をとります。installで使えるオプションはこっちでも使えるはずです。基本の構文は以下の形式。

helm upgrade <release名> <Helmチャートの圧縮ファイル名>

アップグレードコマンドでインストールもしたい

エンジニアは あたらしく helm upgradeを おぼえたい......

しかし エンジニアは コマンドを 4つ
おぼえるので せいいっぱいだ!

helm upgradeの かわりに
ほかの コマンドを わすれさせますか?

みたいな状況で使えるのが--installオプションです。既に初回デプロイ済みの内容であればオプションは無視されるので、CI/CDをやる場合なんかは設定ファイルに書くコマンドは全部これでいいです。

helm upgrade --install <release名> <Helmチャートの圧縮ファイル名>

COMFIRMATION OF DEPLOYED RELEASES

helm listを使います。全namespaceのreleaseの表示をしたい場合は-Aオプションを使います。もちろん-nでnamespaceを指定する使い方もできます。

helm list -A

DELETE DEPLOYED RESOURCES

デプロイしたk8sリソースはチャートのrelease単位でまとめて削除することができます。チャートが不要になったときとか、検証環境のお掃除をするときとかはこれを使いましょう。

helm uninstall <release名>

OUTRODUCTION

今回はtemplate構文の書き方を中心にHelmを使ったk8sリソースの実装について解説しました。Helmのtemplate構文は結構な癖がある割に、ググっても良いExampleが得られなくて困ることもあると思います。この記事が皆さんのHelmの導入・実装を高速にするHighwayとなれば幸いです。

脚注
  1. Linux Foundationが提供するKubernetesの公式認定試験であるCertified Kubernetes Application Developerの略称。主に実装作業能力が問われる。 ↩︎

  2. https://helm.sh/ ↩︎

  3. この記事ではミドルウェア等が特に入っていないKubernetesをkubectlを使って操作するようなことを生のk8sと呼ぶ。 ↩︎

  4. https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools ↩︎

Discussion