Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 142 additions & 16 deletions .github/workflows/backend-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,29 @@ 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:
- name: set lower case owner name
- 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: |
echo "OWNER_LC=${OWNER,,}" >> ${GITHUB_ENV}
INSTANCE_ID=$(aws ec2 describe-instances \
--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}"

- name: AWS SSM Send-Command
uses: peterkimzz/aws-ssm-send-command@master
Expand All @@ -90,22 +106,132 @@ 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 변수 확인
echo "OWNER_LC = ${{ env.OWNER_LC }}"
set -Eeuo pipefail

# 1. EC2 인스턴스 아이디 확인
echo "INSTANCE_ID=${INSTANCE_ID}"

# 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}"





# 1. 최신 이미지 pull
docker pull ghcr.io/${{ env.OWNER_LC }}/catfe-backend:latest

# 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)
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
15 changes: 11 additions & 4 deletions infra/terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,15 @@ sudo mkswap /swapfile
sudo swapon /swapfile
sudo sh -c 'echo "/swapfile swap swap defaults 0 0" >> /etc/fstab'

# git 설치
yum install git -y

#도커 설치 및 실행/활성화
# 환경변수 세팅(/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
systemctl start docker
Expand Down Expand Up @@ -229,6 +234,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
}
Expand Down
3 changes: 3 additions & 0 deletions infra/terraform/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
variable "catfe_domain_1" {
description = "api.catfe.site"
}
1 change: 1 addition & 0 deletions src/main/java/com/back/global/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)

Expand Down