VPS上でのプロダクションアプリのデプロイとDocker、GitHub Actions、AWS ECRを用いたプロセスの自動化方法。

この記事では、アプリケーションを本番環境にプッシュすることとは何か、そしてそれを自動化する方法について学びます。そのためにDockerとGitHub Actionsを見ていきます。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をコピーして後で保管しておきます。URIは以下の形式です AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/REPOSITORY_NAME .

また、このリポジトリからプル/プッシュする権限があるAWS IAMクレデンシャルを設定する必要があります。IAMサービスに移動し、新しいユーザーをクリックして、以下のポリシーをアタッチします:AmazonEC2ContainerRegistryFullAccess、AWSコンソールへのアクセスを有効にする必要はありません。このプロセスの最後に、AWSから2つのキーを受け取ります。一つは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 keyおよびsecret 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 CLIを使用してAWSアカウントにログインして、DockerがDockerイメージを取得する際に使用するトークンを取得することです。レジストリはプライベートであることを覚えておき、認証なしではプルすることができないので、忘れないようにしてください。

次にリポジトリリストを宣言し、それを特定の識別子に関連付けます。指定された識別子はコマンドライン引数として使用されます。その詳細は後で説明します。この後、ユーザーが既存のサービス識別子に対応する引数を提供したかどうかを確認します。例えば、ユーザーが./redeploy webと入力すると、スクリプトは第2ステップのリポジトリwebに関連付けるために引数webを関連付けます。

サービス識別子を取得した後、リポジトリURLを動的に生成し、それでDocker pullを実行します。これにより、Dockerイメージが私たちのシステムにダウンロードされることが確認されます。

スクリプトはアプリケーションフォルダー/home/ubuntu/[APP_FOLDER]にcdします。これはすべてユーザー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コンテナが停止しても失われないようにします。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サービスが動作している間は常に動作するようにします。

  • コードの「Unit」セクションでは、私たちのサービスについて説明し、どのサービスの一部として私たちのユニットが属しているかを指定します。この場合、それはdockerサービスです。これにより、dockerサービスが動いている間は、私たちのサービスも常に動作します。

    コードの「Service」セクションでは、どのようにサービスを実行するかを説明します。興味深い部分は「WorkingDirectory」、「ExecStart」、「ExecStop」コマンドです。これらはその名前の意味に従って使用されます。例えば、サービスが「myapp」と名付けられていて、コマンド「systemctl start myapp」を入力すると、「ExecStart」コマンドが実行されます。Linuxサービスについて更多信息を学ぶには、以下のURLを参照してくださいhttps://www.redhat.com/sysadmin/systemd-oneshot-service。Dockerサービスを「systemd」と一緒に実行する方法について更多信息を学ぶには、以下のURLを参照してください: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イメージはsupposedly実行中であり、例えばポート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を通じてアプリサーバーのポート8080に送信するように指示します。行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アプリケーションの場合、Dockerfileは以下のようになります:

# 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

このDockerfileをビルドして実行すると、アプリケーションがポート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 buildおよびdocker pushコマンドを実行してDockerイメージを作成します
  • SSHを通じてサービスを再起動する このステップでは、サーバーに接続し、アプリケーション全体を一度に再起動します。

このパイプラインは、deploy/mainブランチに対してプルリクエストがマージされるたびに実行されます。

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

この時点でシステム全体が整備され、組み合わせられており、特定のケースに編集して適用することができます。今後の記事では、アプリケーション自体をプロダクション用にビルドし、Dockerファイルで実行するプロセスを共有します。

この記事では、デプロイメントに関連する自動化のためのVPSを設定する私のプロセスを説明しようとしています。サーバー内でのアプリケーションの実行プロセスとアプリケーションのビルドプロセスを説明しています。各部分は別のツールで行うこともできます。例えば、nginxをTreafikに変更することや、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