Hanbit the Developer

AWS와 Docker를 활용한 Spring + React.JS CI/CD 구축: CD + SSM(4) 본문

Back-end

AWS와 Docker를 활용한 Spring + React.JS CI/CD 구축: CD + SSM(4)

hanbikan 2024. 8. 15. 20:21

SSM 도입 배경

이전 글까지의 과정으로 CI, Docker, ECR, IAM 설정을 완료했습니다. 이제 마지막으로 CD를 구축하면 됩니다.

CD 파일을 통해 다음과 같은 동작을 수행하면 됩니다:

  • Checkout GitHub Repository & Build docker images & Push docker images
  • (EC2에서) Pull docker images & Deploy docker images

이때 문제는 EC2에서 동작을 수행해야 한다는 점입니다. 이를 수행할 수 있는 방법에는 여러 가지가 있습니다:

  • SSH: GitHub Actions가 실행되는 인스턴스의 IP가 고정된 범위가 아니기 때문에 SSH 보안그룹을 열어야 해서 보안에 매우 취약합니다.
  • k8s + Jenkins + ArgoCD: 복잡한 파이프라인 구축이 가능하며 대규모 마이크로서비스나 복잡한 배포 전략을 관리할 수 있습니다. 러닝커브가 있으며 비용이 발생합니다.(프리티어 불가)

우선 SSH는 보안에 너무 취약하여 소거하였습니다. 또한 저는 비용 문제로 k8s를 도입하지 않았기 때문에 두 번째 방법도 소거하였습니다.

위 방법을 제외하면 아래 옵션들이 남습니다.


ChatGPT 4o:

1. AWS CodeDeploy

  • 장점:
    • GitHub Actions와 쉽게 연동하여 ECR에서 가져온 이미지를 배포할 수 있습니다.
    • 블루/그린 배포와 같은 고급 배포 전략을 사용할 수 있어, 배포 중 문제가 발생할 경우 롤백이 용이합니다.
    • 배포 수명 주기 관리가 용이하여 단계별로 배포를 제어할 수 있습니다.
  • 단점:
    • 설정과 유지 관리가 다른 옵션들에 비해 복잡할 수 있으며, 특히 소규모 인프라에서는 과할 수 있습니다.

2. AWS Systems Manager (SSM) SendCommand

  • 장점:
    • 매우 간단한 설정으로, 특정 명령을 EC2 인스턴스에서 원격으로 실행할 수 있습니다.
    • GitHub Actions에서 Docker 이미지를 빌드하고 ECR에 푸시한 후, SSM SendCommand를 사용하여 인스턴스에서 이미지를 풀하고 컨테이너를 실행하는 방식으로 간단하게 배포할 수 있습니다.
    • 추가적인 인프라 관리 도구나 설정 없이도 충분히 효과적입니다.
  • 단점:
    • CodeDeploy나 ECS에 비해 배포의 복잡한 관리나 롤백 기능이 부족합니다.
    • 인프라의 상태를 세밀하게 제어하기 어려울 수 있습니다.

3. AWS ECS (Elastic Container Service)

  • 장점:
    • 컨테이너 오케스트레이션 기능을 제공하여, 컨테이너 기반 애플리케이션의 배포와 관리를 자동화할 수 있습니다.
    • 자동 스케일링이 가능하여, 수요 변화에 따라 리소스를 조정할 수 있습니다.
    • GitHub Actions와의 통합도 잘 지원되며, ECR에서 이미지를 가져와 쉽게 배포할 수 있습니다.
  • 단점:
    • 소규모 인프라에서는 ECS의 오버헤드가 클 수 있습니다. 특히, 단일 인스턴스 환경에서는 ECS를 사용하는 것이 불필요하게 복잡할 수 있습니다.
    • 초기 설정 및 관리를 위한 학습 곡선이 있을 수 있습니다.

결론:

소규모 인프라와 단일 Docker 이미지로 구성된 단일 인스턴스 환경이라면, AWS Systems Manager (SSM) SendCommand를 사용하는 것이 가장 간단하고 효율적일 수 있습니다. 이는 최소한의 설정으로 필요한 배포 작업을 수행할 수 있으며, GitHub Actions와의 연동도 쉽게 가능합니다.

다만, 배포 관리의 복잡성이 높아지거나 추가적인 배포 전략이 필요할 경우, AWS CodeDeploy를 고려하는 것도 좋은 선택이 될 수 있습니다. ECS는 이 환경에서는 다소 과할 수 있지만, 컨테이너 오케스트레이션이 필요해질 때 유용할 수 있습니다.


제 경우 소규모 인프라에 단일 인스턴스 환경이기 때문에 SSM 방식을 택했습니다.

IAM 역할에 SSM 권한 추가

Users

SSM을 사용하기 위해 GitHub Actions에서 로그인하게 될 IAM User에 아래 권한을 부여해주었습니다.(Add permissions - Create inline policy - Json)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:SendCommand",
                "ssm:ListCommandInvocations"
            ],
            "Resource": "*"
        }
    ]
}

Roles

IAM Role을 추가했습니다.(IAM - Access management - Roles - Create role)

  • Trusted entity type: AWS service
  • Use case: EC2 - EC2 Role for AWS Systems Manager

이후 방금 추가한 IAM Role을 EC2와 연결(EC2 Instance - Actions - Security - Modify IAM role)하였고, 규칙이 바로 적용되지 않아서 EC2 인스턴스를 재부팅 하였습니다.

 

위 권한 설정을 통해 EC2 인스턴스에서 SSM을 통해 원격 명령을 실행하고 command 결과를 출력할 수 있습니다.

ssm send-command 결과 확인

ssm send-command를 수행하면 위처럼 CommandId를 확인할 수 있습니다.

위 ID를 아래 명령어에서 사용해 실행 결과를 확인할 수 있습니다.

aws ssm list-command-invocations --command-id <CommandId> --details

GitHub Actions로 send-command를 테스트 하기에는 시간이 오래 걸리므로, 위 명령어를 통해 로컬에서 테스트 해보고 GitHub Actions에 적용하는 것을 권장합니다.

CD 파일 작성

다음은 CD 워크플로우를 작성해야 합니다. GitHub Actions를 사용하여 코드가 main 브랜치에 푸시될 때 자동으로 Docker 이미지를 빌드하고, AWS ECR에 푸시한 후, EC2 인스턴스에 배포하는 과정을 작성하겠습니다.

cd.yml

jobs를 이미지를 빌드하고 푸시하는 작업과 이미지를 pull 받고 배포하는 작업으로 나누었습니다.

  • build-and-push: ECR 권한을 가진 IAM User로 로그인한 뒤 이미지를 빌드하고 푸시합니다.(제 경우 보안상 깃에 추가하지 않은 파일들을 추가해 주었습니다.)
  • deploy: SSM 권한을 가진 IAM User로 로그인한 뒤 SSM SendCommand 명령으로 EC2가 명령줄을 수행하도록 합니다. SSM 명령어를 통해 EC2는, 이미지 실행 시 필요한 환경변수들을 추가하고 ECR 권한을 가진 IAM User로 로그인한 뒤 ECR로부터 이미지를 받고 배포하게 됩니다.(제 경우 EC2 저장 공간 관리를 위해, 이미지를 받기 전에 docker rmi 명령어로 다른 이미지를 제거해주었습니다.)
name: CD

on:
  push:
    branches:
      - main

jobs:
  build-and-push:
    name: Build and Push Docker Images to ECR
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.IAM_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.IAM_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Login to Amazon ECR
        run: aws ecr get-login-password --region ${{ secrets.AWS_REGION }} | docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY_URL }}

      - name: Add secret files manually and Build and push Docker images
        run: |
          echo "${{ secrets.ENV_SERVER }}" > ./server/.env
          echo "${{ secrets.ENV_FRONTEND }}" > ./frontend/.env
          echo "${{ secrets.ENV_ROOT }}" > .env
          echo "${{ secrets.APPLICATION_LOCAL }}" > ./server/src/main/resources/application-local.properties
          echo "${{ secrets.APPLICATION_TEST }}" > ./server/src/main/resources/application-test.properties
          docker compose build
          docker compose push

  deploy:
    name: Deploy to EC2
    needs: build-and-push
    runs-on: ubuntu-latest

    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.IAM_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.IAM_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Deploy application using SSM
        run: |
          aws ssm send-command \\
            --instance-ids "${{ secrets.EC2_INSTANCE_ID }}" \\
            --document-name "AWS-RunShellScript" \\
            --comment "Deploy application" \\
            --parameters commands='[
              "cd ${{ vars.DOCKER_COMPOSE_PATH }}",
              "printf %s \\"${{ secrets.ENV_SERVER }}\\" > ./server/.env",
              "printf %s \\"${{ secrets.ENV_FRONTEND }}\\" > ./frontend/.env",
              "printf %s \\"${{ secrets.ENV_ROOT }}\\" > .env",
              "aws configure set aws_access_key_id ${{ secrets.IAM_ACCESS_KEY_ID }}",
              "aws configure set aws_secret_access_key ${{ secrets.IAM_SECRET_ACCESS_KEY }}",
              "aws configure set region ${{ secrets.AWS_REGION }}",
              "aws ecr get-login-password --region ${{ secrets.AWS_REGION }} | docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY_URL }}",
              "sudo docker rmi $(docker images -aq)",
              "sudo docker-compose pull",
              "sudo docker-compose up -d"
            ]' \\
            --region ${{ secrets.AWS_REGION }}
  • aws-actions/configure-aws-credentials@v1: 이 GitHub Action은 AWS 자격 증명을 설정하는 역할을 합니다. aws-access-key-id, aws-secret-access-key, aws-region을 사용하여 AWS CLI 명령어가 GitHub Actions 워크플로 내에서 안전하게 실행될 수 있도록 합니다. AWS 리소스(ECR, EC2 등)에 접근하기 위해 필요한 설정입니다.
  • aws ecr get-login-password: 이 명령어는 AWS ECR에 로그인하기 위한 암호를 가져옵니다. AWS CLI를 통해 ECR에 인증을 수행하며, 가져온 암호를 사용해 Docker가 ECR에 접근할 수 있도록 Docker 레지스트리에 로그인합니다. 이 과정이 성공적으로 완료되면, Docker 이미지를 ECR에 푸시할 수 있게 됩니다.
  • aws ssm send-command: 이 명령어는 AWS Systems Manager(SSM)를 통해 EC2 인스턴스에 명령을 원격으로 실행할 수 있도록 합니다. send-command를 사용하면 EC2 인스턴스에서 스크립트를 실행하거나 소프트웨어를 설치하는 등의 작업을 수행할 수 있습니다. 이 방법을 통해 SSH 없이도 안전하게 인스턴스에 접근하고, 필요한 작업을 수행할 수 있습니다.
  • EC2_INSTANCE_ID: EC2 인스턴스 ID로, EC2 서비스에서 확인하실 수 있습니다.(아래 사진 참고)

  • DOCKER_COMPOSE_PATH: docker-compose.yml 파일이 있는 프로젝트 루트 폴더를 절대경로로 지정해주었습니다.

이러한 과정을 통해 코드가 푸시될 때마다 자동으로 최신 Docker 이미지를 생성하고, 이를 EC2 인스턴스에 배포하여 애플리케이션이 최신 상태로 유지됩니다.

SSM 명령 결과 확인

SSM 수행 결과를 확인해야 하는 경우가 있습니다. aws ssm send-command 명령을 수행하면 위처럼 CommandId를 확인할 수 있습니다. 이 ID를 아래 명령어에서 사용해 실행 결과를 확인할 수 있습니다.

aws ssm list-command-invocations --command-id <CommandId> --details

 

 

마무리

이제 main 브랜치가 변경되면 CI/CD를 자동으로 수행하여 귀찮은 과정 없이 React.JS와 Spring Boot 서비스를 배포할 수 있습니다.

 

CI/CD 도입을 통해 얻은 이점은 다음과 같습니다:

배포 자동화

CI/CD를 도입하기 전에는 다음 과정을 수행했어야 했습니다.(19줄)

  • git push
  • CI
    • ./gradlew clean build -x test
    • ./gradlew test -i
  • merge into main branch
  • CD
    • sudo docker buildx build --no-cache --push --platform linux/amd64 -t https://123123.amazonaws.com/myapp:frontend ./frontend
    • sudo docker buildx build --no-cache --push --platform linux/amd64 -t https://123123.amazonaws.com/myapp:frontend ./server
    • docker compose push
    • ssh -i /path/myaws.pem user@123.45.6.7
    • cd ./myapp
    • git remote update
    • git pull
    • vim .env
    • vim ./frontenv/.env
    • vim ./server/.env
    • vim ./server/src/main/resources/application-local.properties
    • vim ./server/src/main/resources/application-test.properties
    • aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <registry url>
    • docker compose pull
    • docker compose up -d

저는 노트 앱에 각 과정을 메모해두고 붙여넣기를 하면서 매우 번거롭게 배포를 하곤 했습니다. 하지만 CI/CD를 도입한 뒤에는 아래처럼 획기적으로 줄게 되었습니다.(19줄 -> 2줄)

  • git merge feature/a
  • git push origin main

버그 감소

CI 파이프라인을 통해 코드 변경 사항을 자주 테스트하고 검증함으로써 코드에서 발생하는 버그를 조기에 발견할 수 있습니다.

수동 추가 파일 관리 용이성

.env, application-local.properties와 같은 보안상 중요한 설정 파일이 변경되면, 이를 EC2 인스턴스에 수동으로 적용해야 합니다. 이 과정에서 파일 업데이트를 누락하거나 실수할 경우, 서비스에 버그가 발생할 수 있습니다. 그러나 GitHub Actions secrets를 통해 이러한 파일들을 관리하고, CI/CD 파이프라인을 통해 자동으로 배포 과정에 포함시키면, 수동 업데이트의 필요성을 줄이고 실수를 방지할 수 있습니다. 이로 인해 서비스의 안정성을 높일 수 있으며, 수동 추가 파일을 체계적으로 관리할 수 있습니다.