Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Archives
Today
Total
관리 메뉴

참새의 이야기

배포 자동화 도전기 (github actions+docker+ec2) 본문

deploy

배포 자동화 도전기 (github actions+docker+ec2)

참새짹짹! 2024. 1. 6. 16:24

GDSC-Hongik의 인프라 초기 세팅을 담당하게 되었다.

docker, github actions 등 CI/CD 기술들을 말로만 들어보거나 블로그 몇 개 보면서 기계적으로 따라 하기만 해왔는데, 마침 팀원들이 다들 유경험자라서 내가 우선 구축해 보고 피드백을 받을 수 있게 되었다.

전체 흐름

설계를 요약하자면 이렇다.

github의 특정 브랜치에 push와 같은 이벤트가 일어났을 때, github actions가 이를 감지하고 workflow에 따라 빌드와 배포를 시작한다.

workflow의 빌드 과정에서 도커 이미지가 빌드되면 docker hub로 푸시한다.

이후 workflow의 배포 과정에서 ec2에 접속하고, docker hub로부터 도커 이미지를 pull 해와서 실행한다.

여기까지가 당장 구축할 인프라의 흐름이고 향후 도커나 무중단 배포와 관련해서 몇 가지 추가 사항이 있을 것 같다.

task

자 그럼 해야 할 일들을 적어보자.

  • EC2 인스턴스 생성 후 인스턴스에 docker install
  • Github actions의 workflow 작성
  • Dockerfile 작성

당장은 도커에 Nginx나 Redis 같은 다른 이미지를 담지 않고 단순히 github actions에서 빌드한 파일을 ec2로 운반하는 역할만 하기 때문에 task가 적다.
그래도 여러 문제들을 겪다보니 하루는 걸린 것 같다.

본격적인 작업

EC2에 docker와 docker-compose를 설치

ec2에 우선 putty나 mobaxterm을 사용해 접속해 준다.

docker에 집중하기 위해 ec2 인스턴스를 생성한다거나 ec2에 연결하는 방법은 생략하려 한다.

 

리눅스에서 자주 사용하게 될 명령어 sudo를 설명하자면, super user do의 약자이고 관리자 권한으로 명령할 때 사용된다.

이걸 설명한 이유는 docker는 관리자 권한으로만 실행할 수 있기 때문이다.

지금부터 ec2에 docker와 docker-compose를 설치하기 위한 명령어가 잔뜩 있는데, 하나씩 이해해 보자.

  1. 이하의 명령어를 super user로 실행하기 위해 switch user 한다
    명령어를 실행시키면 root 계정으로 전환된다.
sudo su
  1. apt-get은 ubuntu에서 패키지 관리를 목적으로 사용하는 명령어이다.
    update로 사용가능한 패키지들을 가져오고, upgrade로 가져온 패키지를 업그레이드한다.
apt-get update
apt-get upgrade
  1. 이제 apt가 https를 패키지를 다운로드하기 위해 필요한 도구들을 설치해 보자.
    1. apt-transport-https
    2. ca-certificates
    3. curl
    4. gnupg-agent
    5. software-properties-common
apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
  1. docker의 GPG 키를 다운로드하여 저장한다.
    GPG는 docker가 배포하는 소프트웨어가 원본에서 변조되지 않았음을 검증하기 위한 수단이다.
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
  1. apt 패키지 관리자에 docker 저장소를 추가하는 명령어이다.
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
  1. docker를 설치한다.
apt-get update
apt-get install docker-ce docker-ce-cli containerd.io
  1. docker-compose를 ‘/usr/local/bin/docker-compose’ 경로에 설치하고
    ’/usr/local/bin/docker-compose'에 대한 실행 권한을 ‘chmod +x’로 부여한다.
curl \
    -L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" \
    -o /usr/local/bin/docker-compose

chmod +x /usr/local/bin/docker-compose

workflow

아래는 workflow의 초안이다.
초안이라는 말에서 알 수 있듯, 이 workflow는 최종본이 아니다.

두 세 차례의 수정 끝에 완성할 수 있었다.

완성본만 보고 싶다면 이 링크로 가서 볼 수 있다.

 

각 작업의 목적은 완성본과 함께 설명하기로 하고 우선 내가 겪은 문제들을 순서대로 기록하고자 한다.

name: build and deploy

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Run chmod to make gradlew executable
        run: chmod +x ./gradlew
      - name: Build with Gradle
        run : ./gradlew clean build --exclude-task test

      - name: Login to DockerHub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Build Docker
        run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/infra_practice_server .
      - name: Push Docker
        run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/infra_practice_server:latest

  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Docker compose
        uses: appleboy/ssh-action@master
        with:
          username: ubuntu
          host: ${{ secrets.INFRA_PRACTICE_SERVER_IP }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/infra_practice_server:latest
            sudo docker run -d -p 8080:8080 --name deploy_container

사실 이 workflow는 build부터 fail 했다.

Build Docker 작업에서 Dockerfile을 사용하는데, Dockerfile을 작성하는 걸 깜빡했기 때문이고 아래의 로그를 통해 금방 파악할 수 있었다.

ERROR: failed to solve: failed to read dockerfile: open /var/lib/docker/tmp/buildkit-mount3494417144/Dockerfile: no such file or directory

또 한 가지 수정한 게 있다면, push가 발생했을 때만 실행하도록 pull_request 발생 시 작동하도록 하는 부분을 workflow에서 삭제했다.

Dockerfile

바로 도커파일을 추가하는 작업을 했다.

도커파일은 Dockerfile이라는 파일명에 확장자 없이 생성하면 된다.

// eclipse-temurin의 17 버전의 자바를 기본 이미지로 사용함
FROM eclipse-temurin:17

// JAR_FILE이라는 인자에 'build/libs/*.jar'라는 경로 패턴을 설정
// 디렉토리 내에서 jar파일의 경로를 지정할 때 사용
ARG JAR_FILE=build/libs/*.jar

// github actions가 돌아가는 머신에서 ${JAR_FILE}를 docker로 복사
// docker에 저장되는 이름은 app.jar
COPY ${JAR_FILE} app.jar

// ENTRYPOINT는 컨테이너가 시작될 때 실행할 일을 설정
// "java"와 "-jar"는 jar 파일을 실행하라는 명령
// "/app.jar"는 실행할 jar파일의 위치를 나타냄
ENTRYPOINT ["java","-jar","/app.jar"]

내 도커 파일에는 위와 같은 내용이 들어있다.

자세한 설명은 주석의 형태로 남겼다.

build 성공, deploy 실패

 deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Docker compose
        uses: appleboy/ssh-action@master
        with:
          username: ubuntu
          host: ${{ secrets.INFRA_PRACTICE_SERVER_IP }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/infra_practice_server:latest
            sudo docker run -d -p 8080:8080 --name deploy_container

deploy의 실패는 마지막 한 줄 때문이었다.

err: "docker run" requires at least 1 argument.
err: See 'docker run --help'.
e:  docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

이번에도 로그를 확인해 보니 docker run 명령에 필요한 인자를 덜 넣어준 것이 문제였다.

컨테이너의 이름 뒤에 도커 허브의 유저네임과 이미지 이름을 로그의 지시에 따라 아래와 같이 명령을 수정했다.

sudo docker run -d -p 8080:8080 --name deploy_container ${{ secrets.DOCKERHUB_USERNAME }}/infra_practice_server:latest

build, deploy 성공 그러나,

위의 수정 사항으로 빌드와 배포는 성공했다.

기쁜 마음으로 AWS에서 발급받은 탄력적 IP 주소로 접속했으나 ERR_CONNECTION_REFUSED 가 발생했다.

배포 과정에서 문제가 있었던 것인지 확인하기 위해 ec2 인스턴스에 접속해 봤다.

docker ps -a로 컨테이너 목록을 확인해 보면 이미 컨테이너가 잘 실행되고 있다는 것을 확인할 수 있었다.

구글링을 통해 알아보니 apache를 직접 설치해줘야 한다는 글이 있어서 바로 apache를 직접 설치해 봤다.

다시 접속해 보니 이번에는 ERR_CONNECTION_REFUSED가 뜨지는 않지만, apache의 default page로 연결되었다.

이로써, EC2 인스턴스의 보안 그룹 문제는 아님을 확인했다.

우연히 apache가 몇 번 포트에서 돌아거고 있는지 확인을 했다가, 원인을 파악할 수 있었다.

 

앞서 수정한 worklow의 명령에서 8080 포트를 8080 포트로 연결하도록 작성했다.

하지만, 나는 https가 아니라 http를 사용하고 있기 때문에 8080 포트가 아니라 80 포트로 들어온다는 것을 놓치고 있었던 것이다.

workflow를 아래와 같이 한 번 더 바꿔준 후에야 드디어 배포 자동화를 마칠 수 있었다.

sudo docker run -d -p 80:8080 --name deploy_container ${{ secrets.DOCKERHUB_USERNAME }}/infra_practice_server:latest

workflow 최종본

// 워크플로우의 이름
name: build and deploy

// 워크플로우가 언제 작동하도록 할 것인가?
on:
  push:
    branches: [ "master" ]

// 워크플로우가 코드를 읽을 권한을 부여
permissions:
  contents: read

jobs:
  build:
        // 최신 버전 ubuntu에서 실행하라
    runs-on: ubuntu-latest

    steps:
            // 코드를 가져오고, 자바 17 버전을 사용하라고 지정
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

            // 빌드
      - name: Run chmod to make gradlew executable
        run: chmod +x ./gradlew
      - name: Build with Gradle
        run : ./gradlew clean build --exclude-task test

            // docker에 로그인, 빌드, 푸시
      - name: Login to DockerHub
        uses: docker/login-action@v2
        with:
                    // ${{secrets.a}} 형식은 github actions의 repository secrets에서 읽어오라는 의미
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Build Docker
        run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/infra_practice_server .
      - name: Push Docker
        run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/infra_practice_server:latest

  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Docker compose
        uses: appleboy/ssh-action@master
        with:
          username: ubuntu
          host: ${{ secrets.INFRA_PRACTICE_SERVER_IP }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/infra_practice_server:latest
            sudo docker run -d -p 80:8080 --name deploy_container ${{ secrets.DOCKERHUB_USERNAME }}/infra_practice_server:latest

마무리

아직, 기존 도커 컨테이너를 멈추고 새로운 버전으로 돌리려면 deploy의 script 부분에 두 줄 정도 추가를 해야 한다.

이 부분은 다른 부분을 수정할 때 같이 수정하려 한다.

 

여태 배포라고 하면 진짜 막막하기만 했는데 한 번 해보니까 또 할 만한 것 같다는 생각이 든다.

끝.

 

'deploy' 카테고리의 다른 글

nginx와 spring boot 서버를 docker-compose로 배포  (0) 2024.01.17