2019년 말 GitHub Action CI/CD 가 ga 되고 거의 바로 이용했으니까 벌써 2년 정도 사용한 것 같다. 그 동안 이용해오면서 GitHub action 이 어떻게 변해왔고, 개인적으로 느끼고 배웠던 점들을 이야기 해 보려고한다.
원래는 ci/cd 를 이용하는 프로젝트는 gitlab 에 구성을하고 gitlab ci/cd 를 이용하고 있었다. Gitlab 에서는 self-hosted runner 를 이용하고 있었는데, 그 당시 consistency 이슈가 있어서 애를 먹고 있었다. Push 후 self-hosted runner 에서는 바로 mater branch 를 pull 했었는데 push 되기 전 코드가 pull 되는 경우가 있어서 매번 최신 코드가 pull 되었는지 확인하느라 번거로웠던 기억이 있다. 아마도 gitlab 의 push 내용이 전체 노드에 반영되는 것이 eventually consistency 로 동작하는 것 같은 느낌이 들었지만 gitlab 측에 문의를 해도 답변을 받지 못해서 github action 이 나오자마자 바로 테스트를 했고, 그 와중에 github 에서도 self-hosted runner 를 지원하기 시작해서 바로 github action 으로 갈아탔고, 그 이후로 죽 github action 을 이용해왔다. 개인적으로는 지금도 기능적인 측면은 gitlab ci/cd 가 github action 보다 우수한 것 같다. 다만 그 당시 Gitlab 의 느린 성능과 불안정성으로 인하여 사용 경험이 매우 좋지 않았다.
GitHub Action CI/CD
Self-hosted runner
Github action 을 이용하면서 느꼈던 첫 불편함은 repository group 이 없다는 것이었다. GitHub 에는 organization 이라고 비슷한 개념이 있긴한데 Gitlab 처럼 마음대로 repository 를 그룹으로 자유자재로 묶고 빼고 할 수는 없었다. 그리고 초기 Github 에서는 organization 단위의 secret 을 지원하지 않아서, 매번 수 십개의 repo 를 찾아서 일일이 secret 을 설정해야 했고, self-hosted runner 도 repository 단위로만 동작을 해서 30개의 repository 를 위한 self-hosted runner 를 띄운다고 하면 ha 를 위해 repo 당 3개의 runner instance 를 띄운다고 했을 때 총 90개의 self-hosted runner instance 를 띄워야해서 매우 번거로웠다. 지금은 organization 단위의 secret 과 self-hosted runner 를 지원한다.
Organization self-hosted runner
Organization secrets
처음에는 ci/cd 전체 과정에서 GitHub Action 을 적용하려고 했다. 그래서 ssh 나 public cloud 접근을 포함해서 단순한 로직도 모두 특화된 action 을 이용해서 구현했다. CI/CD 를 단순하게 보면 git repo 에 특정 이벤트가 발생했을 때, 관련된 코드를 실행하는 것인데 실행되어야 하는 로직도 action 으로 구현한 것이다. 그런데 CI/CD 를 항상 github action 으로만 실행하는 것이 아니라, 테스트나 부득이한 사정으로 local 에서도 실행해야 할 필요가 있는데 github action 은 local 에서 실행하기 번거롭다는 점이 문제였다. 그리고 yaml 기반의 DSL 로 구성되는 workflow 는 로직이 조금만 복잡해지면 구현하기 힘들고, action 마다 사용법이 달라서 매번 새로 학습해야하는 번거로움도 있었다. 직접 action 을 만들어보기도 했지만, 간단한 로직 하나 바꾸는데도 action 을 수정하고, 테스트 해 봐야하고 사실 action 을 직접 만들어야 될 정도면 특정 프로젝트에 specific 한 요건이 있는 경우라서 action 의 재활용성도 떨어졌다.
https://github.com/marketplace/actions/s3-sync 와 https://github.com/aws-actions/configure-aws-credentials 두 action 을 비교해보자. 전자의 경우 workflow 파일이 아래와 같고,
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: jakejarvis/s3-sync-action@master
with:
args: --acl public-read --follow-symlinks --delete
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: 'us-west-1' # optional: defaults to us-east-1
SOURCE_DIR: 'public' # optional: defaults to entire repository
후자의 경우 workflow 파일이 다음과 같다.
jobs:
deploy:
name: Upload to Amazon S3
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Configure AWS credentials from Test account
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Copy files to the test website with the AWS CLI
run: |
aws s3 sync . s3://my-s3-test-website-bucket
첫번째 workflow 의 문제점은 우선 정확히 어떤 명령어가 실행되는지 알기 어렵고, endpoint url 을 변경하는 등의 추가 옵션을 설정할 경우 aws cli 문서가 아니라 다른 곳에서는 아무짝에도 쓸모없는 jakejarvist/s3-sync-action 의 문서를 봐야 한다는 것이다. 그것도 문서화가 잘 되어 있는 경우고, 최악의 경우는 action 코드를 직접 들여댜 봐야 할 경우도 있다. 이건 aws 나 gcp 의 official action 도 마찬가지다. 그리고 local 에서 수동 배포할 경우 첫 번째 workflow 는 답이 없다. 그래서 첫번째처럼 무엇이 실행되는지 action 내부에 가려져 있는 action 의 경우는 무조건 피하라고 말하고 싶다. action 은 checkout, setup, configure 처럼 환경 설정 목적으로만 쓰고 실제 실행 로직은 전부 run 안에 구현하는 것을 추천한다.
그런데 두번째 workflow 도 프로젝트 규모가 커지면 문제가 발생하기 시작한다. 프로젝트가 커지면 monorepo 처럼 한 repo 에 여러 프로젝트가 들어가기도 하고, 한 프로젝트 내에서도 매번 전체를 ci/cd 하는 것은 비효율적이기 때문에 변경이 발생한 모듈만 ci/cd 를 해야하는 순간이 온다.(repo 를 작게 분리하는 것은 또 다른 문제를 발생시킨다.)
on:
push:
branches:
- "main"
paths:
- "project-a/module_a/**"
그러면 위와 같이 paths 기반으로 triggering 을 해야하는데 배포 로직이 workflow 의 run 에 정의되고 변경 될 경우 ./github/workflows 내의 워크플로우 파일이 변경되었으므로 action 이 실행되지 않는다. 또한 workflow 에서 변수를 사용하는 경우 workflow file 에서 변수 및 환경 변수가 어떻게 interpolation 되는지 직관적이지 않고 bash 에서 다시 interpolation 할 경우 더 복잡해진다. 마찬가지로 이 경우도 다른 곳에서는 아무짝에도 쓸모 없는 github action workflow 의 문법을 공부하느라 인생을 낭비해야한다. 이런 문제들로 인해서 지금은 아래와 같은 형태로 workflow 를 구성해서 쓰고있다.
on:
push:
branches:
- "main"
paths:
- "project-a/**"
jobs:
deploy:
name: Upload to Amazon S3
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Configure AWS credentials from Test account
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- run: make
working-directory: project-a
각 프로젝트 디렉토리에는 Makefile 에 ci/cd 관련 로직을 정리해두고, action 에서는 해당 디렉토리로 이동하여 make 실행만을 한다. 이렇게 하면 로컬에서 배포가 필요할 경우 간단하게 make 로 배포할 수 있고, ci/cd 로직이 변경되더라도 Makefile 이 변경되기 때문에 github action 이 자동으로 실행된다.
요약하자면 다음과 같다.
이건 어디까지나 개인적의 의견입니다. 손대기 힘들 정도의 거대한 레거시가 아니라면 Jenkins 는 버리세요. 5년만 지나도 쓸모없어질 툴에 인생을 낭비하지 마세요. Circle CI 도 이용해봤습니다만 아무리 managed 라고 해도 CI/CD 를 위한 별도의 시스템을 이용한다는 것 자체가 시간낭비라고 생각합니다. 대세는 GitHub Action 입니다.