Home 1차 배포 환경 구축하기
Post
Cancel

1차 배포 환경 구축하기

프론트엔드 3명, 백엔드 3명으로 팀을 구성해서 최종 프로젝트를 진행하게 됐다.

이번 최종 프로젝트는 배포까지 해볼 예정인데, 최종적인 배포 환경을 구축하기 전 개발 단계에서도 QA, 멘토링 부분에서 더 편하게 하고자 미리 1차 배포를 해놓고 하기로 했다.

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 메모리를 추가한다.

  • swap 메모리 추가
1
2
$ sudo dd if=/dev/zero of=/swapfile bs=128M count=16
$ sudo chmod 600 /swapfile

쉘에 위 명령어를 사용해 2GB의 스왑파일을 생성해준다.

  • swap 메모리를 swap 파일로 포맷
1
$ sudo mkswap /swapfile
  • swap 메모리 활성화
1
2
$ sudo swapon /swapfile
$ sudo swapon -s

위 명령어들을 사용하면 Swap 메모리가 활성화된다.

  • swap 메모리 자동 활성화
1
$ sudo nano /etc/fstab
1
/swapfile swap swap defaults 0 0

위 명령어로 /etc/fstab 파일을 열고 아래 문구를 추가 후 저장하면 된다. 그러면 재부팅 후에도 Swap 메모리가 유지된다.

서버 준비 (메인, MongoDB)

메인 (Spring, Nginx, 모니터링 툴)

  • 시스템 업데이트
1
sudo apt update && sudo apt upgrade -y
  • Docker 설치
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
  • Docker Compose 설치
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
  • Nginx 설치
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 서버

도커 설치.

  • 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

모니터링 도구 설치

  • Docker Compose 파일 생성
1
2
mkdir -p ~/monitoring && cd ~/monitoring
nano docker-compose.yml
  • 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
  • docker compose 실행
1
docker-compose up -d
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 생성 시 테스트만 진행하고 병합 시 실제 빌드 및 배포가 이루어지도록 작성.

  • 루트 디렉토리에 DockerFile 생성
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 설정

  • 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로 접근한 후 로컬에서 그라파나를 사용한 것 처럼 사용해주면 대시보드로 운영서버 모니터링이 가능하다.

This post is licensed under CC BY 4.0 by the author.

대용량 트래픽 대응 (선착순 이벤트 응모 시스템 구현기)

소셜로그인 구현 (구글, 네이버)