Spring cloud로 MSA 개발 및 배포(2)
프로젝트가 MSA 구조인 만큼, 어떻게 하면 클라우드를 잘 활용하여 효율적이고 비용 면에서 괜찮은 인프라를 구축할 수 있을까 고민해봤다.
일단 따로 PaaS를 쓰기보다는 AWS의 기능부터 제대로 조금씩 익혀보고 싶어서 어떤 기능이 있는지 살펴봤다.
전에는 AWS로 배포한다고 하면 그냥 EC2 인스턴스에 서버 올리고, RDS로 디비 연동하는 것밖에 몰랐는데, 생각보다 너무 많은 서비스들을 제공하고 있었다.
일단 컨테이너 단위로 배포하고 운영 및 관리해야 하기 때문에 컨테이너 오케스트레이션을 할 관리 도구가 필요했다. AWS의 EKS를 쓸 수 있다면 좋겠지만, 아직 쿠버네티스를 제대로 공부해본 적 없어 시간적으로, 또 비용적으로 많이 부담이 됐다. 따라서 대안으로 ECS를 적용하고자 했고, 각 모듈들은 도커 이미지를 생성해서 ECR로 관리하기로 했다. ECS는 EC2 인스턴스와 함께 쓸 것이다!
ALB도 적용해보고 싶은데 이는 일단 나중에 조금 더 공부해보고 쓸 생각이다.
그래서 일단 각각의 도커 파일을 만드는 것부터 시작하겠다.
도커 파일을 잘 만드는 게 중요하다고 들음.~~~
도커 이미지를 만들 때 ubuntu, alpine, simple 등으로 만들 수 있는데, 어떤 것으로 만드는지에 따라 이미지 크기가 달라진다.
alpine은 linux 경량화를 하면서, 보안을 강화한 이미지로 꼭 필요한 라이브러리와 시스템 데몬이 포함되어 있기 때문에 일반적인 OS 이미지에 비해서 크기가 매우 작다.
Java 이미지
Java는 openjdk 기반의 이미지가 있다.
openjdk:버전 이런 식으로 작성하며, 이 뒤에 openjdk:버전-alpine을 붙이면 alpine 리눅스 용 openjdk 이미지가 된다.
openjdk:8과 openjdk:8-alpine 이미지의 크기를 비교해보면 큰 차이가 난다.
자바 런타임용 컨테이너를 만들 때, 보통은 jdk가 아닌jre만 사용해도 충분하다.
jdk보다는 jre가 조금 더 가볍기 때문에 jre를 사용한 이미지를 만드는 것이 좋지만, 항상 OpenJDK에 대한 alpine JRE 이미지를 사용할 수 있는 것은 아니다. JDK 버전에 따라서 alpine 리눅스를 안정적으로 지원하지 않는 경우에는 다른 linux를 사용하기 때문에, 오히려 이미지 용량이 커질 수 있다. 그럴 때에는 Docker slim 이미지를 사용하면 된다.
나는 이번에 jdk 17을 사용하였는데 java 17에서 JRE 전용 이미지는 지원되지 않기 때문에 우선 jdk 이미지를 만들어야 하고, alpine은 glibc 대신 musl libc를 사용하기 때문에 일부 라이브러리에서 호환성 문제가 생길 수 있어, slim 이미지를 만들기로 했다.
# 공통 Dockerfile (예: 모듈마다 복붙해서 사용)
FROM openjdk:17-jdk-slim
WORKDIR /app
# JAR 복사
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
# 포트는 각자 다름 (예: 8080, 8761 등)
EXPOSE 8080
# 실행
ENTRYPOINT ["java", "-jar"]
CMD ["app.jar"]
이렇게 도커파일을 작성하고 github에 푸시했으면, 이제
1. .dockerignore 작성
2. ./gradlew build 로 JAR 생성
3. GitHub에 코드 push
4. GitHub Secrets에 ECR 정보/환경변수 등록
5. GitHub Actions 워크플로우 작성
6. push 시 자동으로 Docker 빌드 + ECR push
이 단계를 거치면 된다.
(대신 이전에 S3, ECR, EC2 인스턴스 RDS 생성했어야 함)
dockerignore 작성
이제 Dockerfile을 만들었으니, 불필요한 파일들이 Docker 이미지 안에 포함되지 않도록 .dockerignore 파일도 설정해줘야 한다.
이걸 제대로 안 하면 빌드 속도 느려짐 + 이미지 용량 증가 + 보안상 문제까지 생길 수 있다.
내 경우는 Gradle 기반 프로젝트라서, 다음처럼 기본적인 빌드 산출물과 IDE 관련 파일들을 제외했다.
# 빌드 디렉토리 제외
build/
.gradle/
# IDE 설정 파일 제외
.idea/
*.iml
# Git 관련 파일 제외
.git
.gitignore
# OS 및 기타 불필요한 파일 제외
.DS_Store
*.log
# JAR 파일 제외 (필요한 경우 제외하지 않을 수도 있음)
*.jar
GitHub Actions 워크플로우 예시 (.github/workflows/deploy.yml)
name: Build and Push All Modules to ECR
on:
push:
branches: [ "main" ]
jobs:
build-and-push:
runs-on: ubuntu-latest
strategy:
matrix:
service: [account-service, api-gateway, eureka-server, external-api-proxy] # 너 프로젝트의 모듈 이름들
env:
ECR_URI: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.ECR_REPOSITORY }}
steps:
- name: Set image tag
run: echo "IMAGE_TAG=${{ github.ref_name }}-$(date +%Y%m%d-%H%M)" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v3
# - name: Check changes
# id: changes
# run: |
# echo "CHANGED=false" >> $GITHUB_ENV
# if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep "${{ matrix.service }}/"; then
# echo "CHANGED=true" >> $GITHUB_ENV
# fi
- name: Skip if no change
# if: env.CHANGED == 'false'
run: echo "No changes in ${{ matrix.service }}. Skipping..."
- name: Set up JDK 17
# if: env.CHANGED == 'true'
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission to Gradle
# if: env.CHANGED == 'true'
run: chmod +x gradlew
- name: Build JAR for ${{ matrix.service }}
# if: env.CHANGED == 'true'
run: ./gradlew :${{ matrix.service }}:bootJar -x test
- name: Configure AWS credentials
# if: env.CHANGED == 'true'
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
# if: env.CHANGED == 'true'
uses: aws-actions/amazon-ecr-login@v1
- name: Build and tag Docker image
# if: env.CHANGED == 'true'
run: |
docker build -t ${{ env.ECR_URI }}:${{ matrix.service }}-$IMAGE_TAG ./${{ matrix.service }}
- name: Push to ECR
# if: env.CHANGED == 'true'
run: |
docker push ${{ env.ECR_URI }}:${{ matrix.service }}-$IMAGE_TAG
❗ 주의사항
- IAM 권한에 ECR 관련 권한 (ecr:PushImage, ecr:GetAuthorizationToken) 포함돼야 함
이렇게 작성해봤다!