|
1 | | -name: Deploy to Dev EC2 (Docker Compose) |
| 1 | +name: Deploy to Dev EC2 |
2 | 2 |
|
3 | 3 | on: |
4 | 4 | push: |
|
7 | 7 |
|
8 | 8 | jobs: |
9 | 9 | deploy: |
| 10 | + name: Build and Deploy to EC2 |
10 | 11 | runs-on: ubuntu-latest |
11 | 12 | environment: dev |
12 | | - permissions: |
13 | | - contents: read |
14 | | - packages: write |
15 | | - |
16 | | - env: |
17 | | - CONTAINER_NAME: saerok-dev |
18 | | - COMPOSE_FILE_REMOTE: docker-compose.dev.yml |
19 | 13 |
|
20 | 14 | steps: |
21 | | - - name: 🧾 Checkout |
22 | | - uses: actions/checkout@v4 |
23 | | - |
24 | | - # GHCR 레포(org/repo) 소문자 정규화 → outputs + env |
25 | | - - name: 🔧 Normalize image repo |
26 | | - id: normalize |
27 | | - run: | |
28 | | - echo "image_repo=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" |
29 | | - echo "IMAGE_REPO=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" |
30 | | -
|
31 | | - - name: 🔧 Set up Docker Buildx |
32 | | - uses: docker/setup-buildx-action@v3 |
| 15 | + - name: Checkout source code |
| 16 | + uses: actions/checkout@v3 |
33 | 17 |
|
34 | | - - name: 🔐 Log in to GHCR |
35 | | - uses: docker/login-action@v3 |
| 18 | + - name: Set up JDK 21 |
| 19 | + uses: actions/setup-java@v3 |
36 | 20 | with: |
37 | | - registry: ghcr.io |
38 | | - username: ${{ secrets.GHCR_USERNAME }} |
39 | | - password: ${{ secrets.GHCR_TOKEN }} |
| 21 | + distribution: 'corretto' |
| 22 | + java-version: '21' |
40 | 23 |
|
41 | | - - name: 🏷️ Extract metadata (tags, labels) |
42 | | - id: meta |
43 | | - uses: docker/metadata-action@v5 |
44 | | - with: |
45 | | - images: ${{ steps.normalize.outputs.image_repo }} |
46 | | - tags: | |
47 | | - type=raw,value=dev-latest |
48 | | - type=sha,format=long,prefix=dev-,suffix= |
| 24 | + - name: Grant execute permission for Gradle |
| 25 | + run: chmod +x gradlew |
49 | 26 |
|
50 | | - - name: 📌 Show image tags |
51 | | - run: | |
52 | | - echo "::group::Image tags" |
53 | | - echo "${{ steps.meta.outputs.tags }}" | tr ' ' '\n' |
54 | | - echo "::endgroup::" |
55 | | - { |
56 | | - echo "### 🏷️ Image tags"; |
57 | | - echo '${{ steps.meta.outputs.tags }}' | tr ' ' '\n' | sed 's/^/- /'; |
58 | | - } >> $GITHUB_STEP_SUMMARY |
| 27 | + - name: Build with Gradle |
| 28 | + run: ./gradlew test bootJar |
59 | 29 |
|
60 | | - - name: 🏗️ Build and push image |
61 | | - uses: docker/build-push-action@v6 |
| 30 | + - name: Copy JAR to EC2 |
| 31 | + uses: appleboy/scp-action@v0.1.7 |
62 | 32 | with: |
63 | | - context: . |
64 | | - file: ./Dockerfile |
65 | | - push: true |
66 | | - tags: ${{ steps.meta.outputs.tags }} |
67 | | - labels: ${{ steps.meta.outputs.labels }} |
68 | | - cache-from: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache |
69 | | - cache-to: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache,mode=max |
70 | | - |
71 | | - - name: ✅ Verify image pushed |
72 | | - run: | |
73 | | - echo "::notice::Pushed to $IMAGE_REPO (primary tag: dev-latest)" |
74 | | - { |
75 | | - echo "### ✅ Build & Push"; |
76 | | - echo "- Repo: \`$IMAGE_REPO\`"; |
77 | | - echo "- Primary tag: \`dev-latest\`"; |
78 | | - } >> $GITHUB_STEP_SUMMARY |
| 33 | + host: ${{ secrets.EC2_HOST }} |
| 34 | + username: ubuntu |
| 35 | + key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} |
| 36 | + source: build/libs/*.jar |
| 37 | + target: ~/saerok/ |
| 38 | + strip_components: '2' |
79 | 39 |
|
80 | | - - name: 🚀 Deploy on EC2 via SSH |
| 40 | + - name: Restart application on EC2 |
81 | 41 | uses: appleboy/ssh-action@v1 |
82 | 42 | with: |
83 | 43 | host: ${{ secrets.EC2_HOST }} |
84 | 44 | username: ubuntu |
85 | 45 | key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} |
| 46 | + debug: true |
86 | 47 | script: | |
87 | 48 | set -euxo pipefail |
88 | | - # ======== bind workflow values into remote shell ======== |
89 | | - COMPOSE_FILE_REMOTE="${{ env.COMPOSE_FILE_REMOTE }}" |
90 | | - CONTAINER_NAME="${{ env.CONTAINER_NAME }}" |
91 | | - IMAGE_REPO="${{ steps.normalize.outputs.image_repo }}" |
92 | | - export COMPOSE_FILE_REMOTE CONTAINER_NAME IMAGE_REPO |
93 | | - # ======================================================= |
94 | | -
|
95 | | - banner () { echo "::notice::$1"; } |
96 | | - section () { echo "::group::$1"; } |
97 | | - endsection () { echo "::endgroup::"; } |
98 | | -
|
99 | | - section "0) Ensure Docker & Compose" |
100 | | - if ! command -v docker >/dev/null 2>&1; then |
101 | | - banner "Installing Docker..." |
102 | | - curl -fsSL https://get.docker.com | sh |
103 | | - sudo usermod -aG docker $USER || true |
104 | | - fi |
105 | | - if ! docker compose version >/dev/null 2>&1; then |
106 | | - banner "Installing docker compose plugin..." |
107 | | - DOCKER_COMPOSE_VERSION="v2.29.7" |
108 | | - sudo mkdir -p /usr/local/lib/docker/cli-plugins |
109 | | - sudo curl -fsSL https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 \ |
110 | | - -o /usr/local/lib/docker/cli-plugins/docker-compose |
111 | | - sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose |
112 | | - fi |
113 | | - docker --version |
114 | | - docker compose version |
115 | | - endsection |
116 | | -
|
117 | | - section "1) Prepare workspace" |
118 | | - mkdir -p ~/saerok |
| 49 | + |
119 | 50 | cd ~/saerok |
120 | | - pwd |
121 | | - endsection |
122 | | -
|
123 | | - section "2) Docker login (GHCR pull)" |
124 | | - echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin |
125 | | - endsection |
126 | | -
|
127 | | - section "3) Generate ${COMPOSE_FILE_REMOTE}" |
128 | | - cat > "${COMPOSE_FILE_REMOTE}" <<'YAML' |
129 | | - services: |
130 | | - app: |
131 | | - image: ${{ steps.normalize.outputs.image_repo }}:dev-latest |
132 | | - container_name: saerok-dev |
133 | | - restart: always |
134 | | - ports: |
135 | | - - "8080:8080" |
136 | | - environment: |
137 | | - SPRING_PROFILES_ACTIVE: "dev" |
138 | | - DB_URL: "${{ secrets.DB_URL }}" |
139 | | - DB_USERNAME: "${{ secrets.DB_USERNAME }}" |
140 | | - DB_PASSWORD: "${{ secrets.DB_PASSWORD }}" |
141 | | - IAM_ACCESS_KEY: "${{ secrets.IAM_ACCESS_KEY }}" |
142 | | - IAM_SECRET_KEY: "${{ secrets.IAM_SECRET_KEY }}" |
143 | | - UPLOAD_IMAGE_BUCKET_NAME: "${{ vars.UPLOAD_IMAGE_BUCKET_NAME }}" |
144 | | - UPLOAD_IMAGE_DOMAIN: "${{ vars.UPLOAD_IMAGE_DOMAIN }}" |
145 | | - DEX_IMAGE_BUCKET_NAME: "${{ vars.DEX_IMAGE_BUCKET_NAME }}" |
146 | | - DEX_IMAGE_DOMAIN: "${{ vars.DEX_IMAGE_DOMAIN }}" |
147 | | - APPLE_CLIENT_ID: "${{ secrets.APPLE_CLIENT_ID }}" |
148 | | - APPLE_TEAM_ID: "${{ secrets.APPLE_TEAM_ID }}" |
149 | | - APPLE_KEY_ID: "${{ secrets.APPLE_KEY_ID }}" |
150 | | - APPLE_PRIVATE_KEY: "${{ secrets.APPLE_PRIVATE_KEY }}" |
151 | | - JWT_SECRET: "${{ secrets.JWT_SECRET }}" |
152 | | - KAKAO_ADMIN_KEY: "${{ secrets.KAKAO_ADMIN_KEY }}" |
153 | | - KAKAO_CLIENT_ID: "${{ secrets.KAKAO_CLIENT_ID }}" |
154 | | - KAKAO_REDIRECT_URI: "${{ secrets.KAKAO_REDIRECT_URI }}" |
155 | | - KAKAO_CLIENT_SECRET: "${{ secrets.KAKAO_CLIENT_SECRET }}" |
156 | | - KMS_KEY_ID: "${{ secrets.KMS_KEY_ID }}" |
157 | | - FIREBASE_ADMIN_KEY: "${{ secrets.FIREBASE_ADMIN_KEY }}" |
158 | | - healthcheck: |
159 | | - test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health >/dev/null || exit 1"] |
160 | | - interval: 10s |
161 | | - timeout: 3s |
162 | | - retries: 15 |
163 | | - start_period: 60s |
164 | | - YAML |
165 | | - banner "Compose file written: ${COMPOSE_FILE_REMOTE}" |
166 | | - docker compose -f "${COMPOSE_FILE_REMOTE}" config --services || true |
167 | | - endsection |
168 | | -
|
169 | | - section "4) Pull & Up (recreate with port-fallback)" |
170 | | - set +e |
171 | | - docker compose -f "${COMPOSE_FILE_REMOTE}" pull |
172 | | - docker compose -f "${COMPOSE_FILE_REMOTE}" up -d --force-recreate |
173 | | - UP_RC=$? |
174 | | - set -e |
175 | | -
|
176 | | - if [ $UP_RC -ne 0 ]; then |
177 | | - echo "::warning::compose up failed (rc=$UP_RC). Checking port 8080..." |
178 | | - ss -lntp || true |
179 | | - if ss -lntp | grep -q ':8080 '; then |
180 | | - echo "::warning::Port 8080 is busy. Trying to free it." |
181 | | - if ! command -v fuser >/dev/null 2>&1; then |
182 | | - sudo apt-get -qq update |
183 | | - sudo DEBIAN_FRONTEND=noninteractive apt-get -y -qq install psmisc |
184 | | - fi |
185 | | - sudo fuser -k 8080/tcp || true |
186 | | - sleep 2 |
187 | | - docker compose -f "${COMPOSE_FILE_REMOTE}" up -d --force-recreate |
188 | | - else |
189 | | - echo "::error::Compose up failed and port 8080 is not busy. Aborting." |
190 | | - exit 1 |
191 | | - fi |
192 | | - fi |
193 | | -
|
194 | | - docker compose -f "${COMPOSE_FILE_REMOTE}" ps |
195 | | - endsection |
196 | | -
|
197 | | - section "5) Wait for health=healthy" |
198 | | - set +e |
199 | | - ATTEMPTS=40 |
200 | | - for i in $(seq 1 ${ATTEMPTS}); do |
201 | | - STATUS=$(docker inspect -f '{{.State.Health.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "unknown") |
202 | | - echo "[$i/${ATTEMPTS}] health: $STATUS" |
203 | | - if [ "$STATUS" = "healthy" ]; then |
204 | | - banner "Container is healthy." |
205 | | - OK=1 |
206 | | - break |
207 | | - fi |
208 | | - sleep 2 |
209 | | - done |
210 | | - if [ "${OK:-0}" != "1" ]; then |
211 | | - echo "::warning::Container did not reach healthy state in time." |
212 | | - docker logs --tail=200 "${CONTAINER_NAME}" || true |
213 | | - exit 1 |
214 | | - fi |
215 | | - set -e |
216 | | - endsection |
217 | | -
|
218 | | - section "6) Tail last logs" |
219 | | - docker logs --tail=200 "${CONTAINER_NAME}" || true |
220 | | - endsection |
221 | | -
|
222 | | - banner "✅ Deploy done" |
223 | | -
|
224 | | - - name: 🧾 Summary |
225 | | - if: always() |
226 | | - run: | |
227 | | - { |
228 | | - echo "## 🚀 Deploy Summary"; |
229 | | - echo ""; |
230 | | - echo "- Image repo: \`$IMAGE_REPO\`"; |
231 | | - echo "- Tags pushed:"; |
232 | | - echo '${{ steps.meta.outputs.tags }}' | tr ' ' '\n' | sed 's/^/ - /'; |
233 | | - echo ""; |
234 | | - echo "### Health"; |
235 | | - echo "- Target container: \`${{ env.CONTAINER_NAME }}\`"; |
236 | | - echo "- Health check: \`GET /health\`"; |
237 | | - echo ""; |
238 | | - echo "### Useful commands (on EC2)"; |
239 | | - echo "\`\`\`bash"; |
240 | | - echo "ssh ubuntu@${{ secrets.EC2_HOST }}"; |
241 | | - echo "cd ~/saerok"; |
242 | | - echo "docker compose -f ${COMPOSE_FILE_REMOTE} ps"; |
243 | | - echo "docker logs -f ${CONTAINER_NAME}"; |
244 | | - echo "curl -i localhost:8080/health"; |
245 | | - echo "\`\`\`"; |
246 | | - } >> $GITHUB_STEP_SUMMARY |
| 51 | + echo ">>> 현재 위치: $(pwd)" |
| 52 | + echo ">>> 파일:" |
| 53 | + ls -al |
| 54 | + |
| 55 | + # JAR 확인 |
| 56 | + JAR_NAME=$(ls -1t *.jar | head -n1) |
| 57 | + [ -n "$JAR_NAME" ] || { echo "❌ JAR 없음"; exit 2; } |
| 58 | + echo ">>> 선택된 JAR: $JAR_NAME" |
| 59 | + |
| 60 | + # 기존 프로세스 종료 |
| 61 | + pkill -f "$JAR_NAME" || true |
| 62 | + |
| 63 | + # 백그라운드 실행 |
| 64 | + DB_URL="${{ secrets.DB_URL }}" \ |
| 65 | + DB_USERNAME="${{ secrets.DB_USERNAME }}" \ |
| 66 | + DB_PASSWORD="${{ secrets.DB_PASSWORD }}" \ |
| 67 | + IAM_ACCESS_KEY="${{ secrets.IAM_ACCESS_KEY }}" \ |
| 68 | + IAM_SECRET_KEY="${{ secrets.IAM_SECRET_KEY }}" \ |
| 69 | + UPLOAD_IMAGE_BUCKET_NAME="${{ vars.UPLOAD_IMAGE_BUCKET_NAME }}" \ |
| 70 | + UPLOAD_IMAGE_DOMAIN="${{ vars.UPLOAD_IMAGE_DOMAIN }}" \ |
| 71 | + DEX_IMAGE_BUCKET_NAME="${{ vars.DEX_IMAGE_BUCKET_NAME }}" \ |
| 72 | + DEX_IMAGE_DOMAIN="${{ vars.DEX_IMAGE_DOMAIN }}" \ |
| 73 | + APPLE_CLIENT_ID="${{ secrets.APPLE_CLIENT_ID }}" \ |
| 74 | + APPLE_TEAM_ID="${{ secrets.APPLE_TEAM_ID }}" \ |
| 75 | + APPLE_KEY_ID="${{ secrets.APPLE_KEY_ID }}" \ |
| 76 | + APPLE_PRIVATE_KEY="${{ secrets.APPLE_PRIVATE_KEY }}" \ |
| 77 | + JWT_SECRET="${{ secrets.JWT_SECRET }}" \ |
| 78 | + KAKAO_ADMIN_KEY="${{ secrets.KAKAO_ADMIN_KEY }}" \ |
| 79 | + KAKAO_CLIENT_ID="${{ secrets.KAKAO_CLIENT_ID }}" \ |
| 80 | + KAKAO_REDIRECT_URI="${{ secrets.KAKAO_REDIRECT_URI }}" \ |
| 81 | + KAKAO_CLIENT_SECRET="${{ secrets.KAKAO_CLIENT_SECRET }}" \ |
| 82 | + KMS_KEY_ID="${{ secrets.KMS_KEY_ID }}" \ |
| 83 | + FIREBASE_ADMIN_KEY="${{ secrets.FIREBASE_ADMIN_KEY }}" \ |
| 84 | + nohup java -Duser.timezone=Asia/Seoul -jar $JAR_NAME --spring.profiles.active=dev > app.log 2>&1 & |
| 85 | + |
| 86 | + echo "✅ 배포 완료" |
| 87 | + |
0 commit comments