「Docker/Kubernetes 実践コンテナ開発入門」の7章をHelm v3でやってみた

# はじめに
[「Docker/Kubernetes 実践コンテナ開発入門」](https://gihyo.jp/book/2018/978-4-297-10033-9)を読みながらKubernetesを勉強していたのですが、コンテナ技術の発展が早く、出版が2018年8月ということで内容が少し古くなってきました。特に7章3節のHelmの解説では、Helmのバージョンがv3[^1]に上がったことによるコマンド変更があり、Helm v3を使用する場合には手順がそのまま使えなくなっています。この記事ではHelm v3を使用した場合の手順について解説します。

[^1]: [Helm v3](https://github.com/helm/community/blob/master/helm-v3/000-helm-v3.md) は2019年11月13日にリリースされました。

## 前提
- [「Docker/Kubernetes 実践コンテナ開発入門」](https://gihyo.jp/book/2018/978-4-297-10033-9)を購入し、読んでいる方向けです。
- あくまで差分の紹介なので、技術の解説等は本を読んでください。



## 環境
- mac OS Catalina 10.15.5
- プロセッサ: 2.3 GHz デュアルコアIntel Core i5
- メモリ: 8 GB 2133 MHz LPDDR3

dockerはdocker for macを使用してローカルで実行しました。

```bash
$ docker --version
Docker version 19.03.8, build afacb8b
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.4", GitCommit:"c96aede7b5205121079932896c4ad89bb93260af", GitTreeState:"clean", BuildDate:"2020-06-18T02:59:13Z", GoVersion:"go1.14.3", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"16+", GitVersion:"v1.16.6-beta.0", GitCommit:"e7f962ba86f4ce7033828210ca3556393c377bcc", GitTreeState:"clean", BuildDate:"2020-01-15T08:18:29Z", GoVersion:"go1.13.5", Compiler:"gc", Platform:"linux/amd64"}
```

また、VS Codeでyamlを編集していたのですが、自動整形をした際の中括弧の挙動がおかしかったので、VS Code拡張機能の`vscode-helm`をインストールしました。

## 手順
### Helmのセットアップ

kubectlをローカルkubernetesクラスタに切り替えておきます。
HomebrewでHelmをインストールします。

```bash
$ brew install helm
$ helm version
version.BuildInfo{Version:"v3.2.4", GitCommit:"0ad800ef43d3b826f31a5ad8dfbb4fe05d143688", GitTreeState:"dirty", GoVersion:"go1.14.3"}
```

v3でTillerサーバが廃止されたので、`helm init`は不要です。
v2ではkubernetesクラスタ上にTillerサーバがデプロイされ、クライアントのhelmコマンドでTillerサーバのAPIを呼び出し、命令を受けたTillerサーバがkubernetesクラスタのAPIを呼び出すという流れになっていました。拡張性のある構成だとは思いますが、Tillerサーバ周りのサービス・アカウントの設定が複雑になるということで使い勝手が悪かったようです。v3 ではTillerサーバが廃止され、クライアントから直接kubernetesクラスタのAPIを呼び出すようになりました。マニフェストの生成もクライアントで行われるようになっています。詳細は下記のサイトをご覧ください。

- [Do you know what’s in Helm 3?](https://developer.ibm.com/blogs/kubernetes-helm-3/)
- [Helm 3 Design Proposal](https://github.com/helm/community/blob/master/helm-v3/000-helm-v3.md)

### Helmの概念

v2とは違いstableリポジトリはデフォルトでは利用できなくなっています。次のようにすればstableリポジトリを追加できます。

```bash
$ helm repo add stable https://kubernetes-charts.storage.googleapis.com/
```
stableリポジトリは将来廃止される予定で、分散型のリポジトリに移行するようです。

>The stable repository is no longer added by default. This repository will be deprecated during the life of Helm v3 and we are now moving to a distributed model of repositories that can be searched by the Helm Hub.
Ref: https://helm.sh/blog/helm-v3-beta/

リポジトリの検索は`helm search`で行いますが、`repo`と`hub`の２つで検索を指定します。hubの検索は一部のリポジトリのみが表示されます。そのリポジトリからChartを使用するには、関連するリポジトリをHelmリポジトリリストに追加する必要があります。

```bash
$ helm search -h

Search provides the ability to search for Helm charts in the various places
they can be stored including the Helm Hub and repositories you have added. Use
search subcommands to search different locations for charts.

Usage:
  helm search [command]

Available Commands:
  hub         search for charts in the Helm Hub or an instance of Monocular
  repo        search repositories for a keyword in charts

Flags:
...
```

### Chartをインストールする
[redmine](https://bitnami.com/stack/redmine/helm)をインストールします。`bitnami`リポジトリを追加します。一応リポジトリの情報を最新化しておきます。

```bash
$ helm repo add bitnami https://charts.bitnami.com/bitnami
$ helm repo update
```
インストールするChartの情報です。

```bash
$ helm show chart bitnami/redmine
helm show chart bitnami/redmine
apiVersion: v1
appVersion: 4.1.1
dependencies:
- condition: mariadb.enabled
  name: mariadb
  repository: https://charts.bitnami.com/bitnami
  version: 7.x.x
- condition: postgresql.enabled
  name: postgresql
  repository: https://charts.bitnami.com/bitnami
  version: 8.x.x
description: A flexible project management web application.
home: http://www.redmine.org/
icon: https://bitnami.com/assets/stacks/redmine/img/redmine-stack-220x234.png
keywords:
- redmine
- project management
- www
- http
- web
- application
- ruby
- rails
maintainers:
- email: containers@bitnami.com
  name: Bitnami
name: redmine
sources:
- https://github.com/bitnami/bitnami-docker-redmine
version: 14.2.4
```

インストールします。パラメータを`redmine.yaml`で定義します。

```yaml:redmine.yaml
redmineUsername: abeT
redminePassword: abeT
redmineLanguage: ja

service:
  type: NodePort
```

リリース名は`--name`で指定せず、`helm install`の直後に指定します。

```bash
$ helm install -f redmine.yaml redmine bitnami/redmine
NAME: redmine
LAST DEPLOYED: Fri Jun 26 15:42:19 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the Redmine URL:

  export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services redmine)
  export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
  echo "Redmine URL: http://$NODE_IP:$NODE_PORT/"

2. Login with the following credentials

  echo Username: abeT
  echo Password: $(kubectl get secret --namespace default redmine -o jsonpath="{.data.redmine-password}" | base64 --decode)
```

インストールされたものの一覧を表示します。

```bash
$ helm ls
NAME   	NAMESPACE	REVISION	UPDATED                             	STATUS  	CHART         	APP VERSION
redmine	default  	1       	2020-06-26 13:55:26.228257 +0900 JST	deployed	redmine-14.2.4	4.1.1 
```

作成されたリソースを確認します。

```bash
$ kubectl get service,deployment
NAME                      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/kubernetes        ClusterIP   10.96.0.1        <none>        443/TCP        24m
service/redmine           NodePort    10.97.203.207    <none>        80:30677/TCP   114s
service/redmine-mariadb   ClusterIP   10.110.204.194   <none>        3306/TCP       114s

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/redmine   1/1     1            1           114s
```

`service/redmine`が`NodePort`になっており、`http://localhost:30677`でredmineにアクセスできるようになっています。

※ deployment.apps/redmineがREADYにならず、止まってしまう事象が発生しました。原因は不明ですが（多分メモリの不足？）、`Reset Kubernetes Cluster`でクラスターを全削除してから実行するとうまくいく場合があります。

<img width="1036" alt="スクリーンショット 2020-06-26 16.09.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211712/caf8258b-05eb-508b-d467-9103bafde6ce.png">


### アンインストール
次のコマンドでアンインストールを実行できます。v3ではデフォルトではリビジョンの記録が残りません。`--keep-history`を付与することで、リビジョンを残しておくことができます。

```bash
$ helm uninstall redmine --keep-history
release "redmine" uninstalled
``` 

リビジョンを確認できます。

```bash
$ helm ls --all
NAME   	NAMESPACE	REVISION	UPDATED                             	STATUS     	CHART         	APP VERSION
redmine	default  	1       	2020-06-26 16:14:29.972481 +0900 JST	uninstalled	redmine-14.2.4	4.1.1   
```
ロールバックを実行します。

```bash
$ helm rollback redmine 1
Rollback was a success! Happy Helming!
```
削除がロールバックされて、実行状態になっています。

```bash
$ helm ls
NAME   	NAMESPACE	REVISION	UPDATED                             	STATUS  	CHART         	APP VERSION
redmine	default  	2       	2020-06-29 14:21:46.552373 +0900 JST	deployed	redmine-14.2.4	4.1.1  
```

リビジョンの記録を残さないで削除するには`--keep-history`なしで削除します。

```bash
$ helm uninstall redmine
release "redmine" uninstalled
$ helm ls --all
NAME	NAMESPACE	REVISION	UPDATED	STATUS	CHART	APP VERSION
```

### Chartを自作する
Chartのテンプレートを作成します。

```bash
$ helm create echo
Creating echo
$ tree .
.
└── echo
    ├── Chart.yaml
    ├── charts
    ├── templates
    │   ├── NOTES.txt
    │   ├── _helpers.tpl
    │   ├── deployment.yaml
    │   ├── hpa.yaml
    │   ├── ingress.yaml
    │   ├── service.yaml
    │   ├── serviceaccount.yaml
    │   └── tests
    │       └── test-connection.yaml
    └── values.yaml

4 directories, 10 files
```
Macには標準で`tree`コマンドは入っていないので、Homebrewでインストールしました。

```bash
$ brew install tree
```
v2と比べると、次の３つファイルが増えています。これらのファイルは今回は使用しません。

- hpa.yaml  # HorizontalPodAutoscalerのマニフェスト
- serviceaccount.yaml  # ServiceAccountのマニフェスト
- test-connection.yaml  # Chartを[テスト](https://helm.sh/docs/topics/chart_tests/)するのに使用する

Chartのテンプレートの構成は[こちら](https://helm.sh/docs/chart_template_guide/getting_started/)を参照してください。

生成された`deployment.yaml`の中身は次のようになっています。

```yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "echo.fullname" . }}
  labels:
    {{- include "echo.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
{{- end }}
  selector:
    matchLabels:
      {{- include "echo.selectorLabels" . | nindent 6 }}
  template:
    metadata:
    {{- with .Values.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
    {{- end }}
      labels:
        {{- include "echo.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "echo.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
```
あらかじめ様々なパラメータが抽象化されています。開発者が編集するのは`spec.template.spec`の部分です。次のように変更します。

```yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "echo.fullname" . }}
  labels:
    app: {{ include "echo.name" . }}
    chart: {{ include "echo.chart" . }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}

spec:
{{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
{{- end }}
  selector:
    matchLabels:
      {{- include "echo.selectorLabels" . | nindent 6 }}
  template:
    metadata:
    {{- with .Values.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
    {{- end }}
      labels:
        {{- include "echo.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "echo.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: nginx
          image: "{{ .Values.nginx.image.repository }}:{{ .Values.nginx.image.tag }}"
          imagePullPolicy: {{ .Values.nginx.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
          livenessProbe:
            httpGet:
              path: {{ .Values.nginx.healthCheck }}
              port: http
          readinessProbe:
            httpGet:
              path: {{ .Values.nginx.healthCheck }}
              port: http
          env:
            - name: BACKEND_HOST
              value: {{ .Values.nginx.backendHost | quote }}
        - name: echo
          image: "{{ .Values.echo.image.repository }}:{{ .Values.echo.image.tag }}"
          imagePullPolicy: {{ .Values.echo.image.pullPolicy }}
          ports:
            - containerPort: {{ .Values.echo.httpPort }}
          env:
            - name: HTTP_PORT
              value: {{ .Values.echo.httpPort | quote }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
```

パラメータは`values.yaml`で指定します。生成された`values.yaml`の中身は次のようになっています。

```yaml:values.yaml
# Default values for echo.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

image:
  repository: nginx
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: ""

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
  # Specifies whether a service account should be created
  create: true
  # Annotations to add to the service account
  annotations: {}
  # The name of the service account to use.
  # If not set and create is true, a name is generated using the fullname template
  name: ""

podAnnotations: {}

podSecurityContext: {}
  # fsGroup: 2000

securityContext: {}
  # capabilities:
  #   drop:
  #   - ALL
  # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: false
  annotations: {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: chart-example.local
      paths: []
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

resources: {}
  # We usually recommend not to specify default resources and to leave this as a conscious
  # choice for the user. This also increases chances charts run on environments with little
  # resources, such as Minikube. If you do want to specify resources, uncomment the following
  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
  # limits:
  #   cpu: 100m
  #   memory: 128Mi
  # requests:
  #   cpu: 100m
  #   memory: 128Mi

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity: {}
```
これを次のように書き換えます。

```yaml:values.yaml
replicaCount: 1
nginx:
  image:
    repository: gihyodocker/nginx
    pullPolicy: Always
    tag: latest
  healthCheck: /
  backendHost: localhost:8080

echo:
  image:
    repository: gihyodocker/echo
    pullPolicy: Always
    tag: latest
  httpPort: 8080
```

Serviceも同様に書き換えます。生成されたServiceの中身は次のようになっています。

```yaml:service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ include "echo.fullname" . }}
  labels:
    {{- include "echo.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP
      name: http
  selector:
    {{- include "echo.selectorLabels" . | nindent 4 }}
```
これを次のようにします。

```yaml:service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ include "echo.fullname" . }}
  labels: 
    app: {{ include "echo.name" . }}
    chart: {{ include "echo.chart" . }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP
      name: {{ .Values.service.name }}
  selector: {{- include "echo.selectorLabels" . | nindent 4 }}
```
`values.yaml`に次の設定を追記します。

```yaml:values.yaml
service:
  name: nginx
  type: ClusterIP
  port: 80
```

次はIngressです。生成されたIngressの中身は次のようになっています。

```yaml:ingress.yaml
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "echo.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
  name: {{ $fullName }}
  labels:
    {{- include "echo.labels" . | nindent 4 }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  {{- if .Values.ingress.tls }}
  tls:
    {{- range .Values.ingress.tls }}
    - hosts:
        {{- range .hosts }}
        - {{ . | quote }}
        {{- end }}
      secretName: {{ .secretName }}
    {{- end }}
  {{- end }}
  rules:
    {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ . }}
            backend:
              serviceName: {{ $fullName }}
              servicePort: {{ $svcPort }}
          {{- end }}
    {{- end }}
  {{- end }}
```
`metadata.labels`を書き換えます。

```yaml:ingress.yaml
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "echo.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
  name: {{ $fullName }}
  labels:
    app: {{ include "echo.name" . }}
    chart: {{ include "echo.chart" . }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  {{- if .Values.ingress.tls }}
  tls:
    {{- range .Values.ingress.tls }}
    - hosts:
        {{- range .hosts }}
        - {{ . | quote }}
        {{- end }}
      secretName: {{ .secretName }}
    {{- end }}
  {{- end }}
  rules:
    {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ . }}
            backend:
              serviceName: {{ $fullName }}
              servicePort: {{ $svcPort }}
          {{- end }}
    {{- end }}
  {{- end }}
```

`values.yaml`に次の設定を追記します。これは`values.yaml`生成時のデフォルトの値です。

```yaml:values.yaml
ingress:
  enabled: false
  annotations:
    {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: chart-example.local
      paths: []
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local
```
また、ServiceAccountは今回作成しないので、`serviceAccount.create`は`false`とします。

```yaml:values.yaml
serviceAccount:
  # Specifies whether a service account should be created
  create: false
  # Annotations to add to the service account
  annotations: {}
  # The name of the service account to use.
  # If not set and create is true, a name is generated using the fullname template
  name: ""
```

`values.yaml`全体は次のようになります。

```yaml:values.yaml
replicaCount: 1
nginx:
  image:
    repository: gihyodocker/nginx
    pullPolicy: Always
    tag: latest
  healthCheck: /
  backendHost: localhost:8080

echo:
  image:
    repository: gihyodocker/echo
    pullPolicy: Always
    tag: latest
  httpPort: 8080

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
  # Specifies whether a service account should be created
  create: false
  # Annotations to add to the service account
  annotations: {}
  # The name of the service account to use.
  # If not set and create is true, a name is generated using the fullname template
  name: ""

podAnnotations: {}

podSecurityContext:
  {}
  # fsGroup: 2000

securityContext:
  {}
  # capabilities:
  #   drop:
  #   - ALL
  # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000

service:
  name: nginx
  type: ClusterIP
  port: 80

ingress:
  enabled: false
  annotations:
    {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: chart-example.local
      paths: []
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

resources:
  {}
  # We usually recommend not to specify default resources and to leave this as a conscious
  # choice for the user. This also increases chances charts run on environments with little
  # resources, such as Minikube. If you do want to specify resources, uncomment the following
  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
  # limits:
  #   cpu: 100m
  #   memory: 128Mi
  # requests:
  #   cpu: 100m
  #   memory: 128Mi

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity: {}
```

`Chart.yaml`にパッケージングに必要な情報を記載します。

```yaml:Chart.yaml
apiVersion: v2
name: echo
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: 0.1.0
```

`helm lint`で文法チェックができます。

```bash
$ helm lint echo
==> Linting echo
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed
```

### Chartをインストールする
作成したChartをインストールします。`helm install --debug --dry-run リリース名 Chart名`コマンドでインストールのdry runを実行できます。dry runでは生成されたマニフェストを確認することができます。

```bash
$ helm install --debug --dry-run echo echo
install.go:159: [debug] Original chart version: ""
install.go:176: [debug] CHART PATH: /hogehoge/echo

NAME: echo
LAST DEPLOYED: Mon Jun 29 16:58:44 2020
NAMESPACE: default
STATUS: pending-install
REVISION: 1
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
affinity: {}
autoscaling:
  enabled: false
  maxReplicas: 100
  minReplicas: 1
  targetCPUUtilizationPercentage: 80
echo:
  httpPort: 8080
  image:
    pullPolicy: Always
    repository: gihyodocker/echo
    tag: latest
fullnameOverride: ""
imagePullSecrets: []
ingress:
  annotations: {}
  enabled: false
  hosts:
  - host: chart-example.local
    paths: []
  tls: []
nameOverride: ""
nginx:
  backendHost: localhost:8080
  healthCheck: /
  image:
    pullPolicy: Always
    repository: gihyodocker/nginx
    tag: latest
nodeSelector: {}
podAnnotations: {}
podSecurityContext: {}
replicaCount: 1
resources: {}
securityContext: {}
service:
  name: nginx
  port: 80
  type: ClusterIP
serviceAccount:
  annotations: {}
  create: false
  name: ""
tolerations: []

HOOKS:
---
# Source: echo/templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "echo-test-connection"
  labels:
    helm.sh/chart: echo-0.1.0
    app.kubernetes.io/name: echo
    app.kubernetes.io/instance: echo
    app.kubernetes.io/version: "0.1.0"
    app.kubernetes.io/managed-by: Helm
  annotations:
    "helm.sh/hook": test-success
spec:
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args: ['echo:80']
  restartPolicy: Never
MANIFEST:
---
# Source: echo/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: echo
  labels: 
    app: echo
    chart: echo-0.1.0
    release: echo
    heritage: Helm
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
      name: nginx
  selector:
    app.kubernetes.io/name: echo
    app.kubernetes.io/instance: echo
---
# Source: echo/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo
  labels:
    app: echo
    chart: echo-0.1.0
    release: echo
    heritage: Helm

spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: echo
      app.kubernetes.io/instance: echo
  template:
    metadata:
      labels:
        app.kubernetes.io/name: echo
        app.kubernetes.io/instance: echo
    spec:
      serviceAccountName: default
      securityContext:
        {}
      containers:
        - name: nginx
          image: "gihyodocker/nginx:latest"
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 80
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          env:
            - name: BACKEND_HOST
              value: "localhost:8080"
        - name: echo
          image: "gihyodocker/echo:latest"
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
          env:
            - name: HTTP_PORT
              value: "8080"
          resources:
            {}

NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=echo,app.kubernetes.io/instance=echo" -o jsonpath="{.items[0].metadata.name}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace default port-forward $POD_NAME 8080:80
```
dry runでパラメータに問題ないか確認できたらインストールします。

```bash
$ helm install echo echo
NAME: echo
LAST DEPLOYED: Mon Jun 29 16:59:48 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=echo,app.kubernetes.io/instance=echo" -o jsonpath="{.items[0].metadata.name}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace default port-forward $POD_NAME 8080:80
```

アプリが立ち上がっていることがわかります。

```bash
$ helm ls
NAME	NAMESPACE	REVISION	UPDATED                             	STATUS  	CHART     	APP VERSION
echo	default  	1       	2020-06-29 16:59:48.534198 +0900 JST	deployed	echo-0.1.0	0.1.0    
$ kubectl get deployment,service,ingress --selector app=echo
NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/echo   1/1     1            1           2m10s

NAME           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/echo   ClusterIP   10.111.40.236   <none>        80/TCP    2m10s
```

Ingressの使用がfalseになっていたので、これをtrueにして実行してみます。デプロイ済みのアプリは削除します。

```bash
$ helm uninstall echo
```

ローカルにはL7ロードバランサがないので、`NGINX Ingress Controller`を使用します。インストール方法は[こちら](https://kubernetes.github.io/ingress-nginx/deploy/#docker-for-mac)を参照してください。

```bash
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/cloud/deploy.yaml
$ kubectl -n ingress-nginx get service,pod
NAME                                         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
service/ingress-nginx-controller             LoadBalancer   10.98.29.212    localhost     80:32012/TCP,443:30306/TCP   3m46s
service/ingress-nginx-controller-admission   ClusterIP      10.107.104.62   <none>        443/TCP                      3m46s

NAME                                            READY   STATUS      RESTARTS   AGE
pod/ingress-nginx-admission-create-6plnl        0/1     Completed   0          3m36s
pod/ingress-nginx-admission-patch-csb5s         0/1     Completed   0          3m36s
pod/ingress-nginx-controller-579fddb54f-d47d7   1/1     Running     0  
```

次に、カスタムValueファイルを作成します。

```bash
$ vi echo.yaml
ingress:
  enabled: true
  hosts:
    - host: "ch06-echo.gihyo.local"
      paths: ["/"]
```
カスタムValueファイルを使用してアプリをインストールします。

```bash
$ helm install -f echo.yaml echo echo
NAME: echo
LAST DEPLOYED: Mon Jun 29 17:32:19 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  http://ch06-echo.gihyo.local/
```
`curl`でアクセスしてみます。

```bash
$ curl http://localhost -H 'Host:ch06-echo.gihyo.local'
Hello Docker!!
```
これで成功です！

### パッケージをつくる
`helm package`コマンドでパッケージングできます。

```bash
$ helm package echo
$ ls
echo/   echo-0.1.0.tgz  echo.yaml
```

[v2では開発目的のために、ローカルのチャートリポジトリをローカルマシン上で実行していましたが、開発ツールとしてはあまり受け入れられず、その設計には多くの問題があったため、v3では`helm serve`が無くなり、プラグインになりました。](https://helm.sh/docs/faq/)

# おわりに
今回は[「Docker/Kubernetes 実践コンテナ開発入門」](https://gihyo.jp/book/2018/978-4-297-10033-9)をHelm v3で行いました。コンテナまわりの技術は進歩が速くて追いかけるのが大変です。