프론트엔드 3명, 백엔드 3명으로 팀을 구성해서 최종 프로젝트를 진행하게 됐다.
이번 최종 프로젝트는 배포까지 해볼 예정인데, 최종적인 배포 환경을 구축하기 전 개발 단계에서도 QA, 멘토링 부분에서 더 편하게 하고자 미리 1차 배포를 해놓고 하기로 했다.
1차 배포 구조
우선 위와 같은 구조로 배포 환경을 세팅하려고 한다.
- 애플리케이션 서버 (EC2)
- 스프링부트 서버가 실행되는 주 서버
- Nginx를 이용해 Reverse Proxy 설정하여 클라이언트의 요청을 스프링 부트 서버로 전달한다.
- 정적 파일 요청 시 AWS S3 / CloudFront로 라우팅한다.
- 스프링 부트, Nginx, Grafana, Prometheus, Loki는 모두 도커로 컨테이너화 해 실행한다.
- DB
- MySQL: AWS RDS에서 관리한다.
- Redis: AWS Elasticache를 사용하여 관리한다.
- MongoDB: 메인 EC2 서버와 분리된 EC2 서버에서 실행하여 관리한다.
- 정적 파일 / CDN
- AWS S3: 정적 파일을 저장한다.
- AWS CloudFront: S3에 저장된 정적 파일 배포를 위한 CDN 역할을 한다.
- 모니터링
- 스프링 애플리케이션이 구동되는 EC2 서버에 함께 설치한다.
- 애플리케이션의 상태 및 성능을 모니터링하고 로그를 수집하여 시각화한다.
그리고 CI/CD 환경은 위와 같이 구성하려고 한다.
- Github Actions
- main 브랜치에 코드 변경 시 자동으로 CI/CD 파이프라인을 실행한다.
- Gradle: 코드를 빌드한다.
- Jacoco: 코드 빌드 시 테스트 커버리지를 검증하여 커버리지가 통과됐을 시 빌드된다.
- 빌드된 Jar 파일을 이용해 Docker 이미지를 생성하고 스프링부트 애플리케이션 EC2 서버로 배포한다.
배포 과정
우선 필요한 EC2 서버 2대, RDS, Elasticache, CloudFront, S3 서버를 모두 생성했다.
생성 과정은 생략한다.
배포 환경 구축하기
보안 그룹 설정하기
- EC2 main Server
- 인바운드 규칙
- 22 (SSH) : SSH 요청을 모든 IP에서 허용하지만 키 페어 파일로 보안 관리.
- 80 (HTTP)
- 443 (HTTPS)
- 3000 (grafana)
- 3100 (loki)
- 9090 (prometheus)
- EC2 MongoDB Server
- 인바운드 규칙
- 22 (SSH)
- 27017 (MongoDB) : 소스에 MainServer 보안그룹 (27017 포트로 들어오는 private IP의 요청을 허가.)
- AWS RDS
- 인바운드 규칙
- 3306 (MySQL) : 소스에 MainServer 보안그룹
- AWS Elasticache
- 인바운드 규칙
- 6379 (Redis) : MainServer 보안 그룹
- 보안 그룹으로 열면 해당 보안 그룹을 사용하는 모든 리소스의 Private IP 주소에서 오는 트래픽을 허용.
- Spring 서버가 Auto Scaling으로 추가되거나 IP 주소가 변경되어도 동일한 보안 그룹을 사용하는 서버는 모두 허용된다.
- CloudFront / S3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| {
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::dangdangserver-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::257394469445:distribution/E2F996RVYZE06Q"
}
}
}
]
}
|
- S3 버킷 정책을 위와 같이 설정
- Principal.Service : cloudfront.amazonaws.com
- CloudFront 서비스에 대해 S3 버킷에 대한 액세스를 허용
- CloudFront를 통해서만 S3 객체에 접근할 수 있도록 제한한다.
- s3:GetObject : S3 버킷의 객체를 읽을 수 있는 권한을 허용한다.
- arn:aws:s3:::dangdangserver-bucket/*
- Condition : CloudFront 배포 ID가 E2F996RVYZE06Q 일 때만 정책이 적용된다.
- CloudFront 배포 외 다른 서비스나 사용자가 S3 객체에 접근할 수 없도록 한다.
- CORS 설정: 모든 업로드와 GET 요청이 Spring서버에서 처리되고 S3과 통신한다면 CORS 설정은 따로 필요하지 않다. (추후 프론트 서버와 병합 과정에서 필요해질 수도)
- CloudFront / S3 테스트
- S3에 이미지를 하나 올린다. (dogs.jpg)
- CloudFront 배포 도메인 + dogs.jpg 접근
- S3 URL로 접근하면 AccessDenied 떠야 함.
Swap 메모리 설정
- EC2 서버 접속 (메인 서버, MongoDB 서버)
- EC2 서버의 접속은 pem 키와 public IPv4 주소를 이용해서 접속한다.
- MobaXterm이라는 SSH 클라이언트를 사용하여 EC2 접속과 관리를 보다 쉽게 한다.
- 위 사진처럼 public IP주소를 Remote host에 입력하고, 서버를 생성할 때 저장해두었던 pem 키 파일을 Use private key 부분에 넣어주면 EC2 서버들에 정상적으로 접속됨을 확인할 수 있다.
그리고 이렇게 접속한 EC2 서버들에 Swap 메모리를 설정해주려고 한다.
여러 도커 컨테이너들을 동시에 실행하는 과정에서 메모리가 초과되면 서버가 멈추는 현상이 발생한다.
멈추면 인스턴스 중지 후 재시작을 해주었을 때 해결되긴 한다.
하지만 이러면 서버 실행 작업을 다시 해주어야하기 때문에 매우 번거롭다.
이를 방지하기 위해 Swap 메모리를 추가한다.
1
2
| $ sudo dd if=/dev/zero of=/swapfile bs=128M count=16
$ sudo chmod 600 /swapfile
|
쉘에 위 명령어를 사용해 2GB의 스왑파일을 생성해준다.
1
| $ sudo mkswap /swapfile
|
1
2
| $ sudo swapon /swapfile
$ sudo swapon -s
|
위 명령어들을 사용하면 Swap 메모리가 활성화된다.
1
| /swapfile swap swap defaults 0 0
|
위 명령어로 /etc/fstab 파일을 열고 아래 문구를 추가 후 저장하면 된다. 그러면 재부팅 후에도 Swap 메모리가 유지된다.
서버 준비 (메인, MongoDB)
메인 (Spring, Nginx, 모니터링 툴)
1
| sudo apt update && sudo apt upgrade -y
|
1
| sudo apt install docker.io -y
|
1
2
3
| //Docker 서비스 활성화
sudo systemctl enable docker
sudo systemctl start docker
|
1
2
3
4
5
6
| //도커 권한 추가 : 현재 사용자를 Docker 그룹에 추가
sudo usermod -aG docker $USER
newgrp docker // 변경사항 적용
groups // Docker 권한 확인 : 출력에 docker 포함 체크
|
1
2
| //도커 설치 확인
docker --version
|
1
| sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
1
2
| //실행 권한 추가
sudo chmod +x /usr/local/bin/docker-compose
|
1
2
| //설치 체크
docker-compose --version
|
1
| sudo apt install nginx -y
|
1
2
3
| //Nginx 서비스 활성화
sudo systemctl enable nginx
sudo systemctl start nginx
|
1
2
| //Nginx 실행 확인
systemctl status nginx
|
EC2 퍼블릭 IP로 접근했을 때 기본 Nginx 페이지가 나타난다면 성공.
MongoDB 서버
도커 설치.
1
2
3
4
5
| docker run -d --name mongodb \
-p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=1234 \ // 패스워드는 임의 지정
mongo:6.0
|
모니터링 도구 설치
1
2
| mkdir -p ~/monitoring && cd ~/monitoring
nano docker-compose.yml
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
loki:
image: grafana/loki:2.9.0
container_name: loki
ports:
- "3100:3100"
volumes:
- ./loki-config.yml:/etc/loki/local-config.yml
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
volumes:
- grafana-storage:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
promtail:
image: grafana/promtail:latest
container_name: promtail
volumes:
- /var/log:/var/log
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./promtail-config.yml:/etc/promtail/config.yml
command: -config.file=/etc/promtail/config.yml
ports:
- "9080:9080"
volumes:
grafana-storage:
|
docker-compose 파일을 작성한 경로에 그대로 작성한다.
1
2
| nano prometheus.yml
nano loki-config.yml
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // prometheus.yml
global:
scrape_interval: 30s
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ['3.36.131.224:9090']
- job_name: 'spring-boot'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['3.36.131.224:8080'] # Spring Boot 메트릭 주소
- job_name: 'loki'
static_configs:
- targets: ['3.36.131.224:3100']
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| //loki-config.yml
auth_enabled: false
server:
http_listen_port: 3100
ingester:
lifecycler:
ring:
kvstore:
store: inmemory
replication_factor: 1
chunk_idle_period: 5m
chunk_target_size: 1048576
chunk_retain_period: 30s
schema_config:
configs:
- from: 2020-10-24
store: boltdb-shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h
storage_config:
boltdb_shipper:
active_index_directory: /loki/index
shared_store: filesystem
cache_location: /loki/boltdb-cache
filesystem:
directory: /loki/chunks
limits_config:
enforce_metric_name: false
reject_old_samples: true
reject_old_samples_max_age: 168h
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| // promtail-config.yml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://3.36.131.224:3100/loki/api/v1/push
scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: varlogs
__path__: /var/log/*.log
- job_name: docker-file-logs
static_configs:
- targets:
- localhost
labels:
job: docker-logs
__path__: /var/lib/docker/containers/*/*.log
- job_name: spring-logs
docker_sd_configs:
- host: unix:///var/run/docker.sock
relabel_configs:
- source_labels: [__meta_docker_container_name]
target_label: container_name
- source_labels: [__meta_docker_container_id]
target_label: container_id
- source_labels: [__meta_docker_image]
target_label: image_name
- source_labels: [__meta_docker_container_name]
target_label: job
replacement: spring-logs
|
1
2
| // 파일이 제대로 생성되었는지 확인.
ls -l ~/monitoring
|
1
2
| //컨테이너 실행 확인
docker ps
|
그리고 EC2 public IP로 3000 포트 접근했을 때 그라파나 페이지가 뜨는 지 확인하면 된다.
CI/CD 설정
- Docker Compose 파일 작성
- 스프링부트 실행 환경을 일관되게 설정할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| version: '3.8'
services:
app:
image: lucky77796/dangdang-salon:latest //실행할 도커 이미지 이름
container_name: dangdang_salon
ports:
- "8080:8080"
environment:
# MySQL
- SPRING_DATASOURCE_URL=jdbc:mysql://dangdangdatabase-1.c9qk602i8wmm.ap-northeast-2.rds.amazonaws.com:3306/dangdang_db
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=1234
# Redis
- SPRING_REDIS_HOST=dangdang-redis.qqm8mx.ng.0001.apn2.cache.amazonaws.com
- SPRING_REDIS_PORT=6379
# MongoDB
- SPRING_MONGODB_HOST=172.31.11.253
- SPRING_MONGODB_PORT=27017
- SPRING_MONGODB_DATABASE=dangdang_db
- SPRING_MONGODB_USERNAME=admin
- SPRING_MONGODB_PASSWORD=1234
# Flyway
- SPRING_FLYWAY_URL=jdbc:mysql://dangdangdatabase-1.c9qk602i8wmm.ap-northeast-2.rds.amazonaws.com:3306/dangdang_db
- SPRING_FLYWAY_USER=root
- SPRING_FLYWAY_PASSWORD=1234
# Logging
- SPRING_LOKI_LOGGING_PATH=/var/log/dangdang_salon.log
# Spring profiles
- SPRING_PROFILES_ACTIVE=prod
restart: always
|
1
| mkdir -p ~/app && cd ~/app
|
1
| nano docker-compose.yml
|
위 내용을 복사한 파일을 작성하면 된다.
Github Actions
- 리포지토리 -> Settings -> Secrets and variables -> Actions -> New repository secret
- DOCKER_USERNAME: 도커 허브 아이디
- DOCKER_PASSWORD: 도커 허브 비밀번호
- EC2_SERVER_IP: 메인 서버 퍼블릭 IP 주소
- EC2_SERVER_USER: EC2 서버 사용자 명 (ubuntu)
- SSH_PRIVATE_KEY: pem 파일 값 그대로 복사
- CI/CD 워크 플로 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
| name: CI/CD Pipeline
on:
pull_request:
branches:
- main # PR 생성 시 테스트 실행
push:
branches:
- main # main 브랜치에 Push 발생 시 배포 진행
jobs:
# 1. PR에서의 테스트 및 코드 검증
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Run tests
run: |
chmod +x ./gradlew
./gradlew clean test -Dspring.profiles.active=test
# 2. Push 이벤트 시 빌드 및 배포
build-and-deploy:
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' # Push 이벤트 시에만 실행
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Build JAR file
run: |
chmod +x ./gradlew
./gradlew clean build -Dspring.profiles.active=test
- name: Copy JAR to build context
run: cp build/libs/dangdang-salon-service-*-SNAPSHOT.jar .
- name: Build Docker image
run: |
docker build -t dangdang-salon:latest .
docker tag dangdang-salon:latest $/dangdang-salon:latest
- name: Push Docker image to Docker Hub
env:
DOCKER_USERNAME: $
DOCKER_PASSWORD: $
run: |
echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
docker push $DOCKER_USERNAME/dangdang-salon:latest
- name: Deploy to Server
env:
SSH_PRIVATE_KEY: $
SERVER_IP: $
SERVER_USER: $
DOCKER_USERNAME: $
DOCKER_PASSWORD: $
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "
docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD && \
cd /home/ubuntu/app && \
docker-compose pull && \
docker-compose up -d
"
|
PR 생성 시 테스트만 진행하고 병합 시 실제 빌드 및 배포가 이루어지도록 작성.
1
2
3
4
5
6
7
8
9
10
11
| # 1. 베이스 이미지 설정
FROM eclipse-temurin:17-jdk-alpine
# 2. 작업 디렉토리 설정
WORKDIR /app
# 3. Gradle 빌드 후 생성된 JAR 파일을 컨테이너로 복사
COPY build/libs/dangdang-salon-service-*-SNAPSHOT.jar app.jar
# 4. JAR 파일 실행 명령어
ENTRYPOINT ["java", "-jar", "app.jar"]
|
루트 디렉토리에 파일 명은 반드시 Dockerfile 이어야 한다.
main 브랜치에 푸시 후 workflow 실행 - 빌드/배포가 완료되면 EC2 서버에서 docker ps 명령어로 스프링부트 컨테이너가 실행되고 있는지 체크하면 된다.
Nginx 설정
1
| sudo nano /etc/nginx/sites-available/default
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| server {
listen 80;
server_name _; # 모든 요청을 수신
# Spring Boot 애플리케이션 프록시
location / {
proxy_pass http://127.0.0.1:8080; # Spring Boot 애플리케이션
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Prometheus 프록시
location /prometheus {
proxy_pass http://127.0.0.1:9090; # Prometheus 서버
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Loki 프록시
location /loki {
proxy_pass http://127.0.0.1:3100; # Loki 서버
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
|
nginx 설정 후 test API가 정상적으로 동작하는 것을 볼 수 있다.
- Nginx 역할
- 클라이언트의 요청을 받아 백엔드 서버로 전달
- 클라이언트는 직접 백엔드 서버에 접근하지 않고 Nginx를 통해 간접적으로 통신
- HTTPS 설정, 정적 파일 제공, 스케일링 등 추가적인 장점을 가질 수 있지만 현재는 리버스 프록시, 보안의 역할정도만 한다.
DB 연결 확인 (MySQL, Redis, MongoDB)
가장 간단한 방법은 도커 컨테이너 실행 로그를 보는 것이다.
1
2
3
4
5
| //RDS (flyway)
2024-11-18T15:31:39.757Z INFO 1 --- [Dangdang Salon Server] [ main] o.f.core.internal.command.DbMigrate : Migrating schema `dangdang_db` to version "1 - initial schema"
2024-11-18T15:31:41.706Z INFO 1 --- [Dangdang Salon Server] [ main] o.f.core.internal.command.DbMigrate : Migrating schema `dangdang_db` to version "4 - Add groomer request"
2024-11-18T15:31:42.067Z INFO 1 --- [Dangdang Salon Server] [ main] o.f.core.internal.command.DbMigrate : Successfully applied 4 migrations to schema `dangdang_db`, now at version v4 (execution time 00:02.040s)
|
1
2
3
| //Redis
2024-11-18T15:31:35.532Z INFO 1 --- [Dangdang Salon Server] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
2024-11-18T15:31:35.547Z INFO 1 --- [Dangdang Salon Server] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 5 ms. Found 0 Redis repository interfaces.
|
1
2
3
| //MongoDB
2024-11-18T15:31:48.647Z INFO 1 --- [Dangdang Salon Server] [ main] org.mongodb.driver.client : MongoClient with metadata {...} created with settings MongoClientSettings{...}
2024-11-18T15:31:48.663Z INFO 1 --- [Dangdang Salon Server] [31.11.253:27017] org.mongodb.driver.cluster : Monitor thread successfully connected to server with description ServerDescription{address=172.31.11.253:27017, type=STANDALONE, state=CONNECTED, ok=true, minWireVersion=0, maxWireVersion=17, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=58942827}
|
정리
모니터링 툴도 보안 그룹에서 열어주고 로컬에서 퍼블릭 IP로 접근한 후 로컬에서 그라파나를 사용한 것 처럼 사용해주면 대시보드로 운영서버 모니터링이 가능하다.