🦁

Mattermost を Amazon Lightsail Container Service に載せる

2022/08/17に公開

前回の記事
https://zenn.dev/fehde/articles/8d530e1118f8a6

これまで Slack の代わりに Mattermost を導入できないか試してきましたが、スマホアプリを使うには HTTPS API 通信が必須のようなので、さらに AWS 環境への導入を試してみます。
Slack から乗り替えるにあたりコストを重視していますので、今回は AWS のサービスで比較的安価で簡単に導入できそうな Lightsail を利用します。
Lightsail には EC2, S3, RDS, Fargate のようなインスタンス、ストレージ、マネージドデータベース、コンテナサービスが用意されているようです。
永続的なデータを保持するには、インスタンスにまとめてインストールするか、ストレージとデータベースを別立てにしてサービス本体はコンテナサービスを利用する等があると思います。

前回までの記事で docker compose の設定は目処がついているので、インスタンスサービスに Docker を導入する方法が一番シンプルだと思いますが、今回はあえてコンテナサービスで動作確認をしてみます。
ストレージとデータベースの永続化は次回にして、まずはデータ非永続化での動作環境を構築します。

構築手順

1. Amazon Lightsai のコンテナ作成

初めの 3 ヶ月無料の Micro プランを選択しました。

コンテナサービスの名称は、container-service-limited-20221031 としました。

コンテナサービスが作成されます。

2. AWS CLI と lightsailctl プラグイン の導入

Lightsail CLI コマンドを実行するため、こちらを参考に AWS CLI と lightsailctl プラグインを導入します。
https://lightsail.aws.amazon.com/ls/docs/ja_jp/articles/amazon-lightsail-install-software

3. CI/CD の設定

コンテナサービスは初めて使いますが、どうやら docker compose には対応していないようで、ローカルで作成した docker image をアップロードする形で構築するようです。
docker-compose.yml で設定していた項目を Docker file に記載する必要がありそうですね。
今回は GitHub Actions を利用して git push からデプロイまでの一連の作業を実行できるようにしました。

1. GitHub の Secrets 設定

GitHub Actions から AWS サービスにアクセスできるよう、GitHub Repository の Secrets に AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY を設定します。

2. Dockerfile の作成

docker compose ではコンテナ間通信はサブネットが組まれますが、Lightsail container service では同じサービスに共存するコンテナ間はローカルホスト 127.0.0.1 接続となるようです。
前回作成した docker-compose.yml の設定では pgAdmin と nginx のポートが被るため、今回は次の三つの Docker image で構成します。

  • postgres:14
  • nginx:latest
  • mattermost/mattermost-enterprise-edition:7.1.2
    このうち、postgres と nginx は Dockerfile で構成します。
./postgresql/Dockerfile
ARG POSTGRES_IMAGE_TAG
FROM postgres:${POSTGRES_IMAGE_TAG}
ARG DB_LANG=en_US  # docker-compose.ymlから上書き
RUN localedef -i $DB_LANG -c -f UTF-8 -A /usr/share/locale/locale.alias $DB_LANG.UTF-8  # ロケール追加
ENV LANG $DB_LANG.utf8
EXPOSE 5432/tcp
# 初期スクリプトを追加
ADD ./postgresql/init-script/* /docker-entrypoint-initdb.d
./nginx/Dockerfile
ARG NGINX_IMAGE_TAG
FROM nginx:${NGINX_IMAGE_TAG}
EXPOSE 80/tcp
# 設定ファイルを追加
ADD ./nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
# 検証時
# CMD ["nginx-debug", "-g", "daemon off;"]

3. コンテナ起動コマンド作成

json 形式でコンテナを起動するコマンドテンプレートを出力します。

% aws lightsail create-container-service-deployment --generate-cli-skeleton
{
    "serviceName": "",
    "containers": {
        "KeyName": {
            "image": "",
            "command": [
                ""
            ],
            "environment": {
                "KeyName": ""
            },
            "ports": {
                "KeyName": "TCP"
            }
        }
    },
    "publicEndpoint": {
        "containerName": "",
        "containerPort": 0,
        "healthCheck": {
            "healthyThreshold": 0,
            "unhealthyThreshold": 0,
            "timeoutSeconds": 0,
            "intervalSeconds": 0,
            "path": "",
            "successCodes": ""
        }
    }
}

出力を元にコマンドファイルを作成します。

./container.template.json
{
    "serviceName": "",
    "containers": {
        "db": {
            "image": "",
            "command": [
            ],
            "environment": {
                "DB_LANG": "ja_JP",
                "POSTGRES_USER": "",
                "POSTGRES_PASSWORD": "",
                "POSTGRES_DB": ""
            },
            "ports": {
                "5432": "TCP"
            }
        },
        "mattermost": {
            "image": "",
            "command": [
            ],
            "environment": {
                "TZ": "",
                "MM_SQLSETTINGS_DRIVERNAME": "",
                "MM_SQLSETTINGS_DATASOURCE": "",
                "MM_SERVICESETTINGS_SITEURL": "",
                "MM_SERVICESETTINGS_ENABLEAPICHANNELDELETION": "",
                "MM_SERVICESETTINGS_ENABLETUTORIAL": "",
                "MM_LOCALIZATIONSETTINGS_DEFAULTSERVERLOCALE": "",
                "MM_LOCALIZATIONSETTINGS_DEFAULTCLIENTLOCALE": "",
                "MM_TEAMSETTINGS_MAXUSERSPERTEAM": "",
                "MM_TEAMSETTINGS_MAXCHANNELSPERTEAM": "",
                "MM_PLUGINSETTINGS_ENABLE": "",
                "MM_PLUGINSETTINGS_ENABLEUPLOADS": "",
                "MM_BLEVESETTINGS_INDEXDIR": "",
                "MM_BLEVESETTINGS_ENABLEINDEXING": "",
                "MM_BLEVESETTINGS_ENABLESEARCHING": "",
                "MM_BLEVESETTINGS_ENABLEAUTOCOMPLETE": ""
            },
            "ports": {
                "8065": "HTTP"
            }
        },
        "nginx": {
            "image": "",
            "command": [
            ],
            "environment": {
            },
            "ports": {
                "80": "HTTP"
            }
        }
    },
    "publicEndpoint": {
        "containerName": "nginx",
        "containerPort": 80,
        "healthCheck": {
            "healthyThreshold": 0,
            "unhealthyThreshold": 0,
            "timeoutSeconds": 30,
            "intervalSeconds": 60,
            "path": "",
            "successCodes": ""
        }
    }
}

4. GitHub Actions の設定

GitHub に push したら Docker build が走り、作成された Docker image が Lightsail Containers にデプロイされるよう Workflow を設定します。

./.github/workflows/deploy.yml
name: Build and deploy Docker app to Lightsail
on:
  push:
    branches:
      - main
env:
  AWS_REGION: ap-northeast-1
  AWS_LIGHTSAIL_SERVICE_NAME: container-service-limited-20221031
jobs:
  deploy:
    name: Build and deploy
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Install Utilities
        run: |
          sudo apt-get update
          sudo apt-get install -y jq unzip
      - name: Install AWS Client and LightsailControl Plugin
        run: |
          curl https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o awscliv2.zip
          unzip awscliv2.zip
          sudo ./aws/install || true
          aws --version
          curl https://s3.us-west-2.amazonaws.com/lightsailctl/latest/linux-amd64/lightsailctl -o lightsailctl
          sudo mv lightsailctl /usr/local/bin/lightsailctl
          sudo chmod +x /usr/local/bin/lightsailctl
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ${{env.AWS_REGION}}
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      - name: Pull and Build Docker Image
        run: |
          docker pull mattermost/mattermost-enterprise-edition:7.1.2
          docker compose build
          docker image ls
      - name: Push Image
        run: |
          service_name=${{env.AWS_LIGHTSAIL_SERVICE_NAME}}
          aws lightsail push-container-image \
            --region ${{env.AWS_REGION}} \
            --service-name ${service_name} \
            --label mmapp-db \
            --image mmapp-db
          aws lightsail push-container-image \
            --region ${{env.AWS_REGION}} \
            --service-name ${service_name} \
            --label mattermost \
            --image mattermost/mattermost-enterprise-edition:7.1.2
          aws lightsail push-container-image \
            --region ${{env.AWS_REGION}} \
            --service-name ${service_name} \
            --label mmapp-nginx \
            --image mmapp-nginx
      - name: Make deploy setting
        run: |
          export $(cat .env | grep -v '^#' | xargs)
          service_name=${{env.AWS_LIGHTSAIL_SERVICE_NAME}}
          aws lightsail get-container-images --service-name ${service_name} | jq --raw-output '[.containerImages[] | select(.image | contains("mmapp-db")) | .image][0]' > db_image.txt
          aws lightsail get-container-images --service-name ${service_name} | jq --raw-output '[.containerImages[] | select(.image | contains("mattermost")) | .image][0]' > mm_image.txt
          aws lightsail get-container-images --service-name ${service_name} | jq --raw-output '[.containerImages[] | select(.image | contains("mmapp-nginx")) | .image][0]' > nginx_image.txt
          cat container.template.json \
          | jq --arg service_name ${service_name} '.serviceName=$service_name' \
          | jq --arg db_image $(cat db_image.txt) '.containers.db.image=$db_image' \
          | jq --arg mm_image $(cat mm_image.txt) '.containers.mattermost.image=$mm_image' \
          | jq --arg nginx_image $(cat nginx_image.txt) '.containers.nginx.image=$nginx_image' \
          | jq --arg POSTGRES_USER $POSTGRES_USER '.containers.db.environment.POSTGRES_USER=$POSTGRES_USER' \
          | jq --arg POSTGRES_PASSWORD $POSTGRES_PASSWORD '.containers.db.environment.POSTGRES_PASSWORD=$POSTGRES_PASSWORD' \
          | jq --arg POSTGRES_DB $POSTGRES_DB '.containers.db.environment.POSTGRES_DB=$POSTGRES_DB' \
          | jq --arg TZ $TZ '.containers.mattermost.environment.TZ=$TZ' \
          | jq --arg MM_SQLSETTINGS_DRIVERNAME $MM_SQLSETTINGS_DRIVERNAME '.containers.mattermost.environment.MM_SQLSETTINGS_DRIVERNAME=$MM_SQLSETTINGS_DRIVERNAME' \
          | jq --arg POSTGRES_USER $POSTGRES_USER --arg POSTGRES_PASSWORD_ENCODED $POSTGRES_PASSWORD_ENCODED --arg POSTGRES_DB $POSTGRES_DB '.containers.mattermost.environment.MM_SQLSETTINGS_DATASOURCE="postgres://'$POSTGRES_USER':'$POSTGRES_PASSWORD_ENCODED'@localhost:5432/'$POSTGRES_DB'?sslmode=disable&connect_timeout=10"' \
          | jq --arg MM_SERVICESETTINGS_SITEURL $MM_SERVICESETTINGS_SITEURL '.containers.mattermost.environment.MM_SERVICESETTINGS_SITEURL=$MM_SERVICESETTINGS_SITEURL' \
          | jq --arg MM_SERVICESETTINGS_ENABLEAPICHANNELDELETION $MM_SERVICESETTINGS_ENABLEAPICHANNELDELETION '.containers.mattermost.environment.MM_SERVICESETTINGS_ENABLEAPICHANNELDELETION=$MM_SERVICESETTINGS_ENABLEAPICHANNELDELETION' \
          | jq --arg MM_SERVICESETTINGS_ENABLETUTORIAL $MM_SERVICESETTINGS_ENABLETUTORIAL '.containers.mattermost.environment.MM_SERVICESETTINGS_ENABLETUTORIAL=$MM_SERVICESETTINGS_ENABLETUTORIAL' \
          | jq --arg MM_LOCALIZATIONSETTINGS_DEFAULTSERVERLOCALE $MM_LOCALIZATIONSETTINGS_DEFAULTSERVERLOCALE '.containers.mattermost.environment.MM_LOCALIZATIONSETTINGS_DEFAULTSERVERLOCALE=$MM_LOCALIZATIONSETTINGS_DEFAULTSERVERLOCALE' \
          | jq --arg MM_LOCALIZATIONSETTINGS_DEFAULTCLIENTLOCALE $MM_LOCALIZATIONSETTINGS_DEFAULTCLIENTLOCALE '.containers.mattermost.environment.MM_LOCALIZATIONSETTINGS_DEFAULTCLIENTLOCALE=$MM_LOCALIZATIONSETTINGS_DEFAULTCLIENTLOCALE' \
          | jq --arg MM_TEAMSETTINGS_MAXUSERSPERTEAM $MM_TEAMSETTINGS_MAXUSERSPERTEAM '.containers.mattermost.environment.MM_TEAMSETTINGS_MAXUSERSPERTEAM=$MM_TEAMSETTINGS_MAXUSERSPERTEAM' \
          | jq --arg MM_TEAMSETTINGS_MAXCHANNELSPERTEAM $MM_TEAMSETTINGS_MAXCHANNELSPERTEAM '.containers.mattermost.environment.MM_TEAMSETTINGS_MAXCHANNELSPERTEAM=$MM_TEAMSETTINGS_MAXCHANNELSPERTEAM' \
          | jq --arg MM_PLUGINSETTINGS_ENABLE $MM_PLUGINSETTINGS_ENABLE '.containers.mattermost.environment.MM_PLUGINSETTINGS_ENABLE=$MM_PLUGINSETTINGS_ENABLE' \
          | jq --arg MM_PLUGINSETTINGS_ENABLEUPLOADS $MM_PLUGINSETTINGS_ENABLEUPLOADS '.containers.mattermost.environment.MM_PLUGINSETTINGS_ENABLEUPLOADS=$MM_PLUGINSETTINGS_ENABLEUPLOADS' \
          | jq --arg MM_BLEVESETTINGS_INDEXDIR $MM_BLEVESETTINGS_INDEXDIR '.containers.mattermost.environment.MM_BLEVESETTINGS_INDEXDIR=$MM_BLEVESETTINGS_INDEXDIR' \
          | jq --arg MM_BLEVESETTINGS_ENABLEINDEXING $MM_BLEVESETTINGS_ENABLEINDEXING '.containers.mattermost.environment.MM_BLEVESETTINGS_ENABLEINDEXING=$MM_BLEVESETTINGS_ENABLEINDEXING' \
          | jq --arg MM_BLEVESETTINGS_ENABLESEARCHING $MM_BLEVESETTINGS_ENABLESEARCHING '.containers.mattermost.environment.MM_BLEVESETTINGS_ENABLESEARCHING=$MM_BLEVESETTINGS_ENABLESEARCHING' \
          | jq --arg MM_BLEVESETTINGS_ENABLEAUTOCOMPLETE $MM_BLEVESETTINGS_ENABLEAUTOCOMPLETE '.containers.mattermost.environment.MM_BLEVESETTINGS_ENABLEAUTOCOMPLETE=$MM_BLEVESETTINGS_ENABLEAUTOCOMPLETE' \
          > container.json
          cat container.json
      - name: Deploy
        run: |
          aws lightsail create-container-service-deployment --service-name ${{env.AWS_LIGHTSAIL_SERVICE_NAME}} --cli-input-json file://$(pwd)/container.json

ちょっと環境変数を足しすぎましたね。上手くデプロイできなかったりして、ここでだいぶ詰まりました。ポイントは、.env で設定した環境変数を export $(cat .env | grep -v '^#' | xargs) で読込み、jq で コマンドテンプレート container.template.json に設定して出力していることです。

5. push & プロビジョニング

これらファイルを git push すると workflow が働き、Lightsail デプロイが実行され、Lightsail container に アクセスすると Mattermost の初期画面に繋がります。

まとめ

GitHub Actions と Lightsail の動作仕様の把握に手間取りましたが、一度構成ができればワークフローで git push するだけでデプロイ出来るのはとても便利ですね。Lightsail では SSL も自動的に適用されるため、Let's Encrypt などを組み込む必要もありませんでした。

コンテナサービスはステートレスで動作するためデータは保持されないため、次回はさらなる本稼働に向けてデータ永続化などを試行してみます。

手間取って疲れたので、この辺で!
詳しくはソースファイルをご覧ください。
https://github.com/Fehde/mattermost-aws

参考 (感謝です!)

https://note.varu3.me/n/nb9d691f9b307
https://zenn.dev/devneko/articles/196b9befb48b41798071
https://zenn.dev/junkato/books/how-to-deploy-research-web-apps/viewer/amazon-lightsail-containers

Discussion