본문 바로가기

Trouble Shooting/episode 4. dev

Docker Compose with Github Action Troubleshooting

name: Docker Compose CI/CD Pipeline

# STEP1: Main Branch에 push 또는 pull request가 발생할 때 실행한다.
on:
  push:
    branches: ['main']
  pull_request:
    branches: ['main']

jobs:
  # STEP2: SpringBoot_CI_CD_Pipeline이라는 Job을 실행한다.
  Docker_Compose_CI_CD_Pipeline:
    # 최신 버전의 Ubuntu에서 실행한다.
    # docker가 기본적으로 제공되는 환경
    runs-on: ubuntu-latest

    steps:
      # STEP3: CI (1) 공식 Checkout Action을 사용해 저장소의 코드를 가져온다.
      - name: Checkout Repository
        uses: actions/checkout@v4

      # STEP4: CI (2) 공식 Setup Java Action을 사용해 Amazon Corretto 17을 설치한다.
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: 'corretto'
          java-version: '17'

      # STEP5: CI (3) Gradle Wrapper를 실행하기 위해 실행 권한을 부여한다.
      - name: Run chmod to make gradlew executable
        run: chmod +x ./gradlew

      # STEP6: CI (4) Gradle Wrapper를 사용해 Spring Boot 프로젝트를 빌드한다.
      - name: Build Spring Boot Project with Gradle Wrapper
        run: ./gradlew clean build

      # STEP7: CI (5) Dockerfile을 사용해 Docker 이미지를 빌드한다.
      - name: Build Docker Image of Spring boot
        run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo .

      # STEP8: CI (6) 공식 Docker Login Action을 사용해 Docker Hub에 로그인한다.
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PW }}

      # STEP9: CI (7) Docker 이미지를 Docker Hub에 푸시한다.
      - name: docker Hub push
        run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo

      # STEP10: CD (1) haythem의 Public IP Action을 사용해 GitHub Actions Runner의 Public IP를 steps.ip.outputs.ipv4로 가져온다.
      - name: get Runner's Public IP
        id: ip
        uses: haythem/public-ip@v1.3

      # STEP11: CD (2) 공식의 "Configure AWS Credentials" Action을 사용해 IAM 계정의 AWS Credentials를 설정한다.
      - name: Configure AWS IAM credentials in repository secrets
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2

      # STEP12: CD (3) AWS EC2의 보안그룹의 TCP 22번 포트에 GitHub Actions Runner의 Public IP를 추가한다.
      - name: Add GitHub IP to AWS
        run: |
          aws ec2 authorize-security-group-ingress \
          --group-id ${{ secrets.AWS_SG_ID }} \
          --protocol tcp --port 22 \
          --cidr ${{ steps.ip.outputs.ipv4 }}/32

      # STEP13: CD (4) appleboy의 scp(secured copy)를 사용. copy docker-compose.yml to EC2
      - name: Copy docker-compose.yml to EC2
        uses: appleboy/scp-action@v0.1.1
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          password: ${{ secrets.EC2_PASSWORD }}
          port: ${{ secrets.EC2_SSH_PORT }}
          source: "docker-compose.yml"
          target: "/home/${{ secrets.EC2_USERNAME }}/"

      # STEP14: CD (5) appleboy의 SSH Remote Commands Action을 사용해 copy한 파일에 읽기 권한 부여
      - name: Set file permission on docker-compose.yml
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          password: ${{ secrets.EC2_PASSWORD }}
          port: ${{ secrets.EC2_SSH_PORT }}
          timeout: 60s
          script: |
            sudo chmod 644 /home/${{ secrets.EC2_USERNAME }}/docker-compose.yml

      # STEP15: CD (6) appleboy의 SSH Remote Commands Action을 사용해 AWS EC2에 접속해 Docker-compose를 실행
      - name: Executing remote ssh commands using password with AWS EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          password: ${{ secrets.EC2_PASSWORD }}
          port: ${{ secrets.EC2_SSH_PORT }}
          timeout: 60s
          # docker compose mysql이 실행중일때만 spring container를 다시 시작
          script: |
            if ! sudo docker compose ps | grep -q "mysql_container_service"; then
              sudo docker compose up -d --build
            else
              sudo docker compose stop spring_container_service \
              && sudo docker rm spring_container \
              && sudo docker rmi ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo \
              && sudo docker compose up -d --no-deps spring_container_service
            fi

      # STEP16: CD (7) AWS EC2의 보안그룹의 TCP 22번 포트에 GitHub Actions Runner의 Public IP를 삭제한다.
      - name: Remove IP FROM security group
        run: |
          aws ec2 revoke-security-group-ingress \
          --group-id ${{ secrets.AWS_SG_ID }} \
          --protocol tcp --port 22 \
          --cidr ${{ steps.ip.outputs.ipv4 }}/32

 

 

이 pipeline을 구축할 때 사용한 github action의 workflow.yml 이다.

 

1. docker compose의 추가 설치 필요.

우리가 일반적으로 로컬에서 docker를 실행할 때는 docker desktop을 설치하고 실행한다.

docker desktop의 경우, docker compose가 기본적으로 내장이 되어 있기에 추가 설치 할 필요가 없다.

이는 일반적인 docker의 설치에도 마찬가지라고 한다.

 

다만, EC2 에 docker 설치를 할 때, 아래와 같은 command를 통해 설치할텐데,

sudo yum upgrade -y && yum update -y
sudo yum install docker -y
sudo service docker start

 

이렇게 할 경우 설치되는 docker에는 왜인지는 모르겠는데 docker compose가 함께 설치되지 않는다.

따라서 이 경우, 아래와 같이 docker compose를 추가적으로 설치해주어야 한다.

RHEL 기반 yum을 사용하기에, 아래의 공식문서를 참고해주어야 한다.

https://docs.docker.com/engine/install/rhel/

 

RHEL

Learn how to install Docker Engine on RHEL. These instructions cover the different installation methods, how to uninstall, and next steps.

docs.docker.com

 

sudo yum install docker-compose-plugin

위의 명령어를 통해 설치할 경우, docker-compose-plugin의 최신 버전이 설치된다.

참고로, docker-compose-plugin과 그냥 일반 docker compose의 설치가 다른 것 같다.

후자의 경우, 설치가 되었어도 명령어를 통한 실행이 제대로 되지 않는 경우가 존재했다.

 

 

 

2. docker compose의 명령어에서 docker-compose.yml을 찾지 못하는 경우

 

프로젝트 루트 경로에 있는 docker-compose.yml을 찾지 못하는 경우가 존재했었다.

이를 처리하기 위해서 EC2에 직접적으로 docker-compose.yml을 복사한 뒤,

해당 파일에 읽기 권한을 부여해서 하는 방식으로 해결했다.

 

github action에서 SCP(Secured Copy)를 통해 EC2에 직접적으로 docker-compose.yml 파일을 복사한 후

sudo docker-compose -f ./docker-compose.yml up -d

명령어를 통해 해당 파일의 위치를 명시적으로 정해둬서 실행하는 방식을 사용했다.

(다만, 이후에 정상작동되는 workflow.yml에서는 이 방식이 필요가 없다.)

 

 

3. SCP 사용 시 Linux 파일 구조와 action의 사용법에 대한 이해 부족

 

# STEP13: CD (4) appleboy의 SSH Remote Commands Action을 사용. copy docker-compose.yml to EC2
- name: Copy docker-compose.yml to EC2
  uses: appleboy/scp-action@v0.1.1
  with:
    host: ${{ secrets.EC2_HOST }}
    username: ${{ secrets.EC2_USERNAME }}
    password: ${{ secrets.EC2_PASSWORD }}
    port: ${{ secrets.EC2_SSH_PORT }}
    source: "docker-compose.yml"
    target: "/home/${{ secrets.EC2_USERNAME }}"

 

https://github.com/appleboy/scp-action

 

GitHub - appleboy/scp-action: GitHub Action that copy files and artifacts via SSH.

GitHub Action that copy files and artifacts via SSH. - appleboy/scp-action

github.com

여기에서 README에서는 

source에는 복사할 파일을, target에 복사할 path를 입력하라고 했는데,

 

맨처음에는 위의 yml과 같이 작성했으나, 자꾸 

/home/ec2-user/docker-compose.yml

에서 docker-compose.yml을 읽는 데 오류가 생겼다. 

ls -la 명령어를 통해서 확인해보니 d, 즉 디렉토리가 형성이 된 것을 알 수 있었다.

(심지어 해당 폴더 내부에는 docker-compose.yml 파일 또한 없었다.)

왜 이런 현상이 생기는가 보니 Linux에서의 폴더의 경로는 마지막에 반드시 "/"를 붙여야 한다는 사실을 알았다.

따라서 target:의 뒷부분이

"/home/${{ secrets.EC2_USERNAME }}/"

으로 "/" 가 되어야 하는 것이다.

 

 

4. script에 로직을 집어넣는 방법

 

CI/CD 자동화를 제대로 하고자 한다면 다음의 로직을 만족시켜야 한다고 생각했다.

1. 실행되고 있는 docker compose 서비스가 없을 경우, 맨 처음에는 둘 다 실행시키기.

2. 실행되고 있을 경우, DB는 켜놓은 상태에서 Spring Boot Service(Container)만을 종료하고

그 뒤에 해당 image를 삭제한 후, docker Registry로부터 pull 받아서 다시 실행시키는 것.

 

이를 위해서는 로직이 필요하다고 생각했는데,

script와 같이 shell에 집어넣어야하는 command들 또한 로직을 부여하는게 가능하다.

           script: |
            if ! sudo docker compose ps | grep -q "mysql_container_service"; then
              sudo docker compose up -d --build
            else
              sudo docker compose stop spring_container_service \
              && sudo docker rm spring_container \
              && sudo docker rmi ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo \
              && sudo docker compose up -d --no-deps spring_container_service
            fi

 

if ... then 으로 then이하의 if 절의 구문을 실행하고

else구문은 동일하고, fi는 if구문이 끝날 때 사용하는 것이다.