현재의 개발 후 배포 프로세스에 대해서는 지난 포스팅에 정리를 했다. 어차피 다 스크립트를 손으로 실행시키는 방식인데, 이것들 조금만 엮으면 자동 배포가 가능하겠다 싶었다. 그리고 며칠 뒤 삽질을 거쳐서
Github Merge -> CircleCI -> Build and Push Docker Image -> SSH & Trigger Deploy Script on AWS, via CircleCI
로 이어지는 자동배포를 완성했다. 사실 Git Merge 이전에 PR 이 생성되어야 하고 PR 생성시 Circle 이 Test 들을 돌려주는 과정이 있어야 하는데 아직 테스트가 없어서 (...) master branch 에 merge 되는 경우 그 이후 과정만 자동화를 했다.
일단 본인은 Jenkins 보다는 CircleCI 를 선호하는데, 내가 실행하는 Script 들의 형상관리가 가능하다는 장점과 함께 workflow - jobs - steps 로 이어지는 각 단계별 관리가 수월하기 때문이다. 그리고 이정도의 프로젝트는 무료로 계속 돌릴 수 있으며 형상 관리가 되는 덕택에 다양한 셋업들이 공유되고 찾을 수 있다. 아예 오피셜 셋업인 Orbs 도 존재한다.
먼저 CircleCI 에 가입하고 Github 를 연동한 뒤, Add Project 로 들어가면 연동하려는 Repository 를 고를 수 있다.
Set Up Project 를 하고나면 Circle CI 를 어떻게 설정 할 수 있을지 나오는데 기본적으로 Repository 의 root 에 .circleci
라는 폴더, 그리고 그 안에 config.yml
파일을 넣으면 된다. 그 이후 git 에서 commit & push 를 하면 circleci jobs 이 자동으로 돌아간다.
CircleCI 에서 사용 할 docker image 혹은 machine 을 설정 할 수 있고, 해당 환경에서 Github 의 code 를 checkout 한 뒤 테스트를 할 수 있다. native run 으로 test 를 돌려볼 수도 있고, docker 로 container 를 띄워서 테스트를 할 수도 있다. 현재 테스트 코드가 없는 관계로 현재의 설정은 docker image 를 build 하고 push 한 뒤, ECS 에 SSH 로 접속하여 deploy script 를 실행시킨다.
version: 2.1
jobs:
build_and_push_docker_image:
machine: true
steps:
- checkout
- run:
name: build and push docker image
command: |
set -x
REPO="https://github.com/Junyong-Suh/bitfront-telegram-bot.git"
touch confidentials.py && echo "FOREIGN_WORKER_BOT_ID = \"${FOREIGN_WORKER_BOT_ID}\"" >> confidentials.py
LAST_GIT_TAG=$(git ls-remote --tags $REPO | awk '{print $2}' | grep -v '{}' | awk -F"/" '{print $3}' | sort -n -t. -k1,1 -k2,2 -k3,3 | tail -n 1)
NEW_IMAGE="zechery/bitfront-price-alert:${LAST_GIT_TAG}"
docker login -u="$DOCKER_ID" -p="$DOCKER_PWD"
docker build -t ${NEW_IMAGE} .
docker push ${NEW_IMAGE}
deploy:
machine: true
steps:
- checkout
- run:
name: deploy new image
command: |
ssh deployer@ec2-ip.region.compute.amazonaws.com ./circle.sh
workflows:
version: 2.1
build_and_deploy:
jobs:
- build_and_push_docker_image:
filters:
branches:
only: master
- deploy:
requires:
- build_and_push_docker_image
filters:
branches:
only: master
여기서 confidential 에 해당하는 부분은 Git 에 올라가지 않게 하고 있기에 CircleCI Project 의 environment varible 에 setup 을 해두고 쓰고 있다.
docker hub 에 접근하기 위한 credential, telegret bot 의 secret key 등은 이쪽에 설정을 해두어 외부 및 그 어떤 log 에도 남지 않게 할 수 있다.
touch confidentials.py && echo "FOREIGN_WORKER_BOT_ID = \"${FOREIGN_WORKER_BOT_ID}\"" >> confidentials.py
현 프로젝트에서는 아예 credential 을 파일로 관리하여 git 에 commit 되지 않게 하고 있는데 위의 명령을 통해 해당 파일을 생성하고 하나뿐인 해당 상수를 inject ㅎ해서 사용하고 있다. Git 의 경우는 연동 과정에서 Deploy 키가 자동으로 생성되어 셋업되어 있는 것을 볼 수 있다.
이로 인해
steps:
- checkout
라는 과정에 Git repository 를 추가 셋업 없이 check out 할 수 있으면,
LAST_GIT_TAG=$(git ls-remote --tags $REPO | awk '{print $2}' | grep -v '{}' | awk -F"/" '{print $3}' | sort -n -t. -k1,1 -k2,2 -k3,3 | tail -n 1)
같은 코드로 숫자로 되어있는 최신 버젼 태그를 가져올 수도 있다. Docker 의 경우는 별도로 로그인을 해줘야 하는데 이 역시 environment variable 에 설정해 둔 것으로 다음과 같이 가져다 쓸 수 있다.
docker login -u="$DOCKER_ID" -p="$DOCKER_PWD"
docker build -t ${NEW_IMAGE} .
docker push ${NEW_IMAGE}
이렇게 Git repository 를 checkout 해서 최신 태그로 Docker image 를 만들어 올리는 것이 build_and_push_docker_image
라는 이름으로 정의되어 사용중이다.
다음 단계는 이렇게 Docker Hub 에 올라가있는 image 를 EC2 에 배포하는 과정이다. AWS 에서 제공하는 ECS 를 사용중이면 Docker Hub 와 연동해서 직접 가져갈 수 있다고 알고 있으나 EC2 에서는 일단 instance 에 script 를 설치해두고 CircleCI 에서 실행시키고 있다.
CircleCI project setting 에 SSH key 를 등록하고,
해당 키를 EC2 instance 에 ~/.ssh/authorized_keys
등록해둔 뒤, AWS EC2 의 SSH inbound IP 를 열어주면 Circle CI 에서 SSH 로 EC2 instance 에 접근해서 특정 스크립트를 실행시킬 수 있다.
혹여 CircleCI 에 SSH key 등록에 실패한다면 이 포스팅을 참조. (https://support.circleci.com/hc/en-us/articles/360015467253-Adding-SSH-Permissions-fails)
ssh deployer@ec2-ip.region.compute.amazonaws.com ./circle.sh
해당 스크립트는 Git 의 최신 tag 와 docker hub image 의 최신 tag 를 비교하고 같다고 image 를 download 한 뒤, 기존 container 를 종료 및 제거 후 최신 버젼을 실행한다.
#!/bin/sh
CYAN='\033[0;36m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
# validate if both git and docker have the same tag
LAST_TAG=$(./get_last_tag.sh) # execute and capture stdout
STATUS=$? # find out exit status
if [ $STATUS -eq 0 ]; then
printf "${CYAN}Current Git and Docker tag: ${LAST_TAG}${NC}\n"
else
printf "${LAST_TAG}"
exit 1
fi
# pull the new docker image
DOCKER_HUB="zechery/bitfront-price-alert"
NEW_IMAGE="$DOCKER_HUB:$LAST_TAG"
printf "${CYAN}Pulling ${NEW_IMAGE}:${NC}\n"
docker pull ${NEW_IMAGE}
# stop and remove current containers
printf "${CYAN}Stop all running containers:${NC}\n"
docker stop "$(docker ps -aq)"
printf "${CYAN}Remove all containers:${NC}\n"
docker rm "$(docker ps -aq)"
# run the new image
printf "${CYAN}Run ${NEW_IMAGE}:${NC}\n"
docker run -d "$NEW_IMAGE"
# display running containers
printf "${CYAN}Docker containers:${NC}\n"
docker ps
printf "${GREEN}Deployed ${NEW_IMAGE}${NC}\n"
이제 셋업이 완료되었다. 현재 Circle CI 의 master branch 에 merge 되는 경우만 실행이 되도록 해놓았기 때문에 master brnach 에 merge 발생 시 Circle CI 가 알아서 docker tag 를 추출해 docker image 를 build 하여 docker hub 에 push 하고, EC2 instance 에 배포한다.
추후 테스트를 추가하고 filter 를 제거하면 PR 이 생길 때마다 unit test 및 integration test 를 수행하고, docker image 를 build 후 test 환경에서 실행시켜서 e2e 혹은 integration test 를 한번 더 수행 시킬 수도 있다.
후에 scale 이 되는 서비스를 만들려면 ECS 를 쓰거나 현재의 방식을 활용하여 deployer ssh key 를 circle ci 에 설정 -> 배포 할 instance list 확보 -> instance 별로 deploy script 설치 및 authorized key 설정을 하면 쓸 수 있지 않을까 싶다.