개발 환경에서는 Webpack dev server가 포트를 열어주었지만 배포를 위해 빌드를 하고나면 최적화된 정적 파일들만 남기 때문에 이를 제공하기 위해 nginx를 사용한다. Dockerfile.prod
파일을 만들고 아래와 같이 작성한다.
FROM node:lts as build
WORKDIR /app
COPY package.json .
RUN npm i
COPY . .
RUN npm run build
FROM nginx:stable-alpine
# nginx의 기본 설정을 삭제하고 앱에서 설정한 파일을 복사
RUN rm -rf /etc/nginx/conf.d
COPY conf /etc/nginx
# 위 스테이지에서 생성한 빌드 결과를 nginx의 샘플 앱이 사용하던 폴더로 이동
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
# nginx 실행
CMD [ "nginx", "-g", "daemon off;" ]
.dockerignore
도 작성한다.
node_modules
build
.git
.gitignore
.github
README.md
*.pem
nginx를 설정하기 위해 nginx 설정파일을 미리 만들어둬야 한다. 프로젝트 폴더에 conf/conf.d/default.conf
폴더와 파일을 생성한다.
default.conf
의 내용은 아래와 같다.
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
80번 포트를 사용하고 root 경로를 리액트의 빌드 결과가 있는 폴더로 설정한다.
더 자세한 내용은 여기 참고.
Dockerfile을 빌드한다: docker build -t jmdb -f Dockerfile.prod .
빌드한 이미지를 실행한다: docker run -d --rm --name jmdb -p 80:80 jmdb
http://localhost
에 접속하여 잘 실행되는지 확인한다.
잘 실행되면 Docker Hub의 username을 넣어 이름을 변경한 다음 푸시한다:
docker tag jmdb jwlee134/jmdb && docker push jwlee134/jmdb
m1 맥을 사용하면 아래와 같이 빌드한 후 push한다.
docker buildx create --name mybuilder --use
docker buildx build --platform=linux/amd64,linux/arm64 -t jwlee134/jmdb --push .
그냥 빌드하면 arm64 아키텍처 이미지가 만들어지는데 아래에서 EC2를 x86 아키텍처 리눅스 서버로 만들 것이기 때문에 아키텍처별로 만들어준다.
더 자세한 내용은 여기 참고.
인스턴스를 시작한다.
글 쓴 날짜 기준 최신 버전은 Amazon Linux 2023 AMI이다. 인스턴스는 프리티어 사용중이니 t2.micro로 고른다. 그리고 새 키 페어 생성 버튼을 눌러 키페어를 생성하면 pem 파일이 다운로드 받아진다.
보안 그룹을 생성한다. 기본 설정들은 그대로 둔다.
다른 모든 설정들은 그대로 두고 인스턴스를 시작 버튼을 누르면 인스턴스가 실행된다.
이제 ssh를 통해 인스턴스에 연결하자. Linux나 MacOS 에서는 ssh 명령이 내장되어 있어 일반 터미널으로도 연결할 수 있다. Windows의 경우 WSL2를 설치하거나 PuTTY를 사용해야만 한다. PuTTY는 ssh 클라이언트이다. 설치하면 PuTTY 내장 명령 프롬프트가 실행되며 ssh 명령을 실행할 수 있다.
실행중인 인스턴스에 들어가서 오른쪽 상단의 연결 버튼을 누른 후 지시를 따른다.
다운로드 받아진 pem 파일이 있는 폴더에서 chmod 400 secret.pem
, ssh -i "secret.pem" ec2-user@ec2-54-180-105-16.ap-northeast-2.compute.amazonaws.com
를 순서대로 입력하면 인스턴스에 연결된다.
연결이 완료됐으면 docker를 설치한다.
1. sudo yum update -y
명령어를 실행하여 인스턴스의 모든 필수 패키지가 최신 상태인지 확인한다.
2. sudo yum install docker
명령어를 실행하면 docker가 설치된다.
3. sudo service docker start
명령어로 도커를 실행하면 docker 명령어를 사용할 수 있다.
이제 인바운드 규칙을 수정해야 한다. 인스턴스의 보안 탭에 들어가면 인바운드 규칙과 아웃바운드 규칙이 있는데,
이다. 아웃바운드 규칙이 0.0.0.0
으로, 모든 IP 주소가 허용되어 있기 때문에 인스턴스 내부에서 docker hub에 접근하여 이미지를 받아올 수 있다. 하지만 인바운드 규칙에는 22번 포트 단 하나만 열려 있으며, 이는 ssh 포트이다. 이는 전 세계에 열려 있으며 이 때문에 pem
키 파일이 중요한 것이다.
인바운드 규칙을 수정하여 HTTP 포트를 노출시키자. HTTP는 기본적으로 80번 포트를 사용하고, 위에서 만든 nginx 이미지도 80번 포트를 사용하니 HTTP 포트만 노출시키면 된다.
이제 hub에 업로드한 이미지를 실행시켜보자.
docker run -d --rm --name jmdb -p 80:80 jwlee134/jmdb
혹시라도
permission denied
에러가 뜬다면 앞에sudo
를 붙이거나 여기를 참고한다.
실행한 후 인스턴스의 퍼블릭 IPv4 주소나 퍼블릭 IPv4 DNS를 주소창에 넣으면 잘 동작할 것이다.
소스코드를 업데이트 하기 위해 매번 ssh를 연결해서 hub에서 이미지를 받아오고 컨테이너를 다시 실행하는 것은 매우 귀찮은 일이다. 이를 자동화 하기 위해 프로젝트 레포지토리의 Actions 탭에서 set up a workflow yourself를 클릭하고 아래와 같이 작성한다.
name은 말 그대로 workflow의 이름이며 on은 workflow를 트리거할 이벤트를 명시한다.
jobs는 이벤트가 발생해 workflow가 실행되면 만들어지는 독립적인 환경들을 나열한다. 아래는 deploy라는 이름을 가진 하나의 job이 있고 여기서 모든 작업을 수행할 것이다. 독립적인 환경이므로 jobs 간의 데이터는 공유되지 않는다.
steps는 말 그대로 단계이며 수행할 작업들을 단계적으로 나열한다. 하나의 job에서 실행되므로 데이터가 공유된다.
name: Deploy
on:
push:
branches: [main] # main 브랜치에 push 발생하면 트리거
workflow_dispatch: # 디버깅용, actions 탭에서 직접 버튼 눌러서 트리거
jobs:
deploy:
runs-on: ubuntu-latest # ubuntu 최신 버전 환경에서 실행
steps:
# GitHub Actions는 해당 프로젝트를 만들어진 환경에 checkout하고 나서 실행한다.
# 마치 브랜치를 만들 때 checkout하는 것처럼 꼭 필요하다.
# 아래 코드는 누군가 만들어놓은 Action을 사용하는 것이다.
# 만들어놓은 Action을 사용할 때는 uses라는 키워드를 사용한다.
- name: Checkout
uses: actions/checkout@v3.5.2
# React 프로젝트이므로 해당 환경을 Node.js 위에서 실행하겠다고 명시한다.
# 마찬가지로 누군가 만들어 놓은 Action이다.
- name: Setup Node.js environment
uses: actions/setup-node@v2.5.2
with:
node-version: lts/Hydrogen
# push할 때마다 npm을 install 해야할까? 아니다.
# 해당 프로젝트의 node_modules가 변했는지 안 변했는지를 이용해서
# 모듈 변화가 있을 때만 npm install을 해줄 수도 있다.
- name: Cache node modules
# 그걸 제공하는 Action도 있다.
uses: actions/cache@v2.1.8
# 해당 step을 대표하는 id를 설정할 수도 있다. 해당 값은 뒤의 step에서 사용한다.
id: cache
with:
# node_modules라는 폴더를 검사하여
path: node_modules
# 아래 키값으로 cache가 돼있는지 확인한다.
key: npm-packages-${{ hashFiles('**/package-lock.json') }}
# 위 step에서 node_modules에 대한 cache 검사를 했다.
# 만약 모듈에 변한 게 있다면 `npm install`을 실행하고 아니면 해당 step을 건너뛰게 된다.
# if 키워드는 해당 스텝을 실행할지 말지를 결정할 수 있는 키워드이다.
# 위 step에서 정했던 cache라는 id를 steps.cache로 가져올 수 있다.
# cache라는 id 값을 가진 step에서는 cache-hit라는 output을 내뱉는다.
# 그걸로 cache가 hit 됐는지 안 됐는지를 알 수 있다.
# 그 값이 true가 아닐 때만 npm install을 한다.
# https://fe-developers.kakaoent.com/2022/220106-github-actions/
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm install
- name: Build
run: npm run build
# Docker에 연결하여 이미지를 빌드하고 Hub에 푸시한다.
# https://docs.docker.com/build/ci/github-actions/#step-three-define-the-workflow-steps
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/jmdb
# 마지막으로 ssh로 인스턴스에 연결하여 이미지를 Pull하고 컨테이너를 재시작한다.
- name: Pull and restart Docker Container
uses: appleboy/ssh-action@master
with:
key: ${{ secrets.SSH_KEY }}
host: ${{ secrets.HOST }}
username: ${{ secrets.USER }}
script: |
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/jmdb
docker stop jmdb
docker run -d --rm --name jmdb -p 80:80 ${{ secrets.DOCKERHUB_USERNAME }}/jmdb
secrets는 레포지토리 Settings -> Secrets and variables -> Actions에서 추가한다.
DOCKERHUB_USERNAME, DOCKERHUB_TOKEN은 각각 hub의 아이디와 비밀번호, HOST는 인스턴스의 퍼블릭 IPv4 주소, USER는 이전에 ssh로 인스턴스에 연결할 때 @
앞에 있던 ec2-user이다.
SSH_KEY는 몇 가지 설정이 필요하다(맥, 리눅스 기준으로 작성됨). 공식문서 참고
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
cat ~/.ssh/id_rsa.pub
명령어를 입력하여 출력되는 내용을 복사하고 ssh로 인스턴스에 연결한다.vim ~/.ssh/authorized_keys
입력 후 방금 복사한 내용을 다음줄에 복사하고 저장한다.여기까지 진행하였다면 로컬에서 ssh ec2-user@{인스턴스 퍼블릭 IPv4}
명령어를 입력했을때 pem key 없이도 인스턴스에 연결되어야 한다.
그 다음 로컬에서 cat ~/.ssh/id_rsa
명령어를 입력하여 출력되는 내용을 복사하여 SSH_KEY에 붙여넣는다.
이제 아래 Run workflow 버튼을 클릭하여 테스트를 해본다.
잘 작동하는 것을 볼 수 있다.
이제 main 브랜치에 push가 발생할 때마다 Github Actions가 React 프로젝트를 빌드하고 docker 이미지를 새로 빌드하며 hub에 업로드하고 ssh로 인스턴스에 연결하여 hub에서 이미지를 받아와 컨테이너를 재시작한다.
무중단 배포같은 고급 내용은 여기서 다루지 않는다.