저번 포스팅에서는 AWS Service(EC2, CodeDeploy, S3)를 세팅하고, 빌드 결과물을 압축한 후 S3에 올리는 작업까지 했습니다.
이번 포스팅에서는 S3에 올린 zip 파일을 EC2로 가져온 후 docker로 띄우는 작업을 하겠습니다. 아래 파란 부분에서는 docker 컨테이너가 두 개지만 일단 1개만 띄우겠습니다.
Github Actions에서 Code Deploy deployment를 생성하는 과정이 제일 아래에 있었습니다.
- name: Code Deploy
run: aws deploy create-deployment --application-name $AWS_CODEDEPLOY_APPLICATION_NAME --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name $AWS_CODEDEPLOY_DEPLOYMENT_GROUP_NAME --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$PROJECT_NAME/$GITHUB_SHA.zip
EC2에서 deployment를 감지하기 위해서는 CodeDeploy Agent가 설치되어 있어야합니다.
설치 방법은 CLI를 사용하여 CodeDeploy Agent를 설치하기를 참고하였습니다.
sudo apt update
sudo apt install ruby-full
sudo apt install wget
cd /home/ubuntu
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto > /tmp/logfile
sudo service codedeploy-agent status
실행된 것까지 확인이 되었다면 다음 단계로 넘어가봅시다!
sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"
sudo apt-get update
sudo apt-get install docker-ce
sudo systemctl status docker
sudo usermod -aG docker ${USER}
docker
2022.07.31 기준 docker-compose
의 최신 버전은 2.8.0
이므로 해당 버전을 설치해주도록 하겠습니다. 최신 버전은 여기서 확인해주세요.
1. sudo curl -L "https://github.com/docker/compose/releases/download/v2.8.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
2. sudo chmod +x /usr/local/bin/docker-compose
3. docker-compose -v
appspec.yml
작성하기애플리케이션 사양 파일(appspec.yml
)은 CodeDeploy에서 배포를 관리하는 데 사용하는 파일입니다. 프로젝트 루트 경로에 생성하시면 됩니다.
appspec.yml
을 작성해봅시다.
version: 0.0
os: linux # Amazon Linux, RHEL, Ubuntu Server일 경우 "linux", Windows Server일 경우 "windows"
files:
- source: / # 인스턴스에 복사할 S3 파일의 경로입니다. / 로 설정하면S3_BUCKET_NAME/PROJECT_NAME/GITHUB_SHA.zip을 가져옵니다.
destination: /home/ubuntu/my-nest-app # S3에서 가져온 파일을 저장할 위치입니다.
overwrite: yes # 덮어쓰기를 허용하는 옵션입니다.
# files에서 가져온 파일들에게 권한을 어떻게 적용해야하는지 지정하는 곳입니다.
permissions:
- object: /
pattern: '**'
owner: ubuntu
group: ubuntu
한 번 푸쉬를 하고 EC2에서는 어떤 일이 일어나는 지 확인해보세요! destination
에서 지정한 경로에 빌드 결과물이 있습니다! 이제 스크립트를 추가해서 애플리케이션을 실행해봅시다. 스크립트를 추가하기 전에 Dockerfile을 먼저 작성해봅시다.
Nest 애플리케이션을 위한 Dockerfile을 작성해주겠습니다.
FROM node:alpine
# Docker container 안의 기본 workdir를 /usr/src/app으로 설정하였습니다.
WORKDIR /usr/src/app
# 현재 프로젝트의 package.json, package-lock.json을 docker container의 /usr/src/app로 복사합니다.
COPY package*.json ./
# 이미지 빌드시 실행되는 명령어입니다.
# 프로덕션을 위한 코드를 빌드하는 경우
RUN npm ci --only=production
# .dockerignore에 지정되어있는 파일 제외한 모든 파일을 docker container의 /usr/src/app로 복사합니다.
COPY . .
# docker container의 3000번 포트를 엽니다.
# EC2 내부에서는 해당 이미지를 사용하는 docker container의 3000번 포트에 접근할 수 있습니다.
EXPOSE 3000
# 이미지가 실행되어 docker container가 되는 시점에 실행될 명령어입니다.
CMD ["npm", "run", "start:prod"]
execute.sh
작성하기저는 스크립트를 scripts/execute.sh
에 넣어두었습니다.
위의 9번에서 나온 이미지를 보면 확인할 수 있습니다. 이렇게 하는 경우, 이미지 빌드시 스크립트에 대한 Dockerfile의 상대 경로를 지정해주어야합니다.
#!/bin/bash
DOCKER_IMAGE_NAME=my-nest-app
DOCKER_CONTAINER_NAME=nest-app
docker build -t ${DOCKER_IMAGE_NAME} ../ # <-- 여기서 Dockerfile의 경로를 제대로 지정해주어야합니다.
docker run -d -p 3001:3000 --name ${DOCKER_CONTAINER_NAME} ${DOCKER_IMAGE_NAME}
위와 같이 상대경로를 지정할 수도 있지만 경로를 지정할 때 실수할 가능성이 있습니다. 그래서 스크립트 파일을 하나 더 만들어서 경로를 지정할 때 실수를 줄일 수 있도록 해보겠습니다. 필요성을 못느끼신다면 넘어가셔도 됩니다. 위에서 만든 execute.sh
에는 아래와 같은 내용으로 대체합니다.
#!/bin/bash
# appspec.yml의 files.destination에 지정한 경로입니다.
# 실행 위치를 프로젝트 루트 경로로 수정해줍니다. 그리고 실제 docker 컨테이너를 컨트롤하는 스크립트를 실행합니다.
cd /home/ubuntu/my-nest-app
sh docker-script.sh
위와 같은 스크립트를 만들면 docker-script.sh
를 Dockerfile의 경로를 프로젝트 루트 경로에 대한 상대경로로 작성할 수 있습니다.
#!/bin/bash
DOCKER_IMAGE_NAME=my-nest-app
DOCKER_CONTAINER_NAME=nest-app
docker build -t ${DOCKER_IMAGE_NAME} . # <--- 프로젝트 루트 경로에 대한 상대경로
docker run -d -p 3001:3000 --name ${DOCKER_CONTAINER_NAME} ${DOCKER_IMAGE_NAME}
쉘 스크립트가 아래와 같은 구조 안에 존재한다고 합시다.
home
└── parent
└── child
└── test.sh
tesh.sh
의 내용은 아래와 같습니다.
#!/bin/sh
pwd
test.sh
를
/home/parent
에서 실행하는 경우 Output : /home/parent
/home/parent/child
에서 실행하는 경우 Output : /home/parent/child
위와 같은 차이점이 있어서 execute.sh
를 통해서 쉘 스크립트의 실행 위치를 프로젝트 루트 경로로 이동시켰습니다.
두 개의 쉘 스크립트 파일을 작성하면 됩니다.
execute.sh
#!/bin/sh
cd /home/ubuntu/my-nest-app
sh scripts/docker-script.sh
docker-script.sh
#!/bin/sh
DOCKER_IMAGE_NAME=my-nest-app
DOCKER_CONTAINER_NAME=nest-app
docker build -t ${DOCKER_IMAGE_NAME} . # <--- 프로젝트 루트 경로에 대한 상대경로
docker run -d -p 3001:3000 --name ${DOCKER_CONTAINER_NAME} ${DOCKER_IMAGE_NAME}
appspec.yml
에 hooks 추가하기CodeDeploy의 LifeCycle Event Hook의 순서는 아래와 같습니다.
조금 더 정확히 말하자면, 인 플레이스(in-place) 배포 상황에서 로드 밸런서 없는 배포 그룹의 경우 위와 같은 순서로 Hook이 진행됩니다. 공식 문서에서 더 자세하게 확인할 수 있습니다.
회색 표시된 hook은 스크립트를 추가할 수 없고, 그 외 hook에서 스크립트를 추가해야합니다.
저는 CodeDeploy Agent가 S3에서 zip 파일을 가져와서 EC2 인스턴스에 설치했을 때 스크립트를 실행하도록 구현을 했기 때문에 AfterInstall
hook만 사용했습니다. 제가 작성하는 방식은 best practice가 아니므로 여러 시도를 한 번 해보세요!!
appspec.yml
에 hook을 추가해보도록 하겠습니다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ubuntu/my-nest-app
overwrite: yes
permissions:
- object: /
pattern: '**'
owner: ubuntu
group: ubuntu
hooks:
AfterInstall: # LifeCycle Event Hook의 이름을 의미합니다.
- location: scripts/execute.sh # 스크립트의 위치입니다. files.destination에 정의한 경로에 대한 상대경로롤 작성해주세요.
timeout: 600 # 스크립트 실행시, 여기에 지정된 시간을 초과하면 안됩니다. 최대 3600초입니다.
runas: ubuntu # 스크립트 실행 시 가장하는 사용자입니다.
appspec.yml
에 hooks까지 추가했다면 푸쉬를 날리고 배포가 완료될 때까지 기다려주세요. 그 다음 EC2의 3001포트로 접속하면 정상적으로 응답이 옵니다.