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-cliaws-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 채널에 빌드 및 배포 성공을 알리는 메세지가 출력됩니다.