예전부터 API 서버는 어떻게 만드는건지 궁금하여 이번에 Lily라는 프로젝트를 진행하면서 만들어보면 좋을 거 같아 API 서버를 만들어보려고 합니다.
익숙한 TypeScript와 이를 효과적으로 활용할 수 있는 Nest.js 프레임워크를 선택하여 프로젝트를 진행하려 합니다. 특히, Nest.js는 ORM을 통해 데이터베이스와의 상호작용을 지원하며, 제가 이전에 Prisma를 활용해 API를 개발했던 경험이 있어, 이 방법을 선택하면 보다 효율적으로 개발을 진행할 수 있을 것이라 판단했습니다.
작업을 무작정 진행하기보다는, 먼저 애플리케이션을 배포 환경에 설정해두고 시작하려고 합니다. 이렇게 하면 배포 과정에서 발생할 수 있는 문제들을 미리 인지하고, 이를 빠르게 해결할 수 있을 것 같아 배포를 한 후 작업을 하려고 합니다.
앞으로 할 작업들의 대한 아키텍쳐이다. 순서를 간단하게 설명드리면
겉으로 보기엔 간단해 보이는 작업일 수 있지만, 네트워크 지식이 부족하고 AWS를 활용한 배포는 처음이라 개인적으로는 쉽지 않은 도전이었습니다. 이번 기회를 통해 네트워크 지식을 쌓고자 했고, 끝까지 포기하지 않고 배포 과정을 완수하기 위해 노력했습니다.(결국 2주만에 성공)
제가 이번에 배포하면서 알게된 내용을 조금이나마 공유하고자 글을 써보는 것이니 혹시라도 틀린 부분이나 개선점이 있으면 댓글 부탁드리겠습니다. 🙏
# Node.js 기반 이미지 사용 (Node.js 18 버전)
FROM node:18
# 컨테이너 내부의 작업 디렉터리를 /app으로 설정
WORKDIR /app
# package.json과 package-lock.json 파일을 컨테이너로 복사
COPY package*.json ./
# 프로젝트 의존성 패키지 설치
RUN npm install
# NestJS CLI 전역 설치
RUN npm install -g @nestjs/cli
# 현재 디렉터리의 모든 소스 파일을 컨테이너로 복사
COPY . .
# 환경 설정 파일 복사
COPY .env .env
# Prisma 데이터베이스 마이그레이션 실행
RUN npx prisma migrate deploy
# 프로덕션용 빌드 실행
RUN npm run build
# 애플리케이션 실행 명령어 설정 (프로덕션 모드)
CMD ["npm", "run", "start:prod"]
컨테이너를 만들기 위해서는 Docker 이미지가 필요합니다. 이 이미지는 애플리케이션과 그 의존성들이 미리 설정된 템플릿으로, 이를 기반으로 실행 가능한 컨테이너가 생성됩니다. 따라서, "이 코드는 도커 이미지를 만들기 위한 코드"라고 생각하면 쉽게 이해할 수 있습니다.
Docker는 애플리케이션과 그에 필요한 모든 의존성, 라이브러리, 설정 파일 등을 하나의 컨테이너라는 단위로 패키징하여 어디서나 일관되게 실행할 수 있게 해주는 플랫폼입니다. 이를 통해 개발, 테스트, 배포 환경에 관계없이 애플리케이션이 동일한 방식으로 실행될 수 있도록 보장합니다.
Dockerfile을 작성한 후 프로젝트를 빌드하면 Docker 이미지가 생성됩니다. 이 이미지만 있으면, EC2와 같은 가상 환경에서 Docker 컨테이너를 생성하여 프로젝트가 실행될 수 있게 됩니다. 즉, 이미지를 통해 애플리케이션을 가상 환경에서도 동일하게 실행할 수 있게 되는 것입니다.
이걸 이제 AWS ECR로 전달하여 EC2에서 가져올 수 있도록 할 것입니다.
배포 자동화를 하기 위해서는 EC2가 저희가 원하는대로 동작해줘야합니다. 이걸 어떻게 하는걸까요?? 바로 codedeploy와 appspec.yml 파일을 이용해서 할 수 있습니다.
appspec.yml 파일은 AWS CodeDeploy에서 사용하는 설정 파일로, 애플리케이션 배포 프로세스를 정의합니다. 이 파일은 배포 단계와 각 단계를 어떻게 처리할지에 대한 지침을 포함합니다. CodeDeploy는 이 파일을 읽고, 지정된 순서대로 배포 작업을 실행합니다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ubuntu/app
permissions:
- object: /home/ubuntu/app/scripts
pattern: '**'
owner: ubuntu
group: ubuntu
mode: 755
hooks:
ApplicationStop:
- location: scripts/stop.sh
timeout: 300
runas: ubuntu
BeforeInstall:
- location: scripts/before_install.sh
timeout: 300
runas: ubuntu
ApplicationStart:
- location: scripts/start.sh
timeout: 300
runas: ubuntu
version: 0.0
버전 정보: appspec.yml 파일의 버전을 지정합니다. 0.0은 기본 버전입니다.
os: linux
운영 체제: 배포 대상이 되는 운영 체제를 지정합니다. 이 예제에서는 linux로 설정되어 있습니다.
files
파일 복사: 배포할 파일들의 소스와 목적지 경로를 설정합니다.
source: 배포할 파일들이 위치한 소스 디렉토리입니다. 여기서는 루트 디렉토리(/) 전체를 의미합니다.
destination: EC2 인스턴스 내의 대상 디렉토리입니다. /home/ubuntu/app 경로로 복사됩니다.
permissions
파일 권한 설정: 배포 후 특정 디렉토리나 파일에 대한 권한을 설정합니다.
object: 권한을 설정할 대상 파일이나 디렉토리입니다.
pattern: 대상 파일의 패턴을 지정합니다. **는 하위 디렉토리까지 모두 포함합니다.
owner, group: 파일의 소유자와 그룹을 설정합니다. ubuntu 사용자와 그룹으로 설정되어 있습니다.
mode: 파일의 권한을 설정합니다. 755는 읽기, 쓰기, 실행 권한을 소유자에게 부여하고, 그룹과 다른 사용자에게 읽기 및 실행 권한을 부여합니다.
hooks
배포 후 작업: 배포 프로세스 동안 실행할 스크립트 및 명령을 설정합니다. 각 단계는 ApplicationStop, BeforeInstall, ApplicationStart 등으로 정의되어 있습니다.
ApplicationStop
배포 전에 애플리케이션을 중지하는 스크립트를 실행합니다.
BeforeInstall
애플리케이션을 설치하기 전에 실행할 스크립트를 지정합니다.
ApplicationStart
애플리케이션이 설치되고, 중지된 후에 실행할 시작 스크립트를 설정합니다.
#!/bin/bash
# 실행 중인 도커 컨테이너 중지
docker stop $(docker ps -a -q) 2>/dev/null || true
# 중지된 컨테이너 삭제
docker rm $(docker ps -a -q) 2>/dev/null || true
# 사용하지 않는 이미지 삭제 (선택사항)
docker image prune -af 2>/dev/null || true
exit 0
위 스크립트는 새로운 버전의 애플리케이션을 배포할 때, 기존 Docker 컨테이너와 이미지를 정리하여 충돌 없이 배포가 이루어지도록 준비하는 작업을 수행합니다.
#!/bin/bash
# 패키지 매니저 업데이트
sudo apt-get update
# Docker 설치를 위한 필수 패키지 설치
sudo apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release
# Docker의 공식 GPG 키 추가
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Docker 레포지토리 설정
echo \
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Docker 설치
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
# Docker 서비스 시작
sudo systemctl start docker
sudo systemctl enable docker
# 현재 사용자를 docker 그룹에 추가
sudo usermod -aG docker ubuntu
# Docker Compose 설치
sudo curl -L "https://github.com/docker/compose/releases/download/v2.17.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# AWS CLI 설치
sudo apt-get install -y awscli
# 스크립트 실행 권한 설정
sudo chmod +x /home/ubuntu/app/scripts/*.sh
if [ -d /home/ubuntu/app ]; then
rm -rf /home/ubuntu/app/*
fi
mkdir -p /home/ubuntu/app
mkdir -p /home/ubuntu/app/scripts
위 스크립트는 애플리케이션을 설치하기 전에 서버에 Docker, Docker Compose, AWS CLI 설치 및 환경 설정을 수행하며, 애플리케이션 배포를 위한 디렉토리와 스크립트 실행 환경을 준비하는 스크립트입니다.
#!/bin/bash
cd /home/ubuntu/app
# .env 파일 로드
export $(cat /home/ubuntu/app/codedeploy.env | xargs)
# 도커 이미지 풀
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $ECR_REGISTRY
docker pull $ECR_REGISTRY:$IMAGE_TAG
# 새 컨테이너 실행
docker run -d -p 3001:3001 --name app $ECR_REGISTRY:$IMAGE_TAG
위 스크립트는 애플리케이션이 설치되고, 중지된 후 새로운 도커 이미지를 ECR에서 가져와, 기존 컨테이너 정리 없이 새로운 컨테이너를 실행하는 스크립트입니다.
이렇게 만든 스크립트 파일들을 압축하여 S3 저장소에 올려줘 EC2가 이 파일들을 가져와서 실행할 수 있게 해주어야합니다.
저희가 매번 배포할 때마다 명령어를 입력하여 저장소에 도커 이미지 올리고 S3에 파일을 업로드할 수 없으니깐 이 작업을 깃허브 액션을 이용하여 자동화를 시켜줄겁니다.
name: Dockerizing to Amazon ECR # 워크플로우의 이름을 'Dockerizing to Amazon ECR'로 설정합니다.
on:
push: # 특정 브랜치에 코드가 푸시될 때 워크플로우가 트리거됩니다.
branches: ['main'] # 'main' 브랜치에 푸시될 때만 워크플로우 실행
pull_request: # 풀 리퀘스트가 열리거나 업데이트될 때 트리거됩니다.
branches: ['main'] # 'main' 브랜치에 대한 풀 리퀘스트일 때만 실행
jobs: # 실행할 작업을 정의합니다.
deploy: # 'deploy'라는 작업을 정의합니다.
name: Deploy # 작업의 이름을 'Deploy'로 설정
runs-on: ubuntu-latest # 최신 Ubuntu 버전에서 실행되도록 설정
environment: production # 이 작업이 'production' 환경에서 실행됨을 정의
steps: # 이 작업에서 실행될 단계들을 정의합니다.
- name: Checkout # 소스 코드를 체크아웃하는 단계
uses: actions/checkout@v3 # GitHub 제공 체크아웃 액션 사용
- name: Create .env file
run: |
echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" > .env
echo "DIRECT_URL=${{ secrets.DIRECT_URL }}" >> .env
- name: Create codedeploy .env file
run: |
echo "ECR_REGISTRY=${{ secrets.ECR_REGISTRY }}" > codedeploy.env
echo "IMAGE_TAG=${{ github.sha }}" >> codedeploy.env
- name: Config AWS credentials # AWS 자격 증명을 구성하는 단계
uses: aws-actions/configure-aws-credentials@v2 # AWS 자격 증명 구성 액션 사용
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: Login To Amazon ECR # Amazon ECR에 로그인하는 단계
id: login-ecr # 이 단계의 ID를 'login-ecr'로 설정 (후속 단계에서 참조 가능)
uses: aws-actions/amazon-ecr-login@v1 # Amazon ECR 로그인 액션 사용
- name: Build, tag, and push image to Amazon ECR # Docker 이미지를 빌드, 태그, ECR에 푸시하는 단계
id: build-image # 이 단계의 ID를 'build-image'로 설정
env:
ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }} # ECR 레지스트리 URL 설정
IMAGE_TAG: ${{ github.sha }} # GitHub의 커밋 SHA 값을 이미지 태그로 사용
run: |
docker build -t $ECR_REGISTRY:$IMAGE_TAG .
docker push $ECR_REGISTRY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY:$IMAGE_TAG" # 빌드된 이미지 URL을 워크플로우 출력으로 설정
- name: Create deployment package
run: |
mkdir -p deploy
cp -r scripts deploy/
cp appspec.yml deploy/
cp codedeploy.env deploy/
cd deploy && zip -r ../deploy.zip .
- name: Upload to S3
run: |
aws s3 cp deploy.zip s3://${{ secrets.S3_BUCKET }}/deploy.zip
- name: Create CodeDeploy Deployment
run: |
aws deploy create-deployment \
--application-name lily-server-deploy \
--deployment-group-name lily-server-deploy-group \
--s3-location bucket=${{ secrets.S3_BUCKET }},bundleType=zip,key=deploy.zip \
--region ${{ secrets.AWS_REGION }}
아래에서 코드를 나눠서 설명드리겠습니다.
- name: Login To Amazon ECR # Amazon ECR에 로그인하는 단계
id: login-ecr # 이 단계의 ID를 'login-ecr'로 설정 (후속 단계에서 참조 가능)
uses: aws-actions/amazon-ecr-login@v1 # Amazon ECR 로그인 액션 사용
- name: Build, tag, and push image to Amazon ECR # Docker 이미지를 빌드, 태그, ECR에 푸시하는 단계
id: build-image # 이 단계의 ID를 'build-image'로 설정
env:
ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }} # ECR 레지스트리 URL 설정
IMAGE_TAG: ${{ github.sha }} # GitHub의 커밋 SHA 값을 이미지 태그로 사용
run: |
docker build -t $ECR_REGISTRY:$IMAGE_TAG .
docker push $ECR_REGISTRY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY:$IMAGE_TAG" # 빌드된 이미지 URL을 워크플로우 출력으로
이 단계에서는 빌드된 도커 이미지를 ECR에 올리는 작업을 진행하게 됩니다.
여기서 보이는 secrets.ECR_REGISTRY
는 깃허브에서 환경변수로 한 후 위 코드처럼 값을 불러와 사용할 수 있습니다.
- name: Create deployment package
run: |
mkdir -p deploy
cp -r scripts deploy/
cp appspec.yml deploy/
cp codedeploy.env deploy/
cd deploy && zip -r ../deploy.zip .
- name: Upload to S3
run: |
aws s3 cp deploy.zip s3://${{ secrets.S3_BUCKET }}/deploy.zip
이 단계에서는 Codedeploy가 EC2에게 작업 명령을 내일 스크립트 파일들을 압축하여 S3에 올리는 작업을 거치게 됩니다.
- name: Create CodeDeploy Deployment
run: |
aws deploy create-deployment \
--application-name lily-server-deploy \
--deployment-group-name lily-server-deploy-group \
--s3-location bucket=${{ secrets.S3_BUCKET }},bundleType=zip,key=deploy.zip \
--region ${{ secrets.AWS_REGION }}
이 단계에서는 AWS에서 미리 만들어둔 Codedeploy에게 배포를 진행하라는 명령을 하게됩니다
만약 위와 같은 방법으로 진행하였는데 배포 실패가 뜨는 경우가 있을겁니다.(글에는 안 썼지만 보안그룹, IAM 설정 등은 다 했다고 가정했을 때)
인스턴스에 기존 AWS 자격 증명 파일이 저장되어있어 IAM 정보를 제대로 못 가져오는 현상이 있을 수 있다고 합니다.
# AWS 자격증명 파일 삭제
$ sudo rm -rf /root/.aws/credentials
# codedeploy-agent 재시작
$ sudo systemctl restart codedeploy-agent
이 문제는 지우고 다시 시작한 후, 로그를 확인해보시면 해결이 될겁니다.
codedeloy를 통해서 스크립트 파일들이 정상적으로 동작되게 하기 위해서는 EC2에서 필요한 프로그램들을 깔아줘야한다.
sudo apt-get update
udo apt-get install -y docker.io
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER
sudo apt-get install -y awscli
sudo apt-get install -y awscli
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
sudo service codedeploy-agent start
sudo service codedeploy-agent status
tail -f /var/log/aws/codedeploy-agent/codedeploy-agent.log
이번에 AWS를 이용해서 처음으로 배포를 해보는 것이라 많은 것이 낯설고 정말 오류가 계속 터져나왔습니다.
그래도 포기하지않고 이곳 저곳 알아보며 결국 배포를 성공하니 정말 기쁘더군요
부족한 글이지만 다른 분들도 이 글을 읽고 조금이나마 도움이 되시길 바랍니다.
다음에는 도메인을 하나 사서 도메인 연결 및 ssl 설정 주제로 글을 써보겠습니다.