[인프라] Docker와 EC2를 이용한 배포하기

windowook·2024년 8월 4일
post-thumbnail

🌱 도커를 사용하는 이유

이번 시간에는 과거 팀 프로젝트에서 배포를 위해 사용했던 도커의 기본적인 사용법과 도커의 compose를 어떻게 활용하는지 설명하도록 하겠습니다.

도커는 각 앱(리액트 앱, MySQL, Redis, 스프링부트 앱)을 Dockerfile로 빌드한 이미지 파일을 도커 컨테이너라는 호스트 OS에서 독립적인 프로그램처럼 실행합니다. 그래서 애플리케이션과 그 종속성을 함께 패키징하여 일관된 환경에서 실행할 수 있도록 해줍니다. 따라서 개발 환경과 운영 환경 간의 차이로 인한 문제를 줄일 수 있습니다.

저는 완성한 팀 프로젝트를 EC2 인스턴스에 배포하기 위해서 도커를 같이 사용하기로 결정했었습니다. 참고로 EC2에 배포하면 HTTPS와 도메인 네임도 적용해야하지만, 팀원들과 협의 끝에 외부 사용자가 사용할 수 있는 서비스로 오픈하지 않기로 해서 EC2에서 서비스를 정상적으로 구동해보는 단계까지만 진행했습니다다. HTTPS와 도메인 네임 적용은 NginX가 필요하기 때문에 기회가 된다면 나중에 올려볼려고 합니다. 이거도 처음했던 거라 시행착오때문에 시간을 많이 잡아먹었는데 결과가 크케 만족스럽지 않았습니다...

🌱 도커 & 도커 데스크탑 설치

https://www.docker.com/get-started/

도커를 사용하기 위해서는 도커를 설치해야 합니다. 로컬에서 도커 이미지를 생성하고 컨테이너 실행 테스트를 위해서 도커 설치는 필수입니다. 그리고 도커 허브에 이미지를 동기화시키는 데에도 필수적인 과정이죠. 위 링크로 접속하면 각자 운영체제 환경에 맞는 실행 프로그램을 다운로드 받을 수 있습니다.

https://www.docker.com/products/docker-desktop/

그리고 이 도커를 GUI로 편리하게 관리할 수 있는 프로그램인 도커 데스크탑도 같이 설치합니다.

도커 데스크탑은 필수는 아닙니다. 하지만 이미지가 생성되었는지 컨테이너가 돌아가는 중인지에 대한 정보를 GUI로 편하게 실행, 중지, 삭제 같은 조작을 할 수 있으니 설치하는 것을 추천드립니다. 그리고 도커를 모두 설치하고 나면 로컬에서 도커 허브의 계정과 연결시키기 위해 로그인이 필요합니다.

docker login

터미널에 다음과 같이 입력해주고 그 다음 Username, 비밀번호를 입력하라고 하니 그대로 입력해주면 됩니다.

🌱 Dockerfile로 프론트, 백 이미지 빌드

리액트로 구현한 프로젝트 디렉토리와 스프링부트로 구현한 프로젝트 디렉토리에 각각 도커 이미지 빌드를 위한 Dockerfile 작성이 필요합니다. 이미지를 생성하기 위한 문서이므로, 도커는 이 Dockerfile에 작성된 명령어를 차례로 실행하여 이미지를 만들어냅니다.

👉 Vite + yarn을 사용하는 리액트 프로젝트의 dockerfile

우선 dockerfile의 위치는 루트 디렉토리여야 합니다. dockerfile에서 실행되는 명령어로 도커가 각 필요한 파일의 경로에 모두 접근할 수 있어야하기 때문입니다. 그 다음, dockerfile을 자신이 사용한 프로젝트의 번들러와 패키지 매니저에 맞게 명령어를 명시해줍니다. 아래는 팀프로젝트에서 웹팩 번들러는 Vite, 패키지 매니저는 yarn berry를 사용중이라 그에 맞게 작성한 내용입니다.

FROM node

WORKDIR /app

COPY . .
COPY index.html .

RUN corepack enable
RUN yarn set version berry
RUN yarn install
RUN yarn add -D vite @vitejs/plugin-react
RUN yarn add vite-plugin-svgr
RUN yarn build

EXPOSE 3000

CMD ["yarn", "start"]

프로젝트에서 같은 환경을 사용한다면 똑같이 설정해주면 됩니다. dockerfile을 작성했다면 이제 터미널에서 명령어로 이미지를 빌드해줘야 합니다.

sudo docker build --platform linux/amd64 -t [도커 허브 아이디/원하는 이름:태그명] .
  • sudo : 관리자 권한을 부여하는 명령어로 제일 앞에 추가해주기
  • --platform : 빌드 플랫폼을 지정하는 명령어
  • -t : 이미지 이름을 설정하는 명령어

linux/amd64를 굳이 지정해준 까닭은 윈도우 환경에서 이미지를 빌드할 때는 EC2의 환경에서 호환되는 플랫폼인 amd64와 다르지 않아서 컨테이너로 이미지를 실행해도 오류가 발생하지 않는데 macOS 환경에서 이미지를 빌드하면 해당 이미지의 플랫폼이 linux/arm64로 설정됩니다. 빌드하는 환경의 CPU를 기준으로 이미지의 플랫폼도 그 환경에서 실행되도록 자동으로 설정되기 때문입니다. 그래서 macOS에서 이미지를 빌드해서 EC2에서 환경에서 사용하고자 한다면 이미지를 생성할 때 꼭 --platform linux/amd64를 포함시켜줘야 합니다.

명령어를 실행하고 에러가 없이 완료되면, 도커 데스크탑을 확인했을 때 이미지가 생성된 것을 확인할 수 있습니다.

👉 스프링 부트 프로젝트의 Dockerfile

마찬가지로 스프링 부트 프로젝트도 루트에 Dockerfile을 위치시켜줍니다.
인텔리제이를 켜서 파일을 생성하고 작성해주면 됩니다. 아래는 팀프로젝트의 환경에 맞게 작성한 내용입니다.

FROM openjdk:17-jdk-slim

VOLUME /tmp

ARG JAR_FILE=build/libs/refrigerator-cleaner-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar

ENTRYPOINT ["java","-jar","/app.jar"]

이미지를 빌드하기 위해서 터미널에서 명령어를 실행해주면 됩니다.

docker build --platform linux/amd64 -t [도커 허브 아이디/원하는 이름:태그명] .

🌱 MySQL, REDIS 이미지 pull

이제 클라이언트와 서버를 이미지로 빌드했으니 데이터베이스에 관련된 이미지를 만들어야 합니다.
로컬에서 이미 사용중이던 MySQL과 REDIS를 이용해서 이미지를 빌드해도 되지만, EC2로 오픈할 서비스에서는 데이터베이스가 초기화 된 상태여도 상관없기 때문에 도커 허브에 이미 올라가 있는 공식 이미지를 pull로 받아와서 사용하는 방법을 택했습니다. 이때 명령어는 다음과 같습니다.

docker --platform linux/amd64 pull mysql:latest
docker --platform linux/amd64 pull redis:latest

이렇게 받아온 이미지를 이름 그대로 사용해도 되지만, 각 이미지들의 통일성과 도커 허브에 동기화를 위해 이름을 변경할 수도 있습니다.

docker tag mysql [도커 허브 아이디/원하는 이름:태그명]
docker tag redis [도커 허브 아이디/원하는 이름:태그명]

tag 명령어는 존재하는 이미지를 내가 원하는 이미지명으로 변경시켜 줍니다.

결과적으로 총 4개의 이미지를 생성하면 다음과 같이 확인할 수 있습니다.

🌱 Docker compose

이미지 생성이 완료되었다면 이제 컨테이너로 이 이미지들을 실행하여 로컬에서 앱을 실행하던 것처럼 잘 돌아가는지, API 호출도 정상적으로 응답을 하고 데이터베이스에 저장도 잘 되는지 확인해봐야 합니다.

여기서 각 컨테이너를 따로 실행하면 문제가 발생합니다. 도커 컨테이너는 하나의 컨테이너가 개별적인 호스트, 즉 하나의 OS에 하나의 이미지를 실행하는 방식이므로 서로 다른 이미지는 서로 다른 호스트에서 실행되고 있는 상태입니다. 리액트 앱에서 사용자가 보내는 http 요청은 다른 컨테이너에서 실행중인 서버로 요청이 전송되지 않습니다.
다른 PC에서 서로 로컬에 실행하고 있는 것이라 생각하면 됩니다.

그래서 하나의 컨테이너에서 실행되고 있는 것처럼 통합 실행을 하게 만드는 도구가 Docker compose입니다. 도커 컴포즈는 여러 개의 도커 컨테이너를 정의하고 실행할 수 있는 도구라고 생각하시면 됩니다.

위의 다이어그램이 도커 컴포즈의 메커니즘을 보여줍니다. docker-compose.yml이라는 파일에 도커 이미지와 그 이미지가 실행될 컨테이너 이름을 명시하고, 플랫폼, 환경설정, 종속, 포트 등을 설정해줍니다.

그럼 도커 컴포즈가 yml에 명시해준 이미지들로 실행하는 컨테이너를 통합으로 묶어줍니다. 이 안에서는 네트워크 설정이 따로 필요하지 않고 포트 설정만 해주면 API 호출도 주고받을 수 있기 때문에 클라이언트와 서버 간 통신과 데이터베이스 저장도 정상적으로 이루어집니다.

👉 docker-compose.yml 작성

도커 컴포즈를 실행하기 위해서는 그 세팅을 하기 위한 야믈(yml, yaml) 파일을 생성하여 내용을 저장해줘야 합니다. 리액트, 스프링 부트, SQL, Redis와 같이 앱 실행에 필요한 이미지들의 플랫폼과 환경변수, 포트 설정 등의 내용을 담습니다.

services:
  mysql:
    platform: linux/amd64
    image: wookoow/refri-sql:latest
    container_name: alc_sql
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PW}
      MYSQL_DATABASE: ${DB_NAME}
    volumes:
      - mysql_data:/var/lib/mysql

  redis:
    platform: linux/amd64
    image: wookoow/refri-redis:latest
    container_name: alc_redis
    ports:
      - "6379:6379"
    environment:
      REDIS_PASSWORD: ${REDIS_PW}

  spring_app:
    platform: linux/amd64
    image: wookoow/refri-back:latest
    container_name: alc_back
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      EMAIL_USER: ${EMAIL_USER}
      EMAIL_PW: ${EMAIL_PW}
      DB_URL: ${DB_URL}
      DB_USER: ${DB_USER}
      DB_PW: ${DB_PW}
      OAUTH_GOOGLE_CI: ${OAUTH_GOOGLE_CI}
      OAUTH_GOOGLE_SECRET: ${OAUTH_GOOGLE_SECRET}
      OAUTH_KAKAO_CI: ${OAUTH_KAKAO_CI}
      OAUTH_KAKAO_SECRET: ${OAUTH_KAKAO_SECRET}
      OAUTH_NAVER_CI: ${OAUTH_NAVER_CI}
      OAUTH_NAVER_SECRET: ${OAUTH_NAVER_SECRET}
      REDIS_PW: ${REDIS_PW}
      S3_BUCKET: ${S3_BUCKET}
      S3_REGION: ${S3_REGION}
      S3_ACCESS_KEY: ${S3_ACCESS_KEY}
      S3_SECRET_KEY: ${S3_SECRET_KEY}
      JWT_SECRET_KEY: ${JWT_SECRET_KEY}
      AI_KEY: ${AI_KEY}
    ports:
      - "8080:8080"
    depends_on:
      - mysql
      - redis

  react_app:
    platform: linux/amd64
    image: wookoow/refri-front:latest
    container_name: alc_front
    ports:
      - "3000:3000"
    depends_on:
      - spring_app

volumes:
  mysql_data:

환경변수 .env 파일에 저장해 둔 값들을 참조하기 위해서 docker-compose.yml은 .env가 있는 디렉토리에 같이 위치해 있어야 합니다. 로컬에서는 서버에서 환경변수의 값들을 사용하기 때문에 스프링부트 프로젝트의 루트에 위치시켰지만 EC2에서는 따로 디렉토리를 하나 만들어서 그 안에 같이 뒀습니다.

yml 파일을 작성한 후에 그 위치로 이동하거나 혹은 인텔리제이 터미널에서 바로 명령어를 입력하여 컴포즈를 실행합시다.

docker-compose -p [그룹 컨테이너 이름] up -d

컴포즈로 컨테이너 실행에 성공하면 이렇게 컨테이너 탭에서 실행 중인 그룹 컨테이너를 확인할 수 있습니다.

만약 여기까지 성공했고, localhost:3000으로 접속이 가능하며 API 호출에도 문제가 없다면 docker-compose.yml은 깃허브의 리포지토리를 하나 생성해서 push합니다.

이렇게 해두면 EC2 환경에서 로컬에서 했던 것과 동일하게 이미지를 이용하여 도커 컴포즈로 그룹 컨테이너를 실행하는 방식을 그대로 사용해서 서비스를 배포하면 됩니다.

🌱 EC2 인스턴스 생성

테스트를 끝내고 도커 허브에 이미지도 올려뒀으니 EC2 인스턴스를 만듭니다.
AWS 계정이 대부분 이미 있겠지만, 없다면 계정부터 생성해 주도록 합시다.
따라하면서 계정을 생성할 수 있는 포스트의 링크를 아래 걸어놨습니다.

https://www.lainyzine.com/ko/article/how-to-create-an-amazon-web-services-account/

AWS에 로그인을 하고 나서 지금부터 EC2 인스턴스를 생성하기 위해 그대로 따라하면 됩니다.

👉 AWS 콘솔

AWS 홈페이지로 접속했다면 우측 상단의 네비게이션 메뉴에서 '콘솔에 로그인'을 클릭합니다.

https://ap-northeast-2.console.aws.amazon.com/console/home?region=ap-northeast-2#

혹은 위에 첨부한 링크로 접속해서 바로 콘솔 홈으로 들어가도 됩니다.
내 계정의 리전 설정이 서울(northeast-2)로 되어있기 때문에 들어가면 서울 리전으로 되어있을겁니다.

이렇게 콘솔 홈으로 갔다면 대시보드에 '최근에 방문한 서비스'나 상단의 네비게이션 바에 즐겨찾기로 등록한 EC2로 곧바로 이동할 수도 있고, 원래 아무런 설정을 해두지 않은 상태라면 검색 상자에서 EC2를 치면 마찬가지로 EC2 페이지로 이동하게 됩니다.

👉 EC2 홈

EC2 페이지로 들어오게 되면 다음과 같은 화면을 만날 수 있습니다.
우리가 이번 배포를 하면서 필요한 건 여기서 '보안 그룹'과 '인스턴스'입니다.

👉 보안 그룹 설정

먼저 보안 그룹을 새로 생성해줍시다.

기본 보안 그룹은 원래 4개가 존재합니다. 이 4개는 default와 launch-로 시작하는 3개의 보안 그룹입니다.
캡쳐를 보면 내가 만들어 둔 refri-security라는 보안 그룹이 보이는데 이걸 참고해서 어떻게 설정할 지 보여주려고 합니다. 처음 생성해야하니 우측 상단에 '보안 그룹 생성' 버튼을 클릭합시다.

그럼 이런 페이지가 나오는데 위에서부터 보안 그룹 이름을 적어주고, 이 보안 그룹이 어떤 서비스와 관련된건지 혹은 어떤 규칙을 담고 있는지 간단한 메모 정도를 설명에 적어주면 됩니다. VPC는 건드릴 필요 없습니다.

그 다음 인바운드 규칙과 아웃바운드 규칙을 정해주는 단계가 있습니다.

  • 인바운드 규칙 : 외부에서 EC2 인스턴스로 접속하는 트래픽을 제어하는 규칙
  • 아웃바운드 규칙 : EC2 인스턴스에서 외부로 나가는 트래픽을 제어하는 규칙

기본적으로 아웃바운드 규칙은 0.0.0.0/0으로 설정되어있는데 건드려 줄 필요가 없고, 인바운드 규칙에서는 직접 설정을 해줘야 합니다.

설정은 캡쳐와 같이 동일하게 해주면 됩니다. 다른 기본 보안 그룹을 보면 SSH, HTTP, HTTPS 유형의 3개의 규칙만 추가되어있을텐데, 우리는 EC2에서 localhost:3000으로 접속하는 리액트 앱을 실행시켜주기 때문에 해당 포트도 열어줘야 합니다.

👉 인스턴스 만들기

보안 그룹도 미리 만들어 줬으니 이제 인스턴스를 생성하면 됩니다. 인스턴스를 생성하면서 보안 그룹을 선택해야하기 때문에 먼저 보안 그룹을 만들어 준 것입니다. 인스턴스 페이지로 들어왔다면 우측 상단에 '인스턴스 시작' 버튼을 눌러줍시다.

버튼을 누르면 인스턴스 생성 페이지로 이동합니다. 그럼 다음과 같은 화면을 마주하게 됩니다.
우선 이름을 적어줍니다. 이름은 서비스와 동일하게 해주거나 리포지토리랑 같게 해주시면 됩니다.

그 다음은 인스턴스의 운영체제를 선택합니다. 우리는 Ubuntu 리눅스를 사용하는게 목적이니 Ubuntu를 선택하고 LTS는 24.04 SSD 볼륨 타입을 선택합시다.

이제 인스턴스 유형을 예산에 맞게 선택하면 되는데, 프리 티어를 사용하지 않았고 배포할 앱의 이미지도 프리 티어 수준의 인스턴스가 감당할 수 있다면 프리티어를 사용하면 됩니다.

프리 티어가 t2.micro인데 보다시피 기본 메모리와 CPU 용량이 매우 낮습니다.
가벼운 앱은 아마 돌릴 수 있을텐데 램 1기가로 컨테이너를 다 돌릴 수 있을지는 모르겠습니다.
팀 프로젝트는 프리 티어 인스턴스에서 램이 터져서 멈추길래 t3.small로 선택하여 다시 만들었었습니다.

그 다음은 키 페어 선택인데, 이미 만들어놨었던 키 페어를 사용해도 되고 새로 만들어서 이 인스턴스의 SSH 접속을 위한 키로 사용해도 됩니다. 저는 인스턴스를 위한 키 페어를 따로 생성해서 바탕화면에 디렉토리를 하나 만들어서 보관했습니다. 키 페어는

RSA 유형으로 .pem 확장자 형식으로 생성하면 됩니다.
키페어는 분실하면 나중에 문제가 발생하기 때문에 저장해 둔 위치를 꼭 기억해둬야 합니다.

다음으로는 네트워크 설정입니다. 네트워크 설정에서 보안 그룹을 선택하면 되는데, 아까 봤듯이 인바운드 규칙, 아웃바운드 규칙을 정해 놓은 규칙이므로 네트워크 설정과 관련이 있음을 알 수 있습니다. 방화벽(보안 그룹)을 '기존 보안 그룹 선택'으로 체크하고 아까 만들어 둔 그룹을 선택하시면 됩니다.

그리고 스토리지 구성을 설정해주면 된다. 스토리지는 SSD 볼륨을 말합니다.
최대 30기가 까지는 사용할 수 있기 때문에 일반적으로 맥시멈까지 지정해두는 편입니다.

마지막으로 고급 설정에서 '구매 옵션'에 스팟 인스턴스를 체크 해줍니다.
스팟 인스턴는 온디맨드 인스턴스보다 저렴한 가격으로 인스턴스를 사용할 수 있는 옵션입니다.
최대 가격을 설정해서 그 가격을 넘어가면 인스턴스가 중단되도록 만들어두는 일종의 상한선 기능입니다.
서비스를 무기한으로 배포해 둘 예정인데 따로 확인하기 귀찮다면 최소한 스팟 가격은 설정해두는 게 비용 관리에는 효과적일 수 있습니다.

🌱 도커 허브에 이미지 올려놓기

로컬에서 이미지들이 문제없이 실행되는 걸 확인했으니, EC2에서 이 이미지들을 내려받아 도커 컴포즈로 그룹핑하여 앱을 실행하는 방법을 똑같이 하면 됩니다. 로컬에서 진행했던 모든 이미지 빌드와 세팅을 할 필요가 없으니 모두 허브에 올려두고 EC2에서는 pull을 하여 컴포즈만 실행하는 방식입니다.

단, 로컬에서 테스트했던 이미지를 그대로 올리면 안 됩니다. EC2 인스턴스 안에서 컴포즈가 이미지들 간에 통신을 담당하지만, 우리가 접속하는 경로는 인스턴스의 IPv4 경로에서 이뤄집니다.

그래서 카카오, 구글의 리다이렉트 경로를 localhost로 된 부분은 다 IPv4로 바꿔주고, API를 호출할 때의 경로도 모두 변경해준 뒤에 이미지를 다시 만들어줘야 합니다. 그런 다음 도커 허브로 네 개의 이미지를 올려줍시다.

docker push wookoow/refri-front:latest
docker push wookoow/refri-back:latest
docker push wookoow/refri-sql:latest
docker push wookoow/refri-redis:latest

허브에 잘 올라갔는지 확인을 해줍시다.

🌱 EC2에서 배포

👉 도커 설치하기

만들어진 인스턴스의 인스턴스 ID를 클릭하여 접속해서, '연결'을 눌러줍시다.

이대로 연결을 다시 눌러줍니다.

그럼 이렇게 우분투로된 EC2의 인스턴스, 즉 AWS가 주는 PC를 하나 받은 셈이 됩니다.
이제 여기에 도커를 설치해주면 되는데 CLI만 사용할 수 있으므로 명령어를 통해 설치해줍시다.
아래는 도커와 도커 컴포즈를 설치하는데 사용했던 명령어를 정리해뒀습니다.

# Add Docker's official GPG key:

sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

# /usr/local/bin 위치에 docker-compose 최신버전 설치
sudo service docker start
sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose

👉 docker-compose.yml과 .env

먼저 루트 디렉토리에서 새로운 디렉토리를 하나 생성해줍니다. 이름은 대충 'app-compose'라고 한다 치면
디렉토리로 들어가서 깃허브에 올려둔 docker-compose.yml을 pull 하는 방식으로 가져오면 됩니다.

하지만 .env는 환경변수 파일입니다. 이를 private로 전환한 뒤에 깃허브로 올려서 pull을 받아도 되지만
추천하는 방법은 아니고 nano .env를 사용해서 파일을 생성하고 내용을 붙여넣는 방식을 추천합니다.

여튼 이 두 개의 파일을 한 디렉토리 안에 위치시켜주면 됩니다.

👉 이미지 내려받기

로컬에서 도커 허브로 올려뒀던 미리 생성해 둔 이미지들을 내려받읍시다.

sudo docker pull wookoow/refri-front:latest
sudo docker pull wookoow/refri-back:latest
sudo docker pull wookoow/refri-sql:latest
sudo docker pull wookoow/refri-redis:latest

docker images를 입력해서 받아온 네 개의 이미지가 잘 있는게 확인이 됐다면 컴포즈를 실행합니다.

docker-compose -p refrigerator-alchemist up -d

sudo docker ps를 입력하여 그룹핑된 컨테이너가 잘 실행되는 것을 확인하고 난 다음에, http://인스턴스의IPv4주소:3000으로 접속해서 서버로부터 데이터를 잘 받아오는 것 까지 확인된다면 배포에 성공한 것입니다.

HTTPS 인증서 발급과 도메인 연결 작업은 하지 못해서 아쉬웠습니다. TLS 발급에도 시간을 투자했으나, 계속 해결이 안 되었고 더 시간을 할애할만큼 여유가 없어서 훗날로 미뤘습니다.. 노드를 배워서 풀스택으로 개발을 하게 된다면 서버를 도커와 EC2를 이용해서 배포해 볼 생각입니다.

profile
안녕하세요

0개의 댓글