SLASH 기술 블로그

Git Hook으로 자동 배포하기 본문

자동화

Git Hook으로 자동 배포하기

SLASH 2021. 7. 8. 23:01
반응형

기존에 AWS의 Elastic Beanstalk을 사용하다 쿠버네티스를 쓰게 되면서 자동 배포가 어려워졌다. Skaffold 같은 개발 도구를 사용해서 여러 과정을 단순하게 압축시킬 수는 있었지만, 이전처럼 푸시할 때 알아서 배포되는 형태가 아니라 내가 직접 호출해야 하는 방식이라 귀찮은 부분이 있었다.

 

더구나 브랜치에 따라 서로 다른 환경으로 푸시해야 했기에, 실수로 개발 중이던 코드를 프로덕션으로 배포하는 일이 생길지도 모르는 상황이었다.

 

아직 CI/CD를 도입하지도 않았기 때문에 별도의 인프라를 구축하지 않고 어떻게 하면 배포를 좀 더 편하게 할 수 있을지 고민한 결과, 간단하게 Git Hook을 사용하면 되겠다는 결론에 도달했다. 이번 글에서는 Git의 훅을 소개하고 몇 가지 사례를 이야기해볼까 한다.

 

Git Hook

우선 Git Hook에 대한 설명이 필요할 것 같다. Git Hook은 커밋이나 푸시같은 Git의 특정 이벤트에 맞추어 원하는 스크립트를 실행할 수 있는 간단한 기능이다. 커밋, 푸시 외에 리베이스나 패치할 때 항상 실행되어야 하는 작업이 있다면 유용하게 사용할 수 있다.

 

훅은 git init을 통해서 초기화되어 있는 레포지터리에서 사용할 수 있고, 해당 폴더에서 .git/hooks 디렉토리를 살펴보면 몇 가지 샘플들을 확인할 수 있다. 이 폴더에 우리가 원하는 훅 이벤트에 맞게 스크립트를 넣으면, 이벤트에 따라 훅이 실행된다.

❯ ls .git/hooks
applypatch-msg.sample     pre-applypatch.sample     pre-rebase.sample         update.sample
commit-msg.sample         pre-commit.sample         pre-receive.sample
fsmonitor-watchman.sample pre-merge-commit.sample   prepare-commit-msg
post-update.sample        pre-push.sample           prepare-commit-msg.sample

 

이러한 훅을 글로벌하게 적용할 수 있는 방법도 없지는 않지만, 이러한 작업이 모든 레포지터리에서 필요하지는 않기 때문에 필요한 레포지터리에서 적절하게 사용하는 것을 권장한다.

 

 

훅의 단점이라면 .git 디렉토리 내부에 있기 때문에 버전 컨트롤의 대상이 되지 않는다는 점이다. 한 가지 해결책은 훅을 저장하는 별도의 디렉토리를 만들고, symlink를 통해서 .git/hooks 디렉토리가 그 디렉토리를 바라보게 만드는 것이다. (훅 디렉토리가 없는 과거의 커밋으로 가면 문제가 되긴 한다)

❯ mkdir hooks

# 심볼릭 링크를 resolve할 때 해당 위치에서 상대 경로로 찾기 때문에 이렇게 해줘야 한다.
❯ ln -s ../hooks .git/hooks

 

Git Hook을 사용하는 오픈 소스 프로젝트로는 gitmoji-cli가 있다. Gitmoji는 커밋 메시지에서 이모지를 활용할 때 표준화된 방법으로 커밋을 할 수 있도록 도와준다. 새로운 기능을 추가했다면 ✨, 버그를 고쳤다면 🐛 와 같은 식이다.

 

Gitmoji는 커밋 메시지를 쓸 때 원하는 이모지를 찾아서 추가할 수 있는 기능을 Hook으로 제공한다. gitmoji-cli --init 명령을 통해서 커밋 메시지를 작성할 때 prepare-commit-msg 훅을 등록할 수 있다.

❯ gitmoji -i
✔ Gitmoji commit hook created successfully
❯ cat .git/hooks/prepare-commit-msg
#!/bin/sh
# gitmoji as a commit hook
exec < /dev/tty
gitmoji --hook $1 $2

무척 복잡한 스크립트는 아니다. 기본적으로 쉘 스크립트이긴 하지만, 파이썬이나 노드로 작성된 스크립트나 다른 바이너리를 실행할 수도 있기 때문에 어떤 식으로 구성하는지에 따라 다양하게 활용할 수 있다.

 

푸시할 때 배포하기

그러면 이러한 훅의 기능을 통해서 초기 목적이었던 푸시할 때 배포하는 기능을 만들어보자. 푸시 이벤트를 잡으려면 어떤 훅을 사용해야 할까? 샘플로 주어지는 훅들의 목록을 보고 유추해보자.

❯ ls .git/hooks
applypatch-msg.sample     post-update.sample        pre-merge-commit.sample   pre-receive.sample
commit-msg.sample         pre-applypatch.sample     pre-push.sample           prepare-commit-msg.sample
fsmonitor-watchman.sample pre-commit.sample         pre-rebase.sample         update.sample

이름을 보아 pre-push 훅을 사용하면 될 것 같다. 관련 문서에는 아래와 같이 소개되어 있다.

 

  • The pre-push hook runs during git push, after the remote refs have been updated but before any objects have been transferred. It receives the name and location of the remote as parameters, and a list of to-be-updated refs through stdin. You can use it to validate a set of ref updates before a push occurs (a non-zero exit code will abort the push).

쿠버네티스에서 배포를 하기 위해서는 아래 3가지 단계를 거쳐야 한다.

 

  1. 이미지 빌드
  2. 이미지 배포
  3. 새로운 이미지로 Deployment의 파드 스펙을 업데이트하기

이 세 단계를 하나로 묶어 손쉽게 관리할 수 있는 Skaffold라는 도구가 있다. Skaffold에 대한 자세한 설명은 다음 포스팅에서 해보려고 한다. 우선 내가 배포할 때 사용하는 커맨드를 아래와 같이 .git/hooks/pre-push 파일에 작성했다.

❯ mkdir hooks
❯ ln -s ../hooks .git/hooks # Create a symbolic link for git hooks
❯ echo "skaffold run -p stage" > hooks/pre-push
❯ chmdo +x !$

 

실제로 푸시를 시도하니, 푸시가 되기 전에 배포가 수행되는 것을 확인할 수 있었다.

❯ git push
Generating tags...
Checking cache...
Starting build...

 

브랜치에 따라 다르게 설정하기

배포를 한다면 환경에 따라 다르게 배포하고 싶을 경우도 있을 것이다. 나같은 경우도 개발 브랜치의 경우에는 스테이지 환경으로 배포를 하고, 마스터 브랜치는 운영 환경으로 배포하고 있는데, Git Hook으로 이러한 환경 관리를 어떻게 할 수 있을지 알아보도록 하자.

 

처음에는 브랜치마다 스크립트를 따로 두면 되지 않을까 생각했는데, 조금 생각해보니 그렇게 간단하지는 않은 것 같다.

 

  1. git/hooks 폴더 내의 스크립트는 버전 컨트롤 대상이 아니기 때문에 브랜치에  따라 내용이 바뀌게 하는 건 불가능하다.
  2. 외부의 hooks 폴더를 만들어서 버전 컨트롤을 하더라도 브랜치 간 병합을 했을 때 내용이 충돌되는 문제가 생긴다.

 

결과적으로 스크립트 내에서 브랜치에 따라 분기를 두어야 한다. pre-push를 작성하는 방법에 대해서 좀 더 알아보자.

 

pre-push

이하 내용들은 pre-push의 작동 방식과 스크립트 작성 시 주의할 점들을 상세히 설명하고 있는 스택오버플로우 답변을 참고했습니다.

 

푸시를 하는 경우의 수를 생각해보자. 상상 이상으로 경우의 수가 다양하다. 몇 가지만 열거해보면...

 

  • 어떤 것을 푸시할지 - 현재 브랜치(HEAD), 다른 브랜치, 태그
  • 몇 개를 푸시할지 - 하나만 푸시할지, 여러 개를 푸시할지
  • 어떻게 푸시할지 - 로컬 브랜치 이름 그대로 쓸지, 다른 이름으로 쓸지
  • 삭제 여부 - git push --delete 명령을 사용하면 리모트의 브랜치를 삭제할 수 있다.

다양한 케이스에 따라 원하는 동작을 실행할 수 있도록 pre-push 훅에서는 로컬/리모트의 브랜치와 커밋 해시 정보가 stdin, 즉 표준 입출력으로 제공된다. 구체적인 형태는 다음과 같다.

<local ref> <local hash> <remote ref> <remote hash>

 

pre-push 훅 내에서 이 정보를 받으려면 bash의 read 빌트인 명령어를 사용하면 된다. 여러 브랜치를 푸시할 수도 있기 때문에 while로 묶어야 한다.

while read localname localhash remotename remotehash; do
    echo $localname
    echo $localhash
    echo $remotename
    echo $remotehash
end

 

몇 가지 경우에 대해 테스트를 해보았다. 먼저 다른 브랜치 이름으로 푸시하는 경우이다. 어떻게 출력될까?

# 다른 브랜치 이름으로 푸시하는 경우
❯ git push origin master:test1
refs/heads/master
e89f2c2ef40f8b694967b763f7229055e86f00d9
refs/heads/test1
0000000000000000000000000000000000000000

출력 내용을 보면 로컬의 master와 리모트의 test1 브랜치가 제대로 나오는 것을 볼 수 있다. 리모트의 커밋 해시는 푸시하기 이전의 브랜치 해시가 나오는 듯 하다. (test1 브랜치를 새로 생성했기 때문에 0으로 나옴)

 

# 여러 브랜치를 푸시하는 경우
❯ git push origin master:test1 test2
refs/heads/master
0227f737f1147d58d3cba581167b82306e17a98f
refs/heads/test1
e89f2c2ef40f8b694967b763f7229055e86f00d9
refs/heads/test2
0227f737f1147d58d3cba581167b82306e17a98f
refs/heads/test2
0000000000000000000000000000000000000000

여러 브랜치를 푸시하는 경우에는, 브랜치 개수만큼 출력이 된다.

 

# 브랜치를 삭제하는 경우
❯ git push --delete origin test2
(delete)
0000000000000000000000000000000000000000
refs/heads/test2
0227f737f1147d58d3cba581167b82306e17a98f

브랜치를 삭제하는 경우에는 ref가 (delete)로 표시되고, 커밋 해시가 0으로 출력된다.

 

구현하기

pre-push에서 푸시 관련 정보를 가져오는 방법을 확인했으니, 이제 스크립트를 수정해보자. 브랜치의 이름을 보고 어떻게 배포를 할지 결정해야 하므로 첫 번째 값만 사용하면 된다. 조건 분기를 위해서 Bash의 case문을 사용했다.

stage_deployed=false

while read ref localhash remotename remotehash; do
  case $ref in
    refs/heads/master)
      skaffold run -p prod
      ;;
    refs/heads/*)
      if [ "$stage_deployed" = true ] ; then
        continue
      fi
      stage_deployed=true
      skaffold run -p stage
      ;;
    *) echo "Skip deployment"
  esac
done

위 스크립트를 보자. ref 에는 푸시할 로컬 브랜치(정확히는 ref, 커밋 해시를 가리키는 포인터)이름이 들어간다.

 

브랜치는 refs/heads/ 로 시작하기 때문에 case문 내부에서 패턴 매칭의 접두사로 넣어주었고, master 브랜치만 프로덕션으로 배포하고, 나머지는 스테이징 환경으로 배포한다.

 

배포를 트리거한 경우, 같은 환경으로 여러 번 배포할 필요는 없으니 플래그를 만들고 continue를 넣어주었다.

 

태그를 푸시하거나(refs/tags/*), 브랜치를 삭제하는 경우((delete))는 아직 특별한 정책이 없어서 비워두었다. 이 글을 보는 여러분은 필요에 따라 case문을 적절히 수정해서 사용하면 될 것 같다.

 


훅은 사용하기에 따라 강력한 도구로 활용될 수 있다. 이 글에서는 푸시를 위주로 다루었지만, 푸시 뿐만 아니라 rebase, merge, patch 등 다양한 동작과 함께 사용할 수 있어 그 활용 방법은 무궁무진하다.

 

자동화를 위해 훅을 써보는 건 어떤가!

 

@turastory

 

반응형
Comments