[CI/CD] ECS에 Spring boot 서버 배포하기
ECS와 EC2
현재 진행하고 있는 팀 프로젝트를 배포를 해야했다.
배포하는 방법에는 EC2와 ECS를 선택지를 두고 있었고, ECS를 선택하게 되었다.
EC2는 docker나 jdk와 같은 따로 설정을 해줘야 하는 번거러움이 있었고
ECS는 컨테이너 서비스이기 떄문에 도커 이미지를 올리고 미리 정의된 Task를 사용하기 때문에 따로 설정이 필요없고 간편하는 점에서 사용하게 되었다.
RDS와 ElastiCache
ECS로 배포를 하면서 auto scaling을 적용해볼 계획을 가지고 있었다.
기존에 localhost로 사용하는 방식으로 한다면 database와 cache가 각 서버에 생기기 때문에 데이터가 공유되지 않는다.
그래서 RDS와 ElastiCache를 사용해 서버가 늘어나도 데이터가 공유될 수 있도록 하였다.
RDS
기존에 사용하던 MySQL로 RDS를 생성하려고 한다.
팀 프로젝트에서 배포를 경험해보고자 사용하는 것이기 때문에 비싼것을 살 필요가 없다고 판단하여 프리티어로 생성했다.
VPC가 없다면 만들어주고, 나중에 ECS를 만들 때, 반드시 같은 VPC로 설정해줘야 한다.
퍼블릭 액세스는 현재는 테스트를 위해 사용하는 것으로 했지만 나중에 배포를 할 때는 퍼블릭 액세스를 막아놔야 한다.
보안 그룹은 RDS 용을 하나 만들어 주는 것이 좋다.
RDS의 보안그룹을 만들었으면 인바운드 규칙을 추가해줘야 한다.
RDS를 생성하면서 새로 생성된 보안그룹에 들어가면 이미 자신의 IP로 3306 포트가 뚫여있을 것이다.
없다면 만들어주면 된다.
여기에 추후 만들어질 ECS의 보안그룹 ID를 추가해주자.
필요하다면 추가 구성을 통해 테이터베이스를 미리 생성할 수 있다.
이렇게 하면 RDS 준비는 끝이 났다.
ElastiCache
ElastiCache도 마찬가지로 배포 경험을 가지기 위해 비싼 것을 구매하지 않을 계획이다.
자체 캐시 설계에 클러스터 캐시를 통해 직접 만들어 주었다.
캐시는 가장 낮은 t2.micro로 설정하였고 복제본도 0으로 맞춰주었다.
이후 서브넷 그룹은 ECS에 있는 VPC와 맞춰줘야 한다.
이렇게 하고 나머지는 기본 설정으로 설정하였다.
ECS
CD 스크립트를 작성하기 전에 ECS를 수동 배포를 먼저 해보는 것이 좋을 것 같다는 생각이 들었다.
ECR
ECS를 설정하기 전에 ECR를 통해 도커 이미지 파일을 올려놔야 한다.
먼저 도커파일을 만들어보자.
FROM openjdk:17-jdk
ARG JAR_FILE=build/libs/빌드파일 이름
ADD ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
도커파일을 프로젝트 최상단에 위치시키고 만들면 된다.
처음에 openjdk:17-alpine으로 설정했는데 배포가 계속 안됐었다. 이것 때문에 하루를 날렸다..
도커파일을 만들었으니 이제 ECR를 생성해야 한다.
프라이빗으로 설정하여 레포지토리의 이름을 정해주고 생성하기만 하면 된다.
만든 레포지토리를 선택하면 `푸시 명령 보기`를 통해 명령어를 알 수 있다.
프로젝트를 윈도우 환경에서 만들었기 때문에 저기 나와있는 Windows를 통해서 했지만 설치부터가 안된다.
찾아보니 AWS 공식 문서에서 AWS CLI를 윈도우에서 설치할 수 있도록 제공해주고 있다.
msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2.msi
PowerShell이나 CMD를 관리자 권한으로 접속하여 해당 스크립트로 다운로드 받으면 된다.
설치가 됐다면 macOS/Linux에 나와있는 푸쉬 명령어를 그대로 사용하면 된다.
명령어를 그대로 따라하고 ECR의 레포지토리를 보면 이미지가 올라간 것을 확인할 수 있다.
ECS
이제 빌드 이미지를 만들었으니 배포를 해보면 된다.
먼저 태스크 정의를 해야한다.
태스크 정의의 이름을 정한다.
Fargate를 사용할 것이고 테스크 성능 같은 것은 기본값을 두었다.
추후에 오토 스케일링을 테스트하기 위해서 cpu와 메모리를 낮출 것 같다.
태스크 역할로는 ecsTaskExcutionRole을 지정해준다.
그리고 IAM의 역할로 들어가 ecsTaskExcutionRole에 권한을 넣어준다.
이렇게 필요한 것들을 모두 넣어주었다.
컨테이너의 이름과 ECR에 올려놓은 이미지의 URI를 넣어준다.
포트는 8080포트이기 때문에 8080으로 설정해준다.
환경 변수는 필요하다면 넣어주면 된다.
나머지 설정은 기본값으로 해두었다.
태스크를 만들었으니 이제 서비스를 만들어줘야 한다.
클러스터로 들어가 클러스터를 생성해준다.
클러스터는 이름만 정해주면 되서 따로 올리지 않겠다.
태스크 정의에서 배포를 선택하면 서비스 생성이 있다. 이걸로 서비스를 만들어 주자.
만들어 놓은 클러스터를 선택해주면 된다.
이전에 만들었던 태스크 정의의 이름을 선택하고 서비스의 이름을 넣어준다.
VPC는 RDS와 ElastiCache와 동일한 VPC를 넣으면 된다.
보안 그룹은 없으면 ECS용을 하나 만들어서 사용하면 된다.
ECS 보안그룹의 인바운드 규칙은 자신의 보안그룹 ID와 로드밸런서를 사용하기 위해 53번과 80번 포트를 뚫어준다.
로드밸런서는 `EC2 -> 로드 밸런서`를 통해서 만들어 준다.
ALB를 선택하고 로드 밸런서 이름을 지정해주면 된다.
VPC를 선택해주고 모든 서브넷을 매핑해주면 된다.
보안그룹은 ECS에서 사용하고 있는 보안그룹을 선택하면 된다.
대상 그룹을 정해야 하는데 현재 없으므로 대상 그룹 생성을 클릭하여 만들어 주자.
대상 유형을 IP 주소로 설정하고 대상 그룹 이름을 정하면 된다.
포트는 Spring Boot 서버이기 떄문에 8080번을 입력하면 된다.
VPC는 지금까지 동일한 VPC로 선택하면 된다.
상태검사는 기본 경로로 설정했다.
`/` 경로로 상태를 검사하고 `400-499`사이의 코드가 나오도록 했다.
다시 로드 밸러서로 돌아가 대상 그룹을 방금 만든 그룹으로 선택하면 된다.
만든 로드밸런서로 선택하여 상태 검사 유예 시간을 5초로 두었다.
리스너나 대상 그룹은 기존 사용을 하였다.
이 부분이 오토 스케일링을 설정하는 곳인데 아직 오토 스케일링을 적용하지 않을 것이기 때문에 기술하지 않겠다.
이렇게 서비스까지 만들었고 기다려주면 배포가 될 것이다.
Continuous Deployment
이제 수동 배포를 적용해봤으니 CD를 구성해야 한다.
Continuous Deployment는 Github Actions를 사용하여 구성했다.
이렇게 해주면 기본 설정의 템플릿을 가져올 수 있다.
name: Deploy to Amazon ECS
on:
push:
branches: [ "dev" ]
env:
AWS_REGION: MY_AWS_REGION # set this to your preferred AWS region, e.g. us-west-1
ECR_REPOSITORY: MY_ECR_REPOSITORY # set this to your Amazon ECR repository name
ECS_SERVICE: MY_ECS_SERVICE # set this to your Amazon ECS service name
ECS_CLUSTER: MY_ECS_CLUSTER # set this to your Amazon ECS cluster name
ECS_TASK_DEFINITION: MY_ECS_TASK_DEFINITION # set this to the path to your Amazon ECS task definition
# file, e.g. .aws/task-definition.json
CONTAINER_NAME: MY_CONTAINER_NAME # set this to the name of the container in the
# containerDefinitions section of your task definition
permissions:
contents: read
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
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: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
# Build a docker container and
# push it to ECR so that it can
# be deployed to ECS.
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ env.ECS_TASK_DEFINITION }}
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.build-image.outputs.image }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
기본적이 스크립트 템플릿을 이렇게 되어 있다.
여기서 CI와 CD를 다른 job을 두고 스크립트를 작성하였다.
name: CI/CD
on:
pull_request:
branches:
- dev
push:
branches:
- main
env:
AWS_REGION: MY_AWS_REGION
ECR_REPOSITORY: MY_ECR_REPOSITORY
ECS_SERVICE: MY_ECS_SERVICE
ECS_CLUSTER: MY_ECS_CLUSTER
ECS_TASK_DEFINITION: MY_ECS_TASK_DEFINITION
CONTAINER_NAME: MY_CONTAINER_NAME
permissions:
contents: read
jobs:
build:
name: CI
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
environment: development
services:
redis:
image: redis
ports:
- 6379:6379
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set Application Test Property
run: |
touch ./src/test/resources/application-test.properties
echo "${{ secrets.APPLICATION_TEST_PROPERTIES }}" > ./src/test/resources/application-test.properties
cat ./src/test/resources/application-test.properties
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Build with Gradle
run: ./gradlew clean build
deploy:
name: CD
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Build executable JAR
run: ./gradlew clean bootJar
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
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: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
# Build a docker container and
# push it to ECR so that it can
# be deployed to ECS.
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Download existing task definition
run: |
aws ecs describe-task-definition --task-definition ${{ env.ECS_TASK_DEFINITION }} --query taskDefinition > task-definition.json
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.build-image.outputs.image }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
if를 사용하여 CI 스크립트와 CD 스크립트 동작을 구분하였다.
위의 템플릿과 바뀐 것이 있다면 환경변수를 넣었기 때문에 이미 존재하는 태스크 정의의 JSON을 가져오고 그 JSON을 통해 태스크 정의를 만들어준다.
에러가 한 번 났었는데, ECR에 대한 IAM의 권한이 없어서 그랬다.
그래서 방법을 찾아 해결하였다.
서비스는 Elastic Container Registry를 선택해주고 모든 작업과 항목에 대해 허용해준다.
정책을 만들어주고 나서 IAM에 권한을 추가해 주면 CD 스크립트가 완성된다.
마무리
이렇게 해서 자동 배포가 성공했다.
어이없는 에러로 2일 동안 잡고 있었는데 배포가 되고 나니 너무 속 시원했다.
VPC와 보안그룹에 대해 뭐가 뭔지 몰랐는데 만들고 나서 한번 슥 보니 이해가 되는 것 같았다.