클라우드 컴퓨팅 & NoSQL/도커 & 쿠버네티스

쿠버네티스용 Continuous Deployment 툴인 Skaffold

Terry Cho 2019. 6. 25. 23:18

쿠버네티스용 Continuous Deployment 툴인 Skaffold

조대협 (http://bcho.tistory.com)

What is skaffold?

쿠버네티스 기반에서 개발을 하고 테스트를 하려면 일반적으로 다음과 같은 절차를 거쳐야 한다.

  • 소스 코드를 수정한 후, 

  • 수정한 코드를 컴파일 한 다음에

  • 컴파일한 소스 코드를 포함해서 Dockerfile을 이용해서 컨테이너로 패키징 한후에

  • 컨테이너를 레파지토리 새로운 버전 태그를 붙여서 업로드 하고

  • 쿠버네티스의 기존 Deployment나 Pod의 yaml 파일에 image 명을 바꾼후

  • kubectl -f apply 를 이용해서 변경된 파일을 반영하고,

  • 다음 public IP가 있는 서비스의 경우에는 public IP로 접속하고, 아닌 경우에는 SSH 터널링을 이용해서 해당 Pod나 Service에 접근해서 테스트를 한다.

한번의 소스 코드 수정을 하고 나서, 이러한 일련의 과정을 거쳐야 하는데, 코드 수정 하나를 반영하기 위해서 해야 할일들이 많다.

이러한 문제를 해결하기 위해서 코드 수정이 쿠버네티스에 반영되기 까지의 과정을 자동화를 통해서 단순화 해주는 프레임워크가 skaffold 이다. 

아래 그림은 skaffold의 개념인 워크 플로우를 도식화한 그림이다. 


<그림. Skaffold의 워크플로우>

출처 https://skaffold.dev/docs/concepts/


Skaffold가 실행되면, Skaffold는 소스 코드 리포지토리(ex git) 또는 로컬 디렉토리를 모니터링 한다. 소스 코드의 변화가 생기면 자동으로 코드를 빌드하고, 이를 도커 컨테이너로 패키징 한다. 

다음 컨테이너화된 이미지에 태그를 붙이는데, 단순하게 SHA256 해쉬를 생성하거나 또는 날짜나 Git commit ID를 태그로 사용한다. Git commit ID를 태그로 사용할 경우에는 코드 변경 내용이 어느 컨테이너 버전에 반영되어 있는지 추적할 수 있는 추적성을 제공한다.

태깅이된 컨테이너 이미지를 지정되어 있는 컨테이너 리파지토리에 저장하고, 미리 정의되어 있는 쿠버네티스 YAML 파일을 이용하여 변경 내용을 대상 쿠버네티스 클러스터에 반영한다. 이 과정은 개발자가 코드 변경만 하게 되면 전 과정이 자동으로 진행되기 때문에, 개발 환경으로의 코드 반영에 대해서 수동 작업이 필요없고, Skaffold를 종료 시키게 되면, 개발을 위해서 생성된 쿠버네티스 리소스 (Service, Pod 등)을 자동으로 삭제해주기 때문에, 매우 쾌적한 개발환경에서 개발이 가능하다.


원래 컨셉 자체는 CD(Continuous Deployment)의 용도이지만, 로컬 환경에서 변경된 소스코드를 바로 쿠버네티스에 배포 해주기 때문에 특히나 개발 환경 구축에 편리한 장점을 가지고 있다.

Hello Skaffold

상세한 개념 이해에 앞서서 먼저 간단한 예제를 통해서 어떻게 동작하는지 살펴보도록 하자

준비

예제 환경에는 쿠버네티스 클러스터가 이미 하나가 있어야 하고, kubectl이 로컬(랩탑)에 설치되어 쿠버네티스 클러스터와 연결이 되어 있는 상태이어야 한다. 

설치

설치 방법은 공식 문서 https://skaffold.dev/docs/getting-started/ 를 참고하면된다.

MAC의 경우에는 아래와 같이 curl 명령어를 이용해서 바이너리를 다운로드 받은 후 사용하면 된다.


curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-darwin-amd64

chmod +x skaffold

sudo mv skaffold /usr/local/bin


예제코드 다운로드

예제 코드는 GoogleContainerTools 깃허브에서 다운로드 받을 수 있다.


%git clone https://github.com/GoogleContainerTools/skaffold


예제코드는 examples 디렉토리에 있는데, 그 안에 node 폴더의 내용을 이용하도록 한다.

%cd examples/node


예제 디렉토리 안에는 다음과 같은 파일들이 있다.

  • backend/Dockerfile :

  • backend/package-lock.json , package.json

  • backend/src/ : node.js 소스 코드

  • k8s/ : 쿠버네티스 리소스 배포용 yaml 파일

  • skaffold.yaml : Skaffold 워크플로우를 정의한  yaml 파일


node.js 소스 코드

src/index.js 는 node.js 소스 코드로, node.js express 웹 프레임웍을 이용해서 “Hello World!” 문자열을 리턴하는 웹 서버를 3000 번 포트로 기동 시키는 코드이다.


const express = require('express')

const { echo } = require('./utils');

const app = express()

const port = 3000


app.get('/', (req, res) => res.send(echo('Hello World!')))


app.listen(port, () => console.log(`Example app listening on port ${port}!`))

소스 코드 이외에 필요한 패키지들을 package.json에 정의되어 있다.

Dockerfile

Dockerfile의 내용은 다음과 같다. 


FROM node:10.15.3-alpine


WORKDIR /app

EXPOSE 3000

CMD ["npm", "run", "dev"]


COPY package* ./

RUN npm install

COPY . .


알파인 리눅스 기반의 node.js 10.15-3 이미지를 베이스 이미지로 하고, /app 디렉토리에 package.json 과 기타 소스코드를 복사 한후에, npm install로 의존성 패키지를 설치하고, package.json에 지정된 nodemon src/index.js 명령을 이용해서 앞에서 작성한 index.js node.js 코드를 실행하도록 한다. 

그리고 EXPOSE 3000 명령어를 이용해서 도커 컨테이너에서 3000번 포트로 Listen을 하도록 한다.

쿠버네티스 설정 파일

그러면 앞에서 만들어진 컨테이너로 쿠버네티스에서 서빙을 하기 위해서 쿠버네티스 리소스를 정의해야 하는데, ./k8s/deployment.yaml에서  Service와, Deployment를 정의 하도록 하였다.


apiVersion: v1

kind: Service

metadata:

  name: node

spec:

  ports:

  - port: 3000

  type: LoadBalancer

  selector:

    app: node

---

apiVersion: apps/v1

kind: Deployment

metadata:

  name: node

spec:

  selector:

    matchLabels:

      app: node

  template:

    metadata:

      labels:

        app: node

    spec:

      containers:

      - name: node

        image: gcr.io/terrycho-personal/node-example

        ports:

        - containerPort: 3000


여기서 컨테이너 리포지토리의 경로는 자신이 사용하는 경로로 변경해야 한다. 이 예제에서는 구글 클라우드 리포지토리 (GCR)에 저장하도록  경로를 지정하였다.

Skaffold 설정 파일

다음으로 skaffold의 설정 파일을 보자.


apiVersion: skaffold/v1beta11

kind: Config

build:

  artifacts:

  - image: gcr.io/terrycho-personal/node-example

    context: backend

    sync:

      manual:

      # Sync all the javascript files that are in the src folder

      # with the container src folder

      - src: 'src/**/*.js'

        dest: .


컨테이너 이미지의 경로를 정해놓고, 소스 코드 파일을 어떻게 sync 할지를 지정한다. sync.manual 로 로컬 디렉토리의 src/**/*.js 파일이 변경이 되면 이를 반영하도록 지정하였다. 

실행

그러면 해당 파일을 실행해보도록 하자 skaffold dev 명령어를 실행하면 위의 파일들을 기반으로 컨테이너화를 하고, deployment.yaml 을 이용해서 쿠버네티스 리소스 (Service, Deployment)를 생성 한후, 컨테이너를 배포하여 서비스할 수 있는 형태로 준비한다. 

이때 --port-foward 옵션을 줄 수 있는데, 위의 예제의 경우에는 Service가 있고, Service 타입이 Load balancer이기 때문에, External IP를 가질 수 있어서, 쿠버네티스에서 배정된 External Service를 통해서 Pod에 접근해 서비스가 가능하지만, 일반적으로 개발을 할때는 Service 까지 배포할 필요가 없고 단순하게 Pod만 배포하고 싶은 경우가 있다. (단순하게 작업하기 위해서)

이때 --port-forward 옵션을 주면 Pod에서 외부로 오픈된 포트로 로컬 환경(랩탑) 포트 Forwarding을 해준다.

즉 이 예제에서는 로컬(랩탑)에서 localhost:3000번을 Pod의 3000번 포트로 포트 포워딩을 해주게 된다.  이 옵션을 추가해서 실행하게 되면 결과는 아래와 같이 된다. 


%skaffold dev --port-forward

Generating tags...

 - gcr.io/terrycho-personal/node-example -> gcr.io/terrycho-personal/node-example:v0.32.0-28-g6bd1d50a

Tags generated in 91.565177ms

Starting build...

Building [gcr.io/terrycho-personal/node-example]...

Sending build context to Docker daemon  101.4kB

Step 1/7 : FROM node:10.15.3-alpine

 ---> 56bc3a1ed035

Step 2/7 : WORKDIR /app

 ---> Running in d4579ab15f5a

Removing intermediate container d4579ab15f5a

 ---> 7c725818faae

Step 3/7 : EXPOSE 3000

 ---> Running in e05be7cb896b

Removing intermediate container e05be7cb896b

 ---> 49ac353388c6

Step 4/7 : CMD ["npm", "run", "dev"]

 ---> Running in 52b216a93e63

Removing intermediate container 52b216a93e63

 ---> 3b7878ac4c42

Step 5/7 : COPY package* ./

 ---> 457e460aae8d

Step 6/7 : RUN npm install

 ---> Running in ed391e236197


> nodemon@1.18.7 postinstall /app/node_modules/nodemon

> node bin/postinstall || exit 0


Love nodemon? You can now support the project via the open collective:

 > https://opencollective.com/nodemon/donate


npm WARN backend@1.0.0 No description

npm WARN backend@1.0.0 No repository field.

npm WARN backend@1.0.0 No license field.

npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules/fsevents):

npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})


added 265 packages from 161 contributors and audited 2359 packages in 5.381s

found 1 high severity vulnerability

  run `npm audit fix` to fix them, or `npm audit` for details

Removing intermediate container ed391e236197

 ---> f7a7217f7a60

Step 7/7 : COPY . .

 ---> 5b713462d7ac

Successfully built 5b713462d7ac

Successfully tagged gcr.io/terrycho-personal/node-example:v0.32.0-28-g6bd1d50a

The push refers to repository [gcr.io/terrycho-personal/node-example]

5723b6d27633: Preparing

4f0634afaa8a: Preparing

51a09ffa90b0: Preparing

4789f3ba672b: Preparing

28bf756b6f8e: Preparing

4c299e1e70d5: Preparing

f1b5933fe4b5: Preparing

4c299e1e70d5: Waiting

f1b5933fe4b5: Waiting

28bf756b6f8e: Layer already exists

4c299e1e70d5: Layer already exists

f1b5933fe4b5: Layer already exists

51a09ffa90b0: Pushed

4789f3ba672b: Pushed

5723b6d27633: Pushed

4f0634afaa8a: Pushed

v0.32.0-28-g6bd1d50a: digest: sha256:bccf087827dd536481974eb28465ce4ce69cf13121589e4a36264ef2279e9d1d size: 1786

Build complete in 27.759843507s

Starting test...

Test complete in 17.963µs

Starting deploy...

kubectl client version: 1.11

kubectl version 1.12.0 or greater is recommended for use with Skaffold

service/node created

deployment.apps/node created

Deploy complete in 1.606308148s

Watching for changes every 1s...

Port Forwarding node-6545db86c5-wkdc4/node 3000 -> 3000

[node-6545db86c5-wkdc4 node] 

[node-6545db86c5-wkdc4 node] > backend@1.0.0 dev /app

[node-6545db86c5-wkdc4 node] > nodemon src/index.js

[node-6545db86c5-wkdc4 node] 

[node-6545db86c5-wkdc4 node] [nodemon] 1.18.7

[node-6545db86c5-wkdc4 node] [nodemon] to restart at any time, enter `rs`

[node-6545db86c5-wkdc4 node] [nodemon] watching: *.*

[node-6545db86c5-wkdc4 node] [nodemon] starting `node src/index.js`

[node-6545db86c5-wkdc4 node] Example app listening on port 3000!


태그를 생성해서 gcr.io/terrycho-personal/node-example:v0.32.0-28-g6bd1d50a 이라는 이름으로 컨테이너를 만들어서 쿠버네티스에 배포하고 Port Forwarding node-6545db86c5-wkdc4/node 3000 -> 3000 에서 보는 것과 같이 Pod  node-6545db86c5-wkdc4의 3000 번 포트로 로컬 포트 3000번을 포워딩 하였다. 


kubectl 명령어를 이용해서 Deployment, Pod 그리고 Service들이 제대로 배포 되었는지를 확인해보면 다음과 같다. 


%kubectl get deploy

NAME                    DESIRED CURRENT UP-TO-DATE   AVAILABLE AGE

node                    1 1 1         1 3m


%kubectl get svc

NAME                               TYPE CLUSTER-IP EXTERNAL-IP      PORT(S) AGE

node                               LoadBalancer 10.23.254.227 35.187.196.76    3000:30917/TCP 3m


%kubectl get pod

NAME                                     READY STATUS RESTARTS AGE

node-6545db86c5-wkdc4                    1/1 Running 0 17m


모든 자원들이 제대로 올라온것을 확인했으면 해당 Pod에 localhost:3000 터널을 이용해서 접속해 보자. 



이 상태에서 src/index.js 내용을 에디터를 이용해서 변경해서 저장하면, skaffold 가 파일이 변경되었다는 것을 인지하고, 도커 컨테이너 빌드와 재 배포를 자동으로 수행한다. 아래는 index.js에서 “Hello World 2!”를 출력하도록 소스코드를 수정하여 저장한 후, 수정 내용이 반영된 결과이다. 


중지

개발을 끝내고 싶을 때는 실행중인 skaffold만 중지하면, 아래와 같이 개발을 위해서 생성했던 이미지와 쿠버네티스 리소스 (Deployment, Service, Pod)등을 클린업 해주기 때문에 깔끔하게 개발했던 환경을 정리할 수 있다. 


^CPruning images...

untagged image gcr.io/terrycho-personal/node-example@sha256:bccf087827dd536481974eb28465ce4ce69cf13121589e4a36264ef2279e9d1d

deleted image sha256:5b713462d7ac7ebb468a1f850fa470c34f7b3aaa91ecb9a98b768e892c7625af

deleted image sha256:164f2f7ad057816d45349ffc9e04c361018cb8ce135accdc437f9c7b0cec7460

untagged image gcr.io/terrycho-personal/node-example:v0.32.0-28-g6bd1d50a

untagged image gcr.io/terrycho-personal/node-example@sha256:608ceeae6724e84d30ea61fcfedbaf94aedd3fb6dfbf38c97d244af8c65cbe54

deleted image sha256:d13071d2094092d50236db037ac7e75efca479ed899ec09e3a9a0b61789d93da

deleted image sha256:f9ff5b561a440490345672fe892eeedcd28e05dc01c4675fea8cf5f465b87898

untagged image gcr.io/terrycho-personal/node-example:v0.32.0-28-g6bd1d50a-dirty

untagged image gcr.io/terrycho-personal/node-example@sha256:0ba775dee9f33dc74cea1158f2bc85190649b8102991d653bc73a8041c572600

deleted image sha256:18c830d66ee7c7d508e4edef2980bd786d039706e9271bd2f963a4ca27349950

deleted image sha256:98d168285f8df5c00d7890d1bb8fcc851e8a9d8f620c80f65c3f33aecb540d28

untagged image gcr.io/terrycho-personal/node-example@sha256:0bb49fc5b4ff6182b22fd1034129aed9d4ca5dde3f9ee2dd64ccd06d68d7c0f8

deleted image sha256:a63e7f73658f4f69d7a240f1d6c0fa9de6e773e0e10ab869d64d6aec37448675

deleted image sha256:7e59e5acb2511a5a5620cf552c40bdcba1613b94931fe2800def4c6c6ac6b2a6

deleted image sha256:f7a7217f7a60b23ee04cf50a9bac3524d45d2fd14b851feedd3dc6ffac8b953f

deleted image sha256:b3f0702d41a7df01932c3d3f0ab852ceae824d8acc14f70b7d38e694caa67dc1

deleted image sha256:457e460aae8d185410b47b0a62e027a7d09c7e876cdfaa0c06bcaf6069812974

deleted image sha256:f8e24fe06e6e2c04b5bf29f6eb748d99107781333dc229d03ff77865c1937f8c

deleted image sha256:3b7878ac4c42f541efb80af06415bec286254912f8c3a64e671e7f2f3808b91b

deleted image sha256:49ac353388c6c1e248ac41262ffef47a26ada4d28955e90308c575407f316c64

deleted image sha256:7c725818faaeb6db8d5650e037dc4fa0f38a95bc858ea396f729dfa9ccdc1e06

deleted image sha256:ea8f04b01ca5be4b4fda950557526c3a0707ebda06d0e9392501cba21cc17327

WARN[1877] builder cleanup: pruning images: Error: No such image: sha256:18c830d66ee7c7d508e4edef2980bd786d039706e9271bd2f963a4ca27349950 

Cleaning up...

deployment.apps "node" deleted

Cleanup complete in 2.677585866s

There is a new version (0.32.0) of Skaffold available. Download it at https://storage.googleapis.com/skaffold/releases/latest/skaffo