0%

쿠버네티스를 운영할때 단일 클러스터만으로 운영하는 경우는 흔치 않습니다.
결국 애플리케이션을 여러 클러스터에 배포해야하는데, 이 과정에서 발생하는 관리 이슈가 생길 수 있습니다.

서비스의 경우는 그렇다 쳐도, 인그레스와 같이 호스트가 지정된 정보들의 경우 클러스터마다 상이하게 설정해야하는데 클러스터마다 매니패스트를 따로관리하는것도 문제입니다.

따라서 환경에 따라 달라지는 값만 정의해두고 이에 따라 배포하는 메커니즘이 필요하며 이를 해결하는것이 바로 “헬름” 입니다.

헬름

헬름이란 쿠버네티스 차트를 관리하기 위한 도구 입니다.
차트 는 사전 구성된 쿠버네티스 리소스 패키지 입니다.

헬름, 차트, 매니페스트, 쿠버네티스의 관계는 다음과 같습니다.

  • 헬름: 차트 관리
  • 차트: 매니페스트 템플릿을 구성하고 패키지로 관리, 차트를 사용하여 매니패스트 파일 생성
  • 매니페스트: 매니페스트 파일에 기초하여 쿠버네티스 리소스(ingress, deployment, replicaset… 등)를 관리

실무에서는 로컬 및 운영 클러스터를 막론하고 여러 환경에 배포해야하는 애플리케이션은 모두 차트로 패키징해 kubectl 대신 헬름으로 배포 및 업데이트를 수행합니다.

kubectl 은 이미 배포된 리소스를 운영중에 수정하는데 주로 사용합니다.

헬름 설치

https://github.com/helm/helm/releases 에서 현재 환경에 맞는 헬름을 설치합니다.

3 버전 받는다면 helm 과 tiller 가 혼합된형태? 가 되는듯하니 2 버전으로 진행하였습니다.

1
2
3
4
5
6
7
8
9
10
11
# 헬름 초기화
$ helm init
Adding stable repo with URL: https://kubernetes-charts.storage.googleapis.com
Adding local repo with URL: http://127.0.0.1:8879/charts
$HELM_HOME has been configured at C:\Users\---\.helm.

Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.

Please note: by default, Tiller is deployed with an insecure 'allow unauthenticated users' policy.
To prevent this, run `helm init` with the --tiller-tls-verify flag.
For more information on securing your installation see: https://v2.helm.sh/docs/securing_installation/

init 명령어를 실행하면 틸러 라는 서버 애플리케이션이 kube-system 네임스페이스에 배포됩니다.

1
2
3
4
5
6
7
8
9
$ kubectl -n kube-system get service,deployment,pod --selector app=helm
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/tiller-deploy ClusterIP 10.98.66.79 <none> 44134/TCP 2m51s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/tiller-deploy 1/1 1 1 2m52s

NAME READY STATUS RESTARTS AGE
pod/tiller-deploy-566d8c9b77-t5bd8 1/1 Running 0 2m52s

틸러는 헬름의 명령에 따라 설치등의 작업을 담당합니다.

헬름과 틸러는 여러 클러스터 작업을 진행할때 버젼을 일치시키는것이 좋습니다.

1
2
3
$ helm version
Client: &version.Version{SemVer:"v2.16.7", GitCommit:"5f2584fd3d35552c4af26036f0c464191287986b", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.16.7", GitCommit:"5f2584fd3d35552c4af26036f0c464191287986b", GitTreeState:"clean"}

헬름 구성

헬름은 클라이언트(cli)서버(쿠버네티스 클러스터에 설치된 틸러) 로 구성됩니다.
클라이언트는 서버를 대상으로 명령어를 지시합니다.
서버는 전달받은 명령에 따라 쿠버네티스 클러스터에 패키지 설치, 업데이트, 삭제등의 작업을 수행합니다.

쿠버네티스는 서비스나 디플로이먼트, 인그레스와 같은 리소스를 생성하고 매니페스트파일을 적용하는 방식으로 애플리케이션을 배포합니다.
이 매니페스트 파일을 생성하는 템플릿을 여러 개 패키징한 것이 차트입니다.

차트는 헬름 리포지토리에 tgz 파일로 저장되며, 틸러가 매니페스트를 생성하는 데 사용합니다.

리포지토리의 종류는 3가지가 있습니다.

  • local: 헬름 클라이언트가 설치된 로컬 저장소
  • stable: 안정 버전에 이른 차트가 존재하는 저장소
  • incubator: stable 요건을 만족하지 못한 차트 저장소

검색을 해보면 stable 차트가 보입니다.

1
2
3
4
5
6
$ helm search
NAME CHART VERSION APP VERSION DESCRIPTION
stable/acs-engine-autoscaler 2.2.2 2.1.1 DEPRECATED Scales worker nodes within agent pools
stable/aerospike 0.3.2 v4.5.0.5 A Helm chart for Aerospike in Kubernetes
stable/airflow 6.10.4 1.10.4 Airflow is a platform to programmatically author, schedul...
stable/ambassador 5.3.1 0.86.1 A Helm chart for Datawire Ambassador

차트

차트는 구성이 정해져있습니다.
애플리케이션의 동작을 제어하는 설정의 기본값을 values.yaml 파일에 저장합니다.
이 기본값을 변경하려면 기본값 value 파일에서 변경할 값만 수정한 커스텀 value 파일이 있습니다.

차트 설치해보기

stable 에 올라온것중 쉽게 stable/redmine 을 쿠버네티스 환경에 설치해보겠습니다.
https://github.com/helm/charts/tree/master/stable/redmine

차트를 이용해 설치하려면 helm install 명령어를 사용해야 합니다.
업데이트와 삭제를 하려면 release 네임이 필요하므로 --name 옵션을 통해 이름을 붙여줘야합니다.
이 이름은 클러스터안에서 유일해야합니다.

helm install 을 이용하면 차트에 포함된 기본값 value 파일에 정의된 설정값으로 애플리케이션이 설치됩니다.
그러나 실무에서는 커스텀해서 사용해야하므로 커스텀 value 파일을 구성해보겠습니다.

https://github.com/helm/charts/blob/master/stable/redmine/README.md 의 config 를 참고하여 바꾸고자 하는 내용에 대한 별도 yaml 파일을 로컬에 구성합니다.

redmine.yaml

1
2
3
4
5
6
redmineUsername: wook
redminePassword: wook

# 로드벨런서가 아니라 그냥 서비스를 노출하고자 하므로 NodePort 로 정정
service:
type: NodePort
1
2
3
4
5
6
7
# -f 옵션을 통해 커스텀 value 를 포함하여 설치해봅니다.
# --name 옵션을 통해 네임을 필히 지정합니다.
$ helm install -f redmine.yaml --name rmine stable/redmine
NAME: rmine
LAST DEPLOYED: Sun May 17 15:23:37 2020
NAMESPACE: default
STATUS: DEPLOYED

설치가 끝나면 배포된 목록을 확인해볼 수 있습니다.

1
2
3
$ helm ls
NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
rmine 1 Sun May 17 15:23:37 2020 DEPLOYED redmine-14.1.12 4.1.0 default

설정한 리소스가 올바르게 반영됬는지 확인해봅니다.

1
2
3
4
5
$ kubectl get service,deployment
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 63m
service/rmine-mariadb ClusterIP 10.109.39.18 <none> 3306/TCP 6m22s
service/rmine-redmine NodePort 10.100.66.7 <none> 80:30912/TCP 6m22s

service.type 을 NodePort 로 바꾸었기때문에 NodePort 로 설정이 된것을 볼 수 있습니다. (기본 Load Balancer)
30912 포트로 연결시 redmine 서비스가 뜨는것을 확인할 수 있습니다.
login 시 id/pw 를 wook/wook 으로 하여 정상적으로 인증이 되는것도 확인할 수 있습니다.

차트 업그레이드

헬름으로 설치한 릴리즈를 업데이트도 가능합니다.

1
$ helm upgrade -f redmine.yaml --name rmine stable/redmine --version 4.1.2

차트 제거

헬름을 통해 설치한 애플리케이션은 지정한 이름을 통해 쉽게 제거 가능합니다.

1
helm delete rmine

서비스

서비스는 쿠버네티스 클러스터 안에서 파드의 집합(주로 레플리카세트) 에 대한 경로나 서비스 디스커버리를 제공하는 리소스입니다.

서비스 대상이 되는 파드는 서비스에서 정의하는 레이블 셀렉터로 정해집니다.

일단 1개의 yml 파일에 2개의 레플리카세트를 정의해보자.

simple-echo-replicaset-with-label.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: simple-echo-spring
labels:
app: simple-echo
release: spring
spec:
replicas: 1
selector:
matchLabels:
app: simple-echo
release: spring
template: # template 아래는 파드 리소스 정의와 같음
metadata:
labels:
app: simple-echo
release: spring
spec:
containers:
- name: goecho
image: lejewk/goecho:latest
ports:
- containerPort: 80

---

apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: simple-echo-summer
labels:
app: simple-echo
release: summer
spec:
replicas: 2
selector:
matchLabels:
app: simple-echo
release: summer
template: # template 아래는 파드 리소스 정의와 같음
metadata:
labels:
app: simple-echo
release: summer
spec:
containers:
- name: goecho
image: lejewk/goecho:latest
ports:
- containerPort: 80

NodePort 를 사용하여 relaese = summer 인 파드만 접근하도록 서비스를 생성한다면

NodePort 는 클러스터 외부에서 서비스에 접근이 가능하도록 하는 설정입니다.

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: simple-echo
spec:
type: NodePort
selector:
app: simple-echo
release: summer
ports:
- name: http
port: 80

이제 각각을 적용해보면

1
2
3
4
5
6
$ kubectl apply -f simple-echo-replicaset-with-label.yaml 
replicaset.apps/simple-echo-spring created
replicaset.apps/simple-echo-summer created

$ kubectl apply -f simple-echo-service.yaml
service/simple-echo created

이후 NodePort 를 확인해봅니다.

1
2
3
4
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 12h
simple-echo NodePort 10.105.219.48 <none> 80:31399/TCP 3m18s

31399 포트로 열려있어 이를 통해 접근해보겠습니다.
http://localhost:31399/

1
[simple-echo-summer-nfvln] Hello~

응답 바디에 호스트를 작성했는데요,

리플리카셋을 2로 설정했지만 어째서인지 계속 동일한 파드로만 진입이 되는것같습니다.
서비스의 로드벨런싱 기능이 존재하는지 아니면 없는건지는 이후 학습하면서 보면 될것같습니다.

인그레스

클러스터 외부로 서비스를 공개하려면 서비스를 NodePort 로 노출시킵니다.
그러나 이 방법은 L4 레벨까지만 다룰 수 있어서 HTTP/HTTPS 처럼 경로를 기반으로 전환하는 L7 레벨의 제어는 불가능합니다.

이를 해결하가 위한 리소스가 인그레스 입니다.

튜토리얼에서는 nginx ingress controller 를 사용하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 메니패스트의 내용중 Deployment 의 apiVersion 이 extensions/v1beta1 을 지원하지 않는다면 
# 해당 파일을 내려받아 yaml 로 만든 후
# extensions/v1beta1 -> apps/v1 으로 변경하여 적용합니다.
https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.16.2/deploy/mandatory.yaml

$ kubectl apply -f simple-echo-ingress-nginx-controller.yaml
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.16.2/deploy/provider/cloud-generic.yaml

# 적용 확인
$ kubectl -n ingress-nginx get service,pod
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/default-http-backend ClusterIP 10.102.121.230 <none> 80/TCP 5m15s
service/ingress-nginx LoadBalancer 10.110.155.253 localhost 80:30942/TCP,443:31597/TCP 4s

NAME READY STATUS RESTARTS AGE
pod/default-http-backend-75b5c88cd6-pvhdk 1/1 Running 0 5m15s
pod/nginx-ingress-controller-86d68989c8-772pv 1/1 Running 0 5m15s

서비스의 NodePort 를 제거후 다시 반영합니다.

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Service
metadata:
name: simple-echo
spec:
selector:
app: simple-echo
ports:
- name: http
port: 80
1
2
3
4
$ kubectl apply -f simple-echo-service.yaml --force
service/simple-echo configured

# --force 를 붙여 강제로 적용시킵니다. type 미정의에 따른 경고를 무시한다고 보면됩니다.

이제 간단한 인그레스를 정의하고 반영해봅니다.
simple-echo-ingress.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: simple-echo
spec:
rules:
- host: wook.ingress.local
http:
paths:
- path: /
backend:
serviceName: simple-echo
servicePort: 80
1
2
3
4
5
6
$ kubectl apply -f simple-echo-ingress.yaml 
ingress.extensions/simple-echo created

kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
simple-echo wook.ingress.local localhost 80 19m

이제 지정한 호스트로 /etc/hosts 파일을 127.0.0.1 아이피와 매핑후 통신을하면

1
2
3
4
5
6
7
$ curl http://wook.ingress.local/
# 또는
$ curl http://localhost -H 'Host: wook.ingress.local'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 33 100 33 0 0 3000 0 --:--:-- --:--:-- --:--:-- 3300
[simple-echo-summer-lszln] Hello~

와 같이 응답하는것을 볼 수 있습니다.
로드벨런스까지 적용되어 새로고침시 다른 호스트의 응답을 볼수 있습니다.

대시보드 설치

https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/

1
2
3
4
5
# 음.. 뭔지모르지만 추천하는 데시보더 설정을 적용한다는 의미같음.
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0/aio/deploy/recommended.yaml

# 음.. 뭔지 모르겠지만 프록시를 처리하는 명령어 같음.
$ kubectl proxy

토큰 생성

1
$ kubectl -n kubernetes-dashboard describe secret $(kubectl -n kubernetes-dashboard get secret | grep admin-user | awk '{print $1}')

대시보드 접속

http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/

쿠버네티스 주요 개념

  • 노드: 컨테이너가 배치되는 서버
  • 네임스페이스: 쿠버네티스 클럿 터 안의 가상 클러스터
  • 파드: 컨테이너의 집합 중 가장 작은 단위, 컨테이너의 실행 방법을 정의
  • 레플리카세트: 같은 스팩을 갖는 파드를 여러개 생성하고 관리하는 역할
  • 디플로이먼트: 레플리카 세트의 리비전을 관리
  • 서비스: 파드의 집합에 접근하기 위한 경로 정의
  • 인그레스: 서비스를 쿠버네티스 클러스터 외부로 노출
  • 컨피그맵: 설정 정보를 정의하고 파드에 전달
  • 퍼시스턴트 볼륨: 파드가 사용할 스토리지의 크기 및 종류 정의
  • 퍼시스턴트볼륨클레임: 포시스턴트 볼륨을 동적으로 확보
  • 스토리지클래스: 퍼시스턴트 볼륨이 확보하는 스토리지의 종류를 정의
  • 스테이트풀세트: 같은 스팩으로 모두 동일한 파드를 여러개 생성하고 관리
  • 잡: 상주 실행을 목적으로 하지 않는 파드를 여러개 생성하고 정상적인 종료를 보장
  • 크론잡: 크론 문법으로 스케줄링되는 잡
  • 시크릿: 인증 정보 같은 기밀 데이터 정의
  • 롤: 네임스페이스 안에서 조작 가능한 쿠버네티스 리소스의 규칙 정의
  • 롤바인딩: 쿠버네티스 리소스 사용자와 롤을 연결
  • 클러스터롤: 클러스터 전체적으로 조작 가능한 쿠버니테스 로스싕 규칙 정의
  • 클러스터롤바인딩: 쿠버니테스 리소스 사용자와 클러스터 롤 연결
  • 서비스 계저이: 파드가 쿠버네티스 리소스를 조작할 때 사용하는 계정

클러스터와 노드

클러스터는 여러 리소스를 관리하기 위한 집합체 입니다.
리소스중 가장 큰 개념은 노드 입니다.
노드는 클러스터 관리대상으로 등록된 도커 호스트로 컨테이너가 배치되는 대상입니다.

기본적으로 전체 클러스터를 관리하는 마스터가 적어도 하나 이상 있어야 하며 마스터와 노드의 구릅으로 구성된다고 볼수있습니다.

1
2
3
4
5
6
7
8
9
Cluster {
Master Node {
kube apiserver
etcd
kube scheduler
kube controller manager
}
Nodes...
}

네임스페이스

클러스터 안에 가상 클러스터를 만들 수 있는데 이를 네임스페이스라고 합니다.
현재 존재하는 네임스페이스 조회

1
2
3
4
5
6
7
8
$ kubectl get namespace
NAME STATUS AGE
default Active 69m
docker Active 68m
kube-node-lease Active 69m
kube-public Active 69m
kube-system Active 69m
kubernetes-dashboard Active 62m

파드 생성 및 배포

파드 생성은 kubectl 로도 가능하지만 버젼관리 관점에서도 yaml 파일로 정의하는것이 좋다.
쿠버네티스의 여러가지 리소스를 정의하는 파일을 매니페스트 파일 이라고 한다.

simple-echo-pod.yaml

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: simple-echo
spec:
containers:
- name: goecho
image: lejewk/goecho:latest
ports:
- containerPort: 80

파드 생성 및 확인

1
2
3
4
5
6
7
8
9
10
11
12
# 생성
$ kubectl apply -f simple-echo-pod.yaml
# 확인
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
simple-echo 1/1 Running 0 4m19s
# 제거
$ kubectl delete pod simple-echo
pod "simple-echo" deleted
# 확인
$ kubectl get pod
No resources found in default namespace.

레플리카세트

파드를 정의한 매니페스트 파일로는 파드 1개밖에 생성할 수 없습니다.
이런 경우 레플리카세트를 통해 동일한 파드를 여러개 관리할 수있습니다.

파드를 정의한 매니페스트까지 포함한 레플리카세트의 매니페스트를 정의할 수 있습니다.

simple-echo-replicaset.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: simple-echo
labels:
app: simple-echo
spec:
replicas: 3
selector:
matchLabels:
app: simple-echo
template: # template 아래는 파드 리소스 정의와 같음
metadata:
labels:
app: simple-echo
spec:
containers:
- name: goecho
image: lejewk/goecho:latest
ports:
- containerPort: 80

리플리카 생성 및 파드 확인

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 레플리카세트 생성
$ kubectl apply -f simple-echo-replicaset.yaml
replicaset.apps/simple-echo created

# 파드 확인
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
simple-echo-9tpxb 1/1 Running 0 48s
simple-echo-xfk5c 1/1 Running 0 48s
simple-echo-zkfkt 1/1 Running 0 48s

# 레플리카세트 삭제
$ kubectl delete -f simple-echo-replicaset.yaml
replicaset.apps "simple-echo" deleted

디플로이먼트

레플리카세트보다 상위에 해당하는 리소스이며, 애플리케이션 배포의 기본단위가 되는 리소스 입니다.

레플리카세트는 똑같은 파드의 레플리케이션 개수 관리에 집중하지만,
디플로이먼트는 레플리카세트를 관리하고 다루기 위한 요소입니다.

관계를 정리하면

1
2
3
Deployment --create--> ReplicaSet --create--> Pod (echo-1a)
(echo) (echo-1) --create--> Pod (echo-1b)
--create--> Pod (echo-1c)

디플로이먼트를 정의한 매니페스트 파일은 다음과 같습니다.

simple-echo-deployment.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
name: simple-echo
labels:
app: simple-echo
spec:
replicas: 3
selector:
matchLabels:
app: simple-echo
template: # template 아래는 파드 리소스 정의와 같음
metadata:
labels:
app: simple-echo
spec:
containers:
- name: goecho
image: lejewk/goecho:latest
ports:
- containerPort: 80

디플로이먼트 반영

1
2
3
4
$ kubectl apply -f simple-echo-deployment.yaml --record
deployment.apps/simple-echo created

# --record 옵션을 통해 어떤 kubectl 명령어를 날렸는지를 기록합니다. 왜인지는...

리소스를 확인해보자

1
2
3
4
5
6
7
8
9
10
11
$ kubectl get pod,replicaset,deployment --selector app=simple-echo
NAME READY STATUS RESTARTS AGE
pod/simple-echo-66f7fffb-792bk 1/1 Running 0 4m44s
pod/simple-echo-66f7fffb-p2fbv 1/1 Running 0 4m44s
pod/simple-echo-66f7fffb-x4ztb 1/1 Running 0 4m44s

NAME DESIRED CURRENT READY AGE
replicaset.apps/simple-echo-66f7fffb 3 3 3 4m44s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/simple-echo 3/3 3 3 4m44s

디플로이먼트의 리비전을 확인해보자

1
2
3
4
$ kubectl rollout history deployment simple-echo
deployment.apps/simple-echo
REVISION CHANGE-CAUSE
1 kubectl.exe apply --filename=simple-echo-deployment.yaml --record=true

현재 리비전은 1이다.

결과적으로 쿠버네티스는 디플로이먼트를 활용해 애플리케이션을 배포합니다.

go 에서 테스트코드 작성과 테스트 실행을 간략하게 적어봅니다.

테스트 코드 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package test

import "testing"

func TestFoo(t *testing.T) {
// todo test code
expected := 1
actual := 0
if result != actual {
t.Fatal("기대값과 결과값이 다릅니다.")
// t.Error()
// t.Fail()
// t.Log()
}
}

go 가 제공하는 기본적인 테스트 방식은 if 문과 testing.T 가 제공하는 오류 함수를 사용하는 것입니다.

테스트 코드의 양에 따라 if 가 난무하는 코드가 될 수 있어 assert 패키지의 도움을 받는것이 좋습니다.

assert 패키지

github.com/stretchr/testify/assert

1
2
3
4
5
6
7
8
9
10
package test

import "testing"

func TestFoo(t *testing.T) {
// todo test code
expected := 1
actual := 0
assert.Equal(t, expected, actual, "기대값과 결과값이 다릅니다.")
}

이 패키지의 장점은 좀더 시각적인 output을 제공합니다.

1
2
3
4
5
6
7
8
9
10
11
12
$ go test
--- FAIL: TestFoo (0.00s)
foo_test.go:9:
Error Trace: foo_test.go:9
Error: Not equal:
expected: 1
actual : 0
Test: TestFoo
Messages: 기대값과 결과값이 다릅니다.
FAIL
exit status 1
FAIL _/D_/repository/foo_test 0.174s

테스트 실행

1
2
3
4
5
6
7
8
9
# 모든 테스트 실행
$ go test

# *_test.go 파일 테스트 실행
# -v : 상세한 결과 확인
$ go test *_test.go -v

# 특정 테스트 함수(TestFunctionName) 실행
$ go test -run TestFunctionName -v

sync의 Once 를 활용하여 go 싱글톤을 구현합니다.
https://golang.org/pkg/sync/#Once

code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package unit

import "sync"

// package level variable
var instance *Component
var once sync.Once

type Component struct {
count int
}

func GetInstance() *Component {
once.Do(func() {
instance = &Component{count: 0}
})
return instance
}

func (s *Component) add() {
s.count++
}

func (s *Component) getCount() int {
return s.count
}

test code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package test

import (
"fmt"
"testing"
)

func TestGetInstance(t *testing.T) {
GetInstance().add()
fmt.Println(GetInstance().getCount())

GetInstance().add()
fmt.Println(GetInstance().getCount())

GetInstance().add()
fmt.Println(GetInstance().getCount())
}

실행 결과

1
2
3
4
5
6
D:\repository\singleton_test\test>go test
1
2
3
PASS
ok _/D_/repository/singleton_test/test 0.171s

window 환경에서 go 의 환경변수를 지정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# cmd
set GOROOT=c:\go
set GOPATH=%USERPROFILE%\go
set GOBIN=%GOPATH%\bin

C:\Users\devwook>echo %GOROOT%
C:\Go

C:\Users\devwook>echo %GOPATH%
C:\Users\devwook\go

C:\Users\devwook>echo %GOBIN%
C:\Users\devwook\go\bin

:warning: 환경변수를 지속시키려면 시스템 설정에서 등록해야 합니다.

go 에서 사용되는 환경변수

변수명 설명 예시
GOROOT go SDK 가 설치된 경로 c:\go
GOPATH go 프로젝트 의존성 경로 c:\users${USERNAME}\go
GOBIN go 프로젝트 의존성 바이너리 경로 c:\users${USERNAME}\go\bin

GOBIN 오류

go install cannot install cross-compiled binaries when gobin is set 과 같은 오류 발생시 GOBIN 환경변수를 제거해주면 해결됩니다.

서론

  • php7.0 에서 사용할 proto grpc stub 이 필요하였습니다.
  • https://grpc.io/docs/quickstart/php/ 을 참고하여 진행해보았으나, 과정이 많고 실패케이스도 많아서 쉽지 않았습니다.
    추후 php 용 grpc stub 을 만들때마다 이 과정을 반복해야하는 끔찍함이 싫었습니다.
  • 따라서 도커 이미지를 만들었고 .proto 를 grpc 용 stub 으로 쉽게 생성할수 있게 제공하려 합니다.

docker image

https://hub.docker.com/r/lejewk/proto-gen-grpc-php7.0.12

실행 방법

  1. .proto 파일 작성
  2. docker 실행시 .proto 파일 경로 마운트
  3. 아래 제공되는 커맨드를 통해 stub 파일 생성
1
2
3
4
5
6
7
8
9
10
# 이미지 실행
docker run -it --rm -v ${local directory}:/proto \
lejewk/proto-gen-grpc-php7.0.12 bash

# stub 파일 생성
protoc --proto_path=/proto \
--php_out=/proto \
--grpc_out=/proto \
--plugin=protoc-gen-grpc=/grpc/bins/opt/grpc_php_plugin \
${proto_filename}

예시

1
2
3
4
5
6
7
8
9
10
11
# 동일 경로의 proto 디렉토리 마운트
# proto 디렉토리 내에는 helloworld.proto 파일이 존재합니다.
docker run -it --rm -v "$(pwd -W)"/proto:/proto \
lejewk/proto-gen-grpc-php7.0.12 bash

# 컨테이너 내부에서 아래 커맨드를 통해 stub 파일 생성
protoc --proto_path=/proto \
--php_out=/proto \
--grpc_out=/proto \
--plugin=protoc-gen-grpc=/grpc/bins/opt/grpc_php_plugin \
/proto/helloworld.proto

gRPC 란

general-purpose Remote Procedure Calls

google 에서 만든 오픈소스로, 원격지의 프로시저를 호출하는 프레임워크 입니다.

gRPC 는 다음과 같은 특징을 갖습니다.

  • 원격지 프로시저를 수행하는 규칙 및 파라미터 전달을 위한 인터페이스로 protocol buffer 라는 오픈소스를 활용하고 있습니다.
  • Blocking & Non-Blocking 을 지원합니다.
  • HTTP/2 프로토콜을 사용합니다.
  • 인증, 로드벨런싱, 트레이싱, 헬스체크 등을 제공합니다.
  • 10개 언어에서 지원되는 라이브러리가 있습니다.

gRPC 사용 및 흐름

  1. 실행하고자 하는 프로시저와, 전달하고자 하는 파라미터 사양을 .proto 파일로 작성합니다.
  2. protoc 를 통해 사용하고자 하는 언어에 맞게 stub 파일을 생성합니다.
    생성된 파일은 각 클라이언트가 참조할 수 있는 언어(.java .c .go 등..) 로써 bean 과 같이 데이터를 엑세스 하거나 핸들링하는 함수가 포함되어 있습니다.
  3. gRPC 에서 각 언어별로 제공하는 SDK 를 제공합니다. 이를 활용해 서버, 클라이언트를 프로그래밍 합니다.
    stub 을 활용해 실행될 프로시저를 구현하거나 전달할 파라미터를 생산할수 있습니다.

.proto 와 stub 파일

protocol buffer 를 사용하는 이점중 하나는 .proto 파일로 구조화된 데이터를 작성하기만 한다면 gRPC가 지원하는 어떤 언어에서든 규약에 상관없이 통신이 가능하다는 것입니다.
작성된 .proto 로부터 언어에 맞는 stub 를 생산하여 참조하게 되면 이후 별도의 사양서를 볼 필요없이 참조한 stub 만으로도 개발이 가능합니다.

gRPC plugin

gRPC 용으로 좀더 활용 가치가 있는 stub 을 만들기위해서는 protoc 에 grpc plugin 바이너리를 전달해야 합니다. 각 언어마다 plugin 이 존재하므로 해당 플러그인을 직접 구해서 stub 을 만들면 됩니다.
go 의 경우

1
protoc -I . config.proto --go_out=plugins=grpc:.

go server client 예제

Server

mod init

1
go mod init config_server

protocol buffer 정의

config/config.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
syntax = "proto3";

package config;

// 설정 조회 서비스
service ConfigStore {
rpc Get (ConfigRequest) returns (ConfigResponse);
}

// 요청
message ConfigRequest {
string profile = 1;
}

// 응답
message ConfigResponse {
string json_config = 1;
}

go grpc 의존성 설치

1
2
go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go

protoc 로 컴파일

protoc -I config config.proto --go_out=plugins=grpc:config 를 통해 config.pb.go 파일을 생성합니다. 추후 로직에서 이 파일을 사용하게 됩니다.

:warning: 경험상 패키지명과 파일명이 일치하지 않으면 추후 오류가 발생되는데 이 부분은 좀더 확인이 필요합니다.

main 함수 작성

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
pb "config_server/config"
"context"
"google.golang.org/grpc"
"log"
"net"
)

type server struct {
pb.UnimplementedConfigStoreServer
}

func (s *server) Get(ctx context.Context, in *pb.ConfigRequest) (*pb.ConfigResponse, error) {
log.Printf("Received profile: %v", in.GetProfile())
return &pb.ConfigResponse{JsonConfig: `"{"main":"http://google.com"}"`}, nil
}

func main() {
lis, err := net.Listen("tcp", ":8088")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

s := grpc.NewServer()
pb.RegisterConfigStoreServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

Client

mod init

1
go mod init config_client

protocol buffer 작성

위에서 컴파일된 config.pb.go 파일을 복사하여 사용해도 됩니다.

config/config.pb.go

main 함수 작성

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
pb "config_client/config"
"context"
"google.golang.org/grpc"
"log"
"time"
)

func main() {
conn, err := grpc.Dial("localhost:8088", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewConfigStoreClient(conn)

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

r, err := c.Get(ctx, &pb.ConfigRequest{Profile: "dev"})
if err != nil {
log.Fatalf("could not request: %v", err)
}

log.Printf("Config: %v", r)
}

실행

1
2
3
4
5
6
7
8
9
10
11
12
13
# 서버 실행
go run main.go

# 클라이언트 실행 및 결과
go run main.go dev
2020/03/18 18:18:29 Config: json_config:"\"{\"main\":\"http://google.com\"}\""

go run main.go prod
2020/03/18 18:25:10 Config: json_config:"\"{\"main\":\"http://aws.com\"}\""

# 서버 결과
2020/03/18 18:18:29 Received profile: dev
2020/03/18 18:25:10 Received profile: prod

원문

기본 https://developers.google.com/protocol-buffers
문법 https://developers.google.com/protocol-buffers/docs/proto3
go 튜토리얼 https://developers.google.com/protocol-buffers/docs/gotutorial

protocol buffer 란

구글에서 개발하고 오픈소스로 공개한 직렬화된 데이터 구조입니다.

다음과 같은 특징을 갖습니다.

  • C++, c#, java, python, object c, javascript, php, go 등 다양한 언어를 지원합니다.
  • 직렬화 속도가 빠르며, 용량이 적습니다.
  • 직렬화 데이터를 파싱할 필요가 없습니다. 데이터를 객체를 그대로 사용 할 수 있습니다.
  • proto 문법 학습이 필요합니다.
  • gRPC 에서도 메시지 전달 방식을 protocol buffer 로 사용하고 있어 숙지한다면 gRPC도 쉽게 접근이 가능합니다.

직렬화: 파일을 바이너리 스트림 형태로 저장하는 행위

구성 및 사용

  1. proto 문법으로 데이터 포맷을 작성합니다.
  2. protoc 를 통해 원하는 프로그래밍 언어 형식의 데이터 파일을 생산합니다.
  3. 개발하고자 하는 언어에서 제공되는 protocol buffer API 를 사용한다.

protoc 컴파일러 설치

컴파일러와 언어별 SDK 를 아래 사이트에서 다운받을 수 있습니다.
https://github.com/protocolbuffers/protobuf/releases

protoc-3.11.4-win64.zip

cmd 또는 bash 에서 protoc 를 인식할 수 있도록 환경변수를 셋팅해야합니다.

1
2
$ protoc --version
libprotoc 3.11.4

.proto 파일 작성

protocl buffer syntax 3 보기

go 를 활용한 예제

.proto 파일 작성

/unit/pencil.proto

1
2
3
4
5
6
7
syntax = "proto3";
package unit;

message Pencil {
string color = 1;
int32 price = 2;
}

protoc-gen-go 의존성 설치

go 에서는 .proto 파일을 protoc 하기위해 별도 의존성을 따로 제공하고 있습니다.

1
$ go install google.golang.org/protobuf/cmd/protoc-gen-go

protoc 로 컴파일

1
2
$ cd unit
$ protoc -I . pencil.proto --go_out=.

컴파일 이후 아래 /unit/pancil.pb.go 파일이 생성된것을 확인할 수 있습니다.

go 직렬화 & 역직렬화

mod init

1
$ go mod init protocol_buffer

code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"github.com/golang/protobuf/proto" // proto api 의존성
"io/ioutil"
"log"
pb "protocol_buffer/unit"
)

const (
filename = "pencil"
)

func main() {
// 쓰기
writeMessage()
// 읽기
readMessage()
}

func writeMessage() {
pencil := &pb.Pencil{Color:"red", Price:1200}

// proto 의존성을 통해 인코딩
out, err := proto.Marshal(pencil)
if err != nil {
log.Fatalf("Failed to encode Pencil: %v", err)
}

if err := ioutil.WriteFile(filename, out, 0644); err != nil {
log.Fatalf("Failed to write Pencil: %v", err)
}
}

func readMessage() {
in, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatalln("Error reading file:", err)
}

pencil := &pb.Pencil{}

// proto 의존성을 통해 디코딩
if err := proto.Unmarshal(in, pencil); err != nil {
log.Fatalln("Failed to parse pencil:", err)
}

log.Printf("color: %s", pencil.GetColor())
log.Printf("price: %d", pencil.GetPrice())
}

Run & Result

1
2
3
$ go run main.go
2020/03/19 16:54:57 color: red
2020/03/19 16:54:57 price: 1200

직렬화된 파일

/pencil

1
2

red�

사람이 읽을 수 없도록 인코딩 되어있습니다.