SLASH 기술 블로그

쿠버네티스로 배포하기 4 - 배포 과정 단순화하기 (Skaffold) 본문

인프라/쿠버네티스

쿠버네티스로 배포하기 4 - 배포 과정 단순화하기 (Skaffold)

SLASH 2021. 8. 3. 20:57
반응형

1. How to containerize your app - 앱을 컨테이너화하는 방법에 대해 알아보자.

2. Running on Kubernetes Cluster - 쿠버네티스 클러스터에서 이미지 실행하기

3. Exposing the service - 서비스를 외부에 공개하기

4. Simplifying deployment process - 배포 과정 단순화하기

 

이전의 글들이 앱을 성공적으로 배포하는 것에 초점을 맞췄다면, 앞으로는 이러한 전반적인 프로세스를 효율적으로 개선하는 것에 초점을 둘 것이다. 첫 번째로 배포하는 과정을 단순하게 만드는 것부터 시작해보자.

 

문제 인식

쿠버네티스 클러스터에 앱을 배포하기 위해서는 개발한 코드를 도커 이미지로 만든 다음 ECR과 같은 이미지 레지스트리에 업로드해야 한다. 클러스터의 Deployment와 같은, 파드를 관리하는 리소스가 필요에 따라 언제든 최신 이미지를 가지고 컨테이너를 실행할 수 있도록 하기 위해서 어쩔 수 없는 과정이긴 하지만, 매번 배포할 때마다 이미지를 교체해주는 작업이 여간 번거로운 게 아니다.

 

배포하는 환경에 따른 설정 역시 번거로운 건 마찬가지. 일반적으로는 개발 환경과 운영 환경이 서로 다른 이미지를 사용하기 때문에 이를 테스트하기 위해서는 서로 다른 이미지를 사용해야 하고, 동일하게 이미지를 교체해주어야 한다. 때에 따라서는 배포를 하는 namespace가 다른 경우도 있을 텐데, 이런 케이스도 처리하기가 쉽지 않다.

 

명령어 하나로 이 과정을 모두 처리할 수 있다면 어떨까? 쿠버네티스를 기반으로 개발하는 많은 개발자가 이미 이러한 문제를 인식하고 자체적으로 스크립트를 만들어서 사용하거나, 라이브러리/서비스 형태로 다양한 솔루션들을 내놓았다. 후자의 경우 쿠버네티스를 사용해 개발하고 배포하는 과정을 간단하게 만들어주는 도구들이 여럿 존재한다. Skaffold, Draft, Tilt, Garden와 같은 툴이 있는데 그 중 Skaffold를 사용해 배포 과정을 간단하게 만들어보자.

 

Draft의 경우 MS에서 제작한 툴인데, 현재 리소스가 부족해 추가적인 개발이 어렵다고 하는 상황이고, 가장 간단한 형태인 Skaffold를 선택하게 되었다. Tilt나 Garden같은 경우는 기회가 되면 써보고 싶은 생각은 있다.

 

Skaffold

Skaffold는 구글에서 만든 쿠버네티스 개발 도구이다. 로컬 환경에서 쿠버네티스 기반으로 개발을 할 때를 초점으로 잡고 이와 관련한 다양한 기능을 제공하고 있다.

 

Skaffold의 특징이라면 굉장히 가볍다는 점을 들 수 있을 것 같다. 핵심이 되는 설정 파일은 skaffold.yaml 단 하나이고, YAML 문법을 사용하기 때문에 기존에 쿠버네티스 배포를 해보았다면 곧바로 사용할 수 있다. 또한 클러스터 내에 별다른 컴포넌트를 추가하지 않고 로컬에서 모든 과정을 처리하기 때문에 별도의 유지보수가 필요하지 않다.

 

그럼 곧바로 Skaffold를 사용해보자! Mac이라면 Homebrew, Windows라면 Chocolatey를 사용해 설치하는 것을 추천한다. 구체적인 설치 방법이 궁금하다면 관련 문서를 참고하자.

# For Mac
$ brew install skaffold

# For Windows
$ choco install -y skaffold

 

Pipeline Stages

Skaffold의 Multi-stage workflow

Skaffold는 빌드/배포의 전 과정을 각각의 단계로 쪼개놓은 다중 스테이지 구조로 이루어져 있다. 빌드하고 배포하는 과정 자체가 여러 단계로 나뉘어있으니 당연한 선택이다. 이렇게 각 단계가 별도의 스테이지로 존재하고 서로 디커플링이 잘 되어있어 각 단계의 세부 구현을 쉽게 바꿀 수 있게 되어 있다.

 

만약 기존에 배포를 kubectl로 하다 Helm을 사용하고 싶다면 배포 스테이지의 설정만 변경하면 된다. Docker로 빌드하다가 Bazel로 빌드를 하고 싶다면 빌드 스테이지만 변경하면 된다. (구글에서 Skaffold를 만들었다 보니 Bazel, Google Cloud Platform과 같은 구글의 개발 도구와 연동이 잘 되어 있다)

 

각 스테이지를 따로따로 진행할 수도 있다. 이미지를 빌드하고 푸시 및 배포하는 skaffold run 명령은 skaffold build, skaffold deploy로 나누어진다. 스테이지의 일부분이 필요 없거나 테스트를 하는 경우 유용하게 쓸 수 있을 것 같다. (CLI 레퍼런스)

 

skaffold.yaml

Skaffold의 설정 파일은 yaml 파일 하나다. 이미지를 빌드하는 방법, 테스트하는 방법, 배포하는 방법까지 모두 이 파일에 작성하게 된다.

apiVersion: skaffold/v2beta16
kind: Config
metadata:
  name: api
build:
  tagPolicy:
    customTemplate:
      template: "{{.DATETIME}}-{{.COMMITHASH}}"
      components:
        - name: DATETIME
          dateTime:
            format: "2006-01-02"
            timezone: "UTC"
        - name: COMMITHASH
          gitCommit:
            variant: AbbrevCommitSha
  artifacts:
    - image: api-dev
      context: .
      docker:
        dockerfile: Dockerfile
deploy:
  kubectl:
    defaultNamespace: app-dev
    manifests:
      - k8s/dev-*.yaml
profiles:
  - name: prod
    patches:
      - op: replace
        path: /deploy/kubectl
        value:
          defaultNamespace: app-production
          manifests:
            - k8s/prod-*.yaml
      - op: replace
        path: /build/artifacts/0/image
        value: api

apiVersion, kind, metadata 같은 키를 보면 기본적인 구조는 일반적인 쿠버네티스 오브젝트와 똑같이 생겼다.

 

  • build 섹션에는 이미지를 어떻게 빌드할 것인지, 태깅을 어떻게 할 것인지 등, 이미지를 빌드하고 푸시하는 것에 관한 설정이 담겨있다.
  • deploy 섹션에는 이미지를 가지고 파드, 디플로이먼트같은 리소스를 배포하는 것에 관한 설정이 담겨있다.
  • profiles 섹션에서는 워크플로우를 실행할 때 프로필에 따라 다른 기능을 수행할 수 있도록 설정할 수 있다. 이 부분은 뒤에서 조금 더 자세히 알아볼 예정이다.

 

그 외에도 test, portForward 등의 섹션이 존재하는데, 배포를 위해서 핵심적인 부분은 아니기에 따로 설명하지는 않으려고 한다.

 

skaffold.yaml 파일의 모든 옵션이 정리된 레퍼런스도 있다. 앞으로 설정하면서 문제가 발생했을 때 문서와 함께 자주 찾아보는 레퍼런스다. 단순히 한 요소에 대한 옵션만 있는게 아니라, 여러 옵션을 구조적으로 확인할 수 있어서 좋은 것 같다.

 

이러한 구조를 머리속으로 그리면서, 앞서 이야기한 문제를 하나씩 해결해보자.

 

1 - 이미지 빌드

우리가 첫 번째로 인식한 문제는 이미지를 빌드하고 레지스트리에 푸시하는 과정이었다. 단순히 태그를 latest로 두고, 이미지 하나를 계속 덮어쓰는 방식도 가능하지만, 버전 관리가 안 된다는 문제점이 있다.

 

이미지 만들기 - Build, Tag

우선 Skaffold에서 이미지를 빌드하려면 어떻게 해야 하는지 알아보자. build.artifacts 섹션은 Skaffold를 통해서 빌드할 이미지를 설명하는 곳이다. 현재 디렉토리에서 Docker를 사용해 api-dev라는 이미지를 만들기 위해서는 다음과 같은 설정이 필요하다.

build:
  artifacts:
    - image: api-dev
      context: .
      docker:
        dockerfile: Dockerfile

 

이미지를 빌드할 때 같은 이미지의 서로 다른 버전을 식별하기 위해서 태그를 사용할 수 있다. 날짜 기반으로 태깅을 하거나, 커밋 해시를 가지고 태깅을 하는 등 다양한 방법이 존재할 텐데, Skaffold에서는 이러한 목적으로 Tag 기능을 제공한다.

 

build.tagPolicy 속성을 통해 이미지를 빌드할 때 어떤 태그를 사용할지 명시할 수 있다. 아래는 태깅할 때 Git의 태그나 커밋 해시를 사용하는 예시이다. 이외에도 날짜와 시간을 태그로 사용하는 dateTime, 환경 변수를 사용하는 envTemplate 등이 있다.

build:
  tagPolicy:
    gitCommit: {}

 

만약 두 가지 형식을 모두 사용하고 싶다면 어떻게 해야 할까? 이럴 때는 customTemplate 옵션을 사용하면 된다. 아래는 현재 내가 사용하고 있는 태그 정책이다.

build:
  tagPolicy:
    customTemplate:
      template: "{{.DATETIME}}-{{.COMMITHASH}}"
      components:
        - name: DATETIME
          dateTime:
            format: "2006-01-02"
            timezone: "UTC"
        - name: COMMITHASH
          gitCommit:
            variant: AbbrevCommitSha

components에 정의된 요소들의 이름을 customTemplate.template에서 사용하고 있는 것을 확인할 수 있다. 이 템플릿 형식은 golang template을 따른다. (Go를 만들고 그 Go로 Skaffold를 만들고 Go의 템플릿을 사용하는 구글 -ㅅ-) 결과적으로 현재 날짜와 커밋 해시를 조합해 태그를 생성해 사용한다.


Skaffold 에서는 같은 태그를 사용하더라도 이미지 다이제스트를 활용해 각 빌드마다의 이미지를 고유하게 만든다. 즉 태그와 이미지 다이제스트를 모두 사용해 이미지를 업로드하거나 가져와 이미지가 소실되는 것을 방지하고, 항상 최신의 이미지를 사용하도록 설계되어 있다.

 

이미지 푸시하기 - Image Repository Handling

이미지를 빌드했다면 저장소에 푸시할 차례! 보통 이미지를 저장하는 레지스트리의 경우 gcr.io, ghcr.io와 같이 지정된 URI가 존재한다. AWS의 Elastic Container Registry와 같은 서비스는 난수로 이루어진 프라이빗 저장소 URI를 사용하기도 한다.

<registry>/<image-name>:<tag>

이미지를 푸시할 때는 위와 같이 저장소의 URI와 이미지의 이름을 합쳐 푸시를 하게 되는데, Skaffold에서는 이를 위해 Image Repository Handling 기능을 제공한다.

# Using --default-repo flag
$ skaffold dev --default-repo <myrepo>

# Using SKAFFOLD_DEFAULT_REPO environmental variable
$ SKAFFOLD_DEFAULT_REPO=<myrepo> skaffold dev

# Using global config
$ skaffold config set default-repo <myrepo>

사용하는 저장소가 하나라면 세 번째, default-repo의 전역 설정을 이용하는 방법이 가장 간편할 것 같고, 혹시나 여러 저장소를 사용한다면 플래그를 사용하는 방식이 아무래도 편하지 않을까 싶다.

 

이렇게 태그와 저장소 설정을 모두 마치고 skaffold build 명령을 실행하면 저장소와 태그가 포함된 형태로 이미지가 만들어지는 것을 볼 수 있다.

Generating tags...
 - api-dev -> 552543234276.dkr.ecr.ap-northeast-2.amazonaws.com/bser/api-dev:2021-08-03-8dff6d7-dirty

 

2 - 이미지 배포

만들어진 이미지가 반영되기 위해서는 배포 절차를 거쳐야 한다. 배포 절차는 보통 다음과 같이 이루어진다.

 

  1. 배포하려는 쿠버네티스 리소스의 설정을 적절히 변경한다. k9s와 같은 도구를 사용해 수정할 수도 있겠지만, 보통은 배포에 필요한 YAML 파일을 수정하게 된다.
  2. kubectl apply와 같은 명령을 사용해 변경한 YAML파일을 클러스터에 반영한다.

우리의 목적은 배포할 때 직접 YAML 파일을 수정하지 않도록 하는 것이고, 배포할 때 YAML 파일에서 바뀌는 부분은 결국 사용하는 이미지다. Skaffold를 사용해서 배포하면, manifest 파일에 명시된 이미지와 이름이 일치하는 이미지를 찾아 대체해서 배포한다. 이게 무슨 말인지 설명을 덧붙여보겠다.

 

deploy 섹션 자체는 크게 특별할 것이 없다. kubectl, Helm 등의 배포 수단을 선택할 수 있는데, 여기서는 kubectl을 사용했다.

deploy:
  kubectl:
    defaultNamespace: bser-stage
    manifests:
      - k8s/dev-*.yaml

 

위의 manifests 옵션에서 배포를 위해 사용할 manifest 파일을 지정하고 있다. 이 매니페스트에는 여러 가지 리소스들이 있을텐데, 그중 Deployment의 파드 스펙에 api-dev 라는 이미지를 사용했다고 하자.

spec:
  containers:
    - name: er-api-node
      image: api-dev
      imagePullPolicy: IfNotPresent

 

앞서 Skaffold 설정에 이미지 이름을 api-dev로 해두었다. 이렇게 이미지 이름이 일치할 경우 배포를 할 때 api-dev라는 이미지는 실제 빌드된 이미지로 치환되어 배포된다.

build:
  artifacts:
    - image: api-dev
      context: .
      docker:
        dockerfile: Dockerfile

 

skaffold run 명령으로 배포 이후 Deployment의 이미지 스펙을 확인해봤다. 이미지 레지스트리와 태그가 정상적으로 달린 것을 볼 수 있다.

❯ kubectl get deploy er-api-dev --template="{{ (index .spec.template.spec.containers 0).image }}"
552543234276.dkr.ecr.ap-northeast-2.amazonaws.com/bser/api-dev:2021-08-03-8dff6d7-dirty@sha256:8ca0d8b78b49fd5fd63c9ea118803c6eb08f5af46060901c7822e56de7a5d5df

 

3 - 배포 환경 관리

처음 서비스를 만들 때는 테스트와 운영 환경을 동일하게 가져가는 경우가 많은데, 서비스의 규모가 커지고 별도의 테스트 환경의 필요성이 생기면 테스트 용도와 운영 용도의 환경을 분리하게 된다.

 

이런 환경을 보통 스테이징 환경이라고 하고, 필요에 따라 두 개, 세 개 이상으로 환경이 늘어날 수 있다.

 

환경이 달라지는 경우 배포하는 코드 역시 달라지기 마련이다. 스테이징 서버에는 내부 테스트를 위한 코드가 배포될 것이고, 운영 서버에는 모든 테스트가 끝난 코드가 배포될 것이다. 이 말은 즉 각각의 환경마다 배포하는 이미지가 달라져야 한다는 것.

 

이 문제는 어떻게 해결할 수 있을까? Skaffold에서는 프로필을 통해서 이를 지원한다.

profiles:
  - name: stage
    deploy:
      kubectl:
        defaultNamespace: bser-stage
        manifests:
          - k8s/dev-*.yaml

profiles 섹션을 보자. stage라는 이름의 프로필을 정의하고 있다.

 

프로필에서 정의하는 build, deploytest 섹션은, 해당 프로필로 Skaffold 워크플로우를 실행했을 때 기존의 각 섹션을 대체하게 된다. 원래 정의되어있던 값이 무엇이든 관계없이, 새로운 값들로 대체되는 것이다.

 

한편 기존의 설정에서 일부분만 변경하고 싶을 수도 있다. 그런 경우에는 patches를 사용한다.

profiles:
  - name: prod
    patches:
      - op: replace
        path: /build/artifacts/0/image
        value: api

prod라는 이름의 프로필에서는 build.artifacts[0].image 옵션값을 api로 변경하고 있다. 이 patch를 사용하는 방법이 처음에는 많이 생소했다. 사실 이 표현 방식은 JSON Patch라는, IETF 표준에 정의된 형식이다.

 

몇 번 테스트와 구글링을 거치면 프로필마다 어느 부분이 바뀌는지 깔끔하게 정리가 된다. 이렇게 만들어진 프로필은 skaffold run 혹은 skaffold dev 명령을 실행할 때 플래그로 지정할 수 있다.

❯ skaffold run -p stage

이 밖에도 환경 변수나 쿠버네티스 클러스터 설정에 따라 자동으로 프로필을 활성화하는 Activation 기능도 있으니, 관심이 있다면 한번 확인해보면 좋을 것 같다.

 


 

배포 과정을 자동화하기 위해 Skaffold라는 도구를 사용해보았다.

 

Skaffold는 문서가 잘 되있기는 하지만, 세부적인 설정으로 들어가면 멘땅에 헤딩을 해가면서 직접 테스트해보고 알아가야 하는 것들이 꽤 있어서 초심자에게 권하기는 조금 애매한 느낌이다. 요새는 또 로컬 빌드의 한계(인터넷 환경에 따라 빌드가 잘 안 되는 경우가 있거나, 로컬에서 빌드하기 위해서 설정해야 하는 것들이 생각보다 많아서 다른 컴퓨터에서 빌드하기가 쉽지 않음)를 조금 느끼고 있어서, 클러스터 내에서 빌드를 할 수 있는 Kaniko같은 툴도 알아보고 있다.

 

그래도 내가 실제로 API 서버를 배포하면서 사용하는 방식이고, 클러스터에 별다른 부담을 주지 않고 로컬 환경에서 빌드를 할 수 있다는 점에서 가볍다는 느낌이라 부담 없이 접근할 수 있었던 것 같다.

 

@turastory

반응형
Comments