CI & CD 전략 선택하기 ( 부제 : CodePipeline 사용기 )

영슈·2024년 7월 27일
1

더 나은 개발자 되기

목록 보기
10/21
post-thumbnail

이번 내용은 우테코 프로젝트 내에서 배포를 진행하며 알게 된 인프라 및 구현 & 사용 방법에 대해 작성하려고 한다.
joyson5582@gmail.com 이나 댓글로 궁금함이나 의견을 나타내면 최선을 다해 설명하겠습니다.

일반적으로 코드 배포를 위해선 다양한 방법들이 존재한다.
이 다양한 방법들에 대해 소개하기에 앞서 CI & CD 가 무엇인지 간략하게 그리고 흐름이 어떻게 동작하는지에 대해서 알아보자.

CI

  • Continuous Integration ( 지속적 통합 )
  • 새로운 코드 변경 사항을 빌드 및 테스트하여 기존 코드에 통합시키는 것이다.

CD

  • Continuous Delivery or Deploy ( 지속적 배달 or 배포 )
  • 변경된 코드를 기반으로 자동으로 코드를 새로 배포한다.

매우 명확하고 간단한 설명이다.
그러면, 이들의 흐름은 어떻게 될까?

흐름

  1. 코드 변경을 감지해서 CI 가 동작한다.
  2. CI 가 통과하면 기존 코드에 통합이 되고, 통합된 코드를 기반으로 빌드를 한다.
  3. 빌드된 결과물을 저장한다.
  4. 빌드된 결과물을 기반으로 CD 가 동작한다.
  5. CD에 명세된 대로 서버를 동작시킨다.

단순히 순서만 보면 헷갈릴 수 있는데 말로 풀어쓰면 명확하다.

변경된 코드가 괜찮은가?
-> 코드를 통합하고 빌드한다 ( CI )
빌드가 성공적으로 됐는가?
-> 빌드 파일로 서버를 배포한다 ( CD )

아래에 있는 모든 방법들은 해당 틀에서 벗어나지 않는다.

그러면 다양한 방법들에 대해 보겠다.

CI

Docker Hub

# Build Gradle
      - name: Build Gradle
        run: |
          chmod +x ./gradlew
          ./gradlew test
        shell: bash

# Docker 이미지 build 및 push
      - name: docker build and push
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -t corea/backend:latest ./backend
          docker push corea/backend:latest
          docker push corea/backend:latest

흐름에서 2번과 3번이다.
매우 간단하다.

Docker hub 에 로그인 -> 빌드된 결과를 기반으로 이미지 빌드 -> 이미지 Push

  • 장점 : 도커가 깔려있다면, 서버도 다른 프로그램 설치 필요 없이 의도대로 동작한다. ( 자바 설치, DB 설치 등등 )

Github Action + ZIP

# Build Gradle
      - name: Build Gradle
        run: |
          chmod +x ./gradlew
          ./gradlew test
        shell: bash

# 빌드 파일 압축
      - name: Create deploy package
        run: |
          mkdir -p scripts
          zip -r deployment-package.zip build/libs/corea-backend.jar

이 역시도 CI의 과정이라 할 수 있다.
빌드된 결과물을 압축해서 Github Action에서 저장하므로 2번과 3번이라 할 수 있다.

CodeBuild

AWS 인프라가 제공해주는 빌드 시스템이다.
나는 이 CodeBuild 를 사용했고, 자세히 설명하겠다.

450

Github Repository 내 소스 버전(브랜치)를 기반으로 빌드를 한다.

  • Git clone 깊이 : 이력을 어디까지 받을지 지정
    -> 빌드에는 전체 이력( Commit Log ) 이 필요없다. 일반적으로 1이면 충분 ( 빌드 내에서 5개 전 커밋 이력이 필요한가? )
  • Git 하위 모듈 : SubModule 을 포함시킬 지 여부이다.
    나머지는 당장 중요하지 않다고 생각

450

빌드 하는 컴퓨터에 대해서 정의하는 부분이다.
운영체제, 이미지를 선택 ( 이미지의 버전 선택 ) 을 선택한다.

  • 추가 구성으로, 컴퓨팅의 사양을 올릴수도 있다.

빌드용으로 만든 도커 이미지가 있다면 그것 역시 사용 가능하다. ( Docker Hub 에 올린 Image )

소스 코드 내 buildspec.yml 에 있는 내용을 기반으로 파일을 빌드해준다. ( AWS CodeBuild 내부에서도 관리 가능 )

version: 0.2  
  
phases:  
  build:  
    commands:  
      - echo Build Starting on `date`  
      - cd backend  
      - chmod +x ./gradlew  
      - ./gradlew build  
  
cache:  
  paths:  
    - '/root/.gradle/caches/**/*'  
  
artifacts:  
  files:  
    - 'appspec.yml'  
    - 'build/libs/backend-0.0.1-SNAPSHOT.jar'  
    - 'scripts/**'  
  base-directory: backend

build 할 때 commands 를 지정한다.
(phases 에는 install 이나 pre_build 같은것도 존재 하나 당장 불필요 )

root/.gradle/cahces 에 있는 모든 경로&파일들을 캐시로 저장
-> 다음 빌드는 이전 빌드에 저장된 캐시를 복원한다.

artifacts-files 를 통해 빌드할 파일들을 포함한다. ( base-directort 를 통해 기본 디렉토리 )

위에 지정한 파일들을 어디에 저장할지 지정한다.
2024-artifacts 버킷 내 2024-corea/corea-backend-deployment 파일명으로 압축으로 저장이 된다.
( 폴더로 저장해도 상관없으나 차후 Code Deploy 시 압축 파일이 필요로 한다. )

450

빌드 출력 로그는 왠만하면 사용하자.
업로드 안하면 빌드 진행 중이나 실패한 이유를 볼 수가 없다.

결과

450

결과를 출력해주고

450

보고서도 보여준다.

CD

Self Hosted Runner

deploy:
    runs-on: self-hosted
    steps:
      - name: Pull Docker image from DockerHub
        run: docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_IMAGE_NAME }}:${{ github.sha }}
        
      - name: Run Docker container
        run: docker run -d --name backend-application -p 8080:8080 ${{ secrets.DOCKERHUB_USERNAME }}/corea/backend:latest
        

이렇게 하면?
Docker Image 를 Pull 받고 서버에서 배포를 실행을 한다.
서버에 대한 명시가 없는 이유는 self-hosted runner 스스로가 명령어를 실행하기 때문이다.

450

EC2에 직접 접속해서 해당 명령어를 실행하면 인스턴스가 직접 깃허브 액션을 수행한다.

이떄의 단점이라면?

  • 결국 주기적으로 통신해 새로운 작업이 있는지 확인한다 - Polling 방식
  • HTTPS 를 통해 통신, 443 포트 이용해 작업
  • 결국 자신이 직접 run 시키므로, 리소스 낭비가 될 수 있다.
    등이 있다.

우테코 프로젝트 사람들은 대부분 self-hosted runner 를 사용했는데
AWS Secret Key 가 없어서 Credentials 를 통해 할 수 없어서 self-hosted runner 로 이렇게 동작을 시켰다.

사실, 해당 부분에서
Docker 가 필수적이라고 생각할 수 있는데 빌드된 파일을 EC2 내부까지 옮길수만 있다면 크게 상관 없다. ( S3 등등 )

ssh-action

...

	uses: appleboy/ssh-action@master
	with:
	  host: ${{ secrets.REMOTE_IP }}
	  username: ${{ secrets.REMOTE_SSH_ID }}
	  key: ${{ secrets.REMOTE_SSH_KEY }}
	  port: ${{ secrets.REMOTE_SSH_PORT }}
	  script: |
			...
			docker run -d --name backend-application -p 8080:8080 ${{ secrets.DOCKERHUB_USERNAME }}/corea/backend:latest
			

서버에 SSH 연결을 통해 명시한 스크립트를 실행해서 배포한다.
self-hosted runner 와 비슷하다고 생각할 수 있으나

  • ssh-action : 우리가 직접 접속해서 실행
  • self-hosted runner : 변화를 Polling 해서 서버가 자체적으로 실행

의 차이가 있다.

우테코 프로젝트는 SSH 연결이 막혀있어서 해당 방식 불가능

위와 똑같이 Docker 가 중요한게 아닌, 서버 접속해 직접 실행한다는게 중요하다

AWS Credentials + S3 + Code Deploy

...
	- name: Configure AWS credentials
	uses: aws-actions/configure-aws-credentials@v4
	with:
	  aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
	  aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
	  aws-region: ${{ secrets.AWS_REGION }}
	
	- name: Upload to S3
	run: |
	  aws s3 cp deployment-package.zip s3://${{ secrets.S3_BUCKET_NAME }}/deployment-package.zip
	
	- name: Deploy to CodeDeploy
	run: |
	  aws deploy create-deployment \
		--application-name ${{ secrets.APPLICATION_NAME }} \
		--deployment-group-name ${{ secrets.DEPLOYMENT_GROUP_NAME }} \
		--s3-location bucket=${{ secrets.S3_BUCKET_NAME }},key=deployment-package.zip,bundleType=zip

아마존 credentials 로 직접 인증 받아서
빌드 업로드 + 업로드 파일 기반 배포를 진행한다.

우테코 프로젝트는 Secret Key를 발급 못 받아서 인증 불가능

Code Deploy

AWS 인프라가 제공해주는 배포 시스템이다.
Build 와 똑같이 이를 사용했으니, 자세히 설명한다.

애플리케이션에서 본인이 배포할 컴퓨팅 플랫폼에 대해서 정한다. ( EC2/온프레미스, AWS Lambda, ECS )
그후, 배포 그룹을 생성한다. - 이때 그룹에 따라서 개발용 / 운영용 등 분리 가능

450

현재 배포 전략에 대해선 중요하지 않고, Auto Scaling / Load Balancer 역시 중요하지 않다.
-> 개발 서버 온전히 구동만 하면 OK

태그의 Key 값을 통해 인스턴스를 명시한다.

450

이때 에이전트를 구성할거냐고 묻는데 몹시몹시 중요하다!!
중요한 이유는 배포 생성까지 끝내고 설명한다.

350

그룹을 생성하면 배포를 생성한다.
배포 그룹 지정 + 저장된 S3 파일을 지정한다.

에이전트가 설치되어 있지 않으면 EC2 는 파일 전송을 받지 못하고

이렇게 not able to receive the lifecycle event. 를 발생시킨다.
해당 에러가 뜨면 Code Deploy Agent 가 작동하는지 확인해보자.

서비스 동작 확인

sudo service codedeploy-agent status

동작했는데 아예 없다면?

#!/usr/bin/env bash

sudo apt-get update -y
sudo apt-get install -y ruby
cd /home/ubuntu
wget https://aws-codedeploy-ap-northeast-2.s3.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto

를 통해 설치하자.

서비스 동작 권한 부족

설치하고 배포를 재 실행했는데 위와 같이 실패가 또 뜬다면?

cat /var/log/aws/codedeploy-agent/codedeploy-agent.log
를 통해 로그를 보자.

InstanceAgent::Plugins::CodeDeployPlugin::CommandPoller: Missing credentials
- please check if this instance was started with an IAM instance profile

해당 로그가 뜨면 EC2 IAM 이 CodeDeploy 를 수행할 권한이 있는지 확인하자.
-> 권한을 부여했는데 안된다면?

sudo service codedeploy-agent restart

로 재시작후 다시 확인해보자

400

이렇게 성공하면 끝이다.

version: 0.0  
os: linux  
files:  
  - source: /build/libs  
    destination: /home/ubuntu/build  
  
  - source: /scripts  
    destination: /home/ubuntu/scripts  
  
  - source: appspec.yml  
    destination: /home/ubuntu/build  
  
  
permissions:  
  - object: /  
    pattern: "**"  
    owner: ubuntu  
  
hooks:  
  ApplicationStart:  
    - location: scripts/deploy.sh  
      timeout: 60  
      runas: ubuntu  
        
  ValidateService:  
    - location: scripts/healthCheck.sh  
      timeout: 30  
      runas: ubuntu

CodeBuild 와 유사하게 appspec.yml 에 있는 내용을 기반으로 서버를 배포한다.

파일을 저장시킬 곳을 지정하고, 파일들에 권한을 주고
각 hooks 마다 실행할 스크립트들을 지정한다.

이때 EC2 에는 우리가 직접 자바 등을 설치해줘야 한다.

1. 자바 설치
wget https://download.oracle.com/java/17/archive/jdk-17.0.11_linux-aarch64_bin.tar.gz -O /tmp/openjdk-17_linux-x64_bin.tar.gz

2. 압축 해제
sudo tar xfvz /tmp/openjdk-17_linux-x64_bin.tar.gz --directory /usr/lib/jvm

3. 자바 압축 파일 삭제

rm -f /tmp/openjdk-17_linux-x64_bin.tar.gz

sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk-17.0.11/bin/java 100

4. 자바 설정
sudo update-alternatives --set java /usr/lib/jvm/jdk-17.0.11/bin/java

5. 자바 환경 변수 설정

export JAVA_HOME=/usr/lib/jvm/jdk-17.0.11
export PATH=$PATH:/usr/lib/jvm/jdk-17.0.11/bin

서버를 8080에 연다면?

sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080

IP 포트포워딩도 진행하자

이렇게 CodeDeploy 를 활용해 배포가 끝이났다.

Code Pipeline

코드 파이프라인은 CodeBuild 와 CodeDeploy 를 합쳐놓은 것으로 정말 무중단 배포가 가능하게 해주는 인프라다.

파이프라인 설정

400

대기를 할 시, 코드 커밋이 계속 일어날 시 차례대로 배포를 진행한다. ( 1.0.0 진행 -> 1.0.1 진행 )
추가로, 밑에 고급 설정에서 아티팩트 스토어에서 기존 버킷을 지정할 수 있다. ( 기본 위치시 자동으로 버킷 생성 )

소스 스테이지

400

WebHook 을 통해 Github 가 자신의 변경을 AWS CodePipeline 에게 알려준다.
-> 이를 기반으로 파이프라인이 동작한다.

빌드 스테이지

400

생성된 빌드를 재사용 가능하다.

배포 스테이지

400

역시 배포 & 배포 그룹 재사용 가능하다

400

이때 세부 정보들을 보면

Source 는 출력으로 SourceArtifact ->
Build 는 입력으로 SourceArtifact -> BuildArtfiact
Deploy 는 입력으로 BuildArtifact 가 들어오고 배포를 하며 끝이난다.

이때 버킷을 들어가보면?

450

이렇게 스스로 폴더 + Build/SourceArtifact 를 만든다.
( 내부에도, 소스 아티팩트와 빌드된 결과물 존재 )

=> 이 결과 우리는 소스를 Github 에 Push 하면 배포가 자동으로 이루어진다.
( Github Action 에 의존적이지 않음 )

마무리

이 밖에도 수많은 CI 방법과 CD 방법이 존재할 것이다.

  • 각자의 팀에 적용할 수 있는지
  • AWS 인프라에 의존을 해도 상관이 없는지
  • 빌드된 결과물을 어떻게 관리할지
  • 서버에 배포를 얼마나 편리하게 할지 ( 로드 밸런싱, 오토 스케일링, 블루-그린 전략 )

이런 내용들을 생각하며 각자 팀에 맞는 CI / CD 를 수립하는게 좋은거 같다.

우리 팀은 AWS SecretKey 가 없으나 AWS 인프라를 사용해야 하는 점 + Self-Hosted Runner 를 사용하기 싫은점 때문에
AWS 에 완벽하게 의존을 하는 CodePipeline 을 사용하기로 했다.

이때, 배포가 성공/실패 했는지 또는 배포된 주소로 바로 들어가기 위해선
AWS Chatbot 이나 AWS CloudWatch + AWS Lambda + Slack Webhook 을 통해서 가능하다고 하다.
이는 차차 도입해볼 예정

그리고, CI-CD 가 무조건 하나로 이어질 필요는 없다.

내가 CodeBuild 를 굳이 사용할 필요가 없다고 판단해
Github Action 에서 직접 빌드하고 파일을 전달하는 것도 가능하다.

CodeBuild는 사용하더라도 CodeDeploy 를 쓰지않고
우리가 빌드된 파일로 직접 실행해도 괜찮지 않을까?

빌드가 되었다고, 꼭 배포가 자동으로 될 필요는 없을수도 있을테니까.

https://github.com/woowacourse-teams/2024-corea 해당 프로젝트에 도입했으며
다시 한번, 부족한 지식이 포함되어 있는 글이기에 댓글은 언제나 환영한다.

참고

https://jojoldu.tistory.com/282
https://github.com/jaeyeonling/reaction-game/tree/main

0개의 댓글