복사되었습니다.

Git-sync로 kubernetes 환경에 앱 배포 자동화하기 - 초간단 CI/CD 구축

Cover Image for Git-sync로 kubernetes 환경에 앱 배포 자동화하기 - 초간단 CI/CD 구축

Git-sync를 사용하면 github에 코드를 올릴 때마다 자동으로 앱이 업데이트되도록 만들 수 있다. 코드를 수정할 때마다 직접 배포하지 말고, git-sync를 통해 kubernetes 환경에 간단하게 CI/CD 환경을 구성해보자.

Git-sync란 무엇인가?

Git-sync의 역할

Git-sync는 이름 그대로 코드를 github와 동기화하는 것을 의미한다. 개발 환경에서 코드를 수정하여 github에 올렸을 때, 이미 배포되어 있는 앱을 업데이트하기 위해서는 다음과 같이 여러가지 작업이 필요하다.

  • 실행 중인 앱을 중지한다.
  • Git pull을 통해 github의 최신 코드를 앱의 로컬에 받아온다.
  • 최신화된 코드가 반영된 새 버전의 앱을 실행한다.

앱이 여러 개라면 이 작업들을 직접 수동으로 하기에는 큰 부담이 된다. 따라서, 개발자가 github에 코드를 올리는 순간 모든 앱이 자동으로 업데이트될 수 있도록 하는 것이 좋다. Git-sync는 이 과정 중 일부를 자동으로 처리해준다. 정확히 말하면, github에 코드 변화가 있을 때 이를 감지하여 앱의 로컬 환경에 최신 코드를 pull로 받아오는 역할을 담당한다.

Git-sync 발동 조건

배포 자동화를 무분별하게 하면 사용자들의 불편을 초래할 수 있다. 개발자가 코드를 수정할 때마다 무조건 배포가 진행된다면 앱이 지나치게 자주 변경되기 때문이다. 따라서, git-sync는 특정 branch의 코드가 업데이트될 때만 코드를 최신화되도록 제한한다.

git-sync with git-flow

Git-sync는 git-flow같은 브랜치 전략과 궁합이 잘 맞는다. 보통 main(master) 또는 release 브랜치를 git-sync의 모니터링 대상으로 지정해둔다. 그리고 개발용으로 관리하는 develop 브랜치에 코드를 자주 업데이트한다. Develop 브랜치에서 개발한 기능 중 일부를 사용자들에게 공개해야 할 시점이 왔을 때에만 main이나 release 브랜치에 코드를 반영한다. 이런 방식으로 운영하면 배포 범위와 시점을 편리하게 컨트롤할 수 있다.

자원 여유가 많다면, 세 가지 주요 브랜치, 즉, main, release, develop 모두에 git-sync를 별도로 세팅하는 것도 좋다. 각 브랜치의 고유의 목적에 맞게 앱을 독립적으로 배포해서 변경된 코드를 빠르게 확인할 수 있다. 관리해야 하는 앱의 수는 많아지지만, git-sync가 알아서 해주기 때문에 자동 코드 최신화의 이점이 극대화된다.

Git-sync의 구성 방법

Git-sync는 지정된 github 브랜치를 주기적으로 모니터링하는 방식, 즉, polling 방식으로 코드가 업데이트되었는지 확인한다. 그리고 코드 변경이 감지될 때마다 코드를 로컬에 pull해서 최신화시켜준다. 따라서, main 프로세스와는 독립적으로 수행되는 일종의 백그라운드 프로세스로 세팅해야 한다.

Kubernetes 환경에서는 보통 sidecar 패턴을 통해 git-sync를 세팅한다. Kubernetes 공식 github repository에서 완성된 기능을 제공하기 때문에 별도로 git-sync를 개발할 필요는 없다. 컨셉은 매우 간단하다. Kubernetes에 배포되는 pod에 git-sync container를 포함시켜서 백그라운드 프로세스처럼 동작하도록 만드는 것이다. Git-sync container는 지정된 github repository의 브랜치를 지속적으로 모니터링하면서, pod에 할당된 로컬 파일시스템에 코드를 최신화한다.

git-sync deployed as sidecar

Git-sync가 정상적으로 동작하도록 하려면, main 앱이 참조하는 로컬 파일 위치를 공유하는 등 세부적인 설정이 필요하다. 이에 대한 내용은 뒤에서 더욱 자세히 알아보도록 하자.

Kubernetes pod에 git-sync 설치하기

Kubernetes git-sync 구축을 위해 필요한 것

Kubernetes 환경에서 직접 git-sync 파이프라인을 구성해보자. 우선 pod 하나에 main 앱을 담은 container와 git-sync container를 같이 포함시키고, 두 container의 파일시스템을 emptyDir volume을 통해 연결시켜주면 된다. 이외에 Git-sync 동작과 관련된 세부설정들은 git-sync container의 환경변수를 건드리면 된다. Git-sync container에 대표적으로 설정해야 할 것들은 다음과 같다.

  • 모니터링할 github repository 관련 정보
  • 코드 업데이트를 진행할 폴더 경로
  • Polling 주기

실습을 통해 git-sync로 간단하게 CI/CD를 구축하는 방법을 살펴보자.

Kubernetes에 git-sync를 구축하는 방법

테스트를 진행하기 전에, github에 새로운 repository를 만들어서 main 브랜치의 README.md 파일에 다음과 같이 내용을 입력해주자.

# Git-sync test

Hello, World!

이 새로운 github repository의 주소를 https://github.com/username/repository.git이라고 가정하겠다.

다음은 github의 코드를 받아올 그릇을 만들 차례다. 실제 kubernetes 환경에 git-sync 사이드카를 포함한 pod를 배포해보자. 테스트에 활용할 코드는 다음과 같다.

apiVersion: v1
kind: Pod
metadata:
  name: git-sync-test
spec:
  containers:
  - name: main-app
    image: ubuntu:18.04
    args:
    - sleep
    - "3600"
    volumeMounts:
    - name: git-volume
      mountPath: "/code"
  - name: git-sync
    image: k8s.gcr.io/git-sync/git-sync:v3.5.0
    env:
    - name: GIT_SYNC_REPO
      value: https://github.com/username/repository.git
    - name: GIT_SYNC_BRANCH
      value: main
    - name: GIT_SYNC_ROOT
      value: "/shared-volume"
    - name: GIT_SYNC_WAIT
      value: "3"
    volumeMounts:
    - name: git-volume
      mountPath: "/shared-volume"
  restartPolicy: Never
  volumes:
  - name: git-volume
    emptyDir: {}

하나의 pod 안에 main-appgit-sync라는 두 개의 컨테이너를 사용하도록 했다. 그리고 git-sync 컨테이너의 env 부분에 기본적인 설정값들이 들어있다. 자세한 설명은 git-sync 공식 repository를 참고하도록 하고, 예제 코드에서 사용한 값 위주로 살펴보자.

  • GIT_SYNC_REPO, GIT_SYNC_BRANCH: 모니터링 대상 github repository와 브랜치를 설정하는 부분이다. 앞서 생성한 repository 주소와 main 브랜치를 지정해주었다.
  • GIT_SYNC_ROOT: 로컬 파일 시스템의 어디에 최신 코드를 저장할지 경로를 설정하는 부분이다. git-sync라는 컨테이너 기준 경로를 적어줘야 한다. 여기에서는 /shared-volume 위치에 저장하도록 설정했다.
  • GIT_SYNC_WAIT: polling 주기를 초 단위로 설정하는 부분이다. 3초 단위로 polling하도록 설정하였다. 만약 이 값을 설정하지 않으면 default로 1초가 적용된다.

이외에도 주목할 점은 git-volume이라는 이름의 emptyDir 볼륨을 사용했다는 것이다. 이는 같은 pod에 속한 여러 container들이 공유된 파일 저장 공간을 사용할 수 있도록 해준다. main-app 컨테이너는 이를 /code 경로에 연결했고, git-sync 컨테이너는 /shared-volume에 연결했다. 이 관계를 그림으로 표현하면 다음과 같다.

git-sync shared volume mount

정리하자면, 위 코드는 다음과 같은 의미로 해석된다.

  • git-sync 컨테이너가 3초마다 지정된 github repository의 main 브랜치에 변경사항이 있는지 확인한다.
  • 변경사항이 있는 경우, /shared-volume 위치에 최신 코드를 다운로드한다.
  • main-app 컨테이너는 /code 경로에서 코드 변경사항을 곧바로 확인할 수 있다.

Git-sync를 통해 자동으로 코드 최신화하기

앞서 살펴본 코드를 git-sync-test.yaml 파일에 저장 후 배포해보자.

kubectl apply -f git-sync-test.yaml

Pod가 정상 실행 상태라면, main-app 컨테이너에 접속하여 코드가 잘 받아졌는지 확인할 수 있다.

$ kubectl exec -it git-sync-test -- /bin/bash
root@git-sync-test:/# cd /code
root@git-sync-test:/code# ls
git-sync-test.git  rev-0cc14e2196fabb801caa6b111b5c27d048e40f8b
root@git-sync-test:/code# cd git-sync-test.git
root@git-sync-test:/code/git-sync-test.git# ls
README.md
root@git-sync-test:/code/git-sync-test.git# cat README.md 
# Git-sync test

Hello, World!

Github repository의 main 브랜치에 있는 README.md 파일의 내용을 변경한 후, 다시 main-app 컨테이너에 접속하여 확인해보자. 다음과 같이 자동 코드 최신화가 정상적으로 동작하는 것을 확인할 수 있다.

$ kubectl exec -it git-sync-test -- /bin/bash
root@git-sync-test:/# cat /code/git-sync-test.git/README.md 
# Git-sync test

Bye, World!

Git-sync 추가 기능

코드 최신화가 끝나고 후속 작업을 할 수는 없을까?

지금까지는 git-sync의 가장 기본적인 동작에 대해 알아보았다. 단순한 코드 최신화 목적이라면 이미 앞선 내용들로도 충분하다. 만약 CI/CD를 구성할 때 좀 더 복잡한 시나리오를 구현할 필요가 있다면 어떻게 해야 할까?

git-sync follow-up actions

CI/CD는 일종의 파이프라인이다. 하나의 툴로 모든 과정을 통제하기보다는 여러 툴이 순차적으로 작업을 진행하도록 구성하는 것이 일반적이다. Git-sync도 파이프라인의 구성요소 중 하나이므로, 혼자서 모든 과정을 담당할 수는 없다. 따라서, git-sync 동작이 완료되었을 때 파이프라인의 다음 구성요소에게 알리는 등의 후속 작업이 필요하다. Git-sync 컨테이너 환경변수 설정을 통해 이러한 후속 작업들을 간편하게 정의하여 사용할 수 있다. 대표적인 두 가지 기능 위주로 살펴보자.

  • Exechook 기능
  • Webhook 기능

Exechook 기능으로 명령어 실행하기

Exechook 기능은 git-sync의 코드 최신화 동작이 완료될 때마다 후속으로 정해진 명령어를 수행하도록 하는 기능이다. 크게 세 가지 환경변수를 설정하면 된다.

  • GIT_SYNC_EXECHOOK_COMMAND: 코드 최신화 이후 수행할 명령어를 설정하는 부분이다.
  • GIT_SYNC_EXECHOOK_TIMEOUT: 명령어 실행이 완료될 때까지 기다릴 시간을 설정하는 부분이다. 실행이 지연되는 만큼 git-sync의 polling도 지연된다. Default는 30초다.
  • GIT_SYNC_EXECHOOK_BACKOFF: 명령어 실행을 실패할 경우, 다시 재시도하는 시간 간격을 설정하는 부분이다. Default는 3초다.

앞서 작성한 git-sync-test.yaml 파일에 다음과 같이 git-sync 컨테이너의 환경변수를 추가해주자.

- name: GIT_SYNC_EXECHOOK_COMMAND
  value: "ls"

이후 pod를 다시 실행하면 git-sync 컨테이너 로그를 통해 다음과 같은 내용을 확인할 수 있다.

I0306 14:53:03.577107      11 main.go:473] "level"=0 "msg"="starting up" "pid"=11 "args"=["/git-sync"]
I0306 14:53:03.577142      11 main.go:923] "level"=0 "msg"="cloning repo" "origin"="https://github.com/username/repository.git" "path"="/shared-volume"
I0306 14:53:09.392115      11 main.go:737] "level"=0 "msg"="syncing git" "rev"="HEAD" "hash"="37822e1052f840c4ef350a556d156c15f8a7ab9a"
I0306 14:53:14.947676      11 main.go:772] "level"=0 "msg"="adding worktree" "path"="/shared-volume/37822e1052f840c4ef350a556d156c15f8a7ab9a" "branch"="origin/main"
I0306 14:53:14.950854      11 main.go:833] "level"=0 "msg"="reset worktree to hash" "path"="/shared-volume/37822e1052f840c4ef350a556d156c15f8a7ab9a" "hash"="37822e1052f840c4ef350a5
I0306 14:53:14.950887      11 main.go:838] "level"=0 "msg"="updating submodules"
I0306 14:53:14.982491      11 exechook.go:69] "level"=0 "msg"="running exechook" "command"="ls" "timeout"=30000000000

지정된 github repository의 최신 코드를 /shared-volume에 다운로드한 뒤, ls 명령어를 정상적으로 실행하였다. 한 가지 주의할 점은 command argument를 허용하지 않는다는 것이다. 공식 안내 문서에서도 다음과 같이 명시하고 있다.

doesn't support the command arguments

따라서, ls -la /shared-volume처럼 ls 명령어에 인자를 전달하면 다음과 같은 오류 로그가 3초 간격으로 찍힌다.

I0306 15:02:30.086136      12 exechook.go:69] "level"=0 "msg"="running exechook" "command"="ls -la /shared-volume" "timeout"=30000000000
E0306 15:02:30.087153      12 hook.go:136] "msg"="hook failed" "error"="Run("ls -la /shared-volume" ): fork/exec ls -la /shared-volume: no such file or directory: { stdout: "", stderr: "" }"

이렇게 exechook은 인자가 없는 단일 명령어만 허용하기 때문에 사용이 제한적이다. 따라서, 코드 최신화 후 복잡한 후속 작업들을 수행하기 위해서는 별도의 노력이 필요하다. 예를 들어, 단일 명령어로 실행 가능하도록 작업들을 묶고, 해당 단일 명령어의 경로를 환경변수에 포함시켜주면 된다.

Webhook 기능으로 HTTP 요청하기

Webhook은 exechook과 마찬가지로 git-sync 작업을 마친 후 실행되는 후속 작업이다. Exechook과 다른 점은, 명령어 실행 대신 특정 URL에 HTTP 요청을 날린다는 것이다. 다음과 같은 설정 값들을 통해 세부적으로 webhook 기능을 설정할 수 있다.

  • GIT_SYNC_WEBHOOK_URL: 요청을 날릴 주소를 설정하는 부분이다.
  • GIT_SYNC_WEBHOOK_METHOD: 요청을 날릴 때 어떤 HTTP 메서드를 사용할지 설정하는 부분이다. Default는 POST다.
  • GIT_SYNC_WEBHOOK_SUCCESS_STATUS: 어떤 응답 상태 코드를 정상이라고 판단할지 설정하는 부분이다. Default 값은 200이다. 만약 -1을 지정한다면, 상태 코드랑 상관없이 모두 정상이라고 생각한다.
  • GIT_SYNC_WEBHOOK_TIMEOUT: 요청에 대한 time out 시간을 설정하는 부분이다. Default는 1초이다.
  • GIT_SYNC_WEBHOOK_BACKOFF: 요청이 실패할 경우, 다시 재시도하는 시간 간격을 설정하는 부분이다. Default는 3초이다.

테스트를 위해 우선 간단한 HTTP 서버를 배포해두어야 한다. 다음 명령어를 사용하면 echoserver라는 namespace에 간단한 웹 서버를 띄울 수 있다.

curl -sL https://raw.githubusercontent.com/Ealenn/Echo-Server/master/docs/examples/echo.kube.yaml > echo.kube.yaml
kubectl apply -f echo.kube.yaml

git-sync-test.yaml 파일에 다음과 같이 git-sync 컨테이너의 환경변수를 추가한 뒤 pod를 실행해보자.

- name: GIT_SYNC_WEBHOOK_URL
  value: "http://echoserver.echoserver:80"
- name: GIT_SYNC_WEBHOOK_METHOD
  value: "GET"

git-sync 컨테이너의 로그를 보면 다음과 같이 코드 최신화 직후 HTTP 요청을 날리는 것을 확인할 수 있다.

I0307 14:17:54.002579      12 webhook.go:71] "level"=0 "msg"="sending webhook" "hash"="37822e1052f840c4ef350a556d156c15f8a7ab9a" "url"="http://echoserver.echoserver:80" "method"="GET" "timeout"=1000000000

이번에는 다음과 같이 정상으로 판단할 응답 상태 코드를 123으로 지정한 뒤 pod를 실행해보자.

- name: GIT_SYNC_WEBHOOK_SUCCESS_STATUS
  value: "123"

git-sync 컨테이너의 로그를 보면 다음과 같이 123을 기대했으나, 200이 응답으로 돌아왔다는 메시지가 반복적으로 찍히는 것을 확인할 수 있다.

I0307 14:23:28.826282      12 webhook.go:71] "level"=0 "msg"="sending webhook" "hash"="37822e1052f840c4ef350a556d156c15f8a7ab9a" "url"="http://echoserver.echoserver:80" "method"="GET" "timeout"=1000000000
E0307 14:23:28.832995      12 hook.go:136] "msg"="hook failed" "error"="received response code 200 expected 123"

만약 exechook 또는 webhook이 정상적으로 실행되지 않으면, git-sync 컨테이너에 지속적인 에러 로그가 찍힌다. 이 상태에서 github의 코드가 변경된다면, 과연 정상적으로 코드 최신화가 될까? 확인 결과, 에러 로그들 사이에서 다음과 같이 정상적으로 코드 최신화가 진행된다는 로그가 찍힌다.

I0310 12:35:08.181830      12 main.go:1068] "level"=0 "msg"="update required" "rev"="HEAD" "local"="8648113ef55bc69ba994515b50102caae6bc33d7" "remote"="c1a27196bfa9edf911315db41bcb
I0310 12:35:08.181868      12 main.go:737] "level"=0 "msg"="syncing git" "rev"="HEAD" "hash"="c1a27196bfa9edf911315db41bcb47a3914f533d"
I0310 12:35:09.044341      12 main.go:772] "level"=0 "msg"="adding worktree" "path"="/shared-volume/c1a27196bfa9edf911315db41bcb47a3914f533d" "branch"="origin/main"
I0310 12:35:09.047917      12 main.go:833] "level"=0 "msg"="reset worktree to hash" "path"="/shared-volume/c1a27196bfa9edf911315db41bcb47a3914f533d" "hash"="c1a27196bfa9edf911315db
I0310 12:35:09.047962      12 main.go:838] "level"=0 "msg"="updating submodules"

이는 특히 webhook을 사용할 때, 요청받는 서비스가 네트워크 문제 등 다양한 원인에 의해 일시적으로 사용 불가능할 경우 발생 가능하다.

그렇다면, 이 상태에서 webhook이 다시 정상 동작하기 시작한다면 어떻게 될까? 코드 최신화는 두 번 진행된 것이기 때문에, 밀려있던 webhook이 두 번 실행될까? 아니면 한 번만 실행될까? Webhook이 접근하는 echoserver를 정상화한 결과, git-sync 컨테이너의 에러 로그는 멈추고 echoserver에는 단 한 번의 요청만 날아가는 것을 확인했다.

정리하자면, exechook 또는 webhook에 문제가 있을 경우의 시나리오는 다음과 같다.

  • Exechook 또는 webhook이 지속적으로 오류를 발생시키고 있더라도 코드 최신화는 정상적으로 진행된다.
  • 다시 정상적으로 실행 가능한 상태가 되었다면, 에러 발생 기간 동안의 코드 최신화 횟수와 상관없이 exechook 또는 webhook은 한 번만 실행된다.

Comments

    More Posts

    Kubernetes 1.23 버전의 최후 - Ubuntu kubernetes apt install 에러

    2024년 3월 초, docker가 익숙해서 끝까지 버티고 있던 kubernetes v1.23.x 개발 환경 설치에 돌연 문제가 생겼다. Ubuntu에서 kubeadm, kubelet, kubectl 등을 apt로 설치할 수 없게 된 것이다. 원인과 해결 방법에 대해 알아보자.

    Kubernetes에서 memory가 부족해지면 어떻게 될까? - Kubernetes Out-Of-Memory(OOM) kill and eviction

    Kubernetes의 주요 역할 중 하나는 클러스터의 자원을 효율적으로 관리해주는 것이다. Kubernetes는 클러스터에 자원이 부족한 상황에서 자원을 재분배하기도 한다. 쉽게 말해서, 어떤 앱들은 잘 동작하고 있다가도 외부의 압력으로 인해 강제로 종료될 수 있다는 의미다. 그렇다면, kubernetes는 어떤 기준으로 pod들을 종료시키는 것일까? 가장 흔히 접하게 되는 Out-Of-Memory(OOM) 현상을 위주로 살펴보자.

    Kubernetes에서 CPU의 request와 limit은 어떻게 설정해야 할까?

    Kubernetes 환경에 다양한 app들을 배포하여 운영하다보면 리소스 할당의 중요성을 깨닫는 순간이 온다. 별도로 설정을 하지 않으면 kubernetes는 CPU와 memory에 대한 request와 limit 값을 알아서 채워주지 않는다. 그렇다면 request와 limit 값을 설정하지 않고 app들을 운영하는 것은 괜찮은 걸까? 만약 몇몇 app들이 CPU를 과도하게 사용한다면 어떤 일이 벌어질까? 실제 stress test를 통해 어떤 현상이 나타나는지 알아보고, CPU의 request와 limit 값은 어떻게 설정하는 것이 좋은지 알아보자.

    Font Size