항상 백엔드 토이프로젝트를 할 때는 로컬에서 개발만 하다가 문득 이런 생각이 들었습니다.
"아...CI/CD를 한 번만 구축하게 된다면 나중에 큰 도움이 되지 않을까?"
위와 같은 생각이 들어서 바로 작업에 들어갔습니다.
먼저, 배포를 하기 위해 사람들은 여러가지 방법을 쓰고 있지만 저는 AWS Elastic Beanstalk을 이용하여 인프라를 구축해보자라는 마음이 들었습니다.
이유는 딱히 없었습니다만...깃헙 액션이나 젠킨스와 같은 도구들도 있지만 AWS의 Code Pipeline을 한 번 써보고 싶었습니다 ㅎㅎ
(사실 이미 사내 CI/CD가 EB로 구축되어 있긴 하나 제가 구축 해본 경험이 없어서 직접 해보면 도움이 되지 않을까 싶어서였습니다 ㅎㅎ...)
먼저 간단하게 백엔드 서버는 NestJS 프레임워크를 사용하여 구축하였습니다.
export class CatService {
private readonly answers: string[] = [
'안된다냥',
'좋다냥',
'당연하다냥',
'돌아가냥',
'그럴 수도 있다냥',
'그럴리가 없다냥',
'그럴지도 모른다냥',
'그럴 가능성이 높다냥',
'그럴 가능성이 낮다냥',
'그럴 가능성이 없다냥',
'가만 있어냥',
'다신 물어보지마라냥',
];
private redisClient: Redis;
constructor(private readonly redisService: RedisService) {
this.redisClient = this.redisService.getClient();
}
public async getAnswer() {
const randomIndex = Math.floor(Math.random() * this.answers.length);
return this.answers[randomIndex];
}
}
위와 같이 아주 간단한 서비스 로직을 구현하였습니다.
(이 때 마법의 소라고동 서비스를 만들고자 하였습니다만, 여자친구가 마법의 고양이로 해달라고 해서 바꿨습니다)
암튼 대충 해당 코드들을 깃헙 레포에 배포하여두었습니다.
하지만 여기서 배포를 할 때 조금 더 유연하게 관리할 수는 없을까...?
FROM node:18-alpine
EXPOSE 3000
ENV PORT=3000
ENV NODE_ENV=production
WORKDIR /app
COPY package.json ./
COPY package-lock.json ./
COPY dist ./dist
RUN npm install
CMD npm run start:prod
바로 위와 같은 DockerFile을 생성하였습니다.
사실 Docker를 사용해서 직접 배포를 해본 적은 없어서 이번 기회에 함께 사용해보면 어떨까 싶어서 해당 부분을 추가하였습니다.
npm run start:prod 명령어는 package.json에서 아래와 같이 명시하였습니다.
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "pm2-runtime -i max ./dist/main.js",
...
},
pm2는 간단하게 설명하자면 NodeJS 환경에서 흔히들 무중단 배포 서비스를 구축하기 위하여 사용하는 프로세스 매니저라고 생각하시면 편할 것 같습니다! (추후 블로깅을 해야겠습니다!)
제일 처음으로 AWS CodePipeline에 들어가서 '파이프 라인 생성'을 클릭합니다.
여기서 자신이 원하는 파이프라인 이름을 적고 다음을 클릭합니다.
여기서 저는 소스 스테이지를 Github 버전2를 사용하였습니다.
자신의 깃헙 계정과 연동하여 레포 및 브랜치를 설정해줍니다.
그 다음으로는 CodeBuild에 대한 설정입니다.
아마 처음 생성하시니 CodeBuild의 프로젝트 생성을 눌러주신 개인의 설정값과 맞게 설정합니다.
다만 여기서 CodeBuild를 사용하신다고 가정하신다면 루트 폴더의 지정하신 yml파일이 필요합니다!
저는 아래와 같이 build.yml를 설정하였습니다.
version: 0.2
phases:
install:
runtime-versions:
nodejs: 18
commands:
- echo Installing dependencies
- npm install
build:
commands:
- echo Build started on `date`
- npm run build
artifacts:
files:
- dist/**/*
- package-lock.json
- package.json
- Dockerfile
cache:
paths:
- node_modules/**/*
저는 운영체제를 Ubuntu로 설정하여 CodeBuild를 구축하였습니다.
그 다음으로는 배포 스테이지입니다.
저는 AWS EB를 선택하였습니다.
여기서 EB 애플리케이션을 먼저 구축을 해야하는데 해당 과정을 살펴봅시다!
저희는 Docker를 통해 관리하기에 플랫폼을 Docker로 세팅하였습니다.
그 다음 EC2 키페어를 생성해주고 EC2 인스턴스 프로파일을 생성하여 선택하여줍시다.
프록시 서버는 Nginx로 해주었으며, 일단은 배포 정책을 단일 인스턴스로 해두었습니다만...
https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/using-features.rolling-version-deploy.html
위 사이트를 잘 참고하시길 바랍니다!
물론 토이프로젝트에서는 상관없지만, rolling deployment를 설정해주지 않는다면 잠깐의 downtime이 존재할 수 있습니다.
(실 서비스에서는 필연적으로 설정해줘야겠습니다 ㅎㅎ)
다시 돌아와서 code pipeline설정을 끝마치게 된다면 앞으로 깃헙에서 지정한 브랜치에 커밋이 일어나게 된다면 즉각적으로 캐치하여 배포되는 것을 확인할 수 있습니다!
추가적으로 저는 Redis도 함께 사용하기 위해 AWS ElasticCache를 사용하여 VPC 설정을 묶어서 구축해두었습니다.
추가적으로 Github actions를 이용하면 NextJS 서비스도 배포해보았습니다!
Github에서 제시하는 예제 파일만 가지고 바로 적용할 수 있었습니다 ㅎㅎ
# Sample workflow for building and deploying a Next.js site to GitHub Pages
#
# To get started with Next.js see: https://nextjs.org/docs/getting-started
#
name: Deploy Next.js site to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["master"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Detect package manager
id: detect-package-manager
run: |
if [ -f "${{ github.workspace }}/yarn.lock" ]; then
echo "manager=yarn" >> $GITHUB_OUTPUT
echo "command=install" >> $GITHUB_OUTPUT
echo "runner=yarn" >> $GITHUB_OUTPUT
exit 0
elif [ -f "${{ github.workspace }}/package.json" ]; then
echo "manager=npm" >> $GITHUB_OUTPUT
echo "command=ci" >> $GITHUB_OUTPUT
echo "runner=npx --no-install" >> $GITHUB_OUTPUT
exit 0
else
echo "Unable to determine package manager"
exit 1
fi
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: "16"
cache: ${{ steps.detect-package-manager.outputs.manager }}
- name: Setup Pages
uses: actions/configure-pages@v3
with:
# Automatically inject basePath in your Next.js configuration file and disable
# server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized).
#
# You may remove this line if you want to manage the configuration yourself.
static_site_generator: next
- name: Restore cache
uses: actions/cache@v3
with:
path: |
.next/cache
# Generate a new cache whenever packages or source files change.
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
# If source files changed but packages didn't, rebuild from a prior cache.
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-
- name: Install dependencies
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
- name: Build with Next.js
run: ${{ steps.detect-package-manager.outputs.runner }} next build
- name: Static HTML export with Next.js
run: ${{ steps.detect-package-manager.outputs.runner }} next export
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: ./out
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
위와 같이 프론트 엔드 페이지까지 Github pages를 이용하여 배포하고 나니 한 가지 문제에 직면했습니다.
바로 프론트엔드는 https, 백엔드는 http여서 서로 호환이 되지 않았던 것이었죠...ㅎㅎ mixed content에러를 겪어서 아...그냥 이왕 이렇게 된거 백엔드 서버도 도메인을 구매해보아서 https로 만들자! 싶어서 AWS route 53 에서 해당 부분을 구현하였습니다.
(자세한건 다음 포스팅에 다루도록 하겠습니다!)