배포 자동화
Docker
와 Github Actions
를 활용해 배포 자동화를 구축하기 위해 간단하게는 Dockerfile, docker-compose.yml, Github 설정 및 Github Actions 워크플로우 파일을 작성해줘야 한다. 오늘은 그중 Dockerfile과 docker-compose.yml에 대해 알아보자.
Dockerfile을 통한 이미지 생성
Dockerfiile
을 통해 docker image를 만들 수 있다. 어떤 docker base image에 파일을 작송하고 컨테이너를 실행시키는 과정으로 이미지를 빌드할 수 있다. Dockerfile
은 결국 이 과정을 담는 스크립트의 파일명이 된다.
문법
- FROM : 베이스 이미지 지정
- RUN : 커맨드를 실행하기 위해 사용
- ENV : 환경 변수 설정
- ADD : 파일/디렉토리 추가
- COPY : 파일 복사
- WORKDIR : 작업 디렉토리
- CMD : 컨테이너 실행 명령
- ENTRYPOINT : 컨테이너 실행 명령
먼저 작성한 Dockerfile 예제는 다음과 같다.
# server base iamge - java 17
FROM adoptopenjdk/openjdk17
# copy .jar file to docker
COPY ./build/libs/novelpark-0.0.1-SNAPSHOT.jar app.jar
# always do command
ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "app.jar"]
FROM
FROM절을 수행할 때 여러 조각을 쌓는 것처럼 보이는데 이를 레이어(Layer)라고 한다.
docker 컨테이너가 실행되면 모든 읽기 전용 레이어들을 순서대로 쌓은 후 쓰기 가능한 신규 레이어를 쌓게 된다. 그다음 컨테이너 내부에서 발생한 결과물들이 쓰기 가능 레이어를 기록되게 한다.
즉, 아무리 많은 도커 컨테이너를 실행해도 기존 읽기 전용 레이어는 변하지 않고 컨테이너마다 생성된 쓰기 가능 레이어에 데이터가 쌓여 서로 겹치지 않게 된다.
FROM 절은 이 Base image를 지정하기 위한 명령어이다.
위의 Dockerfile
에서 Basew image로 adoptopenjdk/openjdk11 을 사용했다.
COPY
호스트 컴퓨터의 파일을 도커 컨테이너 내부의 파일 시스템으로 복사하기 위해 사용되는 명령어이다.
빌드된 .jar 파일을 컨테이너의 파일시스템에 app.jar라는 이름으로 복사한다.
ENTRYPOINT
이미지가 실행되었을 때 항상 실행되어야 하는 커맨드를 정의할 때 사용된다. 컨테이너를 실행파일처럼 사용할 수 있을 때 유용하다.
컨테이너가 올라가면 스프링부트 애플리케이션을 구동한다.
백엔드 프로젝트에 사용한 Dockerifle
# OpenJDK 17과 Gradle 지정
FROM gradle:8.7-jdk17 AS build
# 작업 디렉토리 설정
WORKDIR /app
# 필요한 파일 복사
COPY build.gradle settings.gradle ./
# 종속성 다운로드
RUN gradle dependencies --no-daemon
# 소스 코드 복사
COPY . /app
# 빌드 실행
RUN gradle clean build --no-daemon
# 런타임 베이스 이미지 지정
FROM openjdk:17-slim
# 작업 디렉토리 설정
WORKDIR /app
# 빌드된 JAR 파일 복사
COPY --from=build /app/build/libs/*.jar /app/mingle.jar
# 포트 노출
EXPOSE 8080
# 애플리케이션 실행
ENTRYPOINT ["java", "-jar", "/app/mingle.jar"]
위의 Dockerfile
은 멀티스테이지 빌드는 빌드 환경과 런타임 환경을 분리해 최종 이미지 크기를 줄이고 보안을 강화해 빌드 과정과 런타임 환경을 독립적으로 유지할 수 있도록 해준다.
Dockerfile의 자세한 설명은 다음과 같다.
FROM gradle:8.7-jdk17 AS build
Gradle8.7과 JDK17이 포함된 이미지를 사용한다. 빌드 도구와 JDK를 모두 갖추고 있다. 'AS build'는 이 단계를 "build"라는 이름으로 참조하기 위해 사용된다.
WORKDIR /app
컨테이너 내에서의 작업 디렉토리를 '/app'으로 설정했다.
COPY build.gradle settings.gradle ./
호스트 머신의 'build.gradle'과 'setting.gradle' 파일을 컨테이너의 'app' 디렉토리로 복사한다. 이는 종속성 다운로드에 필요한 작업이다.
RUN gradle dependencies --no-daemon
Gradle을 사용해 필요한 모든 종속성을 다운로드한다. '--no-deamon' 옵션은 데몬 모드를 사용하지 않도록 한다.
COPY . /app
현재 디렉토리의 모든 소스 코드를 컨테이너의 '/app' 디렉토리로 복사한다.
RUN gradle clean build --no-daemon
Gradle을 사용해 프로젝트르 빌드한다. 이 과정에서 JAR 파일이 생성된다
FROM openjdk:17-slim
경량화된 JDK17 이미지를 사용한다. 빌드 도구가 필요 없어 슬림 이미지를 사용해 이미지 크기를 줄인다.
WORKDIR /app
컨테이너 내에서의 작업 디렉토리를 '/app'으로 설정했다.
COPY --from=build /app/build/libs/*.jar /app/mingle.jar
위에서 생성된 JAR파일을 런타임 스테이지로 복사한다. '--from=build'는 첫 번째 빌드 스테이지에서 복사하는 것을 뜻한다.
EXPOSE 8080
컨테이너가 외부와 통신할 포트 8080을 노출한다.
ENTRYPOINT ["java", "-jar", "/app/mingle.jar"]
컨테이너가 시작되면 'java -jar /app/mingle.jar' 명령어를 실행해 Spring Boot 애플리케이션을 시작한다.
즉, 빌드 단계에서 Gradle 이미지를 사용해 종속성을 다운로드하고 애플리케이션을 빌드한다. 그 후 빌드 결과물(JAR파일)을 생성한다.
런타임 단계에서 슬림 JDK 이미지를 사용해 빌드된 JAR 파일을 복사한다. 그 후 컨테이너를 시작할 때 Spring Boot 애플리케이션을 실행하게 되는 단계를 밟게 된다.
멀티스테이지 빌드 형식을 사용한 이유는 빌드 환경과 런타임 환경을 분리해 최종 이미지 크기를 줄이고 보안을 강화해 빌드 과정과 런타임 환경을 독립적으로 유지할 수 있도록 해준다.
이후 docker-compose.yml 파일을 작성한다.
docker-compose
docker compose란?
단일서버에서 여러 개의 컨테이너를 하나의 서비스로 정의해 컨테이너의 묶음으로 관리할 수 있는 작업환경을 제공하는 관리도구이다.
docker compose
를 사용하는 이유는 여러 개의 컨테이너가 하나의 어플리케이션으로 동작할 때 docker compose
를 사용하지 않는다면 각각의 컨테이너를 하나씩 생성해야 한다. 이를테면 웹 어플리케이션을 테스트하려면 웹 서버 컨테이너, 데이터베이스 컨테이너 두 개의 컨테이너를 각각 생성해야한다.
docker compose
를 사용하지 않을 경우 아래와 같이 두 개의 run 명령어를 입력해야 한다.
$ docker run --name wordpress_db -d mysql:8
$ docker run -d -p 8080:80 \
--link wordpress_db:mysql --name holyplace_wordpress \
wordpress:latest
위의 명령어로 wordpress와 mysql 컨테이너를 생성한다.
이와 같은 여러 개의 컨테이너로 구성된 어플리케이션 구축을 위해 run 명령어를 여러 번 사용할 수 있지만 각 컨테이너가 제대로 동작하는지 확인하는 테스트 단계에서 이런 식으로 여러 개의 컨테이너를 실행하기는 번거롭다. 이때 docker compose
를 통해 여러 개의 컨테이너를 하나의 서비스로 정리해 컨테이너 묶음으로 관리할 수 있다면 더 편리할 것이다.
docker compose
는 여러 개의 컨테이너의 옵션과 환경을 정의한 파일을 읽어 컨테이너를 순차적으로 생성하는 방식으로 동작한다. docker compose
의 설정 파일은 도커 엔진의 run 명령어의 옵션을 그대로 사용할 수 있으며 각 컨테이너의 의존성, 네트워크, 볼륨 등을 함께 정의할 수 있다.
docker-compose.yml 작성 및 활용
기존의 docker run 명령어로 컨테이너 생성
$ docker run -d --name wordpress_db \
--network seunghwan_network \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=seosh817 \
-e MYSQL_DATABASE=seosh817 \
-e MYSQL_USER=seosh817 \
-e MYSQL_PASSWORD=seosh817 \
-v mysql:/var/lib/mysql \
--restart unless-stopped \
mysql:8
$ docker run -d --name seunghwan_wordpress \
--network seunghwan_network \
-p 8080:80 \
--link wordpress_db:mysql \
-e WORDPRESS_DB_HOST=db:3306 \
-e WORDPRESS_DB_USER=seosh817 \
-e WORDPRESS_DB_PASSWORD=seosh817 \
-e WORDPRESS_DB_NAME=seosh817 \
--restart unless-stopped \
wordpress:latest
docker-compose.yml로 도커 컴포즈 프로젝트 실행
version: '3.9'
services:
db:
image: mysql:8
volumes:
- db:/var/lib/mysql
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=seosh817
- MYSQL_DATABASE=seosh817
- MYSQL_USER=seosh817
- MYSQL_PASSWORD=seosh817
networks:
- wordpress
wordpress:
depends_on:
- db
image: wordpress:latest
ports:
- "8000:80"
restart: unless-stopped
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: seosh817
WORDPRESS_DB_PASSWORD: seosh817
WORDPRESS_DB_NAME: seosh817
networks:
- wordpress
volumes:
db: {}
networks:
wordpress: {}
* yaml 파일에서의 들여 쓰기는 탭(Tab)이 아닌 2개의 공백(Space)을 사용해 하위 항목을 구분해야 한다.
파일의 옵션
version
yaml 파일 포맷의 버전을 나타낸다. docker compose
버전마다 사용하는 yaml 포맷 버전이 있어 docker compose
버전은 도커 엔진 버전에 의존성이 있으므로 가능하면 최신 버전을 사용하는 것이 좋습니다.
services
docker compose
로 생성할 컨테이너 옵션을 정의한다. 이 항목에 쓰인 각 서비스는 컨테이너로 구현되어 하나의 프로젝트로서 docker compose
에 의해 관리된다. 서비스의 이름은 services의 하위 항목으로 정의하고 컨테이너의 옵션은 서비스 이름의 하위 항목의 정의된다. 즉, 생성될 컨테이너들을 묶어놓는 단위이다.
services:
container_1:
image: ...
container_2:
image: ...
image
서비스의 컨테이너를 생성할 때 쓰일 이미지의 이름을 설정한다. 이미지 이름 포맷은 docker run과 같으며 만일 이미지가 도커에 존재하지 않으면 도커 허브에서 자동으로 내려받는다.
services:
container_1:
image: holyplace/composetest:mysql
links
docker run 명령어의 --link와 같으며 다른 서비스에 서비스명만으로 접근할 수 있도록 설정한다. [SERVICE:ALIAS] 형식을 사용하면 서비스에 별칭으로도 접근할 수 있다.
services:
web:
links:
- db
- db:database
- redis
environment
docker run 명령어의 --env, -e 옵션과 동일하다. 서비스의 컨테이너 내부에서 사용할 환경변수를 지정하며 딕셔너리(Dictionary)나 배열 형태로 사용할 수 있다.
services:
web:
environment:
- MYSQL_ROOT_PASSWORD=mypassword
- MYSQL_DATABASE_NAME=mydb
or
environment:
- MYSQL_ROOT_PASSWORD:mypassword
- MYSQL_DATABASE_NAM:mydb
command
컨테이너가 실행될 때 수행할 명령어를 설정하며 docker run 명령어의 마지막에 붙는 커맨드와 같다. Dockerfile의 RUN과 같은 배열 형태로도 사용할 수 있다.
services:
web:
image: holyplace/composetest:web
command: apachectl -DFOREGROUND
or
web:
image: holypalce/composetest:web
command: [apachectl, -DFOREGROUND]
depends_on
특정 컨테이너에 대한 의존 관계를 나타내며 이 항목 명시된 컨테이너가 먼저 생성되고 실행된다. 다음 예제에서는 web 컨테이너보다 mysql 컨테이너가 먼저 생성된다.
services:
web:
iamge: holyplace/composetest:web
depends_on
- mysql
mysql:
image: holyplace/composetest:mysql
특정 서비스의 컨테이너만 생성하되 의존성이 없는 컨테이너를 생성하려면 --no-deps 옵션을 사용한다.
port
docker run 명령어의 -p와 같으며 서비스 컨테이너를 개방할 포트를 설정한다. 그러나 단일 호스트 환경에서 80:80과 같이 호스트의 특정 포트를 서비스의 컨테이너에 연결하면 docker-compose scale 명령어로 서비스의 컨테이너의 수를 늘리 수 없다.
services:
web:
image: holyplace/composetest:web
ports:
- "8080"
- "8081-8100"
- "80:80"
...
build
build 항목에 정의된 Dockerfile에서 이미지를 빌드해 서비스의 컨테이너를 생성하도록 설정한다.
services:
web:
build: ./composetest
image: holyplace/composetes:web
위의 예제는 ./composetest 디렉토리에 저장된 도커파일로 이미지를 빌드해 컨테이너를 생성한다. 새롭게 빌드될 이미지의 이름은 imaga 항목에 정의된 이름인 holyplace/composetest:web이 된다.
또한 build 항목에서는 Dockerfile에 사용될 컨텍스트나 Dockerifle에서 사용될 인자 값을 설정할 수 있다. 다음과 같이 image 항목을 설정하지 않으면 이미지의 이름은 [프로젝트 이름] :[서비스 이름]이 된다.
services:
web:
build: ./composetest
context: ./composetest
dockerfile: myDokcerfile
args:
HOST_NAME: web
HOST_CONFIG: self_config
extends
다른 yaml 파일이나 현재 yaml 파일에서 서비스 속성을 상속받도록 설정한다.
아래의 docker-compose.yml의 web 서비스는 extends_compose.yml의 extends_web 서비스의 옵션을 그대로 갖게 된다. 즉, web 서비스의 컨테이너는 ubuntu:focal 이미지의 80:80 포트로 설정된다. fiel 항목을 설정하지 않으면 현재 yaml 파일에서 extends 할 서비스를 찾는다.
docker-compose.yml
version: '3.9'
services:
web:
extends:
file: extend_compose.yml
service: extend_web
extend_compose.yml 파일
version: '3.9'
services:
web:
extend_web:
image: ubuntu:focal
ports:
- "80:80"
백엔드 프로젝트에서 사용할 docker-compose.yml
version: '3.9'
services:
backend:
image: kwonseongji129/mingle-backend:latest
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:h2:mem:mingledb
- SPRING_DATASOURCE_USERNAME=sa
- SPRING_DATASOURCE_PASSWORD=
- SPRING_JPA_HIBERNATE_DDL_AUTO=create
- SPRING_H2_CONSOLE_ENABLED=true
- SPRING_H2_CONSOLE_SETTINGS_WEB_ALLOW_OTHERS=true
# 아직은 H2 사용하는 관계로 우선은 주석처리 함.
# mariadb:
# image: mariadb:latest
# environment:
# - MYSQL_DATABASE=mingle
# - MYSQL_USER_HOST='%'
# - MYSQL_ROOT_PASSWORD=1234
# volumes:
# - mariadb_data:/var/lib/mysql
# ports:
# - "3306:3306"
redis:
image: redis:latest
ports:
- "6379:6379"
# 동일하게 H2 사용중으로 주석처리
#volumes:
# mariadb_data:
현재 redis와 Spring Boot 컨테이너를 정의하고 해당 컨테이너들을 함께 배포하고 있다. 현재는 MariaDB를 사용하지 않고 H2 콘솔을 사용하고 있기 때문에 MariaDB 컨테이너를 정의하는 부분은 주석처리가 되어있다.
version: '3.9'
3.9 버전을 사용하고 있다.
1) backend 서비스
이 서비스는 Spring Boot 애플리케이션을 실행하는 컨테이너를 정의하고 있다.
이미지
image: kwonseongji129/mingle-backend:latest
'kwonseongji129/mingle-backend:latest'라는 이름의 Docker 이미지를 사용한다.
포트 매핑
ports:
- "8080:8080"
호스트의 포트 8080을 컨테이너의 포트 8080에 매핑판다. 이는 외부에서 접근할 수 있도록 하기 위함이다.
환경변수
environment:
- SPRING_DATASOURCE_URL=jdbc:h2:mem:mingledb
- SPRING_DATASOURCE_USERNAME=sa
- SPRING_DATASOURCE_PASSWORD=
- SPRING_JPA_HIBERNATE_DDL_AUTO=create
- SPRING_H2_CONSOLE_ENABLED=true
- SPRING_H2_CONSOLE_SETTINGS_WEB_ALLOW_OTHERS=true
Spring Boot 애플리케이션이 H2 메모리 데이터베이스를 사용하도록 설정하는 환경 변수이다.
- 'SPRING_DATASOURCE_URL=jdbc:h2:mem:mingledb' : H2 데이터베이스의 메모리 모드를 사용한다.
- 'SPRING_DATASOURCE_USERNAME=sa' : 데이터베이스 사용자 이름을 'sa'로 설정한다.
- 'SPRING_DATASOURCE_PASSWORD=' : 데이터베이스 비밀번호는 없다.
- 'SPRING_JPA_HIBERNATE_DDL_AUTO=create' : Hibernate가 데이터베이스 스키마를 자동으로 생성하도록 설정한다.
- 'SPRING_H2_CONSOLE_ENABLED=true' : H2 콘솔을 활성화한다.
- 'SPRING_H2_CONSOLE_SETTINGS_WEB_ALLOW_OTHERS=true' : H2 콘솔을 외부에서 접근할 수 있도록 설정한다.
2) redis 서비스
image: redis:latest
'redis:latest'라는 이름의 Dokcer 이미지를 사용한다.
포트 매핑
ports:
- "6379:6379"
호스트의 포트 6379를 컨테이너의 포트 6379에 매핑한다. 이는 Redis가 외부에서 접근할 수 있도록 하기 위함이다.
이후 github actions의 workflow 파일을 작성한다.
오늘은 Docker
의 Dockerfile
과 docker-compose.yml
파일에 대해 알아봤다. 다음시간에는 Github actions
까지 구성해 보겠다.
참고
'CI/CD' 카테고리의 다른 글
[CI/CD] Github Actions과 Jenkins 차이 (0) | 2024.05.16 |
---|