본문 바로가기
Today I Learned 2024. 8. 2.

(24.08.02)[16주차] CD Pipeline 중 GitHubs자동화 배포에 대한 정리

CD Pipeline 중 GitHubs자동화 배포에 대한 정리

마지막 프로젝트의 웹 서비스 제품을 만드는 것을 목표로 해서 중간 MVP 제작을 위해서 배포용 파이프 라인을 구상하고 튜터님들의 조언을 받아서 완성 후 구축을 했다.

위츼 파이프 라인 중에서 CD 부분 즉, GitHub Repository로 Java 기반의 Spring Boot BE 프로젝트에대해서 코드를 Push 시켰을 때, 배포를 할 수 있는 AWS EC2 인스턴스에 도커 컨테이너에 자동으로 실행해서 인스턴스 내에 띄우도록 했다.

물론 FE React 개발도 동시에 똑같이 진행을 하며, 이는 GitHub Actions 를 통한 Workflows 를 통해 진행했다.

이론상은 단순하지만, GitHub Actions YAML에 작업이 기재 하는 방법 등에서 이슈가 많아 이를 해결하고 완성하는데 시간이 많이 소요가 되었다.

더불어 인스턴스 안에서 컨테이너간 소통을 위한 네트워크 연결도 상당히 많은 시간 알아본 것이기 때문에 기록

따라서 이 부분의 CD 자동화에 있어서 점검을 하면서 필요한 단계를 기록


1. GitHub Actions를 통한 자동화 이미지 배포

  • Jenkins 등의 외부 툴을 사용해서 Docker 이미지를 배포할 계획이 있었지만, MVP 제작에 있어서 빠르게 개발에 대응해서 GitHub 과 연동함과 동시에 쉽게 제어, 그리고 학습을 한 부분이 GitHub Actions이기 때문에 진행

Docker Actions YAML파일

name: Deploy to AWS EC2

on:
  push:
    branches:
      - main
      
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up Gradle
        uses: gradle/gradle-build-action@v2
        with:
          gradle-version: '8.1.1'

      - name: setup jdk
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle

      - name: Make Gradle Wrapper executable
        run: chmod +x ./gradlew

      - name: Spring Boot Build
        run: ./gradlew clean build --exclude-task test

      - name: Docker Image Build
        run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/publicclassdev:latest .

      - name: Docker Login
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Docker Hub Push
        run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/publicclassdev:latest

      - name: AWS EC2 Connection
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{ secrets.EC2_WEB_HOST }}
          username: ${{ secrets.EC2_WEB_USERNAME }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          port: ${{ secrets.EC2_WEB_SSH_PORT }}
          timeout: 60s
          script: |
            sudo docker stop publicclassdev || true
            sudo docker rm publicclassdev || true
            
            # Create .env file with environment variables
            echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" | sudo tee /home/ubuntu/.env
	       # ...
	       # GitHubActions 환경변수
	       #...
            echo "RSA_PRIVATE_KEY_BASE64=${{ secrets.RSA_PRIVATE_KEY_BASE64 }}" | sudo tee -a /home/ubuntu/.env
            
            sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/publicclassdev:latest
            docker images --format "{{.Repository}}:{{.Tag}} {{.ID}}" | grep -v "latest" | awk '{print $2}' | xargs -r sudo docker rmi -f
            sudo docker run -d -p 8080:8080 --name publicclassdev --env-file /home/ubuntu/.env ${{ secrets.DOCKERHUB_USERNAME }}/publicclassdev:latest
            docker network connect app-network publicclassdev

  • Java의 Spring Boot 개발 기준
  • Spring Boot Build를 통해 일단 Spring Boot 을 gradle을 test를 제외하고 빌드
    • test는 진행하지 않고 CD 부분이기 때문에 CI 부분을 제외
  • Docker Image Build ~ Docker Hub Push
    • Docker Image 를 제작하는데, Dockerhub에 Image를 올리고, 인스턴스에서 직접 Pull 해서 컨테이너를 빌드해야하는 방향이므로 Dockerhub username 을 적어서 해당 Dockerhub Repository에 올릴 수 있음
      • 따라서 앞서서 Docker Login을 username과 password를 GitHub Actions 환경변수에 미리 저장해서 진행
    • tag를 :latest 형태로 저장을 하면 GitHub Repository에서도 이미지 이름에 포함되는 것이 아닌 태그로 자동으로 분류해서 이미지가 저장이됨
  • AWS EC2 Connection : 접속
    • appleboy/ssh-action@v0.1.6 이렇게 미리 지정해좋은 AWS EC2에 자동으로 연결을 하는 SSH 접근 작업을 한 부분을 그대로 끌고 올 수 있음
    • 따라서 with 를 이용해서 미리 저장한 환경변수에 대해서 host, username, key(인스턴스 접근을 위한 user의 .pem 키), port 를 지정
  • AWS EC2 Connection : Script
    • SSH에 접속한 상태에서 EC2 인스턴스 내에 스크립트를 직접 입력해서 실행시키는 것을 GitHub Actions가 ㅔ할 수 있음
    • SpringBoot 컨테이너 빌드를 할 때, 참조를 할 수 있는 .env 파일을 만들어서 필요한 변수에 대해서 GitHub Actions 환경변수를 가져와서 하나씩 추가
      • 이미지를 빌드할때 같이 첨부할 수 있으나, Dockerhub가 Public 이기 때문에 탈취될 가능성이 매우 크기 때문에 직접적으로 인스턴스에 env를 제작하는 것으로 교체
    • 추가적으로 latest 태그가 없는 이미지 파일이 누적될 수 있기 때문에 이를 삭제
      • 따라서 앞서서 기존에 Up 되어 있는 Container를 stop 후 rm 를 통해 삭제를 진행해서 이미지를 정리할 수 있음
    • docker network connect app-network publicclassdev 를 통해서 React, Nginx 컨테이너와 연결할 수 있는 docker가 제공하고 있는 bridge network 기능을 이용해서 app-network 라는 bridge network에 publicclassdev(연결시 컨테이너 이름 직접 지정가능) 컨테이너 네트워크를 연결 시켜줄 수 있음

2. EC2 Instance에서의 컨테이너 연결과 Nginx 세팅

앞서 말한 계속해서 컨테이너가 삭제되고 새로 만들어지면서 Bridge network에 연결을 시도해야하기 때문에, bridge network 설정과 ngnix가 이 네트워크를 사용해서 결국 user가 React FE 화면을 볼 수 있도록 설정을 해야한다.

Docker Bridge Network 생성

인스턴스 내에서 Nginx, SpringBoot, React가 하나의 Docker에서 제공해주는 Bridge Network를 사용하기 위해서 특정 Bridge Network를 설정 - 인스턴스 SSH에서 작업(Ubuntu 기준)

  • 예시로 app-network라는 bridge network를 도커에 생성
docker network create app-network
  • BE , FE 담당 두개의 컨테이너를 app-network에 직접적으로 container 이름을 사용해서 연결
docker network connect app-network publicclassdev # SpringBoot Container
docker network connect app-network publicclassdev-front # React Container

Nginx 컨테이너 생성과 Bridge Network를 통한 모든 컨테이너 연결

  • Nginx 이미지 다운로드
docker pull nginx:latest
  • nginx.conf 파일 설정
    • 이 부분을 통해서 nginx가 같은 네트워크(app-network)에 있는 컨테이너간 통신을 할 수 있게끔 할 수 있음
mkdir -p /home/ubuntu/nginx # 해당 경로에 nginx.conf 를 추가할 경로(폴더) 생성
vim /home/ubuntu/nginx/nginx.conf # VIM을 통해서 내용을 수정
  • nginx.conf 내용 입력
    • 여기서 docker bridge network 기능에 따라 컨테이너 이름을 직접 명시만 해주는 것으로 연결이 가능
    • 포트는 컨테이너 외부 포트가 아니라 내부에서 작동하는 포트로 설정
      • 이 포트들은 해당 인스턴스의 Security Group의 인바운드 룰에서 접근이 가능하도록 꼭 설정이 되어있어야함!
events {}

http {
    server {
        listen 80;

        location / {
            proxy_pass <http://publicclassdev-front:80>;  # React 컨테이너의 포트
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /api/ {
            proxy_pass <http://publicclassdev:8080>;  # SpringBoot 컨테이너의 포트
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}
  • Nginx Container 생성
    • nginx.conf 파일을 참고해서 Container가 생성될 수 있음
docker run -d \\
  --name nginx-container \\
  -p 80:80 \\ # 기본적으로 nginx는 80 포트를 가져가게 됨
  -v /home/ubuntu/nginx/nginx.conf:/etc/nginx/nginx.conf \\ #지정해줄 nginx.conf
  --network app-network \\ # bridge 네트워크에 연결
  nginx:latest # latest 태그를 달아서 최신 버전임을 지정

연결 확인

docker network inspect app-network

해당 문을 입력하게 되면 app-network에 연결이 되어있는 컨테이너를 확인할 수 있다.

ubuntu@(아이피):~$ docker network inspect app-network
[
    {
        "Name": "app-network",
        "Id": "(아이디)",
        "Created": "(생성일자)",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "(컨테이너아이디)": {
                "Name": "publicclassdev",
                "EndpointID": "(엔드포인트)",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            },
            "(컨테이너아이디)": {
                "Name": "nginx-container",
                "EndpointID": "(엔드포인트)",
                "MacAddress": "02:42:ac:12:00:04",
                "IPv4Address": "172.18.0.4/16",
                "IPv6Address": ""
            },
            "(컨테이너아이디)": {
                "Name": "publicclassdev-front",
                "EndpointID": "(엔드포인트)",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]
  • 정상적으로 3개의 컨테이너가 하나의 bridge network 인 app-network에 연결이 된 것을 확인 할 수있다.

 


이후에는 포트가 정상적으로 nginx.conf 에 맞고, 인스턴스 Security Group 에서 인바운드 룰에 해당 포트에 대한 접근이 추가가 될 경우..

정상적으로 인스턴스에 퍼블릭 주소로 접근을 하게 되면, 정상적으로 작동을 할 수 있게 된다.

 

여러가지 방법을 일주일동안 시도하면서 컨테이너 생성에서 연결까지 시도를 했지만, 굉장히 복잡하고 많은 작업을 구축해야했지만,

 

Docker와 Dockerhub의 브릿지 네트워크 기능이나 컨테이너 활용을 통해서 간편하게 독립적인 환경을 구축하고 CD 를 구축할 수 있었다.