[V1] Backend CD - 100-hours-a-week/KTB4-10th-Cloud GitHub Wiki

[ BE CD 구축 ]

CD구조

- `GitHub Actions → SSM → EC2 → Docker Compose`
- Blue/Green 같은 별도의 배포 미사용

고려사항

- 운영 및 설정 복잡성을 고려하여 배포 명령 전달은 SSM을 사용
- 기존 문서에서의 변경점
    - iam role의 credential 저장 → OIDC

사전 작업

  • EC2 SSM 권한 (IAM Role설정)
  • OIDC구성
  • GHCR pull 토큰 → github repo secret
  • CI → CD로 이미지 태그 전달 가능하게 처리 →(기본의 ci에 deploy추가)

[ 1. OIDC(OpenId Connect) ]

  • SSM권한은 EC2의 SSM Agent가 시스템 매니저와 통신할 권리를 얻기 위한 추가

  • github action에서 SSM 명령을 보낼 권한이 있는가?

    ⇒ 이때 사용하는것이 OIDC

    • 기존에는 보안의 위험을 감수하더라도 credential을 github secret에 저장을 고려 → 추가적인 비용 없고 보안상 이점이 있는 OIDC를 도입하기로 변경

https://docs.github.com/ko/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws

1.1. AWS에 id 공급자 추가

  • IAM에 OIDC 공급자 등록

1.2. 역할 생성

  • ssm:SendCommand
    • 백엔드 ec2에서 이 명령 실행
  • ssm:GetCommandInvocation
    • 방금 실행한 명령이 성공했는지
  • ssm:ListCommandInvocation
    • 명령 실행 상태/결과
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "SendCommandToBackend",
			"Effect": "Allow",
			"Action": "ssm:SendCommand",
			"Resource": [
				"arn:aws:ec2:ap-northeast-2:742875367096:instance/i-0ecb626c01830b39b",
				"arn:aws:ssm:ap-northeast-2::document/AWS-RunShellScript"
			]
		},
		{
			"Sid": "ReadCommandResult",
			"Effect": "Allow",
			"Action": [
				"ssm:GetCommandInvocation",
				"ssm:ListCommandInvocations"
			],
			"Resource": "*"
		}
	]
}

1.3. 1.2에서 나온 Deploy-role을 깃헙에 등록

  • arn:aws:iam::742875367096:role/kgb-github-deploy-role
  • 비밀정보 아님 → variable에 등록
  • 등록의 이유?
    • actions에서 aws에서 어떤 iam role 사용할지 넣어주는 것

[ 2. CI-CD.yml ]

  • healthcheck api의 주소는 아직 미확정
  • actuator 사용여부도 미확정(일단 사용으로 가정) => actuator/health로 확정 (09/23)
#CI-CD 변경
#최종 Push때에만 해당 파이프라인 작동, lint 검사또한 진행
name: KGB Backend CI/CD

on:
  # main Merge 이후 CI/CD 실행
  push:
    branches:
      - main


# GitHub Actions 권한
permissions:
  contents: read
  packages: write

  # GitHub OIDC -> AWS IAM Role Assume에 필요
  id-token: write


jobs:

  # 1. main Merge 이후 CI 검증
  # ==================================================
  ci:
    name: CI

    runs-on: ubuntu-latest

    steps:

      # Repository Checkout
      - name: Checkout
        uses: actions/checkout@v4

      # JDK 설정
      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '21'

      # Gradle Wrapper 실행 권한
      - name: Grant Gradle Permission
        run: chmod +x gradlew

      # Build / Compile 검증
      # 실패 시 Image Build / 배포 차단
      - name: Gradle Build
        run: ./gradlew build

      # Checkstyle
      # 위반 내용 표시
      # build.gradle의 ignoreFailures = true 적용 시 CI 차단 X
      - name: Checkstyle
        run: ./gradlew checkstyleMain

      # SpotBugs
      # Rank 5~20 제외
      # Rank 1~4 발견 시 실패
      - name: SpotBugs
        run: ./gradlew spotbugsMain


  # 2. 실제 배포용 Docker Image 생성 및 GHCR Push
  # ==================================================
  build-image:
    name: Build and Push Image

    # CI 성공 이후 실행
    needs: ci

    runs-on: ubuntu-latest

    # 생성한 IMAGE_TAG를 CD Job으로 전달
    outputs:
      image_tag: ${{ steps.image.outputs.tag }}

    steps:

      # Merge 완료된 main Checkout
      - name: Checkout
        uses: actions/checkout@v4

      # GHCR Login
      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # Image Tag 생성
      # 형식: Commit SHA 앞 7자리-생성일
      # 예: a81c29f-20260921
      - name: Generate Image Tag
        id: image
        run: |
          SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
          DATE=$(date -u +%Y%m%d)
          echo "tag=${SHORT_SHA}-${DATE}" >> "$GITHUB_OUTPUT"

      # 실제 배포용 Docker Image Build
      # Docker Build 자체가 실패하면 이후 Push / CD 차단
      - name: Build Docker Image
        run: |
          docker build \
            -t ghcr.io/${{ github.repository_owner }}/ktb4-10-be:${{ steps.image.outputs.tag }} \
            .

      # GHCR Push
      - name: Push Docker Image
        run: |
          docker push \
            ghcr.io/${{ github.repository_owner }}/ktb4-10-be:${{ steps.image.outputs.tag }}


 
  # 3. Backend EC2 배포
  # ==================================================
  deploy:
    name: CD

    # Image Build / Push 성공 이후 실행
    needs: build-image

    runs-on: ubuntu-latest

    steps:

      # GitHub OIDC를 통해 AWS IAM Role Assume
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
          aws-region: ap-northeast-2


      
      # SSM을 통해 Backend EC2에 배포 명령 전달
      # ==================================================
      - name: Send Deploy Command
        id: deploy

        env:
          # build-image Job에서 생성한 Image Tag
          IMAGE_TAG: ${{ needs.build-image.outputs.image_tag }}

          # GHCR Repository Owner
          GHCR_OWNER: ${{ github.repository_owner }}

          # Private GHCR Pull 인증
          GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
          GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }}

        run: |
          COMMAND_ID=$(aws ssm send-command \
            --instance-ids "i-0ecb626c01830b39b" \
            --document-name "AWS-RunShellScript" \
            --parameters "commands=[
              \"set -e\",
              \"cd /opt/kgb\",
              \"echo '${GHCR_TOKEN}' | docker login ghcr.io -u '${GHCR_USERNAME}' --password-stdin\",
              \"GHCR_OWNER=${GHCR_OWNER} IMAGE_TAG=${IMAGE_TAG} docker compose pull backend\",
              \"GHCR_OWNER=${GHCR_OWNER} IMAGE_TAG=${IMAGE_TAG} docker compose up -d backend\",
              \"docker compose ps backend\",
              \"for i in {1..12}; do if curl -fsS http://localhost:8080/actuator/health; then exit 0; fi; sleep 5; done; exit 1\"
            ]" \
            --query "Command.CommandId" \
            --output text)

          echo "command_id=${COMMAND_ID}" >> "$GITHUB_OUTPUT"


      
      # SSM 명령 완료 대기
      # ==================================================
      - name: Wait for Deploy Command
        run: |
          aws ssm wait command-executed \
            --command-id "${{ steps.deploy.outputs.command_id }}" \
            --instance-id "i-0ecb626c01830b39b"


      # 배포 결과 확인
      # ==================================================
      - name: Check Deploy Result
        run: |
          STATUS=$(aws ssm get-command-invocation \
            --command-id "${{ steps.deploy.outputs.command_id }}" \
            --instance-id "i-0ecb626c01830b39b" \
            --query "Status" \
            --output text)

          echo "Deploy status: ${STATUS}"

          if [ "${STATUS}" != "Success" ]; then
            echo "Backend deployment failed."
            exit 1
          fi

          echo "Backend deployment succeeded."