본 프로젝트에서 빌드 및 배포해야하는 코드는 크게 Frontend(Vue3), Backend(Spring)로 구성됩니다.
Frontend의 경우 Web Hosting 기반의 S3 버킷 위에서 동작하며, Backend는 AWS ECS 클러스터 내부 서비스로 정의되어 동작합니다.
먼저 완성된 파이프라인의 구조를 사진으로 한 번 보고, Frontend 및 Backend 배포 파이프라인을 어떻게 자동화 시켰는지 알아보겠습니다.
1. Git Actions를 활용한 배포 과정
아래에 진행할 동일한 과정을 별도의 인스턴스에 Jenkins를 설치해 진행해봤는데 현재 자동화된 배포 단계에서 필요한 기술은 Git Actions만으로 충분히 구현할 수 있었으며, 시간적 비용 및 실제 과금 여부에서도 Git Actions가 압승이었습니다.
- Frontend
FE의 경우 Vue3 프레임워크를 통한 개발이 이루어졌으며 빌드 및 테스트 과정을 거쳐 Web Hosting 기반으로 동작하는 S3 버킷에 빌드 파일을 전달하는게 목표였습니다.
다음은 배포 스크립트인 deploy.yml 파일입니다.
name: FE CICD
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: git clone
uses: actions/checkout@v2
- name: npm install
run: npm install
- name: create .env file
run: |
echo "VUE_APP_API_BASE_URL=${{secrets.BASE_URL}}" > .env
- name: npm build
run: npm run build
- name: deploy
env:
AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY}}
AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_KEY}}
run:
aws s3 cp --recursive --region ap-northeast-2 dist ${{secrets.S3_BUCKET_URL}}
과정은 단순합니다. 프로젝트를 클론한 뒤 필요한 의존성을 설치하고, 빌드 성공 시 S3 버킷으로 결과물을 전달합니다.
프로젝트 내부 API Call을 위한 API Gateway의 주소, 그리고 S3 버킷 접근에 대해 적절한 IAM 정책을 갖는 유저를 생성하고 Access Key와 Secret Key 및 S3 버킷 URL을 환경변수로 주입해주었습니다.
다음과 같이 성공적으로 실행한 모습을 볼 수 있습니다.
- Backend
Backend의 경우 Spring boot 3.X.X 기반으로 개발되었으며, ECS 클러스터 내부 서비스를 타겟으로 배포하는 것이 최종 목표였습니다.
ECS 클러스터 내부 서비스를 업데이트 하기 위해서는 우선 도커 이미지를 저장할 이미지 저장소가 필요합니다.
AWS는 이를 위해 ECR이라는 이미지 저장소를 지원합니다. AWS 서비스 간 연동이 굉장히 잘 되어있어, 굳이 Docker Hub를 쓰지 않아도 됩니다.
저장소가 생겼으니, 새로운 버전의 도커 이미지가 ECR로 발행된다면 ECS 서비스를 업데이트하기까지 1차 목표를 달성하게됩니다.
다음은 main 브랜치 push시, 도커 이미지를 생성해 ECR로 발행하는 스크립트입니다.
name: WAS container images build to ECR
on:
push:
branches: [ "main" ]
env:
AWS_REGION: ap-northeast-2
ECR_REPOSITORY: ticketing-spring
jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.SUBMODULE_KEY }}
submodules: recursive
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Build with Gradle
uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
with:
arguments: build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_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 }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:latest .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:latest"
스크립트는 다음 과정을 거치게 됩니다.
- JDK 세팅
- Gradle 빌드
- AWS 권한 인증 및 ECR 접속
- 도커 이미지 생성 및 전송
이미지 태그의 경우, 최신 버전을 항상 latest로 관리했습니다. 항상 최신 버전의 이미지 주소는 ~~~:latest로 통일되어 재배포가 간편했습니다.
다음과 같이 성공한 모습을 볼 수 있습니다.
2. AWS CodePipeline을 활용한 배포 과정
이제 ECR에 도커 이미지가 올라왔으니 ECS 서비스를 업데이트할 수 있습니다.
그 과정을 한번 살펴보겠습니다.
- 수동 배포 과정
1. 새 태스크 정의 개정 생성
우선 ECR에 새롭게 업데이트된 latest 버전의 도커 이미지를 통해 새로운 태스크 정의 개정을 생성해야합니다.
도커 이미지 태그를 latest로 설정했기 때문에 이미지 URI를 건들이지 않고 생성할 수 있습니다.
2. 서비스 업데이트
새로 개정된 태스크 정의를 사용하도록 ECS 서비스를 업데이트합니다.
최신 버전으로 서비스를 업데이트합니다.
위 과정을 통해 수동으로 ECS 서비스를 업데이트할 수 있습니다.
- 자동 배포 과정
수동 배포 과정이 크게 어렵진 않았습니다. 하지만 개발 서버에서 반복적인 테스트를 시도할 때 마다 *귀찮음과 피로감이 누적되는건 사실이었습니다.*
따라서 ECR에 새로운 버전의 이미지가 올라오는 이벤트를 캐치하고, ECS 서비스를 자동으로 업데이트해주는 과정이 필요하다고 판단했습니다.
고민했던 방식은 두 가지정도 있었습니다.
- EventBridge를 통해 ECR 트리거를 탐지, Lambda 함수를 호출해 ECS 서비스를 업데이트
- AWS의 관리형 CI/CD 서비스인 CodePipeline을 활용
1번이 가장 먼저 생각난 방식이었습니다. 자연스러운 흐름이었지만 단점이 될 수 있는 요소가 무엇인가 생각해보니 다음과 같았습니다.
- Lambda의 Cold Start 시간을 무시할 수 없다.
- 코드로 파이프라인을 작성해야한다.
yml 파일로 간편하게 배포 스크립트를 작성할 수 있는데, 굳이 API 함수를 직접 만들며 해당 방식을 사용할만한 이점을 느끼지 못했습니다. 게다가 Cold Start 시간도 언어마다 다르지만, 무시할 수 없는 요소였으며 해결하는데 추가적인 리소스가 발생하는 상황이었습니다.
다양한 자료를 찾아보던 중, AWS의 CodePipeline이라는 관리형 CI/CD 툴을 알게되었고, 위 1번 방식의 단점을 보완할 수 있어 적용하게 되었습니다. CodePipeline의 장점은 다음과 같았습니다.
- 프리티어 계정 기준, 하나의 배포 파이프라인에 대해 무료이다.(현재 하나만 필요)
- yml 파일을 통해 스크립트를 작성할 수 있다.
CodePipeline은 전체적인 흐름 역할을 하며 다음 과정을 거치게 됩니다.
- 최초 ECR의 트리거를 잡아내는 것으로 시작
- CodeBuild 서비스를 통해 ECR의 어떤 이미지로 어떤 컨테이너를 생성할 지 지정하는 imagedefinitions.json 파일을 만들어 반환받음
- 새로운 태스크 정의를 생성하고 ECS 서비스를 업데이트
1. ECR 트리거
CodePipeline 생성 시점에 트리거를 설정할 수 있습니다.
2. 스크립트 작성
buildspec입니다.
version: 0.2
phases:
post_build:
commands:
- printf '[{"name":"container-was","imageUri":"${LATEST_URL}}"}]' > imagedefinitions.json
artifacts:
files:
- imagedefinitions.json
소스 코드 빌드라기 보다는, ECR의 어떤 이미지로 어떤 컨테이너(태스크)를 만들지 정의해주는 imagedefinitions.json파일을 만드는 과정입니다. 소스 코드는 이미 Git Actions를 통해 도커 이미지로 빌드되어 할 일이 없습니다.
3. 배포
CodePipeline 생성 마지막 단계에서 어디에 배포할 지 설정할 수 있습니다.
4. 결과
정상적으로 git push -> ECS 서비스 업데이트까지 자동화된 배포 파이프라인이 완성되었습니다.
다만, 위 과정으로 배포 시 이전에 서비스에서 사용중이던 컨테이너수에 맞춰 배포됩니다. 세밀한 배포 정책 제어에 대해서는 추가 설정이 필요할 것으로 보입니다. 추후 관련 포스팅으로 돌아오겠습니다.
'프로젝트 > Ticketing' 카테고리의 다른 글
[Ticketing] 인프라 아키텍처 설계 및 구축 (0) | 2024.06.04 |
---|---|
[Ticketing] Lock을 활용한 동시성 제어 (0) | 2024.06.04 |
[Ticketing] Slack으로 에러 메세지 전송하기 (Feat: Spring AOP) (0) | 2024.06.02 |
[Ticketing] AWS Lambda Authorizer를 활용한 인가 서버 개발 (0) | 2024.05.31 |
[Ticketing] JPQL에서 QueryDSL으로 전환 (0) | 2024.05.18 |