Search

도커는 항상 어렵다 : 멀티모듈 CI/CD 배포일지 (2)

Tags
Infrastructure
Date
2024/05/27

1. 개요

지난 시간 도커는 항상 어렵다 : 멀티모듈 CI/CD 배포일지 (1) 에 이어서 ci.yml, cd.yml에 대해 알아보도록 하겠습니다.

2. CI/CD란

CI/CD(지속적 통합/지속적 제공 또는 배포)는 자동화를 통해 실현 가능한 소프트웨어 개발 방식입니다. 빈번하고 안정적으로 이루어지는 업데이트를 통해 지속적으로 코드를 제공하여 릴리스 주기를 단축합니다.

CI

CI는 주로 주로 코드의 빌드와 테스트 과정을 자동화하는 데 사용됩니다. 대게 많은 프로젝트에서 pull_request를 트리거로서 사용하며, ci를 통과하면 (즉, 테스트 과정을 성공하면) merge를 할 수 있도록 설정하기도 합니다.
ci.yml을 설정이 정말 간단합니다.
name: Backend Server CI on: push: branches: - main pull_request: branches: - main - develop types: [ opened, synchronize ] jobs: build: runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: action checkout uses: actions/checkout@v3 - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Set up environment variables run: | echo "OPENAI_BASE_URL=${{ secrets.OPENAI_BASE_URL }}" >> $GITHUB_ENV echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> $GITHUB_ENV echo "cloud.aws.s3.bucket=${{ secrets.CLOUD_AWS_S3_BUCKET }}" >> $GITHUB_ENV echo "cloud.aws.stack.auto=${{ secrets.CLOUD_AWS_STACK_AUTO }}" >> $GITHUB_ENV echo "cloud.aws.region.static=${{ secrets.CLOUD_AWS_REGION_STATIC }}" >> $GITHUB_ENV echo "cloud.aws.credentials.access-key=${{ secrets.CLOUD_AWS_CREDENTIALS_ACCESSKEY }}" >> $GITHUB_ENV echo "cloud.aws.credentials.secret-key=${{ secrets.CLOUD_AWS_CREDENTIALS_SECRETKEY }}" >> $GITHUB_ENV echo "naver.client-id=${{ secrets.NAVER_CLIENT_ID }}" >> $GITHUB_ENV echo "naver.client-secret=${{ secrets.NAVER_CLIENT_SECRET }}" >> $GITHUB_ENV echo "naver.base-url=${{ secrets.NAVER_BASE_URL }}" >> $GITHUB_ENV echo "DB_HOSTNAME=${{ secrets.DB_HOSTNAME }}" >> $GITHUB_ENV echo "DB_DATABASE=${{ secrets.DB_DATABASE }}" >> $GITHUB_ENV echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" >> $GITHUB_ENV echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> $GITHUB_ENV - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Run tests with Gradle env: SPRING_PROFILES_ACTIVE: test run: ./gradlew test
SQL
복사
다만, API 키 등의 민감한 정보들이 있어 Github Secrets에 등록하였으며 이를 불러오는 과정이 추가되었습니다.

CD

반면 CD는 파일은 배포 과정을 자동화하는 데 사용됩니다. 이번 프로젝트의 경우 Docker 컨테이너를 사용해서 배포를 진행했고, 자연스럽게 도커 허브에 빌드된 애플리케이션을 올리는 과정이 추가되었습니다.
cd.yml
name: Backend Server CD on: push: branches: - main - develop jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Set up environment variables run: | echo "DB_HOSTNAME=${{ secrets.DB_HOSTNAME }}" >> $GITHUB_ENV echo "DB_DATABASE=${{ secrets.DB_DATABASE }}" >> $GITHUB_ENV echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" >> $GITHUB_ENV echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> $GITHUB_ENV echo "OPENAI_BASE_URL=${{ secrets.OPENAI_BASE_URL }}" >> $GITHUB_ENV echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> $GITHUB_ENV echo "CLOUD_AWS_S3_BUCKET=${{ secrets.CLOUD_AWS_S3_BUCKET }}" >> $GITHUB_ENV echo "CLOUD_AWS_STACK_AUTO=${{ secrets.CLOUD_AWS_STACK_AUTO }}" >> $GITHUB_ENV echo "CLOUD_AWS_REGION_STATIC=${{ secrets.CLOUD_AWS_REGION_STATIC }}" >> $GITHUB_ENV echo "CLOUD_AWS_CREDENTIALS_ACCESSKEY=${{ secrets.CLOUD_AWS_CREDENTIALS_ACCESSKEY }}" >> $GITHUB_ENV echo "CLOUD_AWS_CREDENTIALS_SECRETKEY=${{ secrets.CLOUD_AWS_CREDENTIALS_SECRETKEY }}" >> $GITHUB_ENV echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> $GITHUB_ENV echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> $GITHUB_ENV echo "NAVER_BASE_URL=${{ secrets.NAVER_BASE_URL }}" >> $GITHUB_ENV - name: Generate .env file for Docker run: | echo "DB_HOSTNAME=${{ secrets.DB_HOSTNAME }}" >> .env echo "DB_DATABASE=${{ secrets.DB_DATABASE }}" >> .env echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" >> .env echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> .env echo "OPENAI_BASE_URL=${{ secrets.OPENAI_BASE_URL }}" >> .env echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env echo "CLOUD_AWS_S3_BUCKET=${{ secrets.CLOUD_AWS_S3_BUCKET }}" >> .env echo "CLOUD_AWS_STACK_AUTO=${{ secrets.CLOUD_AWS_STACK_AUTO }}" >> .env echo "CLOUD_AWS_REGION_STATIC=${{ secrets.CLOUD_AWS_REGION_STATIC }}" >> .env echo "CLOUD_AWS_CREDENTIALS_ACCESSKEY=${{ secrets.CLOUD_AWS_CREDENTIALS_ACCESSKEY }}" >> .env echo "CLOUD_AWS_CREDENTIALS_SECRETKEY=${{ secrets.CLOUD_AWS_CREDENTIALS_SECRETKEY }}" >> .env echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> .env echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> .env echo "NAVER_BASE_URL=${{ secrets.NAVER_BASE_URL }}" >> .env - name: Verify .env file creation run: cat .env - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Execute Gradle build and tests run: ./gradlew clean build env: DB_HOSTNAME: ${{ secrets.DB_HOSTNAME }} DB_DATABASE: ${{ secrets.DB_DATABASE }} DB_USERNAME: ${{ secrets.DB_USERNAME }} DB_PASSWORD: ${{ secrets.DB_PASSWORD }} OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} CLOUD_AWS_S3_BUCKET: ${{ secrets.CLOUD_AWS_S3_BUCKET }} CLOUD_AWS_STACK_AUTO: ${{ secrets.CLOUD_AWS_STACK_AUTO }} CLOUD_AWS_REGION_STATIC: ${{ secrets.CLOUD_AWS_REGION_STATIC }} CLOUD_AWS_CREDENTIALS_ACCESSKEY: ${{ secrets.CLOUD_AWS_CREDENTIALS_ACCESSKEY }} CLOUD_AWS_CREDENTIALS_SECRETKEY: ${{ secrets.CLOUD_AWS_CREDENTIALS_SECRETKEY }} NAVER_CLIENT_ID: ${{ secrets.NAVER_CLIENT_ID }} NAVER_CLIENT_SECRET: ${{ secrets.NAVER_CLIENT_SECRET }} NAVER_BASE_URL: ${{ secrets.NAVER_BASE_URL }} - name: Set up Docker Build uses: docker/setup-buildx-action@v2 - name: Set up SSH Key run: | mkdir -p ~/.ssh echo "${{ secrets.EC2_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa - name: Copy files to EC2 run: | scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa .env ec2-user@${{ secrets.EC2_HOST }}:/home/ec2-user/.env scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ./docker-compose.yml ec2-user@${{ secrets.EC2_HOST }}:/home/ec2-user/docker-compose.yml scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ./deploy.sh ec2-user@${{ secrets.EC2_HOST }}:/home/ec2-user/deploy.sh - name: Docker build and push to Docker Hub and copy docker-compose.yml to server run: | sudo docker login -u ${{ secrets.DOCKER_ID }} -p ${{ secrets.DOCKER_PASSWORD }} cd ./api-server sudo docker buildx build --push --platform linux/amd64 -t ${{ secrets.DOCKER_USERNAME }}/server . sudo docker push ${{ secrets.DOCKER_USERNAME }}/server cd ../batch sudo docker buildx build --push --platform linux/amd64 -t ${{ secrets.DOCKER_USERNAME }}/batch . sudo docker push ${{ secrets.DOCKER_USERNAME }}/batch - name: Deploy to Prod uses: appleboy/ssh-action@master with: key: ${{ secrets.EC2_PRIVATE_KEY }} host: ${{ secrets.EC2_HOST }} username: ${{ secrets.EC2_USERNAME }} port: 22 script: | sudo docker login -u ${{ secrets.DOCKER_ID }} -p ${{ secrets.DOCKER_PASSWORD }} sudo docker pull ${{ secrets.DOCKER_USERNAME }}/server sudo docker pull ${{ secrets.DOCKER_USERNAME }}/batch sudo chmod +x deploy.sh sudo chmod +x docker-compose.yml sudo ./deploy.sh sudo docker image prune -f
YAML
복사
조금 복잡할 수 있기 때문에 코드를 조금 더 자세히 살펴보겠습니다.
먼저 .pem 키를 복사하는 과정입니다.
- name: Set up SSH Key run: | mkdir -p ~/.ssh echo "${{ secrets.EC2_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
YAML
복사
Github Actions은 러너라고 하는 가상의 리눅스 환경에서 실행됩니다. 우리는 이 환경에서 EC2 인스턴스로 접근할 필요가 있습니다. EC2 인스턴스에 접속하는 대표적인 방법은 인스턴스 생성 시 발급받은 .pem 키를 사용하는 것입니다. 하지만 로컬 환경에서 Github Actions의 러너 환경으로 미리 .pem 키를 옮길 수 없기 때문에, 다음 명령어를 통해 .pem 키를 러너 환경에 추가합니다:
다음으로 중요한 부분은 도커 허브로 빌드된 애플리케이션을 push 하는 것입니다.
- name: Docker build and push to Docker Hub and copy docker-compose.yml to server run: | sudo docker login -u ${{ secrets.DOCKER_ID }} -p ${{ secrets.DOCKER_PASSWORD }} cd ./api-server sudo docker buildx build --push --platform linux/amd64 -t ${{ secrets.DOCKER_USERNAME }}/server . sudo docker push ${{ secrets.DOCKER_USERNAME }}/server cd ../batch sudo docker buildx build --push --platform linux/amd64 -t ${{ secrets.DOCKER_USERNAME }}/batch . sudo docker push ${{ secrets.DOCKER_USERNAME }}/batch
YAML
복사
이제 할 일은 EC2 인스턴스로 접속한 뒤 도커 허브에서 방금 전 push된 이미지를 pull 받는 것입니다.
- name: Deploy to Prod uses: appleboy/ssh-action@master with: key: ${{ secrets.EC2_PRIVATE_KEY }} host: ${{ secrets.EC2_HOST }} username: ${{ secrets.EC2_USERNAME }} port: 22 script: | sudo docker login -u ${{ secrets.DOCKER_ID }} -p ${{ secrets.DOCKER_PASSWORD }} sudo docker pull ${{ secrets.DOCKER_USERNAME }}/server sudo docker pull ${{ secrets.DOCKER_USERNAME }}/batch sudo chmod +x deploy.sh sudo chmod +x docker-compose.yml sudo ./deploy.sh sudo docker image prune -f
YAML
복사
이제 EC2 인스턴스 내에서 deploy.sh 쉘 스크립트를 활용해서 진짜 배포를 진행할 차례입니다. 이는 3편에서 다뤄보겠습니다.