CI-CD

kustomize를 활용한 Manifest 관리

Operation CWAL 2021. 2. 24. 23:37

Manifest의 재사용성

이 글을 읽고 있는 대부분의 사람은 Kubernetes에서 App 배포를 위해 Manifest 파일을 작성한 경험이 있을 것이다. 예를 들어 Nginx로 구성한 Frontend의 Deployment, Service, Persistent Volume, PVC, ConfigMap 등을 정의하고 "kubectl apply -f" 명령어로 적용하는 것이다. 현재 App을 배포한 Namespace를 dev라고 하고 테스트를 완료한 상태에서 production에 배포하려고 한다. spec이 동일하지 않고 일부 필드를 추가해야 하거나 환경변수 값이 다른 경우도 있다고 가정하자.

 

가장 단순한 해결책으로 기존에 작성한 Manifest 파일을 복사해서 production 환경에 맞게 수정하는 작업을 생각할 수 있다. 어떻게 생각하면 직관적이고 가장 빠른 방법일 수도 있다. 하지만 이번엔 staging 환경에도 배포해야 한다는 요구사항이 발생한다면 이 작업을 반복해야한다. 관리할 Manifest 파일이 어느 순간 3배가 되어버린 것이다.

이제 다시 처음부터 다시 되짚어보자. 각자 다른 환경에 배포하더라도 서로간의 Manifest 파일은 겹치는 내용이 대부분이며, 일부 필드만 추가됐거나 값이 다른 것을 알 수 있다. kustomize는 이렇게 공통된 부분은 base, 환경마다 차이가 발생하는 부분을 overlay라는 항목으로 분리하는 개념에서 시작한다. 각각의 환경에 배포할 때, 이 base와 overlay의 Manifest 파일을 merge하여 환경마다 서로 다른 Spec의 리소스를 생성한다.

 

kustomize

kustomize는 이외에도 개별 환경마다 소스를 구분하여 ConfigMap이나 Secret을 생성하거나 Resource 이름에 prefix를 추가하는 등의 기능도 제공한다. kustomize를 적용한 manifest는 사람이 직접 매뉴얼 방식으로 배포할 때 사용할 수 있으나, 일반적으로 Git Repository에 관리하면서 Argo CD, Flux 등을 통해 GitOps 방식으로 배포 과정을 자동화하는데 쓰인다.

 

참고로 Helm도 좋은 해결책이며 필자도 종종 사용하는 편이나 chart를 작성하다 보면 Manifest 내용보다 템플릿 코드가 더 많아질 때가 있어 가독성이 떨어지는 문제가 있다. 개인적으로 자체 서비스의 환경을 구분하여 배포시 kustomize, Application을 정의한 Manifest를 공유할 땐 Helm이 적합하다고 생각한다.

 

사용법

설치

Source로부터 빌드, Docker 등 여러가지 형식으로 설치할 수 있지만 가장 일반적인 Binary 설치 방식으로 진행한다.

curl -s "https://raw.githubusercontent.com/\
kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"  | bash
sudo mv kustomize /usr/local/bin
kustomize version

참고로 k8s v1.14부터 kubectl의 built-in 기능으로 포함되었으며, -k 또는 kustomize 옵션으로 실행할 수 있다. 하지만 k8s의 릴리즈 주기를 따라가기 때문에, 최신 버전의 kustomize가 아닐 수 있음에 유의해야 한다. 개인적인 경험으로는 kubectl 내장 버전은 제한된 기능만 제공하기 때문에 그렇게 추천하지 않는다.

 

kustomize 적용

우선 아래와 같이 Deployment와 Service로 구성한 간단한 Nginx App을 준비해 보았다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80

<deployment.yaml>

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx

<service.yaml>

파일 구조는 아래와 같다.

간단한 Manifest로 리소스 생성에 문제가 없으며, App도 정상적으로 동작하고 있다.

이제 kustomize용 파일들을 작성하여 Manifest 파일들을 기본 뼈대로 하되, 서로 다른 환경과 스펙으로 배포해볼 차례다.

우선 base 디렉토리를 생성하고 방금 작성한 deployment.yaml과 service.yaml을 해당 디렉토리로 이동시킨다.

base 디렉토리에 kustomization.yaml 파일을 생성하고 아래와 같은 내용을 작성한다.

resources:
- deployment.yaml
- service.yaml

<base/kustomization.yaml>

 

해당 파일엔 kustomize 적용할 대상인 Base Manifest 파일들을 resources 필드에 추가해야 한다.

 

다음은 base 디렉토리와 같은 레벨에 overlay 디렉토리를 만들고, 그 밑에 각각 dev, prod 디렉토리를 추가한다.

dev 디렉토리에 아래와 같은 2개 파일을 추가한다. Deployment는 label 및 환경변수, Resource 관련 설정이 추가되며, replica도 4개를 만들고자 한다. Service도 새로운 label을 추가하고 NodePort를 사용하고 싶다.

 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
  labels:
    env: dev
spec:
  replicas: 4
  template:
    spec:
      containers:
        - name: my-nginx
          env:
            - name: FOO
              value: BAR
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "1Gi"
              cpu: "500m"

<overlay/dev/deployment-patch.yaml>

 

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    env: dev
spec:
  type: NodePort

<overlay/dev/service-patch.yaml>

 

한눈에 봐도 부분적으로만 작성된 k8s Manifest인 것을 알 수 있다. kustomize는 이러한 Patch 파일 내용을 Base의 Manifest와 Merge하여 환경마다 차별화된 Manifest를 생산한다. 정확히는 namespace와 name이 모두 일치하는 리소스를 찾아 patch 파일에만 존재하는 항목은 새로 추가하고, 둘 사이에 겹치는 항목은 덮어쓰는 방식으로 동작한다. 즉 base 위에 overlay가 존재하며, 거기서 투영된 것이 최종적으로 만들어낸 manifest이다.

 

다음은 overlay 디렉토리에 아래와 같은 내용으로 kustomization.yaml 파일을 추가할 차례다

bases:
- ../../base
patchesStrategicMerge:
  - deployment-patch.yaml
  - service-patch.yaml

<overlay/dev/kustomization.yaml>

  • bases: base manifest 파일이 위치한 디렉토리 경로를 명시한다. 여러개의 경로를 가질 수 있다.
  • patchesStrategicMerge: 적용할 Patch 파일의 경로, 여러개 파일이 있을 경우 각각 지정

여기까지 진행했다면 현재 파일 계층구조는 아래와 같아야 한다.

아래 명령어를 통해 Base와 Overlay 내용이 Merge되어 우리가 원하는 Manifest 파일을 만들 수 있는지 확인해보자.

kustomize build overlay/dev
apiVersion: v1
kind: Service
metadata:
  labels:
    env: dev
    run: my-nginx
  name: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    env: dev
  name: my-nginx
spec:
  replicas: 4
  selector:
    matchLabels:
      run: my-nginx
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - env:
        - name: FOO
          value: BAR
        image: nginx
        name: my-nginx
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 500m
            memory: 1Gi
          requests:
            cpu: 250m
            memory: 256Mi

e우리가 dev 환경에 배포하고 싶었던 Manifest를 순식간에 하나 만들어냈다. 이 Manifest를 k8s에 바로 배포해보자. dev Namespace를 따로 만들고 여기에 리소스를 생성할 것이다.

kubectl create namespace dev
kustomize build overlay/dev | kubectl -n dev apply -f -

 

이제 이 방식을 그대로 prod 환경에도 적용할 수 있다. prod 디렉토리에 아래 3개 파일을 추가한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
  labels:
    env: prod
spec:
  replicas: 6
  template:
    spec:
      containers:
        - name: my-nginx
          env:
            - name: HELLO
              value: WORLD
          resources:
            requests:
              memory: "512Mi"
              cpu: "500m"
            limits:
              memory: "2Gi"
              cpu: "2"

<overlay/prod/deployment-patch.yaml>

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    env: prod
spec:
  type: LoadBalancer

<overlay/prod/service-patch.yaml>

bases:
- ../../base
patchesStrategicMerge:
  - deployment-patch.yaml
  - service-patch.yaml

<overlay/prod/kustomization.yaml>

 

아래 명령어로 배포해보자.

kubectl create namespace prod
kustomize build overlay/prod | kubectl -n prod apply -f -

테스트 환경의 k8s 클러스터이기 때문에 리소스가 부족해서 Pod 2개는 Pending 상태로 남아있는 걸 빼면, 문제없이 동작하고 있음을 알 수 있다. 

 

최종적으로 완성한 kustomize Application 파일 구성은 다음과 같으며 Github에 공개한 상태이다.

 

 

공통 Label 추가하기

deployment-patch.yaml과 service-patch.yaml 파일 모두 'env: dev' 라는 label이 존재한다. kustomize를 활용하여 환경마다 공통적으로 존재하는 Label이나 Annotation에 대해 kustomization.yaml 파일에서 관리할 수 있다. 

bases:
- ../../base
patchesStrategicMerge:
  - deployment-patch.yaml
  - service-patch.yaml
commonLabels:
  env: dev

<overlay/dev/kustomization.yaml>

위와 같이 commonLabels를 작성하여 모든 리소스에 존재하는 'env: dev' label을 추가하고, 각 patch 파일의  해당 label은 삭제한다. 그리고 아래 명령어를 실행하여 기존과 동일한 Manifest가 나오는지 확인해보자.

kustomize build overlay/dev
apiVersion: v1
kind: Service
metadata:
  labels:
    env: dev
    run: my-nginx
  name: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    env: dev
    run: my-nginx
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    env: dev
  name: my-nginx
spec:
  replicas: 4
  selector:
    matchLabels:
      env: dev
      run: my-nginx
  template:
    metadata:
      labels:
        env: dev
        run: my-nginx
    spec:
      containers:
      - env:
        - name: FOO
          value: BAR
        image: nginx
        name: my-nginx
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 500m
            memory: 1Gi
          requests:
            cpu: 250m
            memory: 256Mi

base의 모든 metadata.labels, metadata.selector 필드에 자동으로 적용된 것을 확인할 수 있으며, 이처럼 공통 label을 리소스마다 따로 작성할 필요 없이 kustomization.yaml에서 추가할 수 있다. 위 방법은 Annotation에도 동일하게 적용(commonAnnotations)할 수 있다.

 

ConfigMap/Secret Generator

Kubernetes의 ConfigMap과 Secret은 kubectl 명령어를 사용한 imperative 방식으로 생성하는게 보통이지만, kustomize는 적용할 파일을 명시하는 것만으로 자동 생성이 가능하다. 예를 들어 아래와 같은 ConfigMap이 base에 위치한다고 가정하자.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-env-vars
data:
  FOO: BAR
  HELLO: WORLD

<base/configmap.yaml>

 

base의 kustomization.yaml 파일의 resource로 추가한다.

resources:
- deployment.yaml
- service.yaml
- configmap.yaml

<base/kustomization.yaml>

 

그리고 overlay/dev/conf 디렉토리 아래에 app.env 파일을 작성한다.

FOO=RAB
HELLO=PEOPLE
GOOD=BYE

<overlay/dev/conf/app.env>

 

마지막으로 overlay/dev/kustomization.yaml 파일에 ConfigMap Generator 관련 내용을 추가한다.

bases:
- ../../base
patchesStrategicMerge:
  - deployment-patch.yaml
  - service-patch.yaml
commonLabels:
  env: dev
configMapGenerator:
  - name: app-env-vars
    behavior: merge
    envs:
    - conf/app.env

<overlay/dev/kustomization.yaml>

configMapGenerator 필드를 확인해보면 'conf/app.env' 파일 내용을 환경변수로 가져와서 base의 'app-env-vars' ConfigMap 리소스와 Merge하는 것을 알 수 있다. 실제 Manifest 파일도 이와 같이 생성되는지 알아볼 차례다.

 

apiVersion: v1
data:
  FOO: RAB
  GOOD: BYE
  HELLO: PEOPLE
kind: ConfigMap
metadata:
  labels:
    env: dev
  name: app-env-vars

ConfigMap 'app-env-vars'의 'FOO=BAR'는 'FOO=RAB'으로, 기존에 없던 'GOOD=BYE', 'HELLO=PEOPLE'은 새로 추가되었다.

 

마치며...

kustomize는 지금까지 설명한 기능 외에도 name prefix/suffix, Image name/tag 변경, 자동화를 위한 CLI 등 다양하고 유용한 기능을 제공하고 있다. 현재 서비스에 적용하고 싶거나 더 자세히 공부하고 싶으신 분들은 Guides - SIG CLI를 참고하기 바란다.

 

'CI-CD' 카테고리의 다른 글

Jenkins - Container 기반 Agent  (0) 2021.03.03
Jenkins Pipeline  (0) 2021.03.01
CD를 위한 Jenkins, Argo CD 연계  (0) 2021.02.21
CI를 위한 Jenkins, GitHub, Docker Hub 연계  (0) 2021.02.20
Kubernetes 위에 Jenkins 설치하기  (0) 2021.02.19