-
Notifications
You must be signed in to change notification settings - Fork 0
200 lines (179 loc) · 8.11 KB
/
Copy pathdeploy.yml
File metadata and controls
200 lines (179 loc) · 8.11 KB
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# ============================================================
# main 푸시 -> 깃허브에서 이미지 빌드 -> EC2 가 pull 받아 재기동.
#
# 단계가 셋으로 나뉜 이유:
# build Gradle 을 한 번만 돌려 jar 6개를 만든다. 서비스가 6개라고 Gradle 을
# 6번 돌리면 common-module 컴파일을 6번 반복하게 된다.
# 테스트는 여기서 돌리지 않는다 - PR 시점에 test.yml 이 이미 돌렸다.
# 배포를 붙잡고 다시 도는 것은 같은 검증을 두 번 하는 셈이다.
# images jar 하나씩 받아 얇은 런타임 이미지로 만들어 GHCR 에 올린다.
# 서비스끼리 의존이 없으니 6개가 동시에 돈다(matrix).
# deploy EC2 에 SSH 로 들어가 pull 하고 compose 를 다시 올린다.
#
# 필요한 저장소 시크릿 (Settings > Secrets and variables > Actions):
# EC2_HOST EC2 퍼블릭 IP 또는 도메인
# EC2_USER ec2-user (Amazon Linux) 또는 ubuntu
# EC2_SSH_KEY EC2 접속용 개인키 전문 (-----BEGIN ... 부터 끝까지)
#
# EC2 에서 한 번만 해둘 것 - GHCR 이미지를 받으려면 로그인이 필요하다
# (레포가 비공개면 패키지도 비공개다):
# echo <read:packages 권한 PAT> | docker login ghcr.io -u <깃허브아이디> --password-stdin
# ============================================================
name: deploy
on:
push:
branches: [main]
# 문서·부하테스트 도구만 바뀐 머지에 6개 컨테이너를 전부 재기동할 이유가
# 없다. 런타임이 읽지 않는 경로는 배포를 건너뛴다.
# 주의: 여기 걸린 경로는 EC2 의 git clone 에도 다음 "진짜 배포" 때까지
# 반영되지 않는다 - 서버가 읽는 파일(compose, 설정)은 절대 넣지 말 것.
paths-ignore:
- 'docs/**'
- 'tools/**'
- '**.md'
- '.gitignore'
# 코드 변경 없이 다시 배포하고 싶을 때 수동 실행
workflow_dispatch:
# 배포가 겹치면 EC2 에서 compose 두 개가 동시에 컨테이너를 만든다.
# 나중에 들어온 것만 남기고 앞의 것은 취소한다.
concurrency:
group: deploy-main
cancel-in-progress: false
env:
# GHCR 경로는 소문자만 허용한다(레포명은 Comatching5_BE 지만 여기선 소문자).
IMAGE_PREFIX: ghcr.io/comatching/comatching5_be
jobs:
build:
name: jar 빌드
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: gradle/actions/setup-gradle@v4
- run: chmod +x gradlew
# bootJar 만 돌린다. `build` 를 쓰면 테스트까지 다시 도는데, 그 검증은
# PR 단계(test.yml)에서 이미 끝났다.
#
# 다만 build/libs 에 jar 가 하나만 생기지는 않는다 - Spring Boot 는
# bootJar(실행 가능)와 별개로 jar 태스크가 -plain.jar 를 만들 수 있어,
# 아래 이미지 잡에서 실행 가능한 쪽을 골라낸다.
- name: jar 빌드
run: ./gradlew bootJar
- name: jar 보관
uses: actions/upload-artifact@v4
with:
name: boot-jars
path: '*/build/libs/*.jar'
retention-days: 1
images:
name: 이미지 (${{ matrix.service }})
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
# 한 서비스가 실패해도 나머지는 끝까지 만든다. 어디가 깨졌는지 한 번에 본다.
fail-fast: false
matrix:
service:
- gateway-service
- user-service
- matching-service
- chat-service
- item-service
- notification
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: boot-jars
path: jars
# 컨텍스트에 app.jar 하나만 둔다. 소스 전체를 컨텍스트로 넘기면
# 도커 데몬에 수십 MB 를 전송하고 캐시도 쉽게 깨진다.
#
# -plain.jar 를 걸러내는 이유: `./gradlew build` 는 bootJar 와 jar 를
# 둘 다 돌려서 build/libs 에 jar 가 두 개 생긴다(실행 가능한 것과
# 클래스만 든 -plain). 그대로 cp 하면 대상이 디렉터리가 아니라고 실패하고,
# 설령 통과해도 어느 쪽이 복사될지 알 수 없다.
- name: 빌드 컨텍스트 준비
run: |
set -euo pipefail
mkdir -p ci-context
JAR=$(find "jars/${{ matrix.service }}/build/libs" -name '*.jar' ! -name '*-plain.jar')
COUNT=$(echo "$JAR" | grep -c . || true)
if [ "$COUNT" -ne 1 ]; then
echo "실행 가능한 jar 가 정확히 하나여야 하는데 ${COUNT}개다:"
ls -la "jars/${{ matrix.service }}/build/libs"
exit 1
fi
cp "$JAR" ci-context/app.jar
ls -la ci-context
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 태그 둘을 같이 올린다.
# :latest 사람이 눈으로 볼 때 쓰는 이름
# :<커밋 SHA> 실제로 배포에 쓰는 이름. 어떤 커밋이 떠 있는지가 명확하고,
# 되돌릴 때 예전 SHA 를 그대로 지목할 수 있다.
- uses: docker/build-push-action@v6
with:
context: ci-context
file: Dockerfile.ci
push: true
tags: |
${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:latest
${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:${{ github.sha }}
cache-from: type=gha,scope=${{ matrix.service }}
cache-to: type=gha,mode=max,scope=${{ matrix.service }}
deploy:
name: EC2 배포
needs: images
runs-on: ubuntu-latest
steps:
- name: SSH 로 pull + 재기동
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: |
set -euo pipefail
cd ~/comatching
# 컴포즈 파일·설정 갱신. .env.prod 는 추적 대상이 아니라 reset 에
# 지워지지 않는다(추적되지 않는 파일은 건드리지 않음).
git fetch origin main
git reset --hard origin/main
# 배포한 태그를 .env.prod 에 박아 둔다. 나중에 사람이 손으로
# docker compose up 을 해도 지금 뜬 것과 같은 버전이 뜬다.
if grep -q '^IMAGE_TAG=' .env.prod; then
sed -i "s|^IMAGE_TAG=.*|IMAGE_TAG=${{ github.sha }}|" .env.prod
else
echo "IMAGE_TAG=${{ github.sha }}" >> .env.prod
fi
docker compose -f docker-compose.prod.yml --env-file .env.prod pull
# --no-build: EC2 에서 소스를 빌드하지 않는다. 이미지는 이미 만들어져 있다.
docker compose -f docker-compose.prod.yml --env-file .env.prod up -d --no-build
# 배포마다 서비스 6개의 이미지(~3GB 세트)가 새로 쌓인다. prune -f 는
# 태그 없는 것만 지우는데, 옛 커밋 SHA 이미지는 태그가 붙어 있어
# 그대로 남는다. -a 로 "컨테이너가 쓰지 않는 것"을 전부 지운다.
# 예전엔 until=72h 로 롤백 여지를 뒀지만, 머지가 잦은 날 세대가
# 쌓여 29GB 디스크가 가득 차 배포 자체가 죽었다(2026-08-26).
# 롤백은 GHCR 에 남아 있는 이미지를 다시 pull 하면 된다.
docker image prune -af
- name: 상태 확인
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
cd ~/comatching
docker compose -f docker-compose.prod.yml --env-file .env.prod ps