# Helm으로 쿠버네티스 리소스 배포하기

최초작성일 \[2020.04.26]

## 1. Helm 이란?

Helm은 간단히 말하면 쿠버네티스 리소스를 패키지로 관리하는 툴이다. Helm을 사용하면 쿠버네티스 리소스를 배포하고 버저닝 할 수 있다. 예를 들어, 새로운 버전의 도커이미지로 쿠버네티스의 deployment를 새로 배포한다고 할 때 deployment 관련 매니페스트에 별도의 코드 변경을 가하지 않고 Helm을 이용하여 새로운 버전의 deployment를 배포할 수 있으며, 리비전도 남기기 때문에 필요 시 롤백도 할 수 있다.&#x20;

Helm을 사용하기 전에 아래 3가지 개념에 대해 먼저 알아야 한다. (더 자세한  내용은 [여기](https://helm.sh/docs/intro/using_helm/) 참고.)

#### 1. 차트(chart)

차트는 쿠버네티스 리소스 집합을 나타내는 파일들을 패키지로 묶은 단위다. 차트는 파드(pod)로만 구성되어 있을 수도 있고 service, configmap 등의 복잡한 오브젝트들로 구성되어 있을 수도 있다.

#### 2. 리퍼지토리(repository)

차트를 저장하고 관리하는 곳으로, Helm이 동작하는 서버의 디스크가 될 수도 있고 AWS의 S3 버킷이 될 수도 있다. 여러 사람들이 같은 템플릿을 관리한다면 리퍼지토리가 필요하다.

#### 3. 릴리즈(release)

쿠버네티스 클러스터에서 차트가 실행된 상태의 단위다. 같은 클러스터에 있는 1개의 차트에서 여러 개의 릴리즈를 생성할 수 있다.

## 2. Helm 설치하기

리눅스 서버에 Helm을 설치하고 실행해보자. 리눅스 버전은 Ubuntu 18.04 이다.

[Helm의 깃헙 리퍼지토리](https://github.com/helm/helm/releases/tag/v2.16.6)에서 바이너리 파일 Linux amd64를 다운로드 한다. `v.3.x.x` 부터는 Helm을 초기화할 때 사용하는 `helm init` 명령어를 지원하지 않으므로 `2.16.6`버전으로 설치했다.

```
$ wget https://get.helm.sh/helm-v2.16.6-linux-amd64.tar.gz
$ tar -xzvf helm-v2.16.6-linux-amd64.tar.gz
$ ls
helm-v2.16.6-linux-amd64.tar.gz   linux-amd64
$ sudo mv linux-amd64/helm /usr/local/bin/helm
$ helm
The Kubernetes package manager

To begin working with Helm, run the 'helm init' command:

	$ helm init

This will install Tiller to your running Kubernetes cluster.
It will also set up any necessary local configuration.
...
```

## 3. Tiller를 위한 role & service account 생성하기

Tiller는 쿠버네티스 클러스터 내부에 설치되어 Helm 클라이언트와 통신하는 API 서버라고 보면 된다. Tiller가 차트를 가지고 쿠버네티스 클러스터에 deployment, service와 같은 리소스를 생성/삭제하려면 그와 관련된 권한이 필요하다. Tiller가 권한을 가지려면

1. Role 오브젝트를 생성한다.
2. Role을 부여할 ServiceAccount를 생성한다.
3. 생성한 Role과 ServiceAccount를 RoleBinding 오브젝트를 통해 연결한다.
4. Helm을 초기화할 때  ServiceAccount를 넘겨준다.

쿠버네티스에서 권한은 롤과 클러스터 롤로 구분한다.

#### role

특정 네임스페이스에 속하는 오브젝트들에 대한 권한을 정의할 때 사용한다.&#x20;

#### cluster role

클러스터 단위의 권한을 정의할 때 사용한다. 네임스페이스에 속하지 않는 오브젝트들에 대한 권한 뿐만 아니라 클러스터 전반에 걸친 기능에 대한 권한이 필요할 때 클러스터 롤을 부여한다.&#x20;

클러스터 롤은 쿠버네티스 컴포넌트가 사용하는 권한도 포함되기 때문에 꽤 많은 수의 클러스터 롤이 미리 생성되어 있다. 쿠버네티스에서 관리자 권한으로 모든 기능을 사용할 수 있는 `cluster-admin`이라는 롤이 그 예다. `cluster-admin` 을 `helm-chart-account`라는 서비스 어카운트에 부여해보자.

`account.yaml`이라는 매니페스트를 생성했다.

```yaml
apiVersion: v1

kind: ServiceAccount

metadata:
  name: helm-chart-account
  namespace: kube-system


---


apiVersion: rbac.authorization.k8s.io/v1

kind: ClusterRoleBinding

metadata:
  name: cluster-rolebinding

subjects:
    # 권한을 부여할 대상은 ServiceAccount 이다.
  - kind: ServiceAccount
    # helm-chart-account 라는 이름의 서비스 어카운트에 권한을 부여한다.
    name: helm-chart-account
    namespace: kube-system

roleRef:
  apiGroup: rbac.authorization.k8s.io
  # cluster-admin 이라는 이름의 클러스터 롤을 대상에게 부여한다.
  kind: ClusterRole
  name: cluster-admin
```

위 매니페스트를 적용한다.

```
$ kubectl apply -f account.yaml
serviceaccount/helm-chart-account created
clusterrolebinding.rbac.authorization.k8s.io/cluster-rolebinding created
```

생성한 서비스 어카운트로 service 오브젝트를 조회해보자.  `--as` 옵션은 쿠버네티스 명령어를 실행할 때 어떤 인증방식과 식별을 사용할 것인지 지정한다. `helm-chart-account` 라는 서비스 어카운트가 `kube-system` 네임스페이스에서 소속되었으므로 `system:serviceaccount:kube-system:helm-chart-account` 라는 값을 넘겨줬다.

```
$ kubectl get svc --as system:serviceaccount:kube-system:helm-chart-account
NAME          TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
kubernetes    ClusterIP   10.96.0.1       <none>        443/TCP          7d8h
service-app   NodePort    10.104.73.103   <none>        8080:30080/TCP   3h48m
```

Helm을 초기화한다.

```
$ helm init --service-account helm-chart-account
```

## 4. 차트 생성하고 템플릿 바인딩하기

차트를 생성할 때는 아래의 명령어를 실행한다.

```
$ helm create chart <차트 이름>
```

`my-helm-chart` 라는 이름의 차트를 생성하면 아래와 같은 구조의 디렉토리와 파일이 생성된다.

```
my-helm-chart
  Chart.yaml          # A YAML file containing information about the chart
  LICENSE             # OPTIONAL: A plain text file containing the license for the chart
  README.md           # OPTIONAL: A human-readable README file
  values.yaml         # The default configuration values for this chart
  values.schema.json  # OPTIONAL: A JSON Schema for imposing a structure on the values.yaml file
  charts/             # A directory containing any charts upon which this chart depends.
  crds/               # Custom Resource Definitions
  templates/          # A directory of templates that, when combined with values,
                      # will generate valid Kubernetes manifest files.
  templates/NOTES.txt # OPTIONAL: A plain text file containing short usage notes
```

| file        | usage                                        |
| ----------- | -------------------------------------------- |
| Chart.yaml  | 차트에 대한 기본적인 정보를 기재한다. (작성자, 차트 이름, 버전 등)     |
| values.yaml | 클러스터의 리소스 관련 config, 템플릿에 들어갈 동적인 값 등을 기재한다. |
| templates   | values.yaml에 정의된 속성들을 바인딩 할 템플릿들의 디렉토리.      |

한가지 예시를 들어보겠다. `flask-gunicorn:0.0.1`이라는 도커이미지로 deployment를 생성하는 매니페스트 `deployment.yaml`이 있다고 하자.

```yaml
apiVersion: apps/v1

kind: Deployment

metadata:
  name: deployment-app

spec:
  ...
    spec:
      containers:
        - name: flask-gunicorn
          image: kde6260/flask-gunicorn:0.0.1
          ports:
            - containerPort: 9090
```

플라스크 애플리케이션 서버 코드에 변경이 있어서 도커이미지에 `0.0.2`라는 새로운 버전이 생겼다면 매니페스트의 `template.spec.containers.image`를 `kde6260/flask-gunicorn:0.0.2`로 직접 고쳐야 한다. 하지만 Helm을 사용하면 직접 고치지 않아도 된다.

`values.yaml`에 아래 내용을 추가했다.

```yaml
deployment:
  flaskGunicorn:
    # use --set option when deploying new version of image
    image: kde6260/flask-gunicorn:0.0.1
```

`deployment.yaml`을 아래와 같이 수정했다.

```yaml
apiVersion: apps/v1

kind: Deployment

metadata:
  name: deployment-app

spec:
  ...
    spec:
      containers:
        - name: flask-gunicorn
          image: {{ .Values.deployment.flaskGunicorn.image }}
          ports:
            - containerPort: 9090
```

`values.yaml`에 정의된 속성을 템플릿에 바인딩할 때는 `{{ .Values.< 속성 이름> }}`과 같은 형식을 사용한다.&#x20;

Helm의 `values.yaml`에 있는 속성을 위처럼 템플릿에 바인딩 시킨 뒤 (이미지 이름을 고정시키지 않고 동적으로 바인딩하므로 앞으로 템플릿이라고 하겠다.)`helm upgrade ... --set deployment.flaskGunicorn.image=kde6260/flask-gunicorn:0.0.2` 를 실행하면 파일에 코드를 직접 수정하지 않고 deployment를 배포할 수 있다. (`helm upgrade ... --set=...` 에 대해서는 뒤에서 다룬다.)&#x20;

단, `deployment.yaml`은 `$ helm create chart my-helm-chart`를 통해 생성된 `my-helm-chart/templates` 디렉토리에 있어야 한다.

이제 생성한 차트를 리퍼지토리에 등록해보자.

## 5. chartmuseum으로 리퍼지토리 생성하기

Helm은 기본적으로 리퍼지토리 2개를 가지고 있다. 모든 리퍼지토리를 조회하려면 아래의 명령어를 실행한다.

```
$ helm repo list
NAME       	URL
stable     	https://kubernetes-charts.storage.googleapis.com
local      	http://127.0.0.1:8879/charts
```

팀원들과 차트를 공유하고 관리하려면 원격에 위치한 리퍼지토리가 필요할 것이다. [chartmuseum](https://github.com/helm/chartmuseum)은 원격으로 공유가능한 리퍼지토리를 생성하고 차트를 생성/수정/삭제하는 API를 제공한다.&#x20;

chartmuseum을 설치한다.

```
$ curl -LO https://s3.amazonaws.com/chartmuseum/release/latest/bin/linux/amd64/chartmuseum
$ chmod +x ./chartmuseum
$ sudo mv ./chartmuseum /usr/local/bin/chartmuseum
$ chartmuseum
2020-04-26 14:01:52.775282 I | Missing required flags(s): --storage
```

설치한 chartmuseum으로 리퍼지토리를 생성한다. 리눅스 서버를 포트번호 8888번으로 노출시켜서 리퍼지토리에 접근할 것이므로 local로 생성했다.

```
$ chartmuseum --port=8888 --storage="local" --storage-local-rootdir="/chart-storage" &
```

| option                  | usage                                            |
| ----------------------- | ------------------------------------------------ |
| --port                  | chartmuseum API 서버가 listening 하는 포트 번호           |
| --storage               | 스토리지 백엔드. local, amazon, google, oracle 중 하나 선택. |
| --storage-local-rootdir | 스토리지 백엔드가 local일 경우 차트를 저장할 위치.                  |

생성한 리퍼지토리를 등록한다. `chartmuseum`이라는 이름으로 등록된 것을 확인할 수 있다.

```
$ helm repo add chartmuseum http://localhost:8888
$ helm repo list
NAME       	URL
stable     	https://kubernetes-charts.storage.googleapis.com
local      	http://127.0.0.1:8879/charts
chartmuseum	http://localhost:8888
```

이제 차트를 리퍼지토리에 등록해보자.

## 6. 차트를 리퍼지토리에 업로드하기

[4. 차트 생성하고 템플릿 바인딩하기](https://kde6260.gitbook.io/dev/helm#4)에서 차트와 관련된 파일을 생성하고 동적으로 변경되는 값들도 템플릿으로 만들었으나 이를 리퍼지토리에 등록하려면 차트를 패키지로 묶는 과정이 필요하다. 차트를 패키징하는 명령어는 아래와 같다.

```
$ helm package <차트 디렉토리 위> --version=<버전 이름>
```

`my-helm-chart`를 첫 패키징하는 것이므로 아래의 명령어를 실행한다.

```
$ helm package my-helm-chart/ --version=0.0.1
```

`my-helm-chart-0.0.1.tgz`라는 파일이 생성된 것을 확인할 수 있다.&#x20;

```
$ ls
my-helm-chart-0.0.1.tgz
```

chartmuseum API로 생성된 파일을 리퍼지토리에 업로드한다.

```
curl --data-binary "@my-helm-chart-0.0.1.tgz" http://localhost:8888/api/charts
```

리퍼지토리의 루트 디렉토리인 `~/chart-storage` 를 확인하면 파일이 업로드된 것을 확인할 수 있다.

```
$ ls ~/chart-storage/
my-helm-chart-0.0.1.tgz  index-cache.yaml
```

## 7. 업로드한 차트로 쿠버네티스 리소스 배포하기(릴리즈 생성하기)

chartmuseum 리퍼지토리에 업로드한 차트로 deployment를 생성해보자.&#x20;

```
$ helm install -f <values.yaml 위치> --repo <리퍼지토리 이름> <차트 이름> --name <릴리즈 이름>
```

| option       | usage               |
| ------------ | ------------------- |
| -f, --values | values.yaml 파일의 위치. |
| --repo       | 리퍼지토리 이름            |
| --name       | 새로 생성 또는 수정될 릴리즈 이름 |

chartmuseum 리포지토리에 있는 `my-helm-chart-0.0.1.tgz`차트로 `my-first-release` 라는 이름의 릴리즈를 생성하겠다. `values.yaml`은 [4.차트 생성하고 템플릿 바인딩하기](https://kde6260.gitbook.io/dev/helm#4) 에서 수정한 `values.yaml`과 같다.

```
helm install -f helm-chart/values.yaml --repo chartmuseum helm-chart-0.0.1.tgz \
--name=my-first-release
```

템플릿 내용대로라면 deployment-app 이라는 이름의 deployment가 생성되어야 하고, pod의 이미지 버전은 `kde6260/flask-gunicorn:0.0.1`어야 한다.

```
$ kubectl get deployments
NAME             READY   UP-TO-DATE   AVAILABLE   AGE
deployment-app   6/6     6            6           5h24m

$ kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
deployment-app-6fc7bcf865-8x5r2   2/2     Running   0          4h33m
deployment-app-6fc7bcf865-jmsh7   2/2     Running   0          4h33m
deployment-app-6fc7bcf865-nffk2   2/2     Running   0          4h33m
deployment-app-6fc7bcf865-ngzf2   2/2     Running   0          4h33m
deployment-app-6fc7bcf865-vxhdf   2/2     Running   0          4h33m
deployment-app-6fc7bcf865-z5tfj   2/2     Running   0          4h33m

$ kubectl describe pods deployment-app-6fc7bcf865-8x5r2
Name:         deployment-app-6fc7bcf865-8x5r2
Namespace:    default
Priority:     0
Node:         ip-10-0-3-217/10.0.3.217
Start Time:   Sun, 26 Apr 2020 10:04:04 +0000
Labels:       app=nginx-gunicorn-flask
              pod-template-hash=6fc7bcf865
Annotations:  cni.projectcalico.org/podIP: 192.168.184.93/32
              cni.projectcalico.org/podIPs: 192.168.184.93/32
Status:       Running
IP:           192.168.184.93
IPs:
  IP:           192.168.184.93
Controlled By:  ReplicaSet/deployment-app-6fc7bcf865
Containers:
  my-nginx:
    Container ID:   docker://f98f0dc40af97c02d055e4280f29a8d85d55d5094479ac1aba5b51bf8df058bf
    Image:          kde6260/my-nginx:stable
    Image ID:       docker-pullable://kde6260/my-nginx@sha256:30700d34592f30421f79c6dc5b8713b4b6b8c6472c843c6197e1b5a66a1dbba3
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Sun, 26 Apr 2020 10:04:05 +0000
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-phpzx (ro)
  flask-gunicorn:
    Container ID:   docker://73fca9307eef7696b7af54c28f56cb96cd7465fd3069cb497ed77497722513c4
    Image:          kde6260/flask-gunicorn:0.0.1
    Image ID:       docker-pullable://kde6260/flask-gunicorn@sha256:8a97f252450934ec0398166f4f2de001e10a20f9da3db6ae8e1b6d2707f8f8cb
```

`kde6260/flask-gunicorn:0.0.2` 버전으로 배포해보자. 기존의 차트의 value를 수정하고 싶거나 릴리즈를 다른 버전의 차트로 변경하고 싶을 때 `$ helm upgrade` 명령어를 사용한다.

```
$ helm upgrade -f helm-chart/values.yaml --repo chartmuseum \
--set deployment.flaskGunicorn.image=kde6260/flask-gunicorn:0.0.2 \
 my-helm-chart ./helm-chart
```

참고자료

* <https://helm.sh/docs/>
* [조대협의 블로그 - 쿠버네티스 패키지 매니저 Helm #2-1. Chart](https://bcho.tistory.com/1337)
* [Helm Chart를 이용한 Kubernetes배포/관리](https://tech.osci.kr/2019/11/23/86027123/)
* [chartmuseum 설치 & 사용 ](https://arisu1000.tistory.com/27861)
