如何在 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。之后,以非 root 用户身份运行它非常重要;以下是官方文档的说法:docs.docker.com/engine/security/rootless

请注意,如果你愿意,你也可以使用另一个仓库达到相同的目标,我这里使用 AWS 是因为它对我来说更简单。

由于我们要使用 Docker,图像将必须存储在某个地方并从中检索。为此,我使用了 AWS ECR(亚马逊网络服务弹性容器仓库)。它是 AWS 账户内的一个 Docker 仓库。它使用起来非常经济且易于设置。你也可以使用 Docker Hub 为你的镜像创建一个私有仓库。一切从在 AWS 账户中创建一个 ECR 私有仓库开始。你将点击 “创建仓库” 并填写仓库名称。

创建仓库后,您可以复制仓库 URI 并保存以供以后使用。它的格式为 AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/REPOSITORY_NAME .

您还需要设置具有从该仓库拉取/推送权限的 AWS IAM 凭据。让我们转到 IAM 服务,点击新建用户并将以下策略附加给他:AmazonEC2ContainerRegistryFullAccess,您不需要为他启用 AWS 控制台的访问权限。在这个过程的最后,您会从 AWS 收到两个密钥,一个 密钥 和一个 密钥 ID,将它们放在一边,我们稍后需要使用。

回到我们的服务器上,我们需要安装AWS CLI。官方安装方法在这里提供。https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions。安装后,你可以通过运行aws --version命令来测试安装。在这个步骤中,你将需要执行aws configure命令,并通过提供aws上生成的先前密钥、密钥密钥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

配置好服务器之后,我们现在需要设置构建管道,它主要包含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重启服务

    通过SSH重启服务

至此,整个系统已经搭建完毕并连接起来,现在可以根据你的具体情况编辑并应用它

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

在未来的文章中,我将分享构建应用程序本身以供生产使用并在Dockerfile中运行的过程

本文尝试描述我用于设置自动化VPS部署的过程。它描述了如何在服务器内部设置应用程序执行的过程以及构建应用程序的过程,每个部分都可以使用其他工具完成,例如,如果你愿意,可以用Treafik替换nginx,也可以用supervisor中的程序替换systemd服务等等。这个过程没有涵盖额外的内容,比如备份服务器或关闭服务器上的默认端口,这些将在未来的文章中解释。如果你想要将这个适应到你的流程中,欢迎提问。在另一篇文章中,我将重点关注如何设置应用程序以便在生产环境中部署,那是构建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