Ghost Season 5 - Helm
ghost를 설치한 지 몇 년이 지났는데 그 동안 여러 가지 방법으로 Ghost 운용 환경을 구축해왔다.
Host 환경, Docker, Ansible, kubernetes 에 이어 이번은 5번째 시즌인데 Helm Chart 를 시용해서 설치해 보는 것이다. 처음 시작은 helm repository에 있는 공식(?) 공개된 helm chart를 이용하여 values.yaml 파일만 내 환경에 맞게 변경해서 사용하려던 것이었는데 아쉽게 아직은 그렇게 하기 힘든 것으로 보여 직접 helm chart를 만들어서 사용하고 있다. 이 문서는 그 과정을 기술한 것으로 향후 공식 helm chart를 활용할 수 있는 때가 오면 시즌 6에 해당하는 글을 또 쓸 듯 하다.
PV
PV(Persistent Volume을 만드는 건 Helm의 역할이 아닌 듯. 대부분의 Helm Chart는 필요한 PVC(Persistent Volume Claim)은 정의하고 있어도 PV 생성에 대한 책임은 지고 있지 않은 듯 하다.
ghost-pv.yaml
kind: PersistentVolume
apiVersion: v1
metadata:
name: ghost-pv
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 12Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/home/cychong/Dropbox/Apps/ghost/content"
Kubectl 명령을 이용하여 직접 PV 생성한다.
cychong@mini1:~/work$ kubectl create -f ghost-in-k8s/ghost-pv.yaml
persistentvolume/ghost-pv created
cychong@mini1:~/work$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
ghost-pv 12Gi RWO Retain Available manual 8s
다음은 helm을 이용해서 ghost를 deploy한다.
처음 helm을 이용해서 손쉽게 ghost를 설치할 수 있다는 걸 알고( How to Install Apps on Kubernetes with Helm ) 부푼 기대감을 가지고 설치해 봤는데 아쉽게 github에 있는 건 mariadb를 backend DB로 사용하는 형태였다. Sqlite를 backend로 해서 가볍게 개인 블로그를 운용하고 있는 입장에서는 이걸 위해 mariadb를 사용하기는 배보다 배꼽이 더 큰 상황이 아닌가 싶어 mariadb를 사용하지 않고 ghost를 설치할 수 있는 지 helm chart를 살펴봤다. 그런데 아무리 봐도 mariadb 혹은 별도의 외부 DB가 없으면 제대로 동작하지 않는(ghost를 deploy할 수 없는) 듯 해 보였다. https://github.com/helm/charts/issues/16989 에도 issue를 올렸는데 반응을 보인 한 명이 별 문제 없을 것 같은데 라고 하길래 현상을 자세히 적어줬지만 그 다음부터는 감감무소식이라. 오픈 소스가 원래 그런 거 지만… 다들 문제가 없는 건지 내가 하는 것처럼 ghost + kubernetes + helm + sqlite 의 조합으로 사용을 안하는 건지…
결국 목마른 사람이 우물을 판다고 이전에 직접 만들어서 사용했던 YAML 파일 들을 이용해서 직접 Helm chart를 만들어 보기로 마음 먹었다. 겸사겸사 이러다 보면 Helm chart의 복잡한(?) 문법도 어쩔 수 없이 조금은 이해할 수 밖에 없는 상황이 되지 않을까 하고
접근 방법은 Helm chart의 convention과는 조금 다르지만 근본적으로 결국 deployment, service 등의 조합으로 이루어지는 거라 일단 잘(적어도 내가 원하는 형태대로) 동작하는 YAML 파일들 과 helm create
명령을 통해 만들어진 기본 형태의 파일들과 비교해 가면서 기존 YAML 파일들의 내용을 helm chart에 반영해서 제대로 된 동작하는 helm chart를 만들기로 했다. 제대로의 기준 중 하나는 helm이 추구하는 것과 같이 배포 site와 무관한 내용은 YAML이나 template 파일들에 정의하고 배포 site별로 다를 수 있는 내용은 values.yaml 파일에 정의하는 것이다. 이를 테면 블로그 URL등을 이전에는 deployment.yaml파일에 직접 정의했지만 이번에는 values.yaml에 정의하고 이 값을 참조하도록 했다.
처음 helm을 사용해 보는 거라 쉽게 되지는 않았다. 밤 늦게 퇴근해서 뭔가를 한다는 건 정말 힘든 일이었고, 주말에 어쩌다 시간이 나면 시도해 보는 건데 이마저도 제대로 집중해서 보질 못하니. 그렇게 trial & error를 통해 만든 ghost helm chart를 github repo 에 올렸다. 참고로 이 repo에 있는 내용은 새로 ghost를 만드는 경우에도 잘 동작하는 지는 확인하지 못했다. 기존 sqlite DB파일을 가지고 있는 내 동작 환경에서만 확인해 본 거라 아직은 얼마나 범용성을 가지고 있는 지는 잘 모르겠다. 그렇게 하려면 완전히 새로운 환경에서 해 봐야 하는데 내 코가 석자라 그건 나중에 시간이 나면…
이 helm chart를 이용해 ghost를 설치하는 과정은 다음과 같다.
helm chart를 ghost-with-helm 이라는 디렉토리에 만들었다.
$ tree -l ghost-with-helm
ghost-with-helm
├── Chart.yaml
├── README.md
├── charts
├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── ingress.yaml
│ ├── pvc.yaml
│ ├── service.yaml
│ └── tests
│ └── test-connection.yaml
└── values.yaml
참고로 ingress.yaml은 아직 사용하지 않고 직접 NodePort를 이용해서 처리하고 있다. 이 부분은 향후 개선할 점 중 하나.
helm chart의 values.yaml을 맞게 고친 후 설치한다.
cychong@mini1:~/work$ helm install --name my-ghost ghost-with-helm
NAME: my-ghost
LAST DEPLOYED: Sun Sep 22 21:39:34 2019
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/Deployment
NAME READY UP-TO-DATE AVAILABLE AGE
my-ghost 0/1 1 0 0s
==> v1/PersistentVolumeClaim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
ghost-pvc Bound ghost-pv 12Gi RWO manual 1s
==> v1/Pod(related)
NAME READY STATUS RESTARTS AGE
my-ghost-5f6578fd76-djd2g 0/1 Pending 0 0s
==> v1/Service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-ghost NodePort 10.97.215.158 <none> 2368:30025/TCP 1s
NOTES:
1. Get the application URL by running these commands:
export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services my-ghost)
export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
helm install
명령을 내리면 chart로 생성된 application과 관련된 resource 들 pod, service, PVCs 등의 상태가 한번에 모두 확인된다. Pod는 명령을 내리자 마자 상태를 조회한 거라 STATUS
가 아직 Pending
상태로 나온다. kubectl 명령으로 pod의 상태를 다시 확인해 본다.
cychong@mini1:~/work$ kubectl get pod my-ghost-5f6578fd76-djd2g
NAME READY STATUS RESTARTS AGE
my-ghost-5f6578fd76-djd2g 1/1 Running 0 2m43s
Pod의 STATUS
가 정상적으로 RUNNING
으로 출력된다. 이제 pod는 정상적으로 deploy가 된 상태인데 Service를 NodePort
타입으로 지정한 경우 Host에서 접속하려면 실제 pod가 listening하는 것과 다른 port를 사용하므로 위 NOTES
에 있는 대로 실제로 서비스에 할당된 port 정보를 확인해야 한다.
cychong@mini1:~/work$ export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services my-ghost)
cychong@mini1:~/work$ export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
cychong@mini1:~/work$ echo http://$NODE_IP:$NODE_PORT
http://192.168.1.100:30025
이번에 할당된 port는 30025다. 이제 NODE_IP와 NODE_PORT값을 이용해서 해당 주소에 접속하면 정상적으로 ghost 화면이 나온다.
이 port 정보를 이용해서 공유기의 port forwarding 규칙에 추가한다. 외부에서 sosa0sa.com:2368
로 접슨 시 실제 ghost 가 실행된 mini1의 내부 IP로 포워딩 하되 port 번호를 30025
로 변환해서(NAPT) 전달하도록 변경한 후 접속해 본다.
On-premise 환경에서 kubernetes를 사용하는 경우 LoadBalancer를 별도로 설치하지 않고 외부와 통신하기 위해 사용하는 NodePort는 이렇게 service를 deploy할 때마다 port가 바뀌는 문제가 존재한다. 내 경우 공유기의 port forwarding을 이용해서 블로그에 접속할 수 있도록 하고 있어 매번 공유기의 port forwarding 규칙을 고쳐야 하므로 개선이 필요하다(포트 값을 고정값으로 할당하거나, Ingress Controller혹은 MetalLB등을 사용하여 NodePort가 아닌 LoadBalancer 타입으로 변경할 예정이다)
helm-test
cychong@mini1:~/work/ghost-with-helm$ helm test my-ghost
RUNNING: my-ghost-test-connection
PASSED: my-ghost-test-connection
Port Number
NodePort를 사용하면 해당 서비스를 위한 30000에서 32767 사이의 포트 번호가 할당된다. 그런데 위와 같이 port와 TargetPort를 지정하면 위 30000번대 포트 외에 의도한 포트로도 접근이 가능하다.
cychong@mini1:~/work/ghost-with-helm$ echo $(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services my-ghost)
32326
cychong@mini1:~/work/ghost-with-helm$ sudo netstat -atnp |grep kube-proxy
[sudo] password for cychong:
tcp 0 0 127.0.0.1:10249 0.0.0.0:* LISTEN 18392/kube-proxy
tcp 0 0 192.168.1.100:2368 0.0.0.0:* LISTEN 18392/kube-proxy
tcp 0 0 192.168.1.100:49778 192.168.1.100:6443 ESTABLISHED 18392/kube-proxy
tcp6 0 0 :::32326 :::* LISTEN 18392/kube-proxy
tcp6 0 0 :::10256 :::* LISTEN 18392/kube-proxy
위 내용을 보면 my-ghost 서비스에 할당된 NodePort 32326 외에 TargetPort인 2368이 그대로 보이는 것을 알 수 있다.
Upgrade
Helm Chart에 변경이 있는 경우에는 upgrade 명령을 이용한다. 만일 value 파일에 변화가 있는 경우에는 전체를 다시 deploy하는 듯 하다. 아래는 targetPort 값을 임시로 다른 값으로 변경해서 upgrade 했다 다시 원복한 경우다.
cychong@mini1:~/work$ helm upgrade --debug my-ghost ghost-with-helm
[debug] Created tunnel using local port: '44111'
[debug] SERVER: "127.0.0.1:44111"
REVISION: 5
RELEASED: Thu Sep 26 01:06:32 2019
CHART: ghost-0.1.0
USER-SUPPLIED VALUES:
{}
...
Release "my-ghost" has been upgraded.
LAST DEPLOYED: Thu Sep 26 01:06:32 2019
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/Deployment
NAME READY UP-TO-DATE AVAILABLE AGE
my-ghost 1/1 1 1 18m
==> v1/PersistentVolumeClaim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
ghost-pvc Bound ghost-pv 12Gi RWO manual 18m
==> v1/Pod(related)
NAME READY STATUS RESTARTS AGE
my-ghost-5f6578fd76-lb7xc 1/1 Running 0 18m
==> v1/Service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-ghost NodePort 10.105.125.54 192.168.1.100 2368:32326/TCP 18m
Troubleshooting
동일한 helm release 이름을 다시 사용하려면
삭제한 helm release를 재사용하려면 delete 외에 delete —purge
옵션으로 삭제할 것
cychong@mini1:~/work$ helm install --name my-ghost ghost-with-helm
Error: a release named my-ghost already exists.
Run: helm ls --all my-ghost; to check the status of the release
Or run: helm del --purge my-ghost; to delete it
cychong@mini1:~/work$ helm ls
cychong@mini1:~/work$ helm ls -A
Error: unknown shorthand flag: 'A' in -A
cychong@mini1:~/work$ helm ls --all my-ghost
NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
my-ghost 1 Sun Sep 22 21:36:42 2019 DELETED ghost-0.1.0 1.0 default
cychong@mini1:~/work$ helm del --purge my-ghost
release "my-ghost" deleted
helm delete 후 PV 연결이 안되는 경우
한번 Release를 생성한 후 삭제한 경우 PVC가 제대로 할당되지 않는 현상이 있다. 이 경우 PV를 삭제한 후 다시 Helm install을 수행한다.
cychong@mini1:~/work$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
ghost-pv 12Gi RWO Retain Bound default/ghost-pvc manual 13m
cychong@mini1:~/work$ helm ls
helm delete NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
my-ghost 1 Sun Sep 22 21:39:34 2019 DEPLOYED ghost-0.1.0 1.0 default
cychong@mini1:~/work$ helm delete --purge my-ghost
release "my-ghost" deleted
cychong@mini1:~/work$ helm ls
cychong@mini1:~/work$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
ghost-pv 12Gi RWO Retain Released default/ghost-pvc manual 14m
이 상태에서 다시 helm install을 수행하면
cychong@mini1:~/work$ helm install --name my-ghost ghost-with-helm
NAME: my-ghost
LAST DEPLOYED: Sun Sep 22 21:53:40 2019
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/Deployment
NAME READY UP-TO-DATE AVAILABLE AGE
my-ghost 0/1 1 0 1s
==> v1/PersistentVolumeClaim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
ghost-pvc Pending manual 1s
==> v1/Pod(related)
NAME READY STATUS RESTARTS AGE
my-ghost-5f6578fd76-kddlg 0/1 Pending 0 1s
==> v1/Service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-ghost NodePort 10.96.105.207 <none> 2368:32615/TCP 1s
NOTES:
1. Get the application URL by running these commands:
export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services my-ghost)
export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
cychong@mini1:~/work$ kubectl describe pod my-ghost-5f6578fd76-kddlg
Name: my-ghost-5f6578fd76-kddlg
Namespace: default
Priority: 0
Node: <none>
Labels: app.kubernetes.io/instance=my-ghost
app.kubernetes.io/name=ghost
pod-template-hash=5f6578fd76
Annotations: <none>
Status: Pending
…
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 26s (x2 over 87s) default-scheduler pod has unbound immediate PersistentVolumeClaims
이 경우 다시 PV를 삭제하고 새로 PV를 정의한 후 helm install을 실행해야 한다.
cychong@mini1:~/work$ helm ls
helm del NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
my-ghost 1 Sun Sep 22 21:53:40 2019 DEPLOYED ghost-0.1.0 1.0 default
cychong@mini1:~/work$ helm del --purge my-ghost
release "my-ghost" deleted
cychong@mini1:~/work$ kubectl delete -f ghost-in-k8s/ghost-pv.yaml
persistentvolume "ghost-pv" deleted
cychong@mini1:~/work$ kubectl create -f ghost-in-k8s/ghost-pv.yaml
persistentvolume/ghost-pv created
cychong@mini1:~/work$ helm install --name my-ghost ghost-with-helm
NAME: my-ghost
LAST DEPLOYED: Sun Sep 22 21:56:42 2019
NAMESPACE: default
STATUS: DEPLOYED
…
cychong@mini1:~/work$ kubectl get pod
NAME READY STATUS RESTARTS AGE
my-ghost-5f6578fd76-k5xcs 1/1 Running 0 50s