본문 바로가기

AWS

AWS CodePipeLine, CodeBuild, CodeDeploy를 통해 EC2에 배포하기, AWS CI/CD 구축하기 - 1

서론

이전 글들에서 CodeDeploy를 이용해 S3Github의 프로젝트를 EC2 Instance에 배포하는 과정을 다루었습니다. 여태까지는 직접 빌드한 파일을 S3에 업로드하거나, git commit ID배포생성을 통해 수동으로 전달해주어야했는데요. 이번에는 단순히 CodeDeploy만 이용하는 게 아니라 CodePipeLine을 통해 이러한 과정들을 완전히 자동화시켜 CI/CD Pipeline을 구축해볼 겁니다.

 

❗❗❗ 겹치는 내용이 많고 그리 간단한 작업이 아니기 때문에, CI/CD를 처음 접하시거나 CodeDeploy에 관한 개념이 안 잡히신 분들은 앞선 글들을 먼저 읽어보시기를 추천드립니다.

 

내용은 이렇습니다.Github Repository에 푸쉬한 이력을 자동으로 체크하여 새로 푸쉬될 때마다 호출되는 Webhook을 통해 CodeBuild를 실행하여 빌드하고, 빌드가 완성된 파일(아티팩트라고 칭함)은 S3에 자동 업로드됩니다. 이후 그 업로드 된 아티팩트를 CodeDeploy에 배포하며, 이 때 실행하고 싶은 명령어를 실행시킬 수 있어요. 앞선 글들에선 단순하게 파일을 배포하기만 했는데, 이번엔 nodejs를 이용해 express 앱을 띄워보고 앱을 수정한 뒤 다시 배포해보겠습니다.

 

다룰 내용을 간단히 정리하자면

  • Github Repository에 푸쉬하면 CodeBuild가 이를 감지하고 build한다.
  • 이전 build 과정 중 node_modules 등 캐싱하면 편리한 파일들을 캐싱해놨다면, 그 파일들을 불러온다. 이후 build한 파일은 S3에 업로드 된다. 또한 마찬가지로 캐싱하면 편리한 파일들을 캐싱한다.
    S3에서 아티팩트(빌드한 파일)를 불러오기 때문에 EC2의 Role에 S3 관련 Policy를 부여하지 않으면 CodeDeploy가 동작하지 않더라.
  • 업로드된 빌드파일을 CodeDeploy를 통해 EC2에 배포한다.
  • 이 모든 과정을 CodePipeLine으로 연결한다.

간단하게 넘어가지만 한 번 더 생각해 볼 점

좀 더 깊게 이해해보고 공부해보고싶으신 분들은 아래 사항들에 대해 알아보시면 도움이 될 것 같습니다.

  • 캐시 관리
  • appspec.yml 의 hooks 에 나온 배포 라이프사이클
  • hooks 에서 이용할 환경변수 설정법

작업 순서

  1. IAM Role 생성
    EC2 Instance에 붙일 IAM Role 생성
    CodeDeploy가 사용할 Role 생성
    CodePipeLine이 사용할 Role, CodeBuild가 사용할 Role은 AWS Console에서 자동 생성되는 것 이용.
  2. EC2 Instance 만들고 설정, 환경 구축
  3. nodejs express app 생성
  4. appspec.yml을 작성하고 github repository에 추가
  5. buildspec.yml을 작성하고 github repository에 추가
  6. CodePipeline을 통해 CodeBuild를 만듦과 동시에 연결, 잠시 CodePipeLine을 나와서 CodeDeploy 애플리케이션과 그의 배포그룹을 만들고 CodePipeLine 생성 마무리
  7. 수많은 배포 실패 이후 성공!
  8. express app 버전을 업데이트 하며 CI/CD 체험!

EC2 Instance를 위한 IAM Role 생성

CodeBuild에서 빌드한 파일들인 아티팩트가 S3에 저장되고, 배포시에 S3 에서 그 아티팩트들을 불러오므로 EC2 Instance의 Role에 S3에 대한 접근 권한이 없다면, CodeDeploy가 제대로 작동하지 않습니다. 따라서 AmazonEC2RoleforAWSCodeDeploy Policy를 부여해줍니다.
EC2에 빈 Policy의 Role 부여하면 DownloadBundle에서 AccessDenied 오류가 발생합니다!

이 과정은 앞선 글(AWS CodeDeploy와 S3 이용해서 배포하기)의 EC2 Instance를 위한 IAM Role 생성과 동일하므로 자세한 설명은 이번엔 생략합니다.

CodeDeploy를 위한 IAM Role 생성

CodeDeploy가 수행할 수 있는 작업이 정의된 IAM Role을 CodeDeploy에 붙여주기 위해 IAM Role을 하나 만들어주어야합니다. 신뢰할 수 있는 개체(Trusted entities)가 CodeDeploy 인 IAM Role을 선택하면 유일하게 주어지는 AWSCodeDeployRole이라는 Policy를 갖는 Role을 생성해주면 되는데, 이 부분 또한 앞선 글(AWS CodeDeploy와 S3 이용해서 배포하기)의 CodeDeploy를 위한 IAM Role 생성과 동일하므로 자세한 내용은 생략합니다.

CodeBuild와 CodePipeLine을 위한 IAM Role 생성

이 IAM Role들은 CodePipeLine을 생성할 때 주어지는 기본적인 IAM Role로 자동생성할 것이므로 잠시 후에 다루겠습니다.

EC2 Instance를 생성하고 IAM Role을 부여한 뒤 codedeploy-agent 설치

평소 EC2 Instance를 만들듯 EC2 Instance를 한 개 만들어줍니다. 저는 Ubuntu18.04 Instance를 만들었습니다. 그리고 앞서 생성했던 EC2 Instance를 위한 Role을 EC2 Instance에 붙여줍니다. 나중에 Instance의 이름으로 CodeDeploy에서 배포 목적지 Instance를 찾아내야하므로 Instance의 이름도 설정해줍니다.

이후 CodeDeploy의 명령을 EC2가 수신하고 수행할 수 있도록 codeagent-deploy를 설치해줍니다. (codedeploy-agent를 설치하기 위해선 ruby도 필요하므로 ruby도 설치해주는 과정)

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

설치를 해줬으면 sudo service codedeploy-agent start 를 통해 codedeploy-agent를 활성화시켜줍시다.

 

❗❗❗ 만약 CodeDeploy 과정 중 오랜 시간동안 실패 결과도 나오지 않은 채 진행 중인 경우

대다수 실패 결과도 나오지 않는 경우는 2가지 중 하나입니다.

  1. 뒤에서 살펴볼 appspec.ymlhooks 부분 .sh 파일의 작업이 끝나지 않은 경우(예를 들어 express 앱을 단순하게 실행시키면 계속해서 node가 종료되지 않는 경우)

  2. codedeploy-agentEC2 에 설치하지 않았거나 codedeploy-agent를 활성화 시키지 않은 경우. 설치가 안정적으로 끝나면 그 짜릿함에 안도감이 찾아오며 종종 활성화를 잊게 되는 경우가 발생하는데 꼭 sudo service codedeploy-agent start를 수행해줍시다.
    이런 경우 아래 사진처럼 진행중 혹은 보류중이라고만 뜹니다.

nodejs express app 생성

nodejs 환경을 구성하는 거나 express app 을 생성하는 것이야 상황나름이지만, codedeploy를 이용할 때는 환경변수가 꽤나 말썽을 부립니다. 서론에서도 말했듯 깊게 공부하고 싶다면 codedeploy에서의 환경변수에 대한 영역을 한 번 더 짚어보아야할 수도 있습니다.

우선 저 같은 경우에는 nvm을 이용해 nodejs, npm 환경을 구축해보겠습니다. nvm을 이용하는 경우가 가장 환경 구축이 편리한 것 같더라구요.

nvm github 에 나온대로 (현시점 2020년 1월 중순)

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash

export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

을 입력해준 뒤에 잠시 Ctrl+D로 자신의 터미널 창을 나오거나 로그아웃을 했다가 터미널 창에 다시 그 유저로 접속하면 nvm --version을 입력했을 때 오류가 뜨지 않고, nvm이 잘 설치되었음을 확인할 수 있을 것입니다.

이제 express-generator라는 모듈을 전역으로 설치해준 뒤 express 앱에 대한 기본 설정이 담긴 세팅을 해줄겁니다.

npm install -g express-generator
express tutorial
cd tutorial
npm install
DEBUG=tutorial:* npm start

를 해준 뒤 http://localhost:3000으로 가면 express version 마다 다르겠지만 아래와 같은 화면을 볼 수 있어야합니다.

express app을 run시키는 데에 성공했다면, 이 내용을 github repository를 생성해 담아줍시다. 우리의 프로젝트의 루트는 tutorial 디렉토리 입니다.

 

❗❗❗ 단 이때 .gitignore 을 이용해 node_modules는 github repository에서 제외시켜주세요. node_modules는 CodeBuild시에 npm install을 통해 담아줄 것이거거든요~!

간단하게 업데이트하는 배포과정의 느낌을 내기 위해 tutorial/views/index.jade에 아래와 같이 h2 태그를 추가해주면 웹페이지가 Version1.0이라는 글을 보여줘야합니다. (여긴 재충 적으세요. 저도 jade 문법은 잘 몰라용 ㅎㅎㅎ 상관없음.)

배포 작업에 대한 설정인 appspec.yml 작성하기

사실 지금까지의 내용들은 대부분이 앞선 글들과 겹치거나 그냥 express 앱을 만드는 과정이었고, 이제부터가 제대로 CodePipeline과 CodeDeploy에 관해 추가되는 내용이라고 볼 수 있습니다.

프로젝트의 최상위 경로에 appspec.yml을 작성합니다.

#/appspec.yml
version: 0.0
os: linux
files:
  - source: /
    destination: /home/ubuntu/build
hooks:
  BeforeInstall:
    - location: /initialize.sh
      runas: root

  ApplicationStart:
    - location: /start.sh
      runas: root
  ValidateService:
    - location: /healthCheck.sh
      runas: ubuntu
      # ubuntu의 $HOME 환경변수를 이용해보려고 runas ubuntu

version은 appspec 에 대한 지원 버전인 것 같은데 0.0으로 맞추어줍니다.

os는 배포려는 EC2 Instance가 Linux 계열이면 linux로, Windows 서버 인스턴스라면 windows로 설정합니다.

files는 빌드 파일 중 sourcedestination에 복사시킨다는 의미입니다. 만약 아래와 같이 작성한다면,

# source는 project 기준,
# destination은 instance 기준
source: /appspce.yml
destination: /home/ubuntu/appspec.yml

Github Repository(프로젝트 디렉토리)의 최상위 경로에 있는 appspec.yml을 instance의 /home/ubuntu/appsepc.yml로 복사한다는 의미입니다.

(destination에 상대경로를 적을 경우 /opt/codedeploy-agent 를 기준으로 합니다.)

hooks는 다양한 이벤트들에 대한 hook을 .shfile로 제공할 수 있는데, 어떠한 이벤트들이 있는지, 그 이벤트들이 언제 발생하는 지에 대한 내용은 AWS CodeDeploy 설명서EC2/온프레미스 배포를 위한 AppSpec 'hooks' 섹션을 참고해주시면 됩니다.

다소 이해가 어려울 수 있거나 부족할 수 있는 내용에 대해 좀 더 설명을 하자면,

  • DownloadBundle이 빌드된 파일을 임시 위치에 복사시킴. (우리가 설정하지 못함. 자동)
  • BeforeInstallInstall 후크 전에 수행할 내용을 정의할 수 있음. 예를 들면 /home/ubuntu/build의 내용을 삭제한 뒤 Install이 제대로 appspec.ymldestination/home/ubuntu/build에 빌드 파일을 배포할 수 있도록 하는 작업 등을 정의할 수 있음.
  • Install은 임시 위치에서 appspec.ymldestination으로 복사(우리가 설정하지 못함. 자동)
  • AfterInstallInstall 후 하고 싶은 작업을 명시해주면 되는데, 크게 설정할 것 없다.
  • ApplicationStart는 이제 실제로 App을 시작시킬 명령을 적어준다. 사실 AfterInstallApplicationStart는 약간 의미론적인 단계일 뿐 어디다가 명령을 적든 비슷하게 작동할 것이다.
  • ValidateServiceApplicationStart이후에 healthcheck을 함으로써 ApplicationStart의 script가 성공했더라도 한 번 더 healthcheck을 할 명령을 담을 수 있다.

이러한 Event들의 흐름을 파악하는 것이 CodeDeploy의 배포과정을 파악하는 데에 도움이 꽤 되고, CodeBuild의 빌드 과정과도 비슷한 점도 많고, 헷갈리는 점도 많기 때문에 차이와 과정을 잘 파악하는 것이 좋습니다.

runas는 해당 hook 이벤트에 대한 명령을 실행할 user입니다.

# /initialize.sh
if [ -d "/home/ubuntu/build" ]; then rm -Rf "/home/ubuntu/build"; fi
# /start.sh
# nvm에 대한 환경변수를 설정하는 것임.
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"

cd $PROJECT_ROOT

# 원래 node 프로세스 종료
sudo kill -9 `ps -ef | grep 'node ./bin/www' | awk '{print $2}'`
nohup npm start >/home/ubuntu/logs 2>&1 </home/ubuntu/errors &
# /healthCheck.sh
curl "http://localhost:${PORT}">$HOME/output.html

healthCheck.sh에서는 ubuntu 유저의 $HOME 환경변수를 이용할 것이기 때문에 appspec.yml에서 runas를 ubuntu로 선언해주었고, $PORT 같이 따로 환경변수를 이용해주고 싶을 때는 EC2 instance의 /etc/environment 에 추가해주는 것이 가장 좋습니다. .bashrc.profile등은 CodeDeploy를 이용할 때 제대로 동작을 안하더라구요.

CodeBuild를 위한 buildspec.yml 작성하기

거의 다 왔습니다..! 집중력의 한계를 달리고 뭔 내용인지 잘 모르겠어도 일단 한 싸이클은 마쳐보자구요..!

사실 정말 다 온 건 아니고 2편 중 1편의 끝을 바라보고 있습니다..

CodeBuild를 이용할 때에는 어떤 식으로 Build를 할 지에 대한 설정파일인 buildspec.yml을 생성해주어야하는데요.

CodeDeploy에서 배포관련 설정을 적는 것은 appspec.yml, CodeBuild에서 빌드관련 설정을 적는 것은 buildspec.yml!!

 

appspec 과 유사하게 version을 명시해주고, phases 라는 appspec에선 hooks 에 있던 이벤트들과 비슷하게 어떤 이벤트에 대한 수행작업을 지정해주는 부분이 있습니다.

그리고 appspec과 달리 artifacts라는 부분에서 어떤 파일들을 빌드 결과물로서 제공할 지 적고,

cache를 통해 캐싱 작업을 설정합니다.(어떤 파일을 캐싱할 지)

buildspec 또한 마찬가지로 AWS CodeBuild 설명서에 좀 더 자세한 설명이 있고, 헷갈릴 만한 부분이나 설명이 부족한 부분에 대해 보충하겠습니다.

파일의 경로에 대한 표기에 대해서는

'**/*'는 모든 파일을 재귀적으로 나타냅니다.

my-subdirectory/*는 my-subdirectory라는 하위 디렉터리에 있는 모든 파일을 나타냅니다.

my-subdirectory/**/*는 my-subdirectory라는 하위 디렉터리에서 시작하는 모든 파일을 재귀적으로 나타냅니다.

**/*을 많이 쓰게 됩니다.

version: 0.2

phases:
  install:
    runtime-versions:
      nodejs: 12
    commands:
      - npm install
      - pwd
    run-as: root

artifacts:
  files:
    - "**/*"
cache:
  paths:
    - "node_modules/**/*"

buildspec 같은 경우엔 version을 보통 0.2로 적는 것 같았습니다. ( 어떤 version들이 있는지는 알아보지 못했음)

phases는 다양한 이벤트들이 있는데 대략적으로 흐름을 살펴봅시다.

 

Download_source-우리가 건드리지 못하는 부분. source와 cache file을 받아옵니다.(source는 github repo에서, cache는 일반적으로 S3에서)

install - appspec에선 내가 건들 수 없는 부분이었지만, 이번엔 내가 필요한 파일과 패키지를 설치하는 부분

pre_build, build, post_build 는 자기가 나누기 나름인 것 같습니다. 필수 사항도 아닙니다.

중요하면서 시간 잡아먹기 딱인 부분은 artifactsfiles의 경로입니다. 이 경로를 이해하는 것은 전반적으로 어떤 식으로 빌드하고 배포될 지에 대한 이해를 돕는데요, files의 경로는 프로젝트에 대한 상대경로로 입력할 경우 기준은 프로젝트 루트입니다만, 저희는 프로젝트를 어디에 다운로드 받을 지 설정한 적이 없죠?(다운로드라고 표현하긴했는데, build 하는 동안 사실 1차적으로 빌드 컨테이너에 배포물(github repo)을 받은 뒤 빌드하고 빌드 파일을 업로드하는 흐름이긴합니다.) 따라서 프로젝트는 사진처럼 컨테이너 내의 임의의 경로에 위치하게 되고, Github Repository의 이름은 없이 src라는 디렉토리 내에 위치합니다.

절대 경로로 설정한다면, 그 절대경로는 Build 하고 있는 컨테이너에 대한 절대경로입니다.

cache 또한 마찬가지로 빌드 중인 컨테이너의 /codebuild/output/임의의경로/src 가 기준입니다. 그리고 cache에서 paths로 등록된 파일들을 일반적으로 S3에 캐시파일로서 업로드 합니다. 그리고 다음 빌드 때 DownloadSource phase에서 이 캐시파일들을 불러옵니다.

 

중요한 것은 이 캐시 파일들은 CodeDeploy와 deploy 받는 EC2 Instance와는 전혀 무관하다는 것입니다. 이 캐시파일은 빌드 과정에서 필요한 것이기 때문입니다.

 

잠시 저의 개인적인 삽질 경험을 말씀드리자면

저는 빌드라는 것에 대한 개념이 잘 잡혀있지 않았었기에 이 cache deploy`에서 쓰이는 것인 줄 알고, ec2 instance에서 계속 cache 파일을 찾아보곤 했습니다. 사실 cache는 빌드 과정에서 사용하는 cache인데 말이지요. CodeBuild와 CodePipleline의 전체적인 흐름을 이해하면서 cache 부분도 좀 더 자세히 이해할 수 있었습니다. 상당한 삽질이었는데, 덕분에 개념을 잘 잡게 되었습니다...쥬륵...)

 

여담으로 캐싱이 잘 동작하는 지 확인하는 방법은 우선 S3의 캐시 디렉토리를 삭제하고 빌드를 한 뒤, 이어서 한 번 더 빌드를 해본 뒤 세부 작업 시간을 비교해보는 것입니다.

 

왼쪽은 캐시 후, 오른쪽은 캐시 전.

튜토리얼용이라 차이가 크진 않지만 npm install babel react vue 등등을 통해 node_modules를 무겁게 만든 뒤 테스트 하시면, Download_source의 시간은 캐시파일을 다운 받느라 늘어나고, Install의 시간은 캐시파일덕에 네트워크 통신이 적어지므로 줄어든다면 캐시가 올바르게 작동하고 있는 것입니다.

그럼 여태까지 개념 잡으랴 따라하랴 고생하며 작성했던 /appspec.yml/buildspec.yml을 git으로 push 해주세요.

마치며

상당히 긴 내용이라 오랜만에 글을 나누어서 적습니다. 다음 편에서는 여태까지 한 셋팅들을 이용해 CodePipeline을 생성하고 실제로 CI/CD를 구축해볼 거에요. 내용이 좀 길지만 다 차근차근 다 읽어보신다면, 제가 했던 삽질들이 거름이 되어 여러분의 경험치가 될 겁니다...ㅜㅜ 그럼 다음 편에서도 화이팅~~