저번 Docker Compose 포스팅 2편이다.
글 쓴 목적은 두 가지.
Docker Compose 써야할 이유
를 보강해왔다실행 시점 함정 해결법
을 보강해왔다Flyway 적용
한 걸 보여주고 싶었다한마디로, 도커를 써야하는 명분작을 하러왔다.
돈이 없다.
서버는 띄워야 하는데 돈이 없다.
지난번보다 가난해졌다.
어떻게든 값싸게 프로젝트를 만들고 싶은 나는 고민했고, 답을 얻었다.
Docker 컨테이너로 필요한 모든 프로그램을 띄워 AWS EC2 하나에 몰아넣는것.
AWS EC2 의 비용정책은 두 가지로 나뉜다.
구분 | 인바운드 통신 | 아웃바운드 통신 |
---|---|---|
설명 | EC2 인스턴스로 요청 | EC2 인스턴스에서 응답 |
비용 비교 | 비쌈 (상대적) | 쌈 (상대적) |
RDS나 다른 서비스를 이용해서 발생하는 인/아웃바운드 비용 부담에서 벗어날 수 있다.
또, 하나의 EC2 인스턴스 내에서 Docker 끼리의 통신에 대해서는 비용이 발생하지 않는다
고 한다.
안 쓸 이유가 없다.
AWS 에서 Micro 버전으로 3개월은 꽁짜라고 하니, 이걸 활용해서 MVP 구현하고 올려볼 생각이다.
이번 플젝 도메인은 독립출판이다.
기획자 친구랑 오래 이야기를 나눠봤는데, 도메인 자체가 마이너라는게 중론이다.
애초에 독립출판이 뭔지도 모르는 사람이 더 많다고 한다.
그렇다면 예상컨데 유저는 많아야 20명 정도가 될거다.
나는 스프링 부트로 백엔드 서버를 구현할거다.
스프링 부트의 최대 스레드 풀은 200이고, 항상 활성화되어 있는 스레드 풀은 10개다.
20명 정도야 아주 가볍게 소화 가능하다는 소리가 된다.
Docker 컨테이너끼리 통신하는 것은 로컬 네트워크를 이용한다.
EC2와 다른 서비스끼리 통신은 네트워크를 이용한다.
따라서 후자가 네트워크 지연이 발생할 가능성이 크다.
단일 서버니까 디도스 공격이나 서버 장애가 발생하면 서비스 전체가 셧다운된다.
일단 디도스는 HTTP 상태코드 활용해서 요청 지연으로 처리할 생각이다.
서버 안정성은 중요하게 생각하지만, 지금 나는 돈이 더 급해서 어쩔 수 없다.
RDS 장점을 활용할 수 없다는 것도 좀 크다.
저번 플젝에서 RDS 모니터링을 확인하면서 Connection Pool 문제나, CI/CD 문제를 해결했다.
이것 이외에도 장점이 많은데, 이걸 활용하지 못하게 된다.
그래도 모니터링 기능은 직접 Grafana 로 모니터링 하는 방법을 적용할 생각이고,
MySQL로 직접 쿼리 작성하면서 성능 튜닝도 해볼겸 사용하지 않기로 했다.
저번 구조에서 달라진건
아래에는 각 설정별로 Docker Compose 파일을 달리 작성한 것처럼 나눠놨다.
이렇게 나눠놓은 경우에는
$ docker compose -f 도커파일1.yaml -f 경로/도커파일2.yaml up
이렇게 해서 여러 개의 파일을 한꺼번에 띄울 수 있다.
하지만 나는 관리가 어려워서 하나로 합쳤다.
예시용으로 나눠뒀단 소리.
이번에 .evn 파일에 필요한 환경변수를 담아서 사용했다.
첨에 멘토님이 환경변수 사용한다길래 OS 레벨에서 명령어로 하는건줄 알았는데 아니었다.
Jaypt로 설정 보안해주는 것보다 훨씬 나은 방법인거 같다.
다만, 실제로 배포할때 환경변수도 함께 넣어주는걸 까먹고 삽질할까봐 조금 걱정된다.
위치는 상관없다.
version: '3.8'
services:
db:
image: mysql:8.0
container_name: db
restart: always
env_file:
- .env
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
ports:
- '3306:3306'
지난번 포스팅과 동일하다
DDL 버전 관리를 위해 Flyway 를 써봤다.
지난 플젝에선 DDL을 구글드라이브에 올려서 복붙으로 관리했었다.
아무래도 관리가 많이 힘들었다.
그런 고민을 이야기했을때 멘토님이 추천해준 방식이었는데, 매우 만족한다.
일단 최고 장점은 버전 관리
가 된다는 것.
또 버전을 바탕으로 마이그레이션이 가능
하다는 것.
진짜 효자다.
버전 별로 관리되니까 정상적으로 실행될때랑 아닐때랑 파일로 비교도 가능하다.
심지어 DB에 형상 정보까지 저장되니까 관리가 엄청 편하다.
JPA 의 ddl-auto 속성을 이용하지 않고, 직접 개발자 답게 쿼리를 작성하는 것도 마음에 든다.
별도로 빌드 의존성을 추가할 필요는 없다.
flyway.url=${FLYWAY_DB_URL}
flyway.user=${FLYWAY_DB_USER}
flyway.password=${FLYWAY_DB_PASSWORD}
flyway.encoding=UTF-8
flyway.outOfOrder=true
flyway.validateOnMigrate=true
flyway.connectRetries=60
flyway.connectRetriesInterval=5
CREATE TABLE IF NOT EXISTS members
(
member_id BIGINT NOT NULL COMMENT '멤버아이디' AUTO_INCREMENT,
name VARCHAR(255) NOT NULL COMMENT '멤버 실명',
social_id VARCHAR(255) NOT NULL COMMENT '로그인 소셜 아이디',
client_provider VARCHAR(255) NOT NULL COMMENT '로그인 클라이언트',
role_type enum('USER', 'AUTHOR', 'ADMIN') NOT NULL COMMENT '멤버 타입',
nickname VARCHAR(255) COMMENT '멤버 닉네임',
profile_img VARCHAR(255) COMMENT '멤버 프로필',
default_address VARCHAR(255) COMMENT '기본 배송지',
default_address_detail VARCHAR(255) COMMENT '기본 배송지 상세',
default_cash_receipt_type ENUM('PERSON', 'COMPANY') COMMENT '기본 현금영수증 타입',
default_cash_receipt_number VARCHAR(255) COMMENT '기본 현금영수증 번호',
PRIMARY KEY (member_id),
UNIQUE KEY unique_socialid_and_clientprovider (social_id, client_provider)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
version: "3.8"
services:
db-migrate:
image: flyway/flyway:7
container_name: db-migrate
env_file:
- .env
environment:
- FLYWAY_DB_URL=jdbc:mysql://db/${MYSQL_DATABASE}
- FLYWAY_DB_USER=${FLYWAY_DB_USER}
- FLYWAY_DB_PASSWORD=${FLYWAY_DB_PASSWORD}
command: migrate
volumes:
- ./goguma-bookstore-server/db/flyway.conf:/flyway/conf/flyway.conf
- ./goguma-bookstore-server/db/migration:/flyway/sql
JWT 토큰 관리 방식을 변경했다.
지난번에 단일 서버라는 이유로 카페인 캐시를 활용해서 토큰 관리를 했다.
단일 서버 한정으로 좋은 방법이겠지만,
캐시의 본래 의도를 제대로 사용하지 않은 것 아니냐는 말
에도 일리가 있다고 생각했다.
그래서 오버디벨롭이라고 하더라도 REDIS 를 활용해서 토큰관리를 하고,
캐시는 조회 성능 향상이라는 원래 목적에 사용해보려고 한다.
dependencies {
// REDIS
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
testImplementation "com.redis.testcontainers:testcontainers-redis-junit:1.6.4"
}
spring:
data:
redis:
host: localhost # Docker 내부에서 돌리니까 로컬 호스트
port: 6379
version: "3.8"
services:
redis:
image: redis:6
container_name: redis
ports:
- '6379:6379'
여기가 핵심이다.
지난 포스팅 내내 도커가 함정을 파놨다느니 뭐니 칭얼댔던 문제를 쉽게 해결하는 방법을 알아냈다.
구분 | 변경 후 | 변경 전 |
---|---|---|
방식 | restart로 컨테이너 실행 | healthcheck 로 실행시점 결정 |
설명 | server가 on_failure이면 컨테이너 재시작 | 의존하는 컨테이너가 정상적으로 실행되어야 다음 컨테이너 실행 |
가독성 | 좋음 | 나쁨 |
성능 | 계속 컨테이너 시작을 하기 때문에 성능상 안 좋음 | 이미 시작한 컨테이너에 ping 같은 명령어만 주기적으로 실행하는 것이므로 성능상 이점 |
의존성 | docker compose 가 제공하는 기능이므로 DB 벤더에 영향 없음 | healthcheck를 위해 DB 벤더 별로 다른 명령어를 사용해야 함 |
이정도로 요약해봤다.
version: "3.8"
services:
server:
container_name: server
# 여기부터
restart: on-failure
depends_on:
- db
# 여기까지
env_file:
- .env
build:
context: ./goguma-bookstore-server
dockerfile: Dockerfile
ports:
- "8080:8080"
links:
- db
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/${MYSQL_DATABASE}
SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
services:
db:
container_name: db
image: postgres:${version}
environment:
- 'POSTGRES_USER=${username}'
- 'POSTGRES_PASSWORD=${password}'
- 'POSTGRES_DB={schema_name}'
ports:
- "5432:5432"
healthcheck:
test: [ "CMD-SHELL", "pg_isready -h db -p 5432" ]
server:
container_name: server
depends_on: # 내 삽질의 주인공
db: # db 서비스에 의존성이 있음
condition: service_healthy # db 서비스의 헬스체크가 성공한 경우에 해당 서비스 실행
build:
context: ./happyparking-server
dockerfile: Dockerfile
environment:
- SPRING_PROFILES_ACTIVE=default,auth,docs,monitor,prod
ports:
- "8080:8080"
healthcheck: # prometheus 를 위한 헬스 체크
test: [ "CMD-SHELL", "curl -f http://localhost:9292/actuator/health || exit 1" ]
interval: 10s
timeout: 5s
retries: 3
이렇게 띄워진다.