땃쥐네

[토이프로젝트] 게시판 시스템(board-system) 5. 프로젝트 멀티모듈화 본문

Project

[토이프로젝트] 게시판 시스템(board-system) 5. 프로젝트 멀티모듈화

ttasjwi 2024. 9. 30. 17:41

 

 

 

보통 프로젝트를 진행하다보면 A -> B -> C -> D -> E 와 같은 의존방향의 코드는 자주 작성됩니다. 이렇게 의존성의 방향을 알 수 있으면 뭐를 고쳐야할지 잘 보일거에요.

 

근데 의존성의 방향을 잘 못 관리한다거나, 계층을 넘어 의존하는 코드를 작성하면서 관리하다보면 위 그림처럼 B에서 E를 강하게 의존한 코드를 작성하게 되는 일이 생기고(특히 테스트 코드), E가 변경되면 D 뿐만 아니라 B의 코드를 변경하게 되는 일이 빈번하게 발생합니다.

 

무엇이 무엇을 의존하는지, 의존성의 방향은 어떤지 통제하기 힘들어지면 향후 코드 관리가 점점 힘들어집니다.

 

 

외부 모듈들을 테스트를 해보고 싶은데 모두 끌어와서 실행해야한다는 점도 저를 귀찮게 하는게 많았습니다.

이메일 발송 라이브러리가 잘 작동되는 지 확인하고 싶어서 스프링 부트 설정을 끌어다 테스트를 해보려는데, 데이터베이스 연결이 안 되어 있어서 데이터베이스 실행을 해주고 스프링부트를 함께 실행해야하는 경우도 빈번히 발생합니다. 데이터베이스 연동이 잘 되는 지 테스트를 해보고 싶어서 스프링 부트 실행을 끌어다 해보려는데 레디스쪽 설정도 끌어와서 실행되니, 레디스도 켜야하는 경우도 생기고, Jwt 인코딩이 잘 되는지 테스트를 하고 싶은데 데이터베이스 설정을 켜야한다거나, ....

 

 

 

전체적인 그림은 이렇고 실제는 상당히 달라질 예정. API/애플리케이션/도메인 은 맥락에 따라 쪼개질 수 있음

 

저는 이런 두 가지 문제를 불편하다고 느껴서 고민해보고 다른 분들의 이야기를 종합한 결과 멀티모듈 기능을 도입하기로 결심했습니다.

 

'의존성 방향 통제를 완벽히는 할 수 없지만 코드에서 어느 정도 강제성을 부여해서, 동료 개발자들에게 가이드 라인을 어느 정도 제시할 수 있고

 

컨트롤러 관련 기능은 컨트롤러 모듈을 모아서 관리하고

애플리케이션 기능은 애플리케이션 모듈에 모아서 관리하고

도메인 기능은 도메인 모듈에 모아서 관리하고

외부와 연동하거나 특정 기술 의존성이 강한 기능은 각각 모듈화해서 따로따로 관리하고 테스트할 수 있게하면 좀 더 작은 단위로 생각할 수 있어서 애플리케이션 관리가 수월해질 것 같다.'

 

 

 

이런 관점에서 멀티모듈화 해서 프로젝트를 진행해보기로 했습니다. (+ 토이프로젝트이기도 하고 멀티모듈 기능을 써보고 싶다는 생각도 더해져서)

 


1. buildSrc 설정 및 프로젝트 구성을 위한 프로퍼티 상수화

1.1 buildSrc 폴더 생성

 

프로젝트 루트 경로에 buildSrc 폴더를 생성하고 이곳에 build.gradle.kts 파일을 추가합니다.

 

plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
}

 

위 스크립트를 buildSrc/build.gradle.kts 에 작성합니다.

 

gradle 공식문서의 설명에 따르면, gradle은 buildSrc 디렉터리를 발견하면 이곳의 코드를 컴파일 및 테스트하고 빌드 스크립트의 클래스 패스에 넣는다고 합니다.

 

이 때 설정에서 kotlin-dsl 플러그인을 추가하지 않으면 디렉터리 하위에 코틀린 파일을 작성하여 추가하더라도 빌드 스크립트의 코틀린 코드를 gradle에 통합할 수 없게 되므로 kotlin-dsl을 활성화해야합니다.(플러그인을 지워서 gradle을 reload해보니 사용할 수 없었더라구요...) 또, 이 플러그인을 끌어올 수 있도록 repositories로 mavenCentral을 지정했습니다.

 

 

설정을 추가하고 인텔리제이 기준 gradle 을 reload 하면

 

 

인텔리제이는 이제 이 경로를 gradle 빌드를 위한 buildSrc 모듈로 인식하게 됩니다.


저는 여기에 src/main/kotlin 폴더를 만들고 여기에 위의 세 파일을 작성하는데요. 그 내용을 설명할게요

 

1.2 ProjectProperties

object ProjectProperties {
    const val GROUP_NAME = "com.ttasjwi"
    const val VERSION = "0.0.1"
    const val JAVA_VERSION = "21"
}


프로젝트 자체의 기본적인 프로퍼티들입니다.

그룹명, 버전, 자바 버전을 여기다 넣어둘거에요. 이 부분은 없어도 큰 상관은 없어요

 

1.3 Plugins

enum class Plugins(
    val id: String,
    val version: String = "",
) {

    KOTLIN_JVM(id = "org.jetbrains.kotlin.jvm", version = "1.9.25"),
    KOTLIN_SPRING(id = "org.jetbrains.kotlin.plugin.spring", version = "1.9.25"),

    SPRING_BOOT(id = "org.springframework.boot", version = "3.3.4"),
    SPRING_DEPENDENCY_MANAGEMENT(id = "io.spring.dependency-management", version = "1.1.6"),
}

 

Plugins 는 프로젝트 전반에서 사용되는 플러그인을 모아둔 enum 입니다.

스프링 부트 플러그인, Koltin Jvm 플러그인 등이 이에 해당합니다.

1.4 Dependencies

enum class Dependencies(
    private val groupId: String,
    private val artifactId: String,
    private val version: String? = null,
    private val classifier: String? = null,
) {

    // kotlin
    KOTLIN_JACKSON(groupId = "com.fasterxml.jackson.module", artifactId = "jackson-module-kotlin"),
    KOTLIN_REFLECT(groupId = "org.jetbrains.kotlin", artifactId = "kotlin-reflect"),

    // spring
    SPRING_BOOT_STARTER(groupId = "org.springframework.boot", artifactId = "spring-boot-starter"),
    SPRING_BOOT_WEB(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-web"),


    SPRING_BOOT_TEST(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-test");

    val fullName: String
        get() {
            if (version == null) {
                return "$groupId:$artifactId"
            }
            if (classifier == null) {
                return "$groupId:$artifactId:$version"
            }
            return "$groupId:$artifactId:$version:$classifier"
        }

}

 

Dependencies 입니다. 하나의 단일 모듈로 프로젝트를 관리하면 이렇게 관리할 필요는 없는데

멀티모듈로 프로젝트를 관리하면 모듈마다 다르게 의존성을 설정하는 경우가 많아집니다.

 

그런데 똑같은 라이브러리의 이름을 매번 복사해서 붙여넣다보면 관리하기 힘들어지므로 이렇게 한 모듈에 상수로 모아둬서 꺼내쓸 수 있도록 했어요.

 

사용할 때는 Dependencies.XXX.fullName 으로 전체 라이브러리 이름을 꺼내서 쓸 수 있게 했습니다.

 

1.5 루트 build.gradle.kts 에 반영

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id(Plugins.KOTLIN_JVM.id) version Plugins.KOTLIN_JVM.version
    id(Plugins.KOTLIN_SPRING.id) version Plugins.KOTLIN_SPRING.version
    id(Plugins.SPRING_BOOT.id) version Plugins.SPRING_BOOT.version
    id(Plugins.SPRING_DEPENDENCY_MANAGEMENT.id) version Plugins.SPRING_DEPENDENCY_MANAGEMENT.version
}

group = ProjectProperties.GROUP_NAME
version = ProjectProperties.VERSION

java {
    sourceCompatibility = JavaVersion.valueOf("VERSION_${ProjectProperties.JAVA_VERSION}")
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(Dependencies.SPRING_BOOT_WEB.fullName)
    implementation(Dependencies.KOTLIN_REFLECT.fullName)
    implementation(Dependencies.KOTLIN_JACKSON.fullName)
    testImplementation(Dependencies.SPRING_BOOT_TEST.fullName)
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs += "-Xjsr305=strict"
        jvmTarget = ProjectProperties.JAVA_VERSION
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks.getByName("bootJar") {
    enabled = true
}

tasks.getByName("jar") {
    enabled = false
}


루트 경로의 build.gradle.kts 에 위에서 만든 상수를 반영한 내용입니다.


2. 모듈 정의 및 분리

2.1 settings.gradle.kts

rootProject.name = "board-system"

include(

    // 설정 최종 구성 및 실행
    "board-system-container",
    
    // api
    "board-system-api:api-deploy",
)

 

루트의 settings.gradle.kts 에서는 현재 프로젝트에서 사용할 모듈들을 선언할 수 있습니다.

 

저는 실행용 모듈로 board-system-container 모듈을 정의하고

배포 api 모듈로 board-system-api:api-deploy 모듈을 정의했어요.

 

":" 을 사용하면 폴더처럼 프로젝트를 분류할 수 있어요. 예를 들어 xxx:yyy, xxx:zzz 이면 yyy, zzz는 xxx 개념과 관련된 모듈이란걸 알 수 있겠죠?

 

 

위 상태로 intellij gradle 을 다시 로드하고

 

루트 하위에 board-system-container 폴더 board-system-api/api-deploy 폴더를 만들면

루트 프로젝트의 하위 모듈로 인식할 수 있어요.

 

2.2 build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

// 루트 프로젝트
plugins {
    id(Plugins.KOTLIN_JVM.id) version Plugins.KOTLIN_JVM.version
    id(Plugins.KOTLIN_SPRING.id) version Plugins.KOTLIN_SPRING.version apply false
    id(Plugins.SPRING_BOOT.id) version Plugins.SPRING_BOOT.version apply false
    id(Plugins.SPRING_DEPENDENCY_MANAGEMENT.id) version Plugins.SPRING_DEPENDENCY_MANAGEMENT.version
}

java {
    sourceCompatibility = JavaVersion.valueOf("VERSION_${ProjectProperties.JAVA_VERSION}")
}

// 루트 프로젝트 + 서브 프로젝트 전체
allprojects {
    group = ProjectProperties.GROUP_NAME
    version = ProjectProperties.VERSION

    repositories {
        mavenCentral()
    }
}

// 서브프로젝트에 적용
subprojects {
    apply { plugin(Plugins.KOTLIN_JVM.id) }
    apply { plugin(Plugins.KOTLIN_SPRING.id) }
    apply { plugin(Plugins.SPRING_BOOT.id) }
    apply { plugin(Plugins.SPRING_DEPENDENCY_MANAGEMENT.id) }

    dependencies {
        implementation(Dependencies.KOTLIN_REFLECT.fullName)
        testImplementation(Dependencies.SPRING_BOOT_TEST.fullName)
    }

	// 기본적으로 실행 불가능한 jar로 생성
    tasks.getByName("bootJar") {
        enabled = false
    }

    tasks.getByName("jar") {
        enabled = true
    }


    tasks.withType<KotlinCompile> {
        kotlinOptions {
            freeCompilerArgs += "-Xjsr305=strict"
            jvmTarget = ProjectProperties.JAVA_VERSION
        }
    }

    tasks.test {
        useJUnitPlatform()
    }
}

 

루트 build.gradle.kts 에서는

- 루트에만 적용할 설정(제일 바깥)

- 루트 + 하위 모듈들에 적용할 설정(allprojects)

- 하위모듈들에만 적용할 설정(subprojects)

 

설정을 할 수 있습니다.

 

여기서 주의깊게 볼 부분은, 기본적으로 각 모듈들에 대해서 bootJar 비활성화 / jar 활성화를 해서 실행 불가능한 jar로 빌드되게 할거에요. 이 작업은 container 모듈에서만 반대 설정을 할겁니다.

 

2.3 Deploy Api 모듈

 

본격적으로 모듈 분리 작업을 해보죠.

 

build.gradle.kts

dependencies {
    implementation(Dependencies.SPRING_BOOT_WEB.fullName)
    implementation(Dependencies.KOTLIN_JACKSON.fullName)
}

 

스프링부트 web , kotlin jackson 의존성만 추가합니다.

 

main/kotlin

기존 HealthCheckController와 DeployProperties 는 이 모듈에 옮겼습니다.

 

main/resources

# deploy-config.yml
spring:
  config:
    activate:
      on-profile: local
    import: deploy-config-local.yml

---

spring:
  config:
    activate:
      on-profile: blue
    import: deploy-config-blue.yml

---

spring:
  config:
    activate:
      on-profile: green
    import: deploy-config-green.yml

---

 

이 설정파일은 이후 컨테이너에서 불러올 하위 설정파일인데요.

로컬 프로필일 때, 블루 프로필일 때, 그린 프로필일 때 어떤 추가 설정을 가져올 지 명시해뒀습니다.

spring.config.activate.on-profile: local
server:
  port: 8080
  profile: local
spring.config.activate.on-profile: blue
server:
  port: 8080
  profile: blue
spring.config.activate.on-profile: green
server:
  port: 8080
  profile: green

 

local, blue, green 일 때 설정입니다.

spring.config.activate.on-profile: test
server:
  port: 9090
  profile: test

 

test 일 때 설정입니다. 이건 원래 test에 두는게 맞지만 컨테이너에서 이 설정을 끌어와서 스프링부트 테스트할 때 이 파일도 필요한데, test에 두면 의존성을 끌어올 수 없게 됩니다.

 

물론 같은 파일을 container의 test 폴더 및 deploy-api 에 복사 붙여넣기로 넣는 방법도 있긴한데

중복되면 더 관리하기 힘들어질 것 같아서 deploy-api의 main에 뒀어요.

 

test/kotlin

HealthCheckControllerMiddleTest 는 그대로 끌어왔지만 이 테스트 실행에 필요한 스프링 환경이 지금은 없어졌습니다. 그게 기존에는 Main 클래스에 있었기 때문이죠.

package com.ttasjwi.board.system

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class DeployApiTestApplication

fun main(args: Array<String>) {
    runApplication<DeployApiTestApplication>(*args)
}

 

그래서 해당 test 폴더 내의 com.ttasjwi.board.system 에 스프링 실행 파일을 뒀습니다.

이렇게 하면 HealthCheckControllerMiddleTest 는 이 스프링 설정을 토대로 테스트 코드를 실행하게 돼요.

 

test/resources

# application.yml

spring.application.name: board-system-deploy-api-test
spring:
  profiles:
    active: test
  config:
    import:
      - deploy-config-test.yml

---

 

여기서는 deploy-config-test.yml 파일을 끌어오도록 설정을 둡니다.

 

2.4 컨테이너 모듈

 

build.gradle.kts

dependencies {
    implementation(Dependencies.SPRING_BOOT_STARTER.fullName)
    implementation(project(":board-system-api:api-deploy"))
}

tasks.getByName("bootJar") {
    enabled = true
}

tasks.getByName("jar") {
    enabled = false
}

 

스프링 최소한의 실행 의존성인 spring-boot-starter 의존성을 추가하고 deploy-api 모듈 의존성을 추가했습니다.

 

그리고 이 모듈은 실행 모듈이므로 bootJar 빌드만 되도록 했어요.

상위에서는(subprojects) jar 모듈 빌드만 되도록 설정했지만 여기서 이렇게 설정하면 이 설정이 우선시됩니다.

 

+

근데 다시 생각해보니까... api-deploy 모듈은 runtimeOnly 의존하게 하고, 테스트에서만 testImplementation 또는 testApi 와 같은 방식으로 의존하게 할 걸 그랬네요. 다음에 그렇게 수정할게요.

 

main/resources

# application.yml
spring.application.name: board-system
spring:
  profiles:
    active: local
  config:
    import:
      - deploy-config.yml

 

이 모듈은 deploy-api 모듈을 의존하기 때문에 해당 모듈의 deploy-config 도 포함되서 빌드됩니다.

해당 설정을 이 애플리케이션 실행에 포함시키기 때문에 deploy-config 도 실행되고 설정이 적용될거에요.

 

test/resources

# application.yml
spring.application.name: board-system-test
spring:
  profiles:
    active: test
  config:
    import:
      - deploy-config-test.yml

---

 

test 폴더 application yml 에서는 deploy-api 모듈의 main/resources/deploy-config-test.yml 을 가져와 쓸 수 있도록 했어요.


2.5 .gitignore 수정

### secret properties files ###
board-system-api/api-deploy/src/main/resources/deploy-config-blue.yml
board-system-api/api-deploy/src/main/resources/deploy-config-green.yml

 

무시해야할 비밀 설정 파일(제 블로그에는 공개적으로 올리긴 했습니다만 실무에서는 민감할 수 있는 내용이므로...)의 위치가 바뀌었으므로 그에 맞게 변경합니다.


 


이제 멀티모듈화 시키고 각 모듈이 독립되면서 api-deploy 모듈은 독립적으로 테스트가 가능해졌습니다.

다만 컨테이너 모듈은 모든 설정을 끌어와서 테스트를 하는 부분에 해당되므로 다른 모듈들에 의존해야합니다.

 


3. Dockerfile 및 빌드 스크립트 수정

3.1 Dockerfile

FROM amazoncorretto:21-alpine3.20-jdk
ARG JAR_FILE=board-system-container/build/libs/*.jar
ARG PROFILES
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-DSpring.profiles.active=${PROFILES}", "-jar", "app.jar"]

 

이전에는 루트의 build/libs/*.jar 를 가져와서 실행했는데 이제는 실행 가능한 jar 가 board-system-container/build/libs 에 있으므로 설정을 이렇게 수정합니다.

 

3.2 deploy.yml (배포 스크립트)

      - name: 도커 이미지 풀링 및 컨테이너 실행
        run: |
          ssh board-ec2 << 'EOF'
            set -e
          
            # 필요한 프로필 파일을 서버로 복사합니다.
            if [ "${{ env.TARGET_UPSTREAM }}" = "blue" ]; then
              echo "${{ secrets.DEPLOY_CONFIG_BLUE_YML }}" | base64 --decode > /home/ec2-user/config/board-system/deploy-config-blue.yml
            else
              echo "${{ secrets.DEPLOY_CONFIG_GREEN_YML }}" | base64 --decode > /home/ec2-user/config/board-system/deploy-config-green.yml
            fi

 

기존 파일에서 설정 파일 위치 경로가 변경된걸 반영해서 배포 스크립트 역시 수정합니다.

name: Deploy

on:
  push:
    branches:
      - master

jobs:
  Deploy:
    runs-on: ubuntu-latest
    steps:
      - name: 체크 아웃
        uses: actions/checkout@v4

      - name: Java 21 설정
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '21'

      - name: Gradle 캐싱
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: gradlew 파일 실행권한 부여
        run: chmod +x gradlew
        shell: bash

      - name: 프로젝트 빌드
        run: |
          ./gradlew build

      - name: DockerHub 로그인
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}

      - name: 도커 이미지 빌드
        run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/board-be .

      - name: 도커 이미지 푸시
        run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/board-be:latest

      - name: 배포 대상 포트/PROFILE 확인
        run: |
          response=$(curl -s -w "%{http_code}" "http://${{ secrets.LIVE_SERVER_IP }}/api/v1/deploy/health-check")
          STATUS="${response: -3}"  # 마지막 3글자 (HTTP 상태 코드)
          BODY="${response::-3}"     # 나머지 (응답 본문)
          
          echo "STATUS=$STATUS"
          
          if [ "$STATUS" = "200" ]; then
            CURRENT_UPSTREAM="$BODY"
          else
            CURRENT_UPSTREAM="green"
          fi
          
          echo "CURRENT_UPSTREAM=$CURRENT_UPSTREAM" >> $GITHUB_ENV
          
          if [ "$CURRENT_UPSTREAM" = "blue" ]; then
            echo "CURRENT_PORT=${{ secrets.BLUE_PORT }}" >> $GITHUB_ENV
            echo "STOPPED_PORT=${{ secrets.GREEN_PORT }}" >> $GITHUB_ENV
            echo "TARGET_UPSTREAM=green" >> $GITHUB_ENV
          elif [ "$CURRENT_UPSTREAM" = "green" ]; then
            echo "CURRENT_PORT=${{ secrets.GREEN_PORT }}" >> $GITHUB_ENV
            echo "STOPPED_PORT=${{ secrets.BLUE_PORT }}" >> $GITHUB_ENV
            echo "TARGET_UPSTREAM=blue" >> $GITHUB_ENV
          else
            echo "error"
            exit 1
          fi

      - name: GitHub Actions 실행자 IP 얻어오기
        id: GITHUB_ACTIONS_IP
        uses: haythem/public-ip@v1.3

      - name: AWS CLI 설정
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: GitHub Actions - SSH 및 컨테이너 실제 포트 접근 권한 부여
        run: |
          aws ec2 authorize-security-group-ingress \
            --group-id ${{ secrets.EC2_SECURITY_GROUP_ID }} \
            --ip-permissions \
              'IpProtocol=tcp,FromPort=${{ secrets.EC2_SSH_PORT }},ToPort=${{ secrets.EC2_SSH_PORT }},IpRanges=[{CidrIp=${{ steps.GITHUB_ACTIONS_IP.outputs.ipv4 }}/32}]' \
              'IpProtocol=tcp,FromPort=${{ env.STOPPED_PORT }},ToPort=${{ env.STOPPED_PORT }},IpRanges=[{CidrIp=${{ steps.GITHUB_ACTIONS_IP.outputs.ipv4 }}/32}]'

      - name: SSH Key 설정
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/board-ec2-key.pem
          chmod 600 ~/.ssh/board-ec2-key.pem
          echo "Host board-ec2" >> ~/.ssh/config
          echo "  HostName ${{ secrets.LIVE_SERVER_IP }}" >> ~/.ssh/config
          echo "  User ${{ secrets.EC2_USERNAME }}" >> ~/.ssh/config
          echo "  IdentityFile ~/.ssh/board-ec2-key.pem" >> ~/.ssh/config
          echo "  StrictHostKeyChecking no" >> ~/.ssh/config

      - name: 도커 이미지 풀링 및 컨테이너 실행
        run: |
          ssh board-ec2 << 'EOF'
            set -e
            # 필요한 프로필 파일을 서버로 복사합니다.
            if [ "${{ env.TARGET_UPSTREAM }}" = "blue" ]; then
              echo "${{ secrets.DEPLOY_CONFIG_BLUE_YML }}" | base64 --decode > /home/ec2-user/config/board-system/deploy-config-blue.yml
            else
              echo "${{ secrets.DEPLOY_CONFIG_GREEN_YML }}" | base64 --decode > /home/ec2-user/config/board-system/deploy-config-green.yml
            fi
            docker pull ${{ secrets.DOCKERHUB_USERNAME }}/board-be:latest
            docker compose -f docker-compose-${{ env.TARGET_UPSTREAM }}.yml up -d
          EOF

      - name: 새로 실행한 서버 컨테이너 헬스 체크
        uses: jtalk/url-health-check-action@v3
        with:
          url: http://${{ secrets.LIVE_SERVER_IP }}:${{ env.STOPPED_PORT }}/api/v1/deploy/health-check
          max-attempts: 3
          retry-delay: 10s

      - name: Nginx 의 대상 서버를 새로 실행한 컨테이너쪽으로 전환
        run: |
          ssh board-ec2 << 'EOF'
            set -e
            docker exec -i nginx bash -c 'echo "set \$service_url ${{ env.TARGET_UPSTREAM }};" > /etc/nginx/conf.d/service-env.inc && nginx -s reload'
          EOF

      - name: 기존 배포 컨테이너 정지
        run: |
          ssh board-ec2 << 'EOF'
            set -e
            docker stop ${{ env.CURRENT_UPSTREAM }}
            docker rm ${{ env.CURRENT_UPSTREAM }}
          EOF

      - name: GitHub Actions - SSH 및 컨테이너 실제 포트 접근 권한 제거
        if: always()
        run: |
          aws ec2 revoke-security-group-ingress \
          --group-id ${{ secrets.EC2_SECURITY_GROUP_ID }} \
          --ip-permissions \
            'IpProtocol=tcp,FromPort=${{ secrets.EC2_SSH_PORT }},ToPort=${{ secrets.EC2_SSH_PORT }},IpRanges=[{CidrIp=${{ steps.GITHUB_ACTIONS_IP.outputs.ipv4 }}/32}]' \
            'IpProtocol=tcp,FromPort=${{env.STOPPED_PORT}},ToPort=${{env.STOPPED_PORT}},IpRanges=[{CidrIp=${{ steps.GITHUB_ACTIONS_IP.outputs.ipv4 }}/32}]'

 

변경된 배포스크립트 전체입니다.

 

3.3 Secrets 변경

 

변경에 맞춰서 secrets 도 다시 새로 수정해서 넣었습니다.

yaml 파일은 기존처럼 base64 인코딩해서 넣었어요.

 


4. 배포


일단 PR의 빌드테스트는 잘 통과되는 것 같습니다.

 

 

배포도 잘 됩니다!

 

원래 green 이였는데 blue가 됐네요.

 

 

 


중간 정리

 

이렇게 기존 소스에서 모듈을 이렇게 한 단계 분리시켜봤습니다.


참고로 커밋 히스토리는 이런 구조가 되어있습니다. master 브랜치는 선형으로 쭉 뻗어나가고(squash merge) 새로 하는 작업물은 가장 최근의 master 커밋에서 나와서 진행되도록 말이죠.(그렇지 않다면 rebase 시켜가면서)

 

실제 작업한 브랜치는 PR 거쳐서 merge 시킨 뒤, 일정 기간 혹은 영구히 보관해두거나 지우면 될 것 같습니다.

 

다음 글에서는 이어서 공용 모듈을 개발해보도록 할게요. 감사합니다!

 


+

 

작업한 소스코드는 제 GitHub 및 PR 에서 볼 수 있습니다.

 

GitHub

https://github.com/ttasjwi/board-system

 

GitHub - ttasjwi/board-system

Contribute to ttasjwi/board-system development by creating an account on GitHub.

github.com

 

 

해당 작업 PR

https://github.com/ttasjwi/board-system/pull/18

 

Comments