혼자 모든 걸 다 해야하는 개발자를 위한 가성비 최고의 기술
소규모 서비스 운영을 위한
Spring Boot 서비스를
gitlab.com의 CI/CD를 이용하여 Docker Image를 생성하고
Docker service를 이용한 무중단 롤링 업데이트를 해보자.
FROM openjdk:8-jdk-alpine
ENV TZ="Asia/Seoul"
ARG JAR_FILE=build/libs/*.war
COPY ${JAR_FILE} app.war
HEALTHCHECK --interval=5s --timeout=5s --start-period=15s --retries=10 CMD wget http://localhost:8080/actuator/health --quiet --output-document - >/dev/null 2>&1
ENTRYPOINT ["java","-jar","/app.war"]
키 포인트는 HEALTHCHECK 커맨드가 있어야 한다.
Spring boot 서비스를 시작하면 요청을 처리할 수 있기까지 약 10초에서 길게는 30초까지 걸리는데, Docker 서비스는 그 시간을 기다려주지 않는다. 일단 컨테이너가 준비되면 바로 트래픽을 보내기 때문에 Docker 서비스에게 실제 준비가 되었을 때 트래픽을 보내라고 알려줘야 한다.
Docker Image 생성을 직접해도 되지만, 프로젝트 빌드 + Docker Image생성 + Registry에 Push 하는 것이 여간 귀찮지 않다.
gitlab.com의 CI/CD 를 사용한다.
image: openjdk:11-stretch
stages:
- build
- docker-build
before_script:
- echo "Start CI/CD"
build:
stage: build
script:
- chmod +x ./gradlew
- ./gradlew bootWar # 프로젝트에_맞게_수정
artifacts:
paths:
- build/libs/*.war # 프로젝트에_맞게_수정
expire_in: 1 week
only:
- docker-ci # 프로젝트에_맞게_수정
docker-build:
image: docker:latest
stage: docker-build
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- docker-ci # 프로젝트에_맞게_수정
after_script:
- echo "End CI/CD"
only
gitlab CI/CD는 지정한 브랜치가 Push되면 자동으로 빌드가 되기 때문에, 특정 브랜치에서만 빌드되도록 only 옵션을 사용한다. 여기서는 docker-ci가 그것이다.
stages
이름은 자유롭게 다만 stages에 있는 이름과 stage에 지정한 이름은 맞아겠지.
docker-ci브랜치를 원격 저장소에 push하면 gitlab ci/cd에 의해 자동으로 빌드가 시작된다.
gitlab.com 사이트에서 진행 상황을 확인하자.
Pipelines
프로젝트 메뉴 > Pipelines 에서 빌드 상황을 확인할 수 있다.
시간이 꽤 걸린다.
Container Registry
프로젝트 메뉴 > Package & Registries > Container Registry 에서 생성된 Docker Image를 확인 할 수 있다.
이미지 이름 옆의 복사하기 아이콘을 마우스 커서를 올리면 URL이 보인다.
이미지 이름은 registry.gitlab.com/프로젝트저장소 로 생성된다.
이미지 이름이나 태그는 .gitlab-ci.yml에서 수정 가능하다.
Deploy tokens 생성
서버에서 gitlab.com의 registry에 로그인할 때 사용하기 위한 토큰을 생성해야 한다.
1. gitlab.com 프로젝트 메뉴의 settings > repository > Deploy tokens 의 'expand' 버튼 클릭
2. Scopes는 최소 read_registry를 선택한다.
3. 토큰 생성
발급된 토큰은 서버에서 로그인할 때 사용된다.
서버에서 gitlab.com에 로그인
$ echo 토큰값 | docker login registry.gitlab.com -u docker-ci --password-stdin
$ docker image pull registry.gitlab.com/이미지명:태그
$ docker service create \
--name myapp \
--publish 8080:8080 \
--replicas=2 \
registry.gitlab.com/이미지명:태그
--replicas=2
나중에 롤링 업데이트를 하기 위해 2개의 컨테이너를 띄운다.
1로 지정 후 업데이트가 필요할 때 1개의 컨테이너를 추가할 수도 있다.
버전 업데이트가 필요한 시점에 docker-ci 브랜치에 변경사항을 병합하고 push해서 새 docker image가 빌드되기를 기다린다.
3~5분...
새로운 이미지가 빌드되었으면 내려받는다.
$ docker image pull registry.gitlab.com/이미지명:새로운태그
현재 2개의 컨테이너가 생성되어 있으며,
아래 명령어를 수행하면 1개씩 롤링 업데이트를 수행한다.
$ docker service update -d --force --image registry.gitlab.com/이미지명:새로운태그 myapp
첫번째 컨테이너를 새 버전으로 재생성하고 healthcheck를 통해 성공적으로 컨테이너가 실행되었을 때
두번째 컨테이너의 업데이트를 시작한다.
물론 docker service 설정으로 업데이트 규칙을 변경할 수도 있다.
동일한 Docker Image Tag를 사용하여 업데이트하면 이전 버전으로 롤백할 수 없다.
최초 생성할 때 --replicas=2 를 지정하여 2개의 컨테이너를 생성했다.
1개의 컨테이너를 추가하여 총 3개로 서비스를 하고 싶다면 하면 된다.
컨테이너 수를 줄이는 것도 당연히 가능.
$ docker service update --replicas=3 myapp
아름답다.
gitlab ci에 의해 docker image가 빌드되는건 너무 편한데, 좀 느려.
느리다보니 빌드가 다 끝났는지 더 기다려야 하는지 서버에서 언제쯤 이미지를 내려받을 수 있을지 모르는 것도 문제.
주기적으로 사이트 새로고침을???
gitlab.com의 프로젝트 메뉴의 settings > Integrations > Slack notifications
을 선택하고
Pipeline 옵션만 체크. 나머지 옵션은 필요하면 선택.
Webhook 에 알람 받을 슬랙 웹훅 url 설정
Username 알람 수신 시 보낸 사람 이름
Branches for which notifications are to be sent All Branches 선택
어차피 docker-ci 브랜치만 pipeline을 사용하고 있기 때문에 All Branches를 해도 무관하다.
저장 후 docker-ci에 또 한번 push를 하면 빌드가 시작되고 빌드가 종료되면 슬랙으로 알림이 온다.
Blue-Green deployments?
여지껏 운영했던 서비스들이 1~2대의 서버로 운영이 되었기 때문에
App1이 현재 v1.0을 서비스 중이고, v2.0을 올려야할 때.
1. App2에 v2.0 배포
2. nginx에서 App2의 포트를 지정
3. nginx 재시작하여 App2로 트래픽을 보냄
문제가 생기면 롤백
1. nginx에서 App1의 포트를 지정
2. nginx에서 재시작하여 App1로 트래픽을 보냄
가장 명확하고 쉬운 방법인데 nginx를 재시작하는 것이 귀찮다.
설정을 바꾸는 것도 귀찮다. 서버에서 vi로 편집해야 하는데, 오타나면 좀 그래.
스크립트를 만들면 좀 덜 귀찮겠지만 App3이 추가된다면?