diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 000000000..5476764b8 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,156 @@ +name: CD Pipeline + +on: + push: + branches: [ "main" ] + +concurrency: + group: mople-production-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +env: + AWS_REGION: ap-northeast-2 + ECR_REPOSITORY: mople-app + CONTAINER_NAME: mople-app + +jobs: + build-and-push: + runs-on: ubuntu-latest + outputs: + image: ${{ steps.build-image.outputs.image }} + + steps: + - name: 저장소 체크아웃 + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: JDK 17 설정 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Gradle 설정 (캐시 포함) + uses: gradle/actions/setup-gradle@v4 + + - name: gradlew 실행 권한 부여 + run: chmod +x ./gradlew + + - name: bootJar 빌드 + run: ./gradlew bootJar --no-daemon -x test + + - name: AWS 자격 증명 설정 + 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: ${{ env.AWS_REGION }} + + - name: ECR 로그인 + id: ecr-login + uses: aws-actions/amazon-ecr-login@v2 + + - name: Docker 이미지 빌드 및 ECR 푸시 + id: build-image + env: + REGISTRY: ${{ steps.ecr-login.outputs.registry }} + IMAGE_TAG: ${{ github.sha }} + run: | + docker build -t $REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker tag $REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $REGISTRY/$ECR_REPOSITORY:latest + docker push $REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + docker push $REGISTRY/$ECR_REPOSITORY:latest + echo "image=$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> "$GITHUB_OUTPUT" + + deploy: + needs: build-and-push + runs-on: ubuntu-latest + + steps: + - name: SSH로 EC2에 배포 + uses: appleboy/ssh-action@v1 + env: + IMAGE: ${{ needs.build-and-push.outputs.image }} + AWS_REGION: ${{ env.AWS_REGION }} + CONTAINER_NAME: ${{ env.CONTAINER_NAME }} + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_SSH_KEY }} + envs: IMAGE,AWS_REGION,CONTAINER_NAME + script: | + set -e + + CANDIDATE_NAME="${CONTAINER_NAME}_candidate" + CANDIDATE_PORT=18080 + + aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin ${IMAGE%%/*} + docker pull $IMAGE + + PREV_IMAGE=$(docker inspect --format='{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || echo "none") + echo "현재 서비스 중인 이미지: $PREV_IMAGE (교체 실패 시 그대로 유지됨)" + + docker rm -f "$CANDIDATE_NAME" 2>/dev/null || true + docker run -d --name "$CANDIDATE_NAME" -p 127.0.0.1:${CANDIDATE_PORT}:8080 --env-file /home/ubuntu/mople.env "$IMAGE" + + READY=false + for i in $(seq 1 30); do + if curl -fs "http://localhost:${CANDIDATE_PORT}/actuator/health" | grep -q '"status":"UP"'; then + READY=true + break + fi + sleep 2 + done + + if [ "$READY" != true ]; then + echo "새 컨테이너 readiness 실패 — 배포 중단, 기존 컨테이너($PREV_IMAGE) 그대로 유지" + docker logs "$CANDIDATE_NAME" --tail 100 || true + docker rm -f "$CANDIDATE_NAME" || true + exit 1 + fi + + echo "새 컨테이너 readiness 확인 완료, 기존 컨테이너 교체 시작" + docker rm -f "$CANDIDATE_NAME" + + ROLLBACK_NAME="${CONTAINER_NAME}_rollback" + docker rm -f "$ROLLBACK_NAME" 2>/dev/null || true + if docker inspect "$CONTAINER_NAME" >/dev/null 2>&1; then + docker stop "$CONTAINER_NAME" 2>/dev/null || true + docker rename "$CONTAINER_NAME" "$ROLLBACK_NAME" + fi + + rollback() { + echo "새 컨테이너 기동/readiness 실패 — 이전 컨테이너($PREV_IMAGE)로 즉시 롤백" + docker rm -f "$CONTAINER_NAME" 2>/dev/null || true + if docker inspect "$ROLLBACK_NAME" >/dev/null 2>&1; then + docker rename "$ROLLBACK_NAME" "$CONTAINER_NAME" + docker start "$CONTAINER_NAME" + fi + exit 1 + } + + if ! docker run -d --name "$CONTAINER_NAME" --restart unless-stopped -p 8080:8080 --env-file /home/ubuntu/mople.env "$IMAGE"; then + rollback + fi + + READY=false + for i in $(seq 1 30); do + if curl -fs "http://localhost:8080/actuator/health" | grep -q '"status":"UP"'; then + READY=true + break + fi + sleep 2 + done + + if [ "$READY" != true ]; then + docker logs "$CONTAINER_NAME" --tail 100 || true + rollback + fi + + echo "배포 완료 — 이전 컨테이너 정리" + docker rm -f "$ROLLBACK_NAME" 2>/dev/null || true + docker image prune -f \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..68e1533b3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# --- 1단계 : 빌드 --- +FROM eclipse-temurin:17-jdk-jammy AS build +WORKDIR /app + +# 의존성 캐시를 위해 gradle 관련 파일 먼저 복사 +COPY gradlew build.gradle settings.gradle ./ +COPY gradle ./gradle +RUN chmod +x ./gradlew +RUN ./gradlew dependencies --no-daemon || true + +# 소스 복사 후 빌드 (테스트는 CI에서 검증되므로 스킵) +COPY src ./src +RUN ./gradlew bootJar --no-daemon -x test + +# --- 2단계 : 실행 --- +FROM eclipse-temurin:17-jre-jammy +WORKDIR /app + +# non-root 사용자로 실행 +RUN useradd -r -u 1001 appuser +USER appuser + +COPY --from=build /app/build/libs/mople-0.0.1-SNAPSHOT.jar app.jar + +EXPOSE 8080 +ENV SPRING_PROFILES_ACTIVE=prod + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file