VPS에 프로덕션 앱을 배포하고 Docker, GitHub Actions 및 AWS ECR로 프로세스를 자동화하는 방법

이 글에서는 프로덕션에 애플리케이션을 푸시하는 것의 의미와 자동으로 이를 수행하는 방법을 배울 것입니다. 또한, Docker와 GitHub Action을 사용하여 이를 어떻게 수행할 수 있는지 살펴보겠습니다. Docker를 사용하기 때문에, 애플리케이션을 작성하는 데 사용된 기술에 대해太多한 시간을 할애하지 않고, Docker 이미지 자체에 대해 무엇을 해야 하는지에 집중하겠습니다. 전제 조건은 여기서 간단한 서버를 의미하며, 그것이 간단한 VPS(가상 사설 서버), 전용 서버, 또는 SSH를 사용할 수 있는 어떤 서버이든 상관 없습니다. 우리는 작업을 수행하고, 서버를 애플리케이션을 수신하고 실행할 수 있도록 준비하고, 배포 파이프라인을 설정해야 합니다. 이 글의 다음 부분에서는 Ubuntu 서버를 가지고 있다고 가정할 것입니다.

서버에서 몇 가지 일회성 설정을 수행하는 첫 번째 일은 다음 단계를 위한 준비를 목표로 합니다. 이를 위해 다음 주제를 고려해야 합니다:

Docker Compose는 여러 Docker 애플리케이션을 서비스로 실행하고 이들이 서로 소통하도록 하며, 파일 저장소 용량과 같은 기능을 제공하는 도구입니다. 먼저 서버에 Docker와 Docker Compose를 설치해 보겠습니다; 공식 설치 가이드를 여기에서 따라할 수 있습니다: docs.docker.com/engine/install/ubuntu. 그 후, 비루트 사용자로 실행하는 것이 중요합니다; 공식 문서에서 다음과 같이 설명하고 있습니다: docs.docker.com/engine/security/rootless.

다른 레지스트리로 동일한 목표를 달성할 수도 있지만, 저는 AWS를 사용하는 것이 더 간단하다고 생각해 사용했습니다.

Docker를 사용하기 때문에, 이미지는 어디서 저장하고检索해야 합니다. 이를 위해 저는 AWS ECR (Amazon Web Services Elastic Container Registry)를 사용하고 있습니다. AWS 계정 내의 Docker 레지스트리입니다. 사용 비용이 매우 저렴하고 설정도 간편합니다. Docker Hub를 사용하여 이미지를 저장할 수 있는 사적인 레지스트리를 만들 수도 있습니다. 모든 것은 AWS 계정 내에서 ECR 사적인 레지스트리를 만드는 것으로 시작됩니다. “레지스트리 생성“을 클릭하고 레지스트리 이름을 입력합니다.

리포지토리를 생성한 후, 리포지토리 URI를 복사하여 나중에 사용할 수 있도록 보관할 수 있습니다. 다음과 같은 형식을 가집니다 AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/REPOSITORY_NAME .

이 리포지토리로부터 pull/push할 권한이 있는 AWS IAM 자격 증명을 설정해야 합니다. IAM 서비스로 이동하여 새 사용자를 클릭하고 다음 정책을 해당 사용자에 연결하세요: AmazonEC2ContainerRegistryFullAccess, AWS 콘솔 접근 권한을 활성화할 필요는 없습니다. 이 과정이 끝나면 AWS에서 2개의 키를 받게 됩니다, secret keysecret key id를 쪽에 두고 다음 작업에 사용할 것입니다.

서버에 다시 돌아와 AWS CLI를 설치해야 합니다. 공식적인 설치 방법은 여기에서 확인할 수 있습니다. https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions. 이 후, aws --version 명령어를 실행하여 설치를 테스트할 수 있습니다. 이 단계에서는 aws configure 명령어를 실행하고, AWS에서 생성한 이전 키인 secret keysecret key id를 제공하여 질문에 응답해야 합니다. 또한 출력 형식을 선택해야 하며, 간단하게 JSON을 선택하고, 기본 지역을 설정해야 합니다. 이전에 ECR 레지스트리를 생성한 지역을 선택하는 것이 좋습니다.

제 워크플로우에서는 특정 작업을 수행하는 작은 셸 스크립트를 작성했습니다. 이 스크립트는 레지스트리에 로그인하고 이미지를 다운로드하며 해당 Docker 서비스를 재시작합니다. 이 스크립트를 redeploy.sh라고 부르고, 애플리케이션을 실행하고 싶은 폴더 아래에 저장합니다. 여기는 그 내용입니다:

#!/bin/bash

# Retrieve AWS ECR login command
aws ecr get-login-password --region [SWS_REGION] | docker login --username AWS --password-stdin [AWS_REGION].dkr.ecr.us-west-2.amazonaws.com

# Associating repositories with identifiers
declare -A repositories=(
    ["web"]="[REGISTRY_NAME]:latest"
)

# Check if service identifier is provided as a command line argument
if [ -z "$1" ]; then
    echo "Please provide a service identifier as a command line argument."
    exit 1
fi

service_identifier=$1

# Check if the provided service identifier exists in the repositories array
if [ -z "${repositories[$service_identifier]}" ]; then
    echo "Invalid service identifier. Available identifiers: ${!repositories[@]}"
    exit 1
fi

# pull the new image from the registry
repository=${repositories[$service_identifier]}
echo "Pulling [AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/$repository"
docker pull "[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/$repository"

# Change directory to [APP_FOLDER] 
cd /home/ubuntu/[APP_FOLDER] || {
    echo "Failed to change directory to /home/ubuntu/[APP_FOLDER]"
    exit 1
}

# stop and restart the service, this wil force docker compose to redownload the lates image
echo "Re-running service $service_identifier"
docker compose stop "$service_identifier"
docker compose up --no-deps "$service_identifier" -d

# Remove old and un-used docker images
echo "Removing unused Docker images"
docker image prune -fa
echo "Removed Dangling Images"

이 스크립트의 첫 번째 단계는 AWS 계정에 로그인하여 Docker가 Docker 이미지를 가져올 때 사용할 토큰을 얻는 것입니다. 레지스트리가 프라이빗이므로 인증하지 않고 가져올 수 없음을 기억하세요.

그런 다음 리포지토리 목록을 선언하고 이를 어떤 식별자와 연결합니다. 지정된 식별자는 명령 줄 인수로 사용될 것입니다. 이에 대해 나중에 더 설명드리겠습니다. 이 후 사용자가 존재하는 서비스 식별자에 해당하는 인수를 제공했는지 확인합니다. 예를 들어, 사용자가 ./redeploy web와 같이 입력하도록 원합니다. 스크립트는 인수 web를 두 번째 단계에서와 같이 리포지토리 web와 연결합니다.

서비스 식별자를 얻은 후, 리포지토리 URL을 동적으로 생성하고 해당 URL로 docker pull을 수행합니다. 이는 Docker 이미지가 우리 시스템으로 다운로드되는 것을 확인하는 것입니다.

스크립트는 이제 애플리케이션 폴더로 이동합니다, /home/ubuntu/[APP_FOLDER] 이는 모든 작업을 ubuntu 사용자로 실행하고 그의 HOME 폴더가 ubuntu라는 이름으로 되어 있으며, APP_FOLDER는 전체 설정을 포함하고 있다고 가정합니다.

다음 단계는 서비스를 중지하고 시작하는 것입니다. 그 후 docker image prune -fa 명령어를 사용하여 오래된 사용되지 않는 이미지를 제거합니다. 자세한 내용은 여기에서 배울 수 있습니다: https://docs.docker.com/reference/cli/docker/system/prune/.

Compose는 우리의 전체 시스템을 실행하는 유틸리티입니다. docker-compose.yml라는 파일이 필요합니다. 여기에서 모든 것을 정의합니다. 예를 들어, 우리 애플리케이션이 redispostgres 서비스를 필요로 한다고 가정해 보겠습니다. 이렇게 보일 것입니다:

version: '3.9'
services:
  web:
    image: "[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/myapp:latest"
    ports:
        - 8080:8080
    depends_on:
        - redis
        - db
    env_file:
        - .env

  redis:
    image: 'redis:alpine'
    ports:
      - '6379:6379'
  db:    
    image: 'postgres:14'
    restart: always
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_USER: postgres
      PGDATA: /var/lib/postgresql/data/pgdata
    healthcheck:
      test: [ "CMD", "pg_isready", "-q", "-d", "postgres", "-U", "postgres" ]
      timeout: 45s
      interval: 10s
      retries: 10
    ports: 
      - '5437:5432'
    volumes:
      - ./opt/postgres/data:/var/lib/postgresql/data

您的卷 ./opt/postgres/data:/var/lib/postgresql/data 将把 Postgres 服务器的目录映射到本地磁盘,这样在 Docker 容器停止运行时内容不会丢失。在 https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216 这里了解更多关于使用 docker-compose 运行 Postgres 的信息。我使用了一个指令名 env_file,这允许 docker-compose 在运行时读取一个文件并将内容加载到 Docker 容器中,我这样做是因为通常 docker-compose 文件会被提交到版本控制系统(VCS),因此我不想通过服务中的 environment 指令直接在其中保留环境变量。请注意,我们的服务在这里被命名为 web,之前我们编写了一个文件 redeploy.sh,我们打算这样运行它:

 ./redeploy.sh web

这里的 web 参数与我们的服务名相关联,该文件简单地将参数映射到 Docker 文件中的服务名。

在这个步骤中,我们必须创建一个 Linux 服务,以确保每次服务器启动或我们的应用程序停止时都重新启动应用程序。以下脚本能帮助您实现这一点:

[Unit]
Description=[APP_NAME] service executed by docker compose
PartOf=docker.service
After=docker.service
After=network.target

[Service]
Type=oneshot
RemainAfterExit=true
WorkingDirectory=/home/ubuntu/[APP_FOLDER]
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=multi-user.target

让我们来分析一下!!!

  • Unit 部分描述了我们的服务并指定了我们的单元是哪个服务的一部分,在这个案例中,它是 Docker 服务,这将确保当 Docker 服务正在运行时我们的服务也始终运行。

  • Service 섹션에서는 서비스를 실행하는 방법을 설명하는데, 흥미로운 부분은 WorkingDirectory , ExecStart, ExecStop 명령어입니다, 예를 들어, 서비스 이름이 myapp인 경우 systemctl start myapp 명령을 입력하면 ExecStart 명령이 실행됩니다. Linux 서비스에 대한 자세한 내용은 https://www.redhat.com/sysadmin/systemd-oneshot-service에서 확인할 수 있습니다. systemd로 도커 서비스를 실행하는 방법에 대한 자세한 내용은 여기를 참조하세요: https://bootvar.com/systemd-service-for-docker-compose/

이 서비스는 시스템이 필요할 때 실행할 수 있도록 설치되어야 합니다. 예를 들어 myapp.service라는 이름의 파일에 저장해야 합니다

touch myapp.service
# open it
nano myapp.service
# paste the previous scrip in it
cp myapp.service /etc/systemd/system/myapp.serivce

이제 리눅스 서비스로 인식되며, systemctl start myapp 명령을 실행하여 시작할 수 있습니다. 다음 필요한 명령은

systemctl enable myapp.service

이렇게 하면 서비스가 서버 재부팅 시 자동으로 실행되도록 합니다. 더 알아보세요: https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6

이 작업에 Nginx를 사용했습니다. Nginx는 작고 강력하며 널리 사용되고 있으며, 로드 뱅싱, 정적 파일 서버, 리버스 프록시 등 다양한 역할을 할 수 있습니다. 먼저 해야 할 일은 설치하는 것입니다.

sudo apt-get install nginx

이 단계에서 docker 이미지는 실행 중이라고 가정해 봅시다. 이 이미지는 포트 8080에서 실행 중인 앱을 포함하고 있으며, 이 포트는 docker-compose 파일을 통해 서버에 바인딩됩니다. Nginx와 우리의 포트 간에 리버스 프록시 설정을 해야 합니다. 다음은 필요한 설정입니다:

upstream app_backend {
  server localhost:8080; # the appliction port
}

server {
  listen 80;
  server_name [DOMAIN_NAME];

  location / {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_cache_bypass $http_upgrade;
  }
}

이 설정을 myapp.conf라고 부르고, Nginx가 찾을 수 있는 디렉토리에 저장합니다. 이 폴더는 /etc/nginx/conf.d/라는 이름입니다.

sudo touch /etc/nginx/conf.d/myapp.conf
sudo nano /etc/nginx/conf.d/myapp.conf
# paste the content there

이제 테스트하고 NGINX 서비스를 다시 시작하기 위해 다음 명령어를 사용할 것입니다:

sudo nginx -t # test if the config is valid 
sudo nginx -s reload # reload the nginx service so it will consider it

이 설정은 Nginx가 포트 80에서 트래픽을 듣고 [DOMAIN_NAME] 도메인 이름을 사용하여 앱 서버의 포트 8080으로 proxy_pass 디렉티브를 통해 전달하도록 지시합니다. location / { 줄은 /로 시작하는 모든 요청을 캡처하고 location 블록 아래에 작성된 작업을 수행하도록 의미합니다. 더 알아보기 위해 https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/How-to-setup-Nginx-reverse-proxy-servers-by-example.

서버를 구성한 후, 이제 빌드 파이프라인을 설정해야 합니다. 주로 1 단계로 구성되어 있으며, Github Action 파이프라인 파일을 작성하고 프로젝트에 추가하는 것입니다. 시작해보겠습니다.

Github action은 우리의 소스 코드에서 Docker 이미지를 빌드하고, 이미지가 서버에서 당겨져 실행되는 레지스트리로 푸시할 것입니다. 이 예제에서는 샘플 Dockerfile을 사용하지만, 실제로는 자신만의 Dockerfile을 작성해야 합니다. express.js 애플리케이션의 경우 Docker 파일은 다음과 같습니다:

# Fetching the minified node image on apline linux
FROM node:slim

# Declaring env
ENV NODE_ENV production

# Setting up the work directory
WORKDIR /express-docker

# Copying all the files in our project
COPY . .

# Installing dependencies
RUN npm install

# Installing pm2 globally
RUN npm install pm2 -g

# Exposing server port
EXPOSE 8080

# Starting our application
CMD pm2 start process.yml && tail -f /dev/null

이 Docker 파일을 빌드하고 실행하면 우리의 애플리케이션이 포트 8000에서 시작되지만, 우리의 설정에서는 docker-compose로 실행해야 합니다.

다음은 Github actions 파이프라인 설정입니다. 이를 위해 프로젝트 루트에 .github/workflows 폴더를 생성하고 docker-build.yml 파일을 만들어 파이프라인을 작성할 것입니다.

name: Build, Push to ECS and Deploy to Server

on:
  push:
    branches: ['deploy/main']

jobs:
  build:
    name: Build Web Image
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: [AWS_REGION]

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: [REPOSITORY_NAME]
          IMAGE_TAG: latest
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . 
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

      - name: Restart the service via SSH
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
          port: ${{ secrets.PORT }}
          script: /home/ubntu/[APP_DIRECTORY]/redeploy.sh web

여기서 주의해야 할 몇 가지 단계가 있습니다:

  • AWS 인증 설정: 여기서 시스템은 앞서 생성한 aws 키를 로드할 것이며, 이를 GitHub 계정의 시크릿에 등록해야 합니다

  • 빌드, 태그 부착 및 Amazon ECR로 이미지 푸시: 이 단계는 docker builddocker push 명령어를 실행하여 Docker 이미지를 생성합니다
  • SSH를 통해 서비스 재시작 이 단계는 서버에 연결하여 전체 애플리케이션을 한 번에 재시작합니다.

이 파이프라인은 deploy/main 브랜치에 풀 리퀘스트가.merge될 때 마다 실행됩니다.

on:
  push:
    branches: ['deploy/main']

이제 전체 시스템이 완비되었으며, 특정 사례에 맞게 편집하고 적용할 수 있습니다. 미래의 기사에서는 생산 환경에서 애플리케이션을 빌드하고 Docker 파일에서 실행하는 과정을 공유할 것입니다.

이 기사는 배포 자동화를 위해 VPS를 설정하는 과정을 설명하려고 합니다. 서버 내에서 애플리케이션 실행 과정과 애플리케이션 빌드 과정을 설명하며, 각 부분은 다른 도구로 수행할 수 있습니다. 예를 들어, nginx를 Treafik으로 바꿀 수 있고, systemd 서비스를 supervisor 프로그램으로 바꿀 수 있습니다. 이 과정은 서버 백업이나 서버의 기본 포트를 닫는 추가 사항을 다루지 않으며, 이러한 내용은 미래의 기사에서 설명될 것입니다. 이를 당신의 흐름에 맞게 적용하고 싶다면 질문을 자유롭게 해 주세요. 다른 기사에서는 배포에 대한 생산 준비가 완료된 애플리케이션을 설정하는 방법에 중점을 두겠습니다. 이는 Docker 이미지 빌드 이전의 과정입니다.

hope you enjoyed the read.

저는 귀사나 팀 내에서 이를 구현하는 데 도움을 드리기 위해 여기에 있습니다. 핵심 작업에 집중하고, 마스트도ن의 거대한 잠재력을 활용하기 전에 돈을 절약할 수 있도록 도와드립니다.

Source:
https://blog.adonissimo.com/how-to-deploy-a-production-app-on-a-vps-and-automate-the-process-with-docker-github-actions-and-aws-ecr