CircleCI를 활용한 코드 통합, 빌드, 배포 파이프라인 구성하기

Overview
CircleCI는 소스코드를 통합하고 빌드, 배포를 자동화하는 파이프라인을 구성하기 위한 툴입니다.
이 문서에서는 아래와 같은 서비스를 통합하여 CI/CD 환경을 구성해보겠습니다.
- 형상 관리: Github
- 빌드, 배포, 파이프라인: Circle CI
- 이미지 저장소: AWS ECR(Elastic Container Registry)
- 애플리케이션 서버: AWS ECS(Elastic Container Service) Fargate
- 알림: Slack
사전 준비
샘플 코드 준비하기
이 문서에서는 circleci-demo 프로젝트를 활용하겠습니다.
위 프로젝트를 자신의 Github 계정으로 fork합니다.
AWS ECR(Elastic Container Registry) 구성
AWS에서 CI/CD 환경 구성 #3 - 코드 빌드, CodeBuild
문서의 사전 준비 > ECR(Elastic Container Registry) 생성
섹션을 참고하여 ECR을 생성하겠습니다.
참고
ECR 이름은 `circleci-demo`로 생성하겠습니다.
ECR에 샘플 코드 이미지 저장
위에서 fork한 프로젝트를 로컬로 복제합니다.
$ git clone REPOSITORY_URL
$ cd circleci-demo
Maven 빌드 명령을 실행해 jar 파일을 생성합니다.
$ mvn clean package
Docker 빌드 명령을 실행해 Docker 이미지를 생성합니다. AWS_ACCOUNT_ID, AWS_DEFAULT_REGION는 자신의 AWS 환경 정보를 입력합니다.
$ export ECR_REPOSITORY_NAME="circleci-demo"
$ export AWS_ACCOUNT_ID="xxx"
$ export AWS_DEFAULT_REGION="xxx"
$ export FULL_IMAGE_NAME="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${ECR_REPOSITORY_NAME}:latest"
$ docker build -t $FULL_IMAGE_NAME .
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
xxx.dkr.ecr.xxx.amazonaws.com/circleci-demo latest d692168175e6 2 minutes ago 122MB
ECR에 로그인하고 Docker 이미지를 ECR에 저장합니다.
$ eval $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
$ docker push $FULL_IMAGE_NAME
ECR Console에 접속해서 circleci-demo 저장소로 이동해 위에서 저장한 이미지의 주소를 복사해둡니다. 이 주소는 아래 ECS Task 구성시 사용하겠습니다.
xxx.dkr.ecr.xxx.amazonaws.com/circleci-demo:latest
AWS ECS Cluster 구성
AWS ECS 구성 및 활용하기 #1 - 사전 준비 및 클러스터 생성하기
문서의 ECS Cluster 생성
섹션을 참고하여 클러스터를 생성하겠습니다.
참고
클러스터 이름은 `circleci-demo`로 생성하겠습니다.
AWS ECS Task 구성
AWS ECS 구성 및 활용하기 #2 - 작업 정의 구성하기(ECS Task) 문서를 참고하여 작업 정의를 생성하겠습니다.
참고
작업 정의 이름은 `circleci-demo`로 생성하겠습니다.
컨테이너 이미지 주소는 `ECR에 샘플 코드 이미지 저장` 섹션에서 복사해둔 주소를 입력하겠습니다.
AWS ECS Service 구성
AWS ECS 구성 및 활용하기 #3 - 서비스(ECS Service) 구성하기 문서를 참고하여 ECS 서비스를 구성하겠습니다.
참고
서비스 이름은 `circleci-demo`로 생성하겠습니다.
CircleCI에 빌드 및 배포 구성하기
CircleCI에 접속하기
-
CircleCI 콘솔에 접속합니다.
-
화면 상단 오른쪽의
Log In
버튼을 선택합니다. -
Log In with GitHub
버튼을 선택해 자신의 Github 계정으로 로그인하겠습니다.
Github 프로젝트 연동하기
- CircleCI Console에 접속 > 왼쪽 메뉴의 Add Project 선택
- Project 리스트 >
circleci-demo
>Set Up Project
버튼 선택
프로젝트 설정하기
-
Settings > Organization > Projects > Followed projects >
circleci-demo
> 설정 버튼(톱니바퀴 아이콘) 선택 -
Permissions > AWS Permissions > Access Key ID, Secret Access Key 입력
-
Permissions > Build Settings > Environment Variables > 아래 환경 변수 입력
이름 값 AWS_ACCOUNT_ID 계정 아이디 AWS_DEFAULT_REGION 리전 이름 AWS_RESOURCE_NAME_PREFIX circleci-demo
빌드 및 배포 설정하기
-
circleci-demo
프로젝트 root에.circleci
디렉토리를 생성합니다.$ mkdir .circleci
-
.circleci
디렉토리 하위에config.yml
파일을 생성하고 단계별로 설정해보겠습니다.$ vi .circleci/config.yml
먼저 Version을 설정합니다.
version: 2.1 ...
다음으로 Orbs를 설정합니다. Orbs는 CircleCI 플랫폼을 빠르게 사용할 수 있도록 도와주는 패키지입니다.
aws-cli
와aws-ecs
패키지를 설정하겠습니다.... orbs: aws-cli: circleci/aws-cli@0.1.4 aws-ecs: circleci/aws-ecs@0.0.3
working directory를 Home의 circleci-demo 디렉토리로 설정합니다.
... jobs: build: ... working_directory: ~/circleci-demo
빌드 서버로 사용할 컨테이너의 base image를 설정합니다.
... jobs: build: ... docker: - image: circleci/openjdk:8-jdk-browsers
소스코드를 체크아웃하겠습니다.
... jobs: build: ... steps: - checkout
Remote에 있는 Docker 데몬을 사용하도록 설정하겠습니다. 빌드 서버로 사용할 컨테이너에는 Docker 클라이언트 툴만 설치하고 실제 빌드는 Remote에 있는 Docker 데몬을 사용합나디.
... jobs: build: ... steps: ... - setup_remote_docker
저장된 프로젝트 의존성 라이브러리의 캐시가 있는 경우 복구합니다.
... jobs: build: ... steps: ... - restore_cache: key: circleci-demo-
프로젝트 의존성 라이브러리를 다운로드합니다.
... jobs: build: ... steps: ... - run: mvn dependency:go-offline
다운로드한 프로젝트 의존성 라이브러리를 캐싱합니다.
... jobs: build: ... steps: ... - save_cache: paths: - ~/.m2 key: circleci-demo-
소스코드를 Maven 빌드해 JAR 파일을 생성합니다.
... jobs: build: ... steps: ... - run: mvn package
빌드 테스트 결과를 저장할 경로를 지정합니다.
... jobs: build: ... steps: ... - store_test_results: path: target/surefire-reports
빌드 결과물인 JAR 파일의 저장 경로를 설정합니다.
... jobs: build: ... steps: ... - store_artifacts: path: target/circleci-demo-0.0.1-SNAPSHOT.jar
ECR의 이미지 경로를 환경 변수로 설정합니다.
... jobs: build: ... steps: ... - run: name: Setup common environment variables command: | echo 'export ECR_REPOSITORY_NAME="${AWS_RESOURCE_NAME_PREFIX}"' >> $BASH_ENV echo 'export FULL_IMAGE_NAME="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${ECR_REPOSITORY_NAME}:${CIRCLE_SHA1}"' >> $BASH_ENV
빌드한 JAR 파일을 구동할 Docker 이미지를 빌드합니다.
... jobs: build: ... steps: ... - run: name: Build image command: | docker build -t $FULL_IMAGE_NAME .
빌드한 Docker 이미지가 정상 동작하는지 테스트합니다.
... jobs: build: ... steps: ... - run: name: Test image command: | docker run -d -p 8080:8080 --name built-image $FULL_IMAGE_NAME sleep 10 docker run --network container:built-image appropriate/curl --retry 10 --retry-connrefused http://localhost:8080 | grep "Hello World"
빌드한 Docker 이미지를 아카이브(TAR) 파일로 저장합니다.
... jobs: build: ... steps: ... - run: name: Save image to an archive command: | mkdir docker-image docker save -o docker-image/image.tar $FULL_IMAGE_NAME
Workflow의 다음 단계에서 사용할 임시 파일을 영구적으로 저장하기 위한 설정을합니다.
... jobs: build: ... steps: ... - persist_to_workspace: root: . paths: - docker-image
다음으로 배포(deploy) 설정을 하겠습니다. 배포 서버로 사용할 컨테이너 이미지를 설정합니다.
... jobs: build: ... deploy: docker: - image: circleci/python:3.6.1
AWS 클라이언트 툴 사용 시 결과 출력의 포맷을 JSON으로 설정합니다.
... jobs: build: ... deploy: ... environment: AWS_DEFAULT_OUTPUT: json
소스코드를 체크아웃하겠습니다.
... jobs: build: ... deploy: ... steps: - checkout
Remote에 있는 Docker 데몬을 사용하도록 설정하겠습니다.
... jobs: build: ... deploy: ... steps: ... - setup_remote_docker
워크플로우의 workspace를 배포 서버로 사용하는 컨테이너에 연결합니다.
... jobs: build: ... deploy: ... steps: ... - attach_workspace: at: workspace
컨테이너에 AWS CLI(Command Line Interface)를 설치합니다.
... jobs: build: ... deploy: ... steps: ... - aws-cli/install
AWS CLI를 사용하기 위해 Access key와 Region 정보를 설정합니다.
... jobs: build: ... deploy: ... steps: ... - aws-cli/configure: aws-access-key-id: "$AWS_ACCESS_KEY_ID" aws-region: "$AWS_DEFAULT_REGION"
빌드 과정에서 저장한 Docker 이미지를 로드합니다.
... jobs: build: ... deploy: ... steps: ... - run: name: Load image command: | docker load --input workspace/docker-image/image.tar
ECS 및 이미지 환경 변수를 설정합니다.
AWS_RESOURCE_NAME_PREFIX
와 같은 변수는 앞의 프로젝트 설정에서 저장한 값이 주입됩니다.... jobs: build: ... deploy: ... steps: ... - run: name: Setup common environment variables command: | echo 'export ECS_CLUSTER_NAME="${AWS_RESOURCE_NAME_PREFIX}"' >> $BASH_ENV echo 'export ECS_SERVICE_NAME="${AWS_RESOURCE_NAME_PREFIX}"' >> $BASH_ENV echo 'export FULL_IMAGE_NAME="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${AWS_RESOURCE_NAME_PREFIX}:${CIRCLE_SHA1}"' >> $BASH_ENV
ECR에 로그인하고 Docker 이미지를 push합니다.
... jobs: build: ... deploy: ... steps: ... - run: name: Push image command: | eval $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email) docker push $FULL_IMAGE_NAME
ECS 서비스를 업데이트합니다. 기존 이미지를 새로 빌드한 Docker 이미지로 빌드합니다.
... jobs: build: ... deploy: ... steps: ... - aws-ecs/update-service: family: "${ECS_SERVICE_NAME}" cluster-name: "${ECS_CLUSTER_NAME}" container-image-name-updates: "container=${ECS_SERVICE_NAME},image-and-tag=${FULL_IMAGE_NAME}" verify-revision-is-deployed: true
배포한 ECS 서비스가 정상 동작하는지 테스트합니다.
... jobs: build: ... deploy: ... steps: ... - run: name: Test deployment (Please manually tear down AWS resources after use, if desired) command: | TARGET_GROUP_ARN=$(aws ecs describe-services --cluster $ECS_CLUSTER_NAME --services $ECS_SERVICE_NAME | jq -r '.services[0].loadBalancers[0].targetGroupArn') ELB_ARN=$(aws elbv2 describe-target-groups --target-group-arns $TARGET_GROUP_ARN | jq -r '.TargetGroups[0].LoadBalancerArns[0]') ELB_DNS_NAME=$(aws elbv2 describe-load-balancers --load-balancer-arns $ELB_ARN | jq -r '.LoadBalancers[0].DNSName') for attempt in {1..50}; do curl -s --retry 10 http://$ELB_DNS_NAME | grep -E "Hello World" done
workflows를 설정합니다. 앞선 과정의 build 및 deploy를 순차적으로 실행합니다. filters를 설정해 Github의 master 브랜치에 변경이 있는 경우에만 빌드를 하도록합니다.
... jobs: build: ... deploy: ... workflows: version: 2 build-deploy: jobs: - build: filters: branches: only: master - deploy: requires: - build filters: branches: only: master
전체 설정 코드
version: 2.1
orbs:
aws-cli: circleci/aws-cli@0.1.4
aws-ecs: circleci/aws-ecs@0.0.3
jobs:
build:
working_directory: ~/circleci-demo
docker:
- image: circleci/openjdk:8-jdk-browsers
steps:
- checkout
- setup_remote_docker
- restore_cache:
key: circleci-demo-
- run: mvn dependency:go-offline
- save_cache:
paths:
- ~/.m2
key: circleci-demo-
- run: mvn package
- store_test_results:
path: target/surefire-reports
- store_artifacts:
path: target/circleci-demo-0.0.1-SNAPSHOT.jar
- run:
name: Setup common environment variables
command: |
echo 'export ECR_REPOSITORY_NAME="${AWS_RESOURCE_NAME_PREFIX}"' >> $BASH_ENV
echo 'export FULL_IMAGE_NAME="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${ECR_REPOSITORY_NAME}:${CIRCLE_SHA1}"' >> $BASH_ENV
- run:
name: Build image
command: |
docker build -t $FULL_IMAGE_NAME .
- run:
name: Test image
command: |
docker run -d -p 8080:8080 --name built-image $FULL_IMAGE_NAME
sleep 10
docker run --network container:built-image appropriate/curl --retry 10 --retry-connrefused http://localhost:8080 | grep "Hello World"
- run:
name: Save image to an archive
command: |
mkdir docker-image
docker save -o docker-image/image.tar $FULL_IMAGE_NAME
- persist_to_workspace:
root: .
paths:
- docker-image
deploy:
docker:
- image: circleci/python:3.6.1
environment:
AWS_DEFAULT_OUTPUT: json
steps:
- checkout
- setup_remote_docker
- attach_workspace:
at: workspace
- aws-cli/install
- aws-cli/configure:
aws-access-key-id: "$AWS_ACCESS_KEY_ID"
aws-region: "$AWS_DEFAULT_REGION"
- run:
name: Load image
command: |
docker load --input workspace/docker-image/image.tar
- run:
name: Setup common environment variables
command: |
echo 'export ECS_CLUSTER_NAME="${AWS_RESOURCE_NAME_PREFIX}"' >> $BASH_ENV
echo 'export ECS_SERVICE_NAME="${AWS_RESOURCE_NAME_PREFIX}"' >> $BASH_ENV
echo 'export FULL_IMAGE_NAME="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${AWS_RESOURCE_NAME_PREFIX}:${CIRCLE_SHA1}"' >> $BASH_ENV
- run:
name: Push image
command: |
eval $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
docker push $FULL_IMAGE_NAME
- aws-ecs/update-service:
family: "${ECS_SERVICE_NAME}"
cluster-name: "${ECS_CLUSTER_NAME}"
container-image-name-updates: "container=${ECS_SERVICE_NAME},image-and-tag=${FULL_IMAGE_NAME}"
verify-revision-is-deployed: true
- run:
name: Test deployment (Please manually tear down AWS resources after use, if desired)
command: |
TARGET_GROUP_ARN=$(aws ecs describe-services --cluster $ECS_CLUSTER_NAME --services $ECS_SERVICE_NAME | jq -r '.services[0].loadBalancers[0].targetGroupArn')
ELB_ARN=$(aws elbv2 describe-target-groups --target-group-arns $TARGET_GROUP_ARN | jq -r '.TargetGroups[0].LoadBalancerArns[0]')
ELB_DNS_NAME=$(aws elbv2 describe-load-balancers --load-balancer-arns $ELB_ARN | jq -r '.LoadBalancers[0].DNSName')
for attempt in {1..50}; do
curl -s --retry 10 http://$ELB_DNS_NAME | grep -E "Hello World"
done
workflows:
version: 2
build-deploy:
jobs:
- build:
filters:
branches:
only: master
- deploy:
requires:
- build
filters:
branches:
only: master
변경사항 반영
변경사항을 Github에 반영합니다.
$ git add --all
$ git commit -m "Updated circleci configuration"
$ git push
빌드 및 배포 상태 확인
Github에 변경사항이 발생하면 CircleCI에서 이를 감지하여 빌드 및 배포를 수행합니다.
CicleCI 콘솔에서 Workflows 메뉴를 선택하면 빌드 및 배포 상태를 확인할 수 있습니다.
빌드 또는 배포 상태를 선택하면 해당 Job의 상세 정보를 확인할 수 있습니다.
CicleCI 콘솔에서 Jobs 메뉴를 선택하면 실행 또는 완료된 Job의 목록을 확인할 수 있습니다.
목록에서 Job을 선택하면 상세정보를 확인할 수 있습니다.
Deploy job의 마지막 단계인 Test deployment 로그를 확인해보면 ECS 서비스가 정상적으로
배포되었는지 테스트하는 로그를 확인할 수 있습니다.
Hello World(Version 1)
메세지가 계속 출력되면 배포가 정상적으로 완료된것입니다.
Slack 연동
다음으로 CircleCI를 Slack Chat 서비스와 연동하여 빌드 및 배포 성공/실패에 대한 알람을 받을 수 있도록 설정해보겠습니다.
-
Slack MyApp 페이지에 접속합니다.
-
Create New App 버튼을 선택합니다.
-
App Name에 circleci-demo를 입력하고 Slack workspace를 선택한뒤 Create App 버튼은 선택합니다.
-
Slack 메신저에서 circleci-demo 채널을 생성합니다.
-
다시 Slack MyApp 페이지로 돌아와서 생성한 circleci-demo 앱을 선택합니다.
-
Features > Incoming Webhooks > Activate Incoming Webhooks를 On으로 변경합니다.
-
하단의 Webhook URL의 Add New Webhook to Workspace 버튼을 선택합니다.
-
Post to에서 circleci-demo 채널을 선택하고 Install 버튼을 선택합니다.
-
생성된 Webhook URL을 복사해둡니다.
-
다시 CircleCI 콘솔로 이동해서 프로젝트 설정 화면으로 이동합니다.
-
Notifications > Chat Notifications > Webhook URL에 복사해둔 URL을 붙여넣고 저장합니다.
-
이제 소스코드를 수정해서 빌드 및 배포를 다시 실행해보겠습니다. 아래 파일에서 Version을 2로 변경하고 Commit & Push합니다.
<circleci-demo/src/main/java/com/example/demo/HomeRestController.java> ... @RequestMapping(value = "/", method = RequestMethod.GET) public String getHome() { return "Hello World(Version 2)"; } ...
-
CircleCI에서 소스 변경을 감지하고 빌드 및 배포 Job을 실행합니다. 완료 되면 아래와 같이 Slack 채널에 빌드 및 배포 성공을 알리는 메세지가 출력됩니다.