1. 설계
CI
내가 정의한 CI의 역할은 pr로 올라온 코드가 정상적으로 빌드되고 단위 테스트가 성공적으로 마무리 됐는지를 확인하기 위함이다.
따라서 GitHub Actions만을 가지고 간단하게 마무리 할 수 있었다.
CI.yml
name: CI
on:
pull_request_target:
types: [opened, synchronize, closed]
branches: [dev]
paths:
- '**'
permissions:
contents: read
jobs:
CI:
runs-on: ubuntu-latest
steps:
- name: 1. checkout
uses: actions/checkout@v4
- name: 2. Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
- name: 3. Gradle Caching
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: 4. Copy application.yml
env:
CREATE_SECRET: ${{ secrets.APPLICATION_YML }}
CREATE_SECRET_DIR: src/main/resources
CREATE_SECRET_DIR_FILE_NAME: application.yml
run: |
mkdir -p $CREATE_SECRET_DIR
echo -n "$CREATE_SECRET" | base64 --decode > $CREATE_SECRET_DIR/$CREATE_SECRET_DIR_FILE_NAME
- name: 5. Grant execute permission for gradlew
run: chmod +x gradlew
- name: 6. Build with Gradle
run: ./gradlew clean build
CD
CD를 설계하는 방법은 매우 다양하다.
요즘은 컨테이너가 대세라고 많이 들어왔지만 EC2보다 ECR, ECS가 일반적으로 돈이 더 부담되는 걸로 알고 있었다.
현재 참여 중인 일경험 직무3기 프로그램을 통해 실비를 지원받고 부담없이 경험해 보고자 했다
다음은 내가 설계한 CD 플로우이다
[ 간단 용어 정리 ]
ECR : 도커 이미지 저장소
ECS : 컨테이너 실행을 관리하는 서비스
Task : 컨테이너 실행(배포)을 위한 설정을 정의
fargate : ec2의 서버리스 버전이라고 생각하면 편하고, ec2보다 설정 및 관리가 용이하지만 컨테이너의 CPU와 메모리 사용량에 따라 비용이 발생하므로 일반적으로 ec2에 비용이 더 많이 부과된다
2. CD 적용
ECR 생성
public repository의 경우 무료로 사용 가능하나 접근 url만 있다면 누구나 접근할 수 있기 때문에 private repository를 활용하였다.
private은 프리 티어의 경우 월 500MB까지는 무료이고, IAM 정책으로 접근이 가능하다
ECR -> Private registry -> Repositories -> 리포지토리 생성
생성된 ECR에 이미지를 push하는 방법은 '푸시 명령 보기' 클릭 시 바로 사용가능한 명령어를 제공해준다
aws 명령어를 활용하기 때문에 aws-cli를 설치하고 aws configure을 통해 aws 사용자를 접근한다
이때 AmazonEC2ContainerRegistryFullAccess 역할이 존재하는 iam user를 만들어서 access key로 접근할 수 있지만 여기서는 다루지 않을 거라 설명은 생략한다
이제 로드 밸런서를 적용하기 전 vpc와 security group을 설정한다.
vpc는 default vpc를 그대로 사용하고 security group만 만들어서 사용하였다
보안 그룹 생성
EC2 -> 네트워크 및 보안 -> 보안 그룹 -> 보안 그룹 생성
대상 그룹 생성
ALB를 적용하여 트래픽을 라우팅할 대상을 선택하기 위해 대상 그룹을 설정해준다
EC2 -> 로드 밸런싱 -> 대상그룹 -> 대상그룹생성
이 프로젝트는 서버를 2대 띄우고 이 서버들을 대상 그룹으로 지정하여 ALB를 통해 트래픽을 분산시킨다.
AWS의 가장 큰 장점은 고가용성으로 대상 그룹이 정상적으로 작동하고 있는지 상태 검사(헬스 체크)를 진행한다.
나의 프로젝트에서는 루트 경로에 api endpoint가 설정되어 있지 않아 404를 반환하기 때문에 루트 경로에 404가 반환되더라도 정상 작동하고 있음을 설정해줘야 한다.
만약 따로 헬스체크 api를 만든다면 해당 경로와 상태값을 지정해주면 된다
로드 밸런서 생성
EC2 -> 로드 밸런싱 -> 로드 밸런서 -> 로드 밸런서 생성
아까 만들어뒀던 보안 그룹과 대상 그룹을 지정해줍니다
Task Definition 생성
먼저 내가 정의한 Task를 실행할 수 있는 역할을 부여하기 위해 iam 역할을 먼저 생성해야 한다
IAM -> 액세스 관리 -> 역할 -> 역할 생성
ECS -> 태스크 정의 -> 새 태스크 정의
운영 체제 / 아키텍처는 원하는 걸로 수정 가능
태스크 역할, 태스크 실행 역할은 좀전에 만들었던 role을 선택
컨테이너 포트는 배포 서버에 사용될 포트 번호
ECS-클러스터 생성
ECS -> 클러스터 -> 클러스터 생성
클러스터-서비스-생성
좀전에 만든 클러스터로 접속 후 서비스 -> 생성
원하는 태스크에 따라서 서버가 띄워지는 갯수가 정해짐
나는 2개의 서버를 띄워서 ALB를 통해 트래픽을 분산할 예정이므로 2개를 지정해주었다.
퍼블릭 IP를 할당해주지 않으면 private subnet으로 통신해야 하기 때문에 nat gateway를 적용해주어야 한다.
우선 간단하게 public ip를 할당해서 바로 사용 가능하게 만들어주자
나는 ALB를 적용했기 때문에 아까 만들어둔 pitchain-alb를 통해 배포 컨테이너를 관리하고 alb에 설정해둔 기존 리스너와 대상그룹을 선택해주었다
여기까지 CD를 적용하기 위한 인프라 설정이 마무리되었다. 이제 Git에 push가 발생하면 자동으로 배포가 되도록 설정해주어야 한다
GitHub Actions - CD.yml
CD.yml을 작성하기 전에 먼저 OIDC를 설정해야 한다.
access key를 관리하며 노출 위험이 있는 iam user를 통한 권환 획득과 달리 OIDC를 통해 권한을 획득하는 방식은 정해진 깃허브 organization 또는 repo에만 권한을 부여할 수 있어 권장되는 방법이다
OIDC
IAM -> 액세스 관리 -> ID 제공업체 -> 공급자 추가
provider URL = https://token.actions.githubusercontent.com
Audience = sts.amazonaws.com
provider URL, Audience는 docs에서 제공하는 값으로 그대로 입력하면 된다
우리는 이 ARN을 활용하여 권한을 획득할 수 있다. 워크플로우에 추가될 값이다
이제 해당 OIDC에 역할을 부여해야 하므로 역할 할당을 클릭해서 들어가보자
ID 제공업체와 Audience는 그대로 선택하였고 pitchain이라는 organization에 server라는 repository에서만 권한을 얻을 수 있도록 설정했다
이 OIDC는 ECR에 빌드된 이미지를 push하고 Task를 실행하기 위한 권한이 필요하기 때문에
AmazonEC2ContainerRegistryFullAccess, AmazonECS_FullAccess 권한을 부여해줬다.
GitHub Actions에서는 아래와 같이 적용하여 사용할 수 있다
permissions:
id-token: write
jobs:
CD:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: {oidc-arn}
aws-region: {region-name}
task-definition.json
aws에 접근할 수 있는 권한을 얻었으니 이제 task를 실행하기 위해 위에서 만들었던 task definition의 json 파일이 필요하다
아까 만들었던 pitchain-task-definition으로 들어가서 아무 개정으로 접속하여 JSON 코드를 복사한다
)
GitHub Secrets
task definition에는 배포 설정이 정의되어 있어서 본인은 secret에다 등록 후 사용했다
task를 실행시키기 위해선 프로젝트 루트에 task-definition.json 파일이 존재해야 한다
위에서 복사한 json에서 "enableFaultInjection"이라는 속성이 있다면 제거하고 secret에다 저장해주자. (이 속성이 있으면 태스크 실행 시 에러가 발생.. 이유는 아직 모르지만 명확히 저 속성이 존재해서 발생하는 에러라고 뜬다)
보안상의 이유로 spring project에서 사용하는 설정 파일 application.yml도 secret에다 저장했다.
yml은 줄바꿈과 탭을 통해서 depth를 구분하기 때문에 그대로 복붙하면 한 줄로 출력되기 때문에 secret에 붙여넣을 때 base64로 encoding해주고 깃헙 액션에서 decoding해서 사용한다
echo "{application.yml내용}" | base64
echo -n {encoding된 applicaion.yml} | base64 --decode
이를 깃헙액션에서 사용하기 위해선 아래와 같이 작성할 수 있다.
- name: Copy Task Definition JSON
run: |
# 반드시 작은 따옴표로 감싸줘야 JSON에서 큰 따옴표가 생성된다 ex) "key":"value"
echo '${{ secrets.TASK_DEFINITION_JSON }}' > {task-definition이름.json}
- name: Copy application.yml
env:
CREATE_SECRET: ${{ secrets.APPLICATION_YML }}
CREATE_SECRET_DIR: src/main/resources
CREATE_SECRET_DIR_FILE_NAME: application.yml
run: |
mkdir -p $CREATE_SECRET_DIR
echo -n "$CREATE_SECRET" | base64 --decode > $CREATE_SECRET_DIR/$CREATE_SECRET_DIR_FILE_NAME
이제 이 코드를 가지고 워크플로우 CD.yml를 완성하면 아래와 같이 마무리 된다
name: CD
on:
push:
branches: [ "main", "master", "dev" ]
env:
AWS_REGION: ap-northeast-2 # set this to your preferred AWS region, e.g. us-west-1
ECR_REPOSITORY: pitchain # set this to your Amazon ECR repository name
ECS_SERVICE: pitchain-service # set this to your Amazon ECS service name
ECS_CLUSTER: pitchain-cluster # set this to your Amazon ECS cluster name
ECS_TASK_DEFINITION: pitchain-task-definition.json # Task Definition의 파일 이름
CONTAINER_NAME: pitchain-cont # set this to the name of the container in the
permissions:
id-token: write
contents: read
jobs:
CD:
runs-on: ubuntu-latest
steps:
- name: 1. checkout
uses: actions/checkout@v4
- name: 2. Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: 3. Gradle Caching
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: 4. Copy application.yml
env:
CREATE_SECRET: ${{ secrets.APPLICATION_YML }}
CREATE_SECRET_DIR: src/main/resources
CREATE_SECRET_DIR_FILE_NAME: application.yml
run: |
mkdir -p $CREATE_SECRET_DIR
echo -n "$CREATE_SECRET" | base64 --decode > $CREATE_SECRET_DIR/$CREATE_SECRET_DIR_FILE_NAME
- name: 5. Grant execute permission for gradlew
run: chmod +x gradlew
- name: 6. Build with Gradle, without test
run: ./gradlew clean build -x test
- name: 7. Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::491085415955:role/gitactions-lsh
aws-region: ap-northeast-2
- name: 8. Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: 9. Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build --platform linux/amd64 -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
- name: 10. Copy Task Definition JSON
run: |
echo '${{ secrets.TASK_DEFINITION_JSON }}' > ${{ env.ECS_TASK_DEFINITION }}
- name: 11. 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: 12. 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
'Project' 카테고리의 다른 글
함수형 프로그래밍을 적용한 분산락 성능 개선 (2) (0) | 2025.02.21 |
---|---|
낙관적 락 vs 비관적 락 vs 분산락 (with Redisson) (0) | 2025.02.20 |
Lambda + CDN을 통한 스트리밍 영상 제공 (1) | 2025.02.19 |