Helm Chart を作成する
簡単な Helm Chart を作成してみます。
環境
- Ubuntu 22.04.1 LTS
- minikube v1.27.1 (Kubernetes v1.25.2)
- Helm v3.10.1
Helm Chart に対する大雑把な理解
Helm Chart 内のファイルは、おおよそ以下の要素により構成されます。
- Helm Chart に関するメタ情報のファイル
- Kubernetes のマニフェストファイル
- マニフェストファイルで利用されるパラメーター値を管理したファイル
マニフェストファイル内で利用される各パラメーター値は、変数として利用することで動的に管理できるようになります。変数を利用するため、マニフェストファイルは Helm Template Language を用いて記述することになり、その他言語の Template Engine を利用する時と同じように、Helm Template Language においても、if 文や foreach 文のような制御構文や様々な関数を利用することができます。
また Helm Template Language の記法は Helm 独自のものではなく、Go の標準パッケージである template
を利用したものとなるため、template
パッケージを知っていれば理解の助けになるとあります。ちなみに、僕は template
パッケージを利用したことはないです。
While we talk about the "Helm template language" as if it is Helm-specific, it is actually a combination of the Go template language, some extra functions, and a variety of wrappers to expose certain objects to the templates. Many resources on Go templates may be helpful as you learn about templating.
Helm Docs - Template Functions and Pipelines
作成した Chart
以下のリソースを作成できる Helm Chart を作成します。
- WEB サーバーの Deployment (Pod)
- 上記 Pod で利用される ServiceAccount
- 上記 Deployment に紐づく Service
Chart 名を sample
として作成しています。作成したものを以下の GitHub リポジトリに push しています。
https://github.com/goodbyegangster/helm_template
なお、利用した Kubernetes 環境は minikube となります。
Helm Chart のファイル構造
今回作成した Helm Chart のファイル構造です。
. └── sample ├── charts # 依存関係のある Chart を置くディレクトリ ├── templates # k8s の間にマニフェストファイルを置くディレクトリ │ ├── NOTES.txt # `helm install` を実行時に表示される文言を記述 │ ├── _helpers.tpl # template helpers。Chart 内で利用されるグローバル変数等を記述 │ ├── deployment.yaml # Deployment を作るマニフェストファイル │ ├── service.yaml # Service を作るマニフェストファイル │ ├── serviceaccount.yaml # ServiceAccount を作るマニフェストファイル │ └── tests # テスト実行時に利用されるマニフェストファイル │ └── test.yaml ├── .helmignore # Chart をパッケージ化した際に無視するファイルを記述 ├── Chart.yaml # Chart に関するメタ情報を管理するファイル ├── README.md └── values.yaml # マニフェスト内で利用される変数のデフォルト値を記述
ファイル構造については、以下のドキュメントが詳しいです。
Helm Docs - The Chart File Structure
helm create
コマンドを実行すると、ファイル構造のスケルトンを作成してくれます。今回作成した Chart は、このスケルトンで作成されたファイルを参考にして作っています。
$ helm create chart-name Creating chart-name $ tree ./chart-name ./chart-name ├── Chart.yaml ├── charts ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ └── test-connection.yaml └── values.yaml
各ファイルたち
幾つかのファイルを、もう少しだけ詳しく確認してしみます。
Chart.yaml
Chart.yaml
は、作成した Chart のメタ情報を管理するファイルです。幾つかの必須パラメーターとオプショナルなパラメーターがあり、詳細は公式のドキュメントを参考に。
Helm Docs - The Chart.yaml File
ここで指定したパラメーターは、Helm Template Language を利用し変数として利用できるようになります。
今回作成したファイル内容は下記です。
sample/Chart.yaml
apiVersion: v2 # Helm 3 の場合は v2 を指定 name: sample type: application # 'application' or 'library' version: 1.0.0 # The chart version. Versions are expected to follow Semantic Versioning. appVersion: 2.0.0 # This is the version number of the application being deployed. 補助的なパラメーター。今回利用するコンテナイメージが「gcr.io/google-samples/hello-app:2.0」のため、2.0.0と設定 kubeVersion: ">= 1.25.0" description: Sample Helm Chart icon: http://dummy.com/ # 設定しないと linter が注意してくるので、今回は dummy の URL を指定
values.yaml
values.yaml
は、Helm Template Language で利用する変数の、デフォルト値を指定したファイルです。
今回作成したファイル内容は下記です。
sample/values.yaml
# for deployment.yaml deployment: replicas: enabled: false replicaCount: 1 strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 25% container: image: name: gcr.io/google-samples/hello-app tag: "2.0" pullPolicy: IfNotPresent port: name: http containerPort: 8080 protocol: TCP livenessProbe: path: / readinessProbe: path: / resources: memory: 128Mi cpu: 100m # for svc.yaml service: type: ClusterIP port: 80 # for serviceaccount.yaml serviceAccount: create: false name: "" annotations: {}
ベストプラクティスに関するドキュメントを公開してくれています。「ネストしすぎるより、なるべくフラットな YAML の方が分かりやすいよ」とか、「デフォルト変数の値を上書きして helm install する時に、リスト形式のパラメーターがあると扱いずらいよ」とか記述されています。(今回は結構ネストさせてしまっていますが。。。)
.helmignore
Chart をリポジトリ化する際には、作成したファイル一式を helm package
コマンドを利用して tar ファイルにするのですが、.helmignore
を利用すると、その tar ファイルに含めたくないファイルを指定できるようなります。
The
.helmignore
file is used to specify files you don't want to include in your helm chart.If this file exists, the
helm package
command will ignore all the files that match the pattern specified in the .helmignore file while packaging your application.
templates/NOTES.txt
templates/NOTES.txt
は、Chart をインストールした際にターミナル上に表示される文言を記述できます。
In this section we are going to look at Helm's tool for providing instructions to your chart users. At the end of a
helm install
orhelm upgrade
, Helm can print out a block of helpful information for users. This information is highly customizable using templates.To add installation notes to your chart, simply create a
templates/NOTES.txt
file. This file is plain text, but it is processed like as a template, and has all the normal template functions and objects available.
Helm Docs - Creating a NOTES.txt File
今回作成したファイル内容は下記です。
sample/template/NOTES.txt
#### SAMPLE HELM CHART #### Chart Name: {{ .Chart.Name }} Chart Version: {{ .Chart.Version }} Chart Resource Name: {{ .Release.Name }} The following resources have been managed. - Deployment - {{ .Values.deployment.container.image.name }}:{{ .Values.deployment.container.image.tag }} - Service - {{ .Values.service.type }} {{- if .Values.serviceAccount.create }} - ServiceAccount - {{ template "sample.serviceAccountName" . }} {{- end }}
{{ .Chart.Name }}
や {{ .Chart.Version }}
といった記述がありますが、これが Helm Template Language となります。Helm Template Language の記法については後述。
templates/_helpers.tpl
templates/\_helpers.tpl
は、マニフェストファイルではなく、マニフェストファイル内で利用されるグローバル変数というか配列(Helm では Named Template と呼ばれます)を定義したファイルとなります。
公式ドキュメントでの説明はこちら。
Sometimes you want to create some reusable parts in your chart, whether they're blocks or template partials. And often, it's cleaner to keep these in their own files.
In the
templates/
directory, any file that begins with an underscore(_
) is not expected to output a Kubernetes manifest file. So by convention, helper templates and partials are placed in a_helpers.tpl
file.
Helm Docs - Using "Partials" and Template Includes
今回作成したファイル内容は下記です。
sample/templates/_helpers.tpl
{{/* Common labels */}} {{- define "sample.labels" -}} app.kubernetes.io/name: {{ .Release.Name | quote }} helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" app.kubernetes.io/managed-by: {{ .Release.Service | quote }} app.kubernetes.io/instance: {{ .Release.Name | quote }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} {{/* Selector labels */}} {{- define "sample.selectorLabels" -}} app.kubernetes.io/name: {{ .Release.Name | quote }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} {{- define "sample.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default .Release.Name .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }}
以下の Named Template を定義しています。
Named Template 名 | 利用用途 |
---|---|
sample.labels | リソースのラベルに利用 |
sample.selectorLabels | 各リソース間の selector で利用されるラベル |
sample.serviceAccountName | 利用するサービスアカウント名 |
Helm で作成されたリソースへのラベルには、設定が推奨されている値があり、以下のベストプラクティス資料が詳しいです。
Helm Docs - Labels and Annotations
Helm Template Language の記法や、Named Template の定義方法についてはについては後述。
Helm Template Language の記法
基本的な記法を確認します。
コメント行
コメントは #
の他、以下のような記法を利用します。
{{/* ... */}}
#
を利用したコメントはデバックモードで表示される、という違いがあります。
The comment above (※
#
を利用したパターンのこと) is visible when the user runshelm install --debug
, while comments specified in{{- /* */}}
sections are not.
Comments (YAML Comments vs. Template Comments)
Built-in Object (定義した values.yaml
の値へのアクセス)
Built-in Object とは、Helm Template Lunguage で利用できるオブジェクトというかインスタンスとなります。values.yaml
等に定義した値を取得するには、Values
オブジェクト内のインスタンス変数 なになに
にアクセスする、みたいな感じで利用するイメージとなります。
例えば、以下記述の {{ .Release.Name | quote }}
は、Release
という Built-in Object の Name
というパラメーターを取得しているものです。Release
オブジェクトは作成されたリソースのメタ情報を取得できるトップレベルオブジェクトとなります。
sample/template/deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name | quote }} ...
今回のサンプルでは、Release
のほか、Values
や Chart
といった Built-in Object を利用しています。Values
は、values.yaml
に定義された値にアクセスできるオブジェクトであり、Chart
は、Chart.yaml
に定義された値にアクセスできるオブジェクトです。
下記のように記述します。{{ .Chart.Name }}
は、Chart.yaml
の Name
パラメーターの値を利用するもので、{{ .Values.deployment.container.image.name }}
は、values.yaml
の deployment.container.image.name
パラメーターの値を利用するものです。
sample/template/NOTES.txt
... Chart Name: {{ .Chart.Name }} Chart Version: {{ .Chart.Version }} Chart Resource Name: {{ .Release.Name }} The following resources have been managed. - Deployment - {{ .Values.deployment.container.image.name }}:{{ .Values.deployment.container.image.tag }} ...
その他に用意されているオブジェクトも含め、Built-in Object については、以下が詳しいです。
関数
Helm Template Language 内では、様々な関数がビルトインされています。
Helm has over 60 available functions. Some of them are defined by the
Go template language
itself. Most of the others are part of theSprig template library
. We'll see many of them as we progress through the examples.
Template Functions and Pipelines
例えば前述の {{ .Release.Name | quote }}
という記述ですが、.Release.Name
という値に対して、パイプを介し、 quote
という引用符を付与する関数を実行しているものになります。こんな感じで、実行したい関数をパイプを介して記述していくことなります。
用意されている関数の一覧が下記です。
制御構文
制御構文は、以下が用意されています。
・
if/else
for creating conditional blocks・
with
to specify a scope・
range
, which provides a "for each"-style loop
if/else
や range
といった構文は、その名前の通りのため公式ドキュメントのサンプルを見れば分かりやすいですが、with
はパッと見では利用ケースを想定できないため、メモしておきます。
with
構文は、その構文内で利用される変数のスコープを限定するものです。
例えば、以下のように利用します。
sample/template/deployment.yaml
... containers: {{- with .Values.deployment.container }} - name: {{ $.Release.Name | quote }} image: "{{ .image.name }}:{{ .image.tag }}" imagePullPolicy: {{ .image.pullPolicy }} ... {{- end }}
{{ with }}
構文で .Values.deployment.container
を宣言しています。その後に続く {{ .image.name }}
の .image.name
とは、実際には Values.deployment.container.image.name
を指すものとなっています。こんな感じでスコープを限定できる訳です。一方で {{ .Release.Name }}
といった記述は Values.deployment.container.Release.Name
と扱われてしまうため、$
を頭に付与することで、そのスコープをルートスコープから開始されるものと指定することができます。
we can use
$
for accessing the objectRelease.Name
from the parent scope.$
is mapped to the root scope when template execution begins and it does not change during template execution.
Helm Docs - Modifying scope using with
Named Templates
Named Templates
のことを、上記ではグローバルな配列みたいなと表現しましたが、公式のドキュメントの定義では以下となります。
A named template (sometimes called a partial or a subtemplate) is simply a template defined inside of a file, and given a name. We'll see two ways to create them, and a few different ways to use them.
Named Template は、{{ define }}
... {{ end }}
アクションで定義を行い、{{ template }}
や {{ include }}
アクションで、その値を利用することとなります。
下記例では "sample.labels"
という Named Template を定義しています。
sample/templates/_helpers.tpl
{{/* Common labels */}} {{- define "sample.labels" -}} app.kubernetes.io/name: {{ .Release.Name | quote }} helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" app.kubernetes.io/managed-by: {{ .Release.Service | quote }} app.kubernetes.io/instance: {{ .Release.Name | quote }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} ...
下記例では、 {{ include }}
アクションを用いて利用しています。
sample/templates/\developer.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name | quote }} labels: {{- include "sample.labels" . | nindent 4 }} ...
{{ template }}
でなく、 {{ include }}
しないと、パイプを介した関数の実行ができないため、{{ include }}
が良いよというドキュメント。
Go provides a way of including one template in another using a built-in
template
directive. However, the built-in function cannot be used in Go template pipelines.To make it possible to include a template, and then perform an operation on that template's output, Helm has a special
include
function:
Helm Docs - Using the 'include' Function
空白の管理
{{- xxx }}
とか {{ XX -}}
とか出てきているハイフンですが、これは Helm Template Lunguate を利用した行の空白を管理するものです。ハイフンの有無により空白の除去を実行してくれます。
First, the curly brace syntax of template declarations can be modified with special characters to tell the template engine to chomp whitespace.
{{-
(with the dash and space added) indicates that whitespace should be chomped left, while-}}
means whitespace to the right should be consumed. Be careful! Newlines are whitespace!
Helm Docs - Controlling Whitespace
Chart のデバッグ
作成した Chart は、デバッグする方法が幾つか用意されています。
Helm Docs - Debugging Templates
リンターの実行。
$ helm lint ./sample ==> Linting ./sample 1 chart(s) linted, 0 chart(s) failed
$ helm template ./sample --- # Source: sample/templates/service.yaml apiVersion: v1 kind: Service metadata: name: "release-name" labels: app.kubernetes.io/name: "release-name" helm.sh/chart: "sample-1.0.0" app.kubernetes.io/managed-by: "Helm" app.kubernetes.io/instance: "release-name" app.kubernetes.io/version: "2.0.0" ...
インストールのドライラン。
$ helm install test --dry-run --debug ./sample
作成した Chart の実行
それでは、Kubernetes 環境へ実際にインストールしてみます。
namespace zunda
に、リソース名 zunda
としてインストールします。
$ helm install --create-namespace -n zunda zunda ./sample NAME: zunda LAST DEPLOYED: Sun Nov 6 20:07:10 2022 NAMESPACE: zunda STATUS: deployed REVISION: 1 NOTES: #### SAMPLE HELM CHART #### Chart Name: sample Chart Version: 1.0.0 Chart Resource Name: zunda The following resources have been managed. - Deployment - gcr.io/google-samples/hello-app:2.0 - Service - ClusterIP
--create-namespace
オプションを付与して実行すると、namespace が不在の場合に自動的に作成してくれます。
Automatically creating namespaces
以下の通りリソースが作成されています。
$ helm list -n zunda NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION zunda zunda 1 2022-11-06 20:07:10.6808611 +0900 JST deployed sample-1.0.0 2.0.0 $ kubectl get pod,deployment,svc -n zunda NAME READY STATUS RESTARTS AGE pod/zunda-6c578977db-l8sk7 1/1 Running 0 3m29s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/zunda 1/1 1 1 3m29s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/zunda ClusterIP 10.111.188.28 <none> 80/TCP 3m29s
values.yaml の値を上書きして実行(コマンドラインにて)
values.yaml
で定義されたデフォルト値を、更新してインストールしてみます。--set
オプションを利用することで上書きすることができ、Replica 3 つの Deployment に、Service を NodePort としてインストールしてみます。
$ helm install --create-namespace -n zunda zunda2 ./sample \ --set deployment.replicas.enabled=true \ --set deployment.replicas.replicaCount=3 \ --set service.type=NodePort NAME: zunda2 LAST DEPLOYED: Sun Nov 6 20:17:00 2022 NAMESPACE: zunda STATUS: deployed REVISION: 1 NOTES: #### SAMPLE HELM CHART #### Chart Name: sample Chart Version: 1.0.0 Chart Resource Name: zunda2 The following resources have been managed. - Deployment - gcr.io/google-samples/hello-app:2.0 - Service - NodePort
確認します。
$ helm list -n zunda NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION zunda zunda 1 2022-11-06 20:07:10.6808611 +0900 JST deployed sample-1.0.0 2.0.0 zunda2 zunda 1 2022-11-06 20:17:00.1188276 +0900 JST deployed sample-1.0.0 2.0.0 $ $ kubectl get pod,deployment,svc,sa -n zunda -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES pod/zunda-6c578977db-l8sk7 1/1 Running 0 11m 172.17.0.2 minikube <none> <none> pod/zunda2-7cd6998b-7kpfz 1/1 Running 0 79s 172.17.0.10 minikube <none> <none> pod/zunda2-7cd6998b-9jtjq 1/1 Running 0 79s 172.17.0.8 minikube <none> <none> pod/zunda2-7cd6998b-f8dz4 1/1 Running 0 79s 172.17.0.9 minikube <none> <none> NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR deployment.apps/zunda 1/1 1 1 11m zunda gcr.io/google-samples/hello-app:2.0 app.kubernetes.io/instance=zunda,app.kubernetes.io/name=zunda deployment.apps/zunda2 3/3 3 3 79s zunda2 gcr.io/google-samples/hello-app:2.0 app.kubernetes.io/instance=zunda2,app.kubernetes.io/name=zunda2 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR service/zunda ClusterIP 10.111.188.28 <none> 80/TCP 11m app.kubernetes.io/instance=zunda,app.kubernetes.io/name=zunda service/zunda2 NodePort 10.109.49.124 <none> 80:32523/TCP 79s app.kubernetes.io/instance=zunda2,app.kubernetes.io/name=zunda2 NAME SECRETS AGE serviceaccount/default 0 11m $ $ minikube ip 192.168.49.2 $ $ curl http://192.168.49.2:32523 Hello, world! Version: 2.0.0 Hostname: zunda2-7cd6998b-9jtjq
values.yaml の値を上書きして実行(ファイルを介して)
コマンドライン経由の他、values.yaml
を上書きするファイルを作成して実行することもできます。
以下コマンドで、values の内容を config.yaml ファイルに出力。出力された config.yaml に対して、変更したい値を更新します。
$ helm show values ./sample > config.yaml
-f
オプションで上書きするファイルを指定して実行。
$ helm install --create-namespace -n zunda -f config.yaml zunda3 ./sample
テストの実行
templates/tests/
ディレクトリ配下においたマニフェストファイルは、helm test
コマンドにより実行することができます。
今回は以下のマニフェストファイルを用意しています。curl を実行できるコンテナイメージから、curl コマンドを実行するものです。
sample/templates/tests/test.yaml
apiVersion: v1 kind: Pod metadata: name: {{ .Release.Name }}-test labels: app: {{ .Release.Name }}-test annotations: "helm.sh/hook": test spec: containers: - name: curl image: curlimages/curl command: - "curl" args: - "{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }}" restartPolicy: Never
以下の様に出力されます。--logs
オプションを付与すると、コンテナ上のログも出力してくれます。
$ helm test --logs -n zunda zunda NAME: zunda LAST DEPLOYED: Sun Nov 6 20:07:10 2022 NAMESPACE: zunda STATUS: deployed REVISION: 1 TEST SUITE: zunda-test Last Started: Sun Nov 6 20:24:44 2022 Last Completed: Sun Nov 6 20:24:50 2022 Phase: Succeeded NOTES: #### SAMPLE HELM CHART #### Chart Name: sample Chart Version: 1.0.0 Chart Resource Name: zunda The following resources have been managed. - Deployment - gcr.io/google-samples/hello-app:2.0 - Service - ClusterIP POD LOGS: zunda-test % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed Hello, world! Version: 2.0.0 Hostname: zunda-6c578977db-l8sk7 100 62 100 62 0 0 8852 0 --:--:-- --:--:-- --:--:-- 10333
Helm Chart のリポジトリ化
最後に、作成した Helm Chart をリポジトリ化して利用してみます。
リポジトリ化するには、index.yaml
というファイルと、Chart 一式を固めた tar ファイルを、静的 WEB ホスティングサイトにアップロードすることで実現されます。静的 WEB ホスティングサイトとして、今回は GitHub Pages を利用します。
helm package
コマンドを利用することで、tar ファイルを作成してくれます。
$ helm package ./sample --destination <tarファイルの作成先パス> Successfully packaged chart and saved it to: ../sample-1.0.0.tgz
tar ファイルを置いたパスを指定して helm repo index
を実行すると、index.yaml
ファイル7を自動的に作成してくれます。
$ helm repo index <tarファイルがあるパス> $ cat ../index.yaml apiVersion: v1 entries: sample: - apiVersion: v2 appVersion: 2.0.0 created: "2022-11-06T20:41:54.2659454+09:00" description: Sample Helm Chart digest: 31f67984128e54967d4aa111368c4b9902ecd5e5239e0b3d1fccf1b5b99dea90 icon: http://dummy.com/ kubeVersion: '>= 1.25.0' name: sample type: application urls: - sample-1.0.0.tgz version: 1.0.0 generated: "2022-11-06T20:41:54.2650981+09:00"
この 2 つのファイルを静的 WEB サイトに置けばリポジトリ化してくれます。今回は、URL https://goodbyegangster.github.io/helm/
の GitHub Pages がリポジトリとなっています。
リポジトリ経由でインストールをしてみます。
$ helm repo add sample https://goodbyegangster.github.io/helm/ "sample" has been added to your repositories $ $ helm repo list NAME URL sample https://goodbyegangster.github.io/helm/ $ $ helm install -n zunda zunda99 sample/sample NAME: zunda99 LAST DEPLOYED: Sun Nov 6 20:54:19 2022 NAMESPACE: zunda STATUS: deployed REVISION: 1 NOTES: #### SAMPLE HELM CHART #### Chart Name: sample Chart Version: 1.0.0 Chart Resource Name: zunda99 The following resources have been managed. - Deployment - gcr.io/google-samples/hello-app:2.0 - Service - ClusterIP $ $ helm list -n zunda NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION zunda zunda 1 2022-11-06 20:07:10.6808611 +0900 JST deployed sample-1.0.0 2.0.0 zunda2 zunda 1 2022-11-06 20:17:00.1188276 +0900 JST deployed sample-1.0.0 2.0.0 zunda3 zunda 1 2022-11-06 20:23:32.4401941 +0900 JST deployed sample-1.0.0 2.0.0 zunda99 zunda 1 2022-11-06 20:54:19.199032 +0900 JST deployed sample-1.0.0 2.0.0