10분만에 만드는 쿠버네티스 Helm chart
쿠버네티스에 서비스를 배포하기 위해 사용하는 대표적인 방법중에 하나가 바로 Helm chart 이다. 한마디로 말해서 Helm chart 는 쿠버네티스 용도의 패키징된 s/w 라 할 수 있다. Helm chart 문법은 go template 을 활용하였기 때문에 go template 을 안다면 조금 더 쉽게 이해할 수 있다.
여기서 설명하는 내용은 사전에 쿠버네티스 클러스터가 있으며 kubectl 로 api 에 접근할 수 있는 환경이 있다는 것을 가정한다.
그럼 도커 이미지를 가지고 쿠버네티스에 Helm 으로 배포할 수 있는 Helm chart 를 만들어 보자.
먼저 Helm (v3)을 설치한다.
$ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3
$ chmod 700 get_helm.sh
$ ./get_helm.sh
$ helm version
version.BuildInfo{Version:"v3.4.0", GitCommit:"7090a89efc8a18f3d8178bf47d2462450349a004", GitTreeState:"clean", GoVersion:"go1.14.10"}
Helm 버전은 틀릴 수 있는데 v.3.0.0 이상이면 상관없다.
1. 기본 코드 생성하기
아무 디렉토리로 이동해서 아래의 명령어로 기본 코드를 생성한다. (여기서 예제는 gRpc 기반의 tks-contract 이라는 프로그램을 기반으로 했다)
$ helm create tks-contract
helm create 명령어는 tks-contract 디렉토리를 만들고 아래와 같은 구조로 디렉토리와 샘플 코드를 자동으로 만들어 준다. 그 구조는 다음과 같다.
$ tree tks-contract
tks-contract
├── Chart.yaml
├── charts
├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── service.yaml
│ ├── serviceaccount.yaml
│ └── tests
│ └── test-connection.yaml
└── values.yaml
3 directories, 10 files
Chart.yaml 은 chart 에 대한 기본적인 정보가 있는 파일이다. chart 명, chart 버전, chart 설명 등을 적을 수 있으며, 지금은 별로 바꿀 내용이 없다.
charts 디렉토리는 의존성을 관리한다. 예를 들면 DB를 이용하는 웹 애플리케이션 chart 를 만들 때 DB 가 설치되어야 한다면 여기에 chart 를 넣거다 chart repo 에 존재하는 chart 의 링크를 기술 할 수 있다. 이 것도 지금은 바꿀 내용이 없다.
templates 디렉토리는 가장 중요한 디렉토리다. 이 디렉토리 안에는 쿠버네티스 리소스 yaml 파일들이 (예를 들면, Deployment, Service 와 같은 리소스 정의 파일) 위치한다.
values.yaml 파일은 한마디로 변수들을 정의한 파일이다. templates 디렉토리 안에 있는 yaml 파일들의 특정 변수 값을 치환하고 싶을 때 값을 선언해 놓는 곳이다. 일반적으로 key: value 형태로 기술한다.
2. Template 작성 방법
먼저 간단한 templates 디렉토리 안의 service.yaml 을 변경해 보자.
apiVersion: v1
kind: Service
metadata:
name: {{ include "tks-contract.fullname" . }}
namespace: {{ .Values.namespace }}
labels:
{{- include "tks-contract.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.args.port }}
protocol: TCP
selector:
{{- include "tks-contract.selectorLabels" . | nindent 4 }}
go 템플릿을 사용하기 때문에 {{ }} 기호로 값을 치환한다는 것을 알 수 있다. 일반적으로 values.yaml 파일에 정의된 값을 치환할 수 있게 적혀있다. 가령 {{ .Values.namespace }} 는 values.yaml 파일의 namespace 를 키로 하는 값을 가져와서 치환하라는 의미이다. 실제 values.yaml 에는 아래와 같이 되어 있다.
namespace: tks
service:
type: LoadBalancer
port: 9110
values.yaml 파일의 service 아래의 type 을 키로 하는 값, 즉 LoadBalancer 라는 값을 가져오기 위해서는 {{ .Values.service.type }} 으로 적어주면 된다.
마지막 라인의 {{- include "tks-contract.selectorLabels" . | nindent 4 }} 와 같이 include 는 정의된 템플릿 호출을 의미한다. 즉 tks-contract.selectorLabels 라는 정의된 템플릿을 호출했다고 할 수 있다. 템플릿 정의들은 보통 templates 디렉토리 아래 _helpers.tpl 파일에 정의하며 define 으로 선언되어 있다. _helpers.tpl 파일의 일부를 살펴보자.
{{/*
Selector labels
*/}}
{{- define "tks-contract.selectorLabels" -}}
app.kubernetes.io/service: tks
app.kubernetes.io/name: {{ include "tks-contract.name" . }}
{{- end }}
{{/* */}} 은 주석처리이다. 그 아래에 {{- define "tks-contract.selectorLabels" -}} ... {{- end }} 은 템플릿 정의이다.
{{- include "tks-contract.selectorLabels" . | nindent 4 }} 에서 {{- 는 맨앞에 쓰였을 때 {{ }} 코드가 차지하는 영역의 줄바꿈과 공백을 없애라는 뜻이며 . 은 values.yaml 에 있는 모든 변수들을 아규먼트로 넘긴다는 뜻이다. 마지막으로 | 는 shell 에서의 파이프라인과 동일하고 nindent 4 는 결과를 프린트 할 때 공백 4개를 멀티라인으로 계속 들여쓰라는 의미이다.
정리해 보면 service.yaml 과 _helper.tpl 파일을 이 활용되어 아래와 같이 작동된다는 것을 알 수 있다.
## service.yaml
selector:
{{- include "tks-contract.selectorLabels" . | nindent 4 }}
+
## _helpers.tpl
{{- define "tks-contract.selectorLabels" -}}
app.kubernetes.io/service: tks
app.kubernetes.io/name: {{ include "tks-contract.name" . }}
{{- end }}
================ 결과 ===============
selector:
app.kubernetes.io/service: tks
app.kubernetes.io/name: tks-contract
app.kubernetes.io/service: tks 와 app.kubernetes.io/name: tks-contract 가 들여쓰기 4 만큼 프린트 됐다. 물론 _helpers.tpl 내의 템플릿 정의를 찾아보면 {{ include "tks-contract.name" . }} 의 결과 값은 tks-contract 이다.
그리고 NOTES.txt 에 적힌 내용은 chart 가 배포되고 나서 터미널에 프린트된다.
3. 전체 소스 작성
코드를 원하는 값으로 수정하고 배포될 때의 최종 yaml 이 어떻게 될지 확인해보자.
먼저 Chart.yaml 이다.
$ vi tks-contract/Chart.yaml
apiVersion: v2
name: tks-contract
description: A Helm chart for tks-contract
type: application
version: 0.1.0
appVersion: 0.1.0
_helpers.tpl 은 selectorLabels 를 원하는 값을 넣기 위해서 49 라인만 수정하였다.
$ vi tks-contract/_helpers.tpl
...
{{/*
Selector labels
*/}}
{{- define "tks-contract.selectorLabels" -}}
app.kubernetes.io/service: tks
app.kubernetes.io/name: {{ include "tks-contract.name" . }}
{{- end }}
...
deployment.yaml 은 container 의 args 값을 추가했다.
$ vi tks-contract/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "tks-contract.fullname" . }}
namespace: {{ .Values.namespace }}
labels:
{{- include "tks-contract.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "tks-contract.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "tks-contract.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "tks-contract.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: tks-contract
containerPort: {{ .Values.args.port }}
protocol: TCP
command:
- /app/server
args: [
"-port", "{{ .Values.args.port }}",
"-dbhost", "{{ .Values.args.dbUrl }}",
"-dbport", "{{ .Values.args.dbPort }}",
"-dbuser", "{{ .Values.args.dbUser }}",
"-dbpassword", "{{ .Values.args.dbPassword }}",
"-info-address", "{{ .Values.args.tksInfoAddress }}",
"-info-port", "{{ .Values.args.tksInfoPort }}"
]
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 }}
조건절은 {{- if }} 혹은 {{- if not }} {{ end }} 로 사용할 수 있고 {{- with 키 }} {{ end }} 는 with 절 안에서는 구조상 해당 키 아래의 값들을 사용하겠다는 의미이다. {{- toYaml 키 }} 는 키의 값을 그대로 Yaml 로 프린트 해 준다.
nodeSelector 를 예를 들어 보자.
## deployment.yaml
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
+
## values.yaml
nodeSelector:
taco-tks: enabled
============ 결과 =================
nodeSelector:
taco-tks: enabled
values.yaml 에 nodeSelector 키 아래에 taco-tks: enabled 라는 값을 그대로 toYaml 로 프린트 한다.
service.yaml 전체는 다음과 같다.
$ vi tks-contract/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "tks-contract.fullname" . }}
namespace: {{ .Values.namespace }}
labels:
{{- include "tks-contract.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.args.port }}
protocol: TCP
selector:
{{- include "tks-contract.selectorLabels" . | nindent 4 }}
values.yaml 에 전체 값을 넣는다.
$ vi tks-contract/values.yaml
replicaCount: 1
namespace: tks
image:
repository: docker.io/seungkyu/tks-contract
pullPolicy: Always
tag: "latests"
imagePullSecrets: []
nameOverride: "tks-contract"
fullnameOverride: "tks-contract"
serviceAccount:
create: true
annotations: {}
name: "tks-info"
args:
port: 9110
dbUrl: postgresql.decapod-db.svc
dbPort: 5432
dbUser: tksuser
dbpassword: tkspassword
tksInfoAddress: tks-info.tks.svc
tksInfoPort: 9110
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: LoadBalancer
port: 9110
ingress:
enabled: false
annotations: {}
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:
taco-tks: enabled
tolerations: []
affinity: {}
마지막으로 배포후 프린트될 내용은 NOTES.txt 을 수정한다.
$ vi tks-contract/templates/NOTES.txt
TKS-Contract
{{- if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Values.namespace }} svc -w {{ include "tks-contract.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Values.namespace }} {{ include "tks-contract.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
gRPC Call => $SERVICE_IP:{{ .Values.service.port }}
{{- end }}
4. 결과 확인
작성된 chart 는 아래 명령어로 최종 yaml 이 어떻게 변환되어 배포되는지 dry-run 으로 확인할 수 있다.
$ helm upgrade -i tks-contract ./tks-contract --dry-run --debug
---
# Source: tks-contract/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: tks-info
labels:
helm.sh/chart: tks-contract-0.1.0
app.kubernetes.io/service: tks
app.kubernetes.io/name: tks-contract
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
---
# Source: tks-contract/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: tks-contract
namespace: tks
labels:
helm.sh/chart: tks-contract-0.1.0
app.kubernetes.io/service: tks
app.kubernetes.io/name: tks-contract
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
spec:
type: LoadBalancer
ports:
- port: 9110
targetPort: 9110
protocol: TCP
selector:
app.kubernetes.io/service: tks
app.kubernetes.io/name: tks-contract
---
# Source: tks-contract/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: tks-contract
namespace: tks
labels:
helm.sh/chart: tks-contract-0.1.0
app.kubernetes.io/service: tks
app.kubernetes.io/name: tks-contract
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/service: tks
app.kubernetes.io/name: tks-contract
template:
metadata:
labels:
app.kubernetes.io/service: tks
app.kubernetes.io/name: tks-contract
spec:
serviceAccountName: tks-info
securityContext:
{}
containers:
- name: tks-contract
securityContext:
{}
image: "docker.io/seungkyu/tks-contract:latests"
imagePullPolicy: Always
ports:
- name: tks-contract
containerPort: 9110
protocol: TCP
command:
- /app/server
args: [
"-port", "9110",
"-dbhost", "postgresql.decapod-db.svc",
"-dbport", "5432",
"-dbuser", "tksuser",
"-dbpassword", "",
"-info-address", "tks-info.tks.svc",
"-info-port", "9110"
]
resources:
{}
nodeSelector:
taco-tks: enabled
NOTES:
TKS-Contract
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace tks svc -w tks-contract'
export SERVICE_IP=$(kubectl get svc --namespace tks tks-contract --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}")
gRPC Call => $SERVICE_IP:9110