如何使用 Docker、GitHub Actions 和 AWS ECR 在 VPS 上部署生產應用程序並自動化過程。

在本文中,我們將了解什麼是將應用程序推送到生產環境,以及如何自動完成這個過程,我們將看到使用Docker和GitHub Action的相關操作。由於我們使用的是Docker,所以我不會花費太多時間解釋應用程序是用哪些技術編寫的,而是關注如何處理Docker镜像本身。這裡的前提意指一個簡單的伺服器,無論是一個簡單的VPS(虛擬私有伺服器),專用伺服器,或者任何你可以使用SSH連接的伺服器。我們必須進行一系列操作,準備伺服器以接收和執行應用程序,並設置部署管道。在本文的接下來部分,我將假定你擁有一台Ubuntu伺服器

首先,我們需要在伺服器上進行一些一次性配置,目的是為了為下一步驟做好準備。為此,我們必須考慮以下主題:

Docker Compose 是一個能夠運行多個 Docker 應用程序作為服務,並讓它們相互通訊的工具,還具有像卷(volumes)這樣的文件存儲等其它功能。我們先在伺服器上安裝 Docker 和 Docker Compose;您可以遵循這裡的官方安裝指南:docs.docker.com/engine/install/ubuntu。之後,重要的是要以非根用戶運行;這是官方文檔中的說法:docs.docker.com/engine/security/rootless

請注意,如果您願意,也可以使用另一個登記庫達到相同的目標,我只是因為 AWS 對我來說更簡單所以才使用它。

由於我們使用 Docker,所以圖像需要存儲在某個地方並從那裡检索。為此目的,我使用的是 AWS ECR(Amazon Web Services 弹性容器登記庫)。它是一個 AWS 帳戶內的 Docker 登記庫。它的使用成本非常低,而且容易設置。您也可以使用 Docker Hub 為您的圖像創建一個私有倉庫。一切從在 AWS 帳戶中創建 ECR 私有登記庫開始。您將點擊“創建倉庫”並填寫倉庫名稱。

創建存儲庫後,您可以複製存儲庫 URI 並將其保存以供稍後使用。它具有以下格式 AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/REPOSITORY_NAME .

您還需要設置具有從/向此存儲庫拉/推權限的 AWS IAM 凭據。讓我們進入 IAM 服務,點擊新用戶並將以下策略附加給他:AmazonEC2ContainerRegistryFullAccess,您不需要為他啟用對 AWS 控制台的訪問權限。在此過程結束時,您會從 AWS 獲得兩個金鑰,一個是 secret key,另一個是 secret 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註冊表的區域。

在我的工作流程中,我編寫了一個小型的shell腚本,它執行某些操作,這是這個過程的關鍵部分,它登錄到註冊表,下載镜像,並重啟對應的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 CLI登錄AWS帳戶以獲取docker在提取docker镜像時將使用的令牌,記住註冊表是私有的,我們不能在没有認證的情況下直接拉取。

然後我們聲明一個倉庫列表並將它們與某些標識符關聯,指定的標識符將被用作命令行參數,稍後再詳細說明。之後我們驗證用戶是否提供了一個對應於現有服務標識符的參數,我們希望他們輸入像./redeploy web之類的東西,腚本會將參數web與第二步中的倉庫web關聯。

在獲得服務標識符後,我們動態地創建倉庫URL並使用它進行docker pull。這樣可以確保docker镜像被下載到我們的系統中。

腚本現在將切換到應用程序目錄,/home/ubuntu/[APP_FOLDER],這假設您在用戶ubuntu下運行所有内容,他的HOME文件夹名為ubuntuAPP_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容器停止運行時,數據不會丟失。在這裡學習更多關於使用docker-compose運行Postgres的知識https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216。我使用了一個指令名稱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」部分描述了如何運行我們的服務,有興趣的部分是WorkingDirectoryExecStartExecStop命令,它們將根據它們的名稱含義被使用,例如,如果服務名為myapp,當您輸入命令systemctl start myapp時,將執行ExecStart命令。您可以在此處了解更多關於Linux服務的資訊:https://www.redhat.com/sysadmin/systemd-oneshot-service。了解更多關於如何使用systemd運行docker服務的資訊,請參考: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

此時它被認為是一個Linux服務,您可以運行「systemctl start myapp」來啟動它。接下來需要的命令是「

systemctl enable myapp.service

」這將確保服務在每次重新啟動時都會被伺服器自動執行。您可以在此處了解更多資訊:「https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6

我使用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],並通過proxy_pass指令將其發送到應用服務器上的端口8080location / {行簡單地意味著捕獲所有以/開頭的請求,並執行在location區塊中寫入的操作。在這裡了解更多https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/How-to-setup-Nginx-reverse-proxy-servers-by-example

and push it to Amazon ECR.

配置完伺服器後,我們現在需要設置構建管道,它主要包含 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 镜像

    並推送它到 Amazon ECR。

  • 透過 SSH 重啟服務 此步驟將連接到伺服器並一次性重啟整個應用。

此管道將在每次對 deploy/main 分支合併拉取請求時運行。

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

至此,整個系統已經設置到位並連接起來,現在可以編輯並將其應用於您的特定情況。在將來的文章中,我將分享构建應用程序本身以供生產並在 docker file 中運行的過程。

本文試圖描述我設置 VPS 用於自動部署的過程。它說明了如何在伺服器內設置應用程序執行過程以及應用程序的构建過程,每一部分都可以使用其他工具完成,例如,如果您願意,可以用 Treafik 替換 nginx,並將 systemd 服務替换為 supervisor 中的程序等。此過程不涉及備份伺服器或關閉伺服器默認端口的額外內容,這些將在將來的文章中解釋。如果您想將其適應到您的流程,請隨時提問。在另一篇文章中,我將著重於如何設置應用程序以供生產部署使用,這是构建 Docker 映像之前的部分。

我希望您喜歡這次的閱讀。

我在此協助您在公司或團隊中實施這項策略,讓您能專注於核心任務,並在深入挖掘Mastodon的巨大潛力之前節省開支。

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