-
Notifications
You must be signed in to change notification settings - Fork 2
feat: AWS EC2 배포를 위한 Dockerfile 및 SSH 기반 CD 파이프라인 작성 #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| name: CD Pipeline | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ "main" ] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CI 워크플로우( 여기서 나오는 시나리오입니다. 테스트를 깨뜨리는 커밋이 main에 들어옴 → CI job과 CD job이 같이 시작 → CD는
두 가지 방향이 있습니다.
1번을 제안드립니다. |
||
|
|
||
| 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 | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 스텝이 만든 jar가 어디에서도 쓰이지 않습니다.
더 아쉬운 건 캐시 쪽입니다. 둘 중 하나로 정리하시는 걸 제안드립니다.
덧붙여, 현재 레포에 |
||
|
|
||
| - 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%%/*} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 질문 하나 드립니다. 이 시점에 EC2는 어떤 자격증명으로 ECR에 로그인하고 있나요? 두 갈래인데 실패의 폭발 반경이 많이 다릅니다.
후자라면 인스턴스 프로파일( 같은 맥락으로, |
||
| 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" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚨 readiness 확인용 컨테이너가 프로덕션에 부수효과를 냅니다. candidate는 실제 서비스 컨테이너와 **완전히 같은 이 컨테이너가 기동하면서 실제로 하는 일들입니다.
readiness만 보고 싶으신 거라면 candidate는 컨슈머·스케줄러를 끈 상태로 띄우는 게 맞습니다. |
||
|
|
||
| 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" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 줄에서 기존 컨테이너를 stop + rename 한 시점부터 서비스가 내려가 있는데, 정작 이 구간의 실패는
시나리오를 펼쳐보면 이렇습니다.
|
||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PR 본문에서 다음 단계를 미리 밝혀주셨습니다.
방향은 맞습니다. 다만 candidate 검증 구조 때문에 지금 파이프라인이 "무중단에 가깝다"고 읽히기 쉬워서, 한 번 정확히 짚어드립니다. 현재 구조에는 애플리케이션 기동 시간만큼의 다운타임이 그대로 있습니다. 시간축으로 펼쳐보면 이렇습니다. 122번 줄 candidate 단계가 막아주는 것은 "이미지가 애초에 안 뜨는 경우"이지 전환 자체의 공백이 아닙니다. 오히려 확인 후 죽였다가 8080으로 다시 띄우므로 기동을 두 번 하게 됩니다. 원인은 |
||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 줄은 의도하신 캐시 효과를 거의 못 냅니다. 세 가지가 겹쳐 있습니다.
3번 때문에 이 Dockerfile을 CI에서 쓰는 한 캐시 이야기 자체가 성립하지 않습니다. 후자를 제안드립니다. |
||
|
|
||
| # 소스 복사 후 빌드 (테스트는 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"] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
다만 JVM 힙 설정이 없습니다. 컨테이너 배포에서는 컨테이너 메모리 제한과 지금 상태를 보면 여기에 배포 흐름이 겹칩니다. cd.yml 98번 줄에서 candidate를 띄우는 동안 기존 컨테이너와 candidate의 JVM 두 개가 동시에 떠 있습니다. 이 구간에 메모리가 몰리면 OOM Killer가 멀쩡히 서비스 중이던 기존 컨테이너를 골라 죽일 수도 있습니다. 배포하다가 서비스가 죽는 경로입니다. ENTRYPOINT ["java", "-XX:MaxRAMPercentage=70.0", "-XX:+ExitOnOutOfMemoryError", "-jar", "app.jar"]그리고 |
||
Uh oh!
There was an error while loading. Please reload this page.