From 81991a2374c6cd3e051fa7b172fa535716afab1e Mon Sep 17 00:00:00 2001 From: namgigun Date: Tue, 30 Sep 2025 10:38:44 +0900 Subject: [PATCH 1/6] =?UTF-8?q?Infra:=20AWS=20=EC=84=B8=ED=8C=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EC2 실행 시, ghcr.io에 자동으로 로그인 되도록 추가 --- infra/terraform/main.tf | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/infra/terraform/main.tf b/infra/terraform/main.tf index 2360153e..7e9cae93 100644 --- a/infra/terraform/main.tf +++ b/infra/terraform/main.tf @@ -194,10 +194,7 @@ sudo mkswap /swapfile sudo swapon /swapfile sudo sh -c 'echo "/swapfile swap swap defaults 0 0" >> /etc/fstab' -# git 설치 -yum install git -y - -#도커 설치 및 실행/활성화 +# 도커 설치 및 실행/활성화 yum install docker -y systemctl enable docker systemctl start docker @@ -229,6 +226,8 @@ docker run -d \ -v /dockerProjects/npm_1/volumes/etc/letsencrypt:/etc/letsencrypt \ jc21/nginx-proxy-manager:latest +# ghcr.io 로그인 +echo "${var.github_access_token_1}" | docker login ghcr.io -u ${var.github_access_token_1_owner} --password-stdin END_OF_FILE } From 7c9c2dc810604295071c54f4428438596cf913b9 Mon Sep 17 00:00:00 2001 From: namgigun Date: Tue, 30 Sep 2025 11:27:52 +0900 Subject: [PATCH 2/6] =?UTF-8?q?Infra:=20CD=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배포할 EC2 인스턴스 아이디를 수동으로 입력할 필요없이 이제는 자동으로 반영 --- .github/workflows/backend-cd.yml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml index e6f00bb1..5dfc9a33 100644 --- a/.github/workflows/backend-cd.yml +++ b/.github/workflows/backend-cd.yml @@ -78,7 +78,23 @@ jobs: needs: [ buildImageAndPush ] env: OWNER: ${{ github.repository_owner }} + EC2_INSTANCE_TAG_NAME: team5-ec2-1 + steps: + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ secrets.AWS_REGION }} + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: get instance id + run: | + INSTANCE_ID=$(aws ec2 describe-instances \ + --filters "Name=tag:Name, Values=${{ EC2_INSTANCE_TAG_NAME }}" "Name=instance-state-name, Values=running" \ + --query "Reservations[].Instances[].InstanceId" --output text) + [[ -n "${INSTANCE_ID}" && "${INSTANCE_ID}" != "None" ]] || { echo "No running instance found"; exit 1; } + echo "INSTANCE_ID=${INSTANCE_ID}" >> "${GITHUB_ENV}" + - name: set lower case owner name run: | echo "OWNER_LC=${OWNER,,}" >> ${GITHUB_ENV} @@ -90,14 +106,17 @@ jobs: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - instance-ids: "i-00e384163ab61f6cc" + instance-ids: ${{ env.INSTANCE_ID }} working-directory: / comment: Deploy command: | - # 0. env 변수 확인 + # 1. env 변수 확인 echo "OWNER_LC = ${{ env.OWNER_LC }}" - # 1. 최신 이미지 pull + # 2. EC2 인스턴스 아이디 확인 + echo "INSTANCE_ID=${INSTANCE_ID}" + + # 3. 최신 이미지 pull docker pull ghcr.io/${{ env.OWNER_LC }}/catfe-backend:latest # 2. 기존 컨테이너 종료 및 제거 From 963e42524d14a068cc594241027df5047410f9a2 Mon Sep 17 00:00:00 2001 From: namgigun Date: Tue, 30 Sep 2025 17:43:50 +0900 Subject: [PATCH 3/6] =?UTF-8?q?Infra:=20Blue/Green=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EC=82=AC=EC=A0=84=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배포 할 도메인 정보 추가 - 스프링 액추에이터 의존성 추가 (서버 헬스 체크를 위해 추가함.) - EC2 추가 환경변수(민감한 정보) 세팅 --- build.gradle.kts | 1 + infra/terraform/main.tf | 8 ++++++++ infra/terraform/variables.tf | 3 +++ 3 files changed, 12 insertions(+) create mode 100644 infra/terraform/variables.tf diff --git a/build.gradle.kts b/build.gradle.kts index 0111e9e7..59746d63 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-websocket") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-mail") + implementation("org.springframework.boot:spring-boot-starter-actuator") // Database & JPA implementation("org.springframework.boot:spring-boot-starter-data-jpa") diff --git a/infra/terraform/main.tf b/infra/terraform/main.tf index 7e9cae93..5f633b54 100644 --- a/infra/terraform/main.tf +++ b/infra/terraform/main.tf @@ -194,6 +194,14 @@ sudo mkswap /swapfile sudo swapon /swapfile sudo sh -c 'echo "/swapfile swap swap defaults 0 0" >> /etc/fstab' +# 환경변수 세팅(/etc/environment) +echo "PASSWORD=${var.password_1}" >> /etc/environment +echo "DOMAIN=${var.catfe_domain_1}" >> /etc/environment +echo "GITHUB_ACCESS_TOKEN_OWNER=${var.github_access_token_1_owner}" >> /etc/environment +ehco "GITHUB_ACCESS_TOKEN=${var.github_access_token_1}" >> /etc/environment +# EC2 환경변수 등록 +source /etc/environment + # 도커 설치 및 실행/활성화 yum install docker -y systemctl enable docker diff --git a/infra/terraform/variables.tf b/infra/terraform/variables.tf new file mode 100644 index 00000000..e22f5ba7 --- /dev/null +++ b/infra/terraform/variables.tf @@ -0,0 +1,3 @@ +variable "catfe_domain_1" { + description = "api.catfe.site" +} \ No newline at end of file From a5a00bb757f7b50154d745db1f7d14a9851e4fa1 Mon Sep 17 00:00:00 2001 From: namgigun Date: Wed, 1 Oct 2025 10:08:39 +0900 Subject: [PATCH 4/6] =?UTF-8?q?Infra:=20Actuator=20=EC=9D=B8=EC=A6=9D/?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 헬스 체크 허용 --- src/main/java/com/back/global/security/SecurityConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/back/global/security/SecurityConfig.java b/src/main/java/com/back/global/security/SecurityConfig.java index 293dddbd..64478985 100644 --- a/src/main/java/com/back/global/security/SecurityConfig.java +++ b/src/main/java/com/back/global/security/SecurityConfig.java @@ -44,6 +44,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //.requestMatchers("/api/rooms/RoomChatApiControllerTest").permitAll() // 테스트용 임시 허용 .requestMatchers("/","/swagger-ui/**", "/v3/api-docs/**").permitAll() // Swagger 허용 .requestMatchers("/h2-console/**").permitAll() // H2 Console 허용 + .requestMatchers("/actuator/health").permitAll() // 헬스 체크 허용 .anyRequest().authenticated() ) From 9b04b29e1f99f19da604fbdab3a0d436ae63968a Mon Sep 17 00:00:00 2001 From: namgigun Date: Wed, 1 Oct 2025 10:34:41 +0900 Subject: [PATCH 5/6] =?UTF-8?q?Infra:=20=EB=B0=B0=ED=8F=AC=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 방식: 최신 이미지를 받아 기존 컨테이너를 지우고 새로운 컨테이너를 생성 (다운 타임 O) - 변경 방식: 최신 이미지를 받아 새로운 컨테이너를 실행 후, NginX의 업스트림을 새로운 컨테이너로 변경하고 기존 이미지를 삭제 (다운 타임 X) --- .github/workflows/backend-cd.yml | 143 +++++++++++++++++++++++++++---- 1 file changed, 125 insertions(+), 18 deletions(-) diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml index 5dfc9a33..e7780214 100644 --- a/.github/workflows/backend-cd.yml +++ b/.github/workflows/backend-cd.yml @@ -75,9 +75,13 @@ jobs: deploy: runs-on: ubuntu-latest - needs: [ buildImageAndPush ] + needs: [ makeTagAndRelease, buildImageAndPush ] env: - OWNER: ${{ github.repository_owner }} + IMAGE_REPOSITORY: catfe_backend # 도커 이미지 명 + CONTAINER_1_NAME: catfe_1 # 슬롯 1 + CONTAINER_2_NAME: catfe_2 # 슬롯 2 + CONTAINER_PORT: 8080 # 컨테이너 내부 포트 + DOCKER_NETWORK: common # 도커 네트워크 EC2_INSTANCE_TAG_NAME: team5-ec2-1 steps: @@ -95,10 +99,6 @@ jobs: [[ -n "${INSTANCE_ID}" && "${INSTANCE_ID}" != "None" ]] || { echo "No running instance found"; exit 1; } echo "INSTANCE_ID=${INSTANCE_ID}" >> "${GITHUB_ENV}" - - name: set lower case owner name - run: | - echo "OWNER_LC=${OWNER,,}" >> ${GITHUB_ENV} - - name: AWS SSM Send-Command uses: peterkimzz/aws-ssm-send-command@master id: ssm @@ -110,21 +110,128 @@ jobs: working-directory: / comment: Deploy command: | - # 1. env 변수 확인 - echo "OWNER_LC = ${{ env.OWNER_LC }}" + set -Eeuo pipefail - # 2. EC2 인스턴스 아이디 확인 + # 1. EC2 인스턴스 아이디 확인 echo "INSTANCE_ID=${INSTANCE_ID}" - # 3. 최신 이미지 pull - docker pull ghcr.io/${{ env.OWNER_LC }}/catfe-backend:latest + # 3. 실행 로그(라인 타임스탬프 부착) + LOG="/tmp/ssm-$(date +%Y%m%d_%H%M%S).log" + exec > >(awk '{ fflush(); print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }' | tee -a "$LOG") + exec 2> >(awk '{ fflush(); print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }' | tee -a "$LOG" >&2) + + # 4. 변수 정의 + source /etc/environment || true + OWNER_LC="${{ github.repository_owner }}" + OWNER_LC="${OWNER_LC,,}" + IMAGE_TAG="${{ needs.makeTagAndRelease.outputs.tag_name }}" + IMAGE_REPOSITORY="${{ env.IMAGE_REPOSITORY }}" + IMAGE="ghcr.io/${OWNER_LC}/${IMAGE_REPOSITORY}:${IMAGE_TAG}" + SLOT1="${{ env.CONTAINER_1_NAME }}" + SLOT2="${{ env.CONTAINER_2_NAME }}" + PORT_IN="${{ env.CONTAINER_PORT }}" + NET="${{ env.DOCKER_NETWORK }}" + + # 도커 이미지 pull 받기 + echo "🔹 Use image: ${IMAGE}" + docker pull "${IMAGE}" + + #5. NPM API 토큰 발급 + TOKEN=$(curl -s -X POST http://127.0.0.1:81/api/tokens \ + -H "Content-Type: application/json" \ + -d "{\"identity\": \"admin@npm.com\", \"secret\": \"${PASSWORD:-}\"}" | jq -r '.token') + + # 조회한 토큰과 도메인 검증 + [[ -n "${TOKEN}" && "${TOKEN}" != "null" ]] || { echo "NPM token issue failed"; exit 1; } + [[ -n "${DOMAIN:-}" ]] || { echo "DOMAIN is empty"; exit 1; } + + # 6. 대상 프록시 호스트 ID 조회(도메인 매칭) + PROXY_ID=$(curl -s -X GET "http://127.0.0.1:81/api/nginx/proxy-hosts" \ + -H "Authorization: Bearer ${TOKEN}" \ + | jq ".[] | select(.domain_names[]==\"${APP_1_DOMAIN}\") | .id") + + # 조회한 프록시 호스트 ID 검증 + [[ -n "${PROXY_ID}" && "${PROXY_ID}" != "null" ]] || { echo "Proxy host not found for ${APP_1_DOMAIN}"; exit 1; } + + # 현재 프록시가 바라보는 업스트림(컨테이너명) 조회 + CURRENT_HOST=$(curl -s -X GET "http://127.0.0.1:81/api/nginx/proxy-hosts/${PROXY_ID}" \ + -H "Authorization: Bearer ${TOKEN}" \ + | jq -r '.forward_host') + + echo "🔎 CURRENT_HOST: ${CURRENT_HOST:-none}" + + # 7. 역할(blue/green) 판정 (blue -> 현재 운영 중인 서버, green -> 교체할 서버) + if [[ "${CURRENT_HOST:-}" == "${SLOT1}" ]]; then + BLUE="${SLOT1}" + GREEN="${SLOT2}" + + elif [[ "${CURRENT_HOST:-}" == "${SLOT2}" ]]; then + BLUE="${SLOT2}" + GREEN="${SLOT1}" + + # 초기 배포 + else + BLUE="none" + GREEN="${SLOT1}" + + # 조건문 종료 + fi + echo "🎨 role -> blue(now): ${BLUE}, green(next): ${GREEN}" + + # 8. Green 역할 컨테이너 + docker rm -f "${Green}" > /dev/null 2>&1 || true + echo "run new container -> ${Green}" + docker run -d --name "${Green}" \ + --restart unless-stopped \ + --network "${NET}" \ + -e TZ=Asia/Seoul \ + "${IMAGE}" + + # 9. 헬스체크 + echo "⏱ health-check: ${GREEN}" + TIMEOUT=120 + INTERVAL=3 + ELAPSED=0 + sleep 8 # 초기부팅 여유 + + while (( ELAPSED < TIMEOUT )); do + CODE=$(docker exec "${GREEN}" curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:${PORT_IN}/actuator/health" || echo 000) + [[ "${CODE}" == "200" ]] && { echo "✅ ${GREEN} healthy"; break; } + sleep "${INTERVAL}" + ELAPSED=$((ELAPSED + INTERVAL)) + done + [[ "${CODE:-000}" == "200" ]] || { echo "❌ ${GREEN} health failed"; docker logs --tail=200 "${GREEN}" || true; docker rm -f "${GREEN}" || true; exit 1; } + + # 10. 업스트림 전환 + NEW_CFG=$(jq -n --arg host "${GREEN}" --argjson port ${PORT_IN} '{forward_host:$host, forward_port:$port}') + curl -s -X PUT "http://127.0.0.1:81/api/nginx/proxy-hosts/${PROXY_ID}" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d "${NEW_CFG}" >/dev/null + echo "🔁 switch upstream → ${GREEN}:${PORT_IN}" + + # 11. 이전 Blue 종료 + if [[ "${BLUE}" != "none" ]]; then + docker stop "${BLUE}" >/dev/null 2>&1 || true + docker rm "${BLUE}" >/dev/null 2>&1 || true + echo "🧹 removed old blue: ${BLUE}" + fi + + # 12. 이미지 정리 + { + docker images --format '{{.Repository}}:{{.Tag}}' \ + | grep -F "ghcr.io/${OWNER_LC}/${IMAGE_REPOSITORY}:" \ + | grep -v -F ":${IMAGE_TAG}" \ + | grep -v -F ":latest" \ + | xargs -r docker rmi + } || true + + echo "🏁 Blue/Green switch complete. now blue = ${GREEN}" + + + + + - # 2. 기존 컨테이너 종료 및 제거 - docker stop catfe-backend 2>/dev/null - docker rm catfe-backend 2>/dev/null - # 3. 새로운 컨테이너 실행 - docker run -d --name catfe-backend -p 8080:8080 ghcr.io/${{ env.OWNER_LC }}/catfe-backend:latest - # 4. dangling 이미지 삭제 - docker rmi $(docker images -f "dangling=true" -q) \ No newline at end of file From 4aae5afce786f2eaf451ec3f3dce7576c12cb8f8 Mon Sep 17 00:00:00 2001 From: namgigun Date: Wed, 1 Oct 2025 11:04:29 +0900 Subject: [PATCH 6/6] =?UTF-8?q?Chore:=20backend-cd=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=AC=B8=EB=B2=95=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인스턴스 아이디 조회 오류 수정 --- .github/workflows/backend-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml index e7780214..0cffccfb 100644 --- a/.github/workflows/backend-cd.yml +++ b/.github/workflows/backend-cd.yml @@ -94,7 +94,7 @@ jobs: - name: get instance id run: | INSTANCE_ID=$(aws ec2 describe-instances \ - --filters "Name=tag:Name, Values=${{ EC2_INSTANCE_TAG_NAME }}" "Name=instance-state-name, Values=running" \ + --filters "Name=tag:Name, Values=${{ env.EC2_INSTANCE_TAG_NAME }}" "Name=instance-state-name, Values=running" \ --query "Reservations[].Instances[].InstanceId" --output text) [[ -n "${INSTANCE_ID}" && "${INSTANCE_ID}" != "None" ]] || { echo "No running instance found"; exit 1; } echo "INSTANCE_ID=${INSTANCE_ID}" >> "${GITHUB_ENV}"