Azure App Service における Redmine の DevOps

## 概要

最近個人的に [Azure App Service](https://azure.microsoft.com/ja-jp/services/app-service/) で Ruby on Rails アプリである [Redmine](https://redmine.org) を稼働させる DevOps プロジェクトに取り組んでおり、いくらか形になってきたので紹介します。

https://github.com/yaegashi/dx2devops-redmine

このプロジェクトで使用する Redmine は、ある程度新しければどのバージョンでも対応できます。先日 v2.1.0 が出たばかりの [RedMica](https://www.redmica.jp) でも大丈夫です。

この記事では、今回 App Service で稼働させた Redmine アプリのコンテナ仕様とデプロイ手順について説明します。上記の GitHub プロジェクトの README.md にも英語で書かれている内容ですが、この記事には Rails アプリの DevOps の設計や実装に関連する内容も含まれていますので、参考にしてください。

## Redmine アプリコンテナ仕様

### DevOps の方針

[App Service on Linux](https://docs.microsoft.com/ja-jp/azure/app-service/overview#app-service-on-linux) で web アプリをホスティングしようとするとき、App Service が提供する各処理系のランタイムスタックを利用するか、コンテナ化したアプリを動かすかという選択肢があります。

App Service で現在利用可能な Linux のランタイムスタックには一応 Ruby も含まれているのですが、2022年6月の時点でバージョンが 2.7.3 と古く、今後継続的にサポートされるのか怪しい状況なので、今回は Redmine アプリをコンテナ化して動かすことにしました。

App Service 向けに web アプリをコンテナ化するにあたり、押さえておきたい App Service の特徴は次の3つです。

- アプリ設定(app settings)がコンテナ内プロセスに環境変数として引き渡される → データベースの接続先などのアプリインスタンスごとの設定のほか、メンテナンスモードのようなアプリ稼働モードを変えるために利用します。
- 読み書き可能な共有ストレージが `/home/site/wwwroot` にマウントされる → 永続化すべきファイルの保存や、テーマやプラグインなどカスタマイズ要素の配置場所として使用します。
- コンテナ内のシェルに SSH 接続でアクセスできる → アプリの初期設定やデータベースのマイグレーションなど、メンテナンス作業に活用します。

特に最後の SSH 接続は非常に便利な機能で、web ブラウザから稼働中のコンテナに接続し、コンテナ内のツールを使ってアプリのメンテナンスやトラブルシューティング作業を行うといったことが手軽にできます。

今回コンテナ化した Redmine アプリではこれを最大限に利用することに決め、 Redmine 運用チームが行う設定やメンテナンスは web ブラウザだけで完結するように構成しています。

なお、利用を見送ったランタイムスタックも実際にはコンテナとして実装されており、そのソースコードは次のリポジトリにあります。

https://github.com/Azure-App-Service/ImageBuilder 

ですので、アプリのコンテナ化の作業ではこれを参考にするとよいでしょう。 Ruby の場合はランタイムスタックコンテナ作成の Dockerfile のテンプレートが [GenerateDockerFiles/ruby フォルダ](https://github.com/Azure-App-Service/ImageBuilder/tree/master/GenerateDockerFiles/ruby)にあります。

コンテナは GitHub Actions により自動ビルドされます。[最新場の Redmine と RedMica を使ったコンテナイメージ](https://github.com/yaegashi?tab=packages&repo_name=dx2devops-redmine)が ghcr.io で公開されていますので、すぐに試すことができます。

### App Settings

Redmine アプリでは次のような app settings (アプリ設定) を使用します。

|Variable|Description|
|---|---|
|`WEBSITES_ENABLE_APP_SERVICE_STORAGE`|App Service 共有ストレージを `/home` にマウントするため常に `true` にします。|
|`RAILS_ENV`|Rails アプリの動作環境。`production` `development` など。|
|`RAILS_IN_SERVICE`|Rails アプリ動作モード。`true` の場合は正常に起動します。 `false` の場合はメンテナンスモードに入ります。|
|`SECRET_KEY_BASE`|Rails アプリのセッションクッキー暗号化に使用するランダムな文字列を指定します。|
|`DATABASE_URL`|Rails アプリのデータベース接続設定。参照: [the guide for configuring a database](https://guides.rubyonrails.org/configuring.html#configuring-a-database)|

### コンテナ内のパス

Redmine アプリコンテナ内で重要なパスの一覧です。

|Path|Description|
|---|---|
|`/redmine/`|Redmine アプリの展開場所|
|`/docker/`|様々なコンテナアプリ運用スクリプト|
|`/home/site/wwwroot/`|永続化ストレージの場所|
|`/home/site/wwwroot/redmine.sqlite3`|Default sqlite3 database in development|
|`/home/site/wwwroot/staticsite/`|メンテナンスモード時の静的サイトのドキュメントルート|
|`/home/site/wwwroot/backups/`|バックアップ|
|`/home/site/wwwroot/files/`|`/redmine/files` からのシンボリックリンク|
|`/home/site/wwwroot/config/*`|`/redmine/config/*` からのシンボリックリンク|
|`/home/site/wwwroot/plugins/*`|`/redmine/plugins/*` からのシンボリックリンク|
|`/home/site/wwwroot/public/themes/*`|`/redmine/public/themes/*` からのシンボリックリンク|
|`/home/site/wwwroot/public/plugin_assets`|`/redmine/public/plugin_assets` からのシンボリックリンク|

### メンテナンスモード

app settings が `RAILS_IN_SERVICE=false` の状態でアプリコンテナが起動すると、メンテナンスモードに入ります。

メンテナンスモードでは Redmine は起動せず、代わりに `/home/site/wwwroot/staticsite/` をドキュメントルートとする静的サイトのサーバが起動しますので、コンテナ内で Redmine のメンテナンス作業を安全に行うことができます。

メンテナンス作業は web ブラウザないし Azure CLI によるコンテナ内シェルの SSH 接続により行います。

作業が終了したら app settings で `RAILS_IN_SERVICE=true` に設定することでアプリコンテナは自動的に再起動し Redmine が通常通り起動します。

### `rmops` コマンド

`rmops` はアプリコンテナ内で利用可能なメンテナンスコマンドです。

データベースの初期化、Redmine アプリのセットアップ、ユーザーパスワードのリセットといったメンテナンス作業を行う様々なサブコマンドが用意されています。

```console
root@6228b40cf3a8:~# rmops
Commands:
  rmops entrypoint      # Container entrypoint
  rmops help [COMMAND]  # Describe available commands or one specific command
  rmops passwd          # Reset user password
  rmops setup           # Set up Redmine instance
  rmops sql             # Generate SQL to initialize database
```

また `rmops entrypoint` はアプリコンテナのエントリポイントとして動作します。

### ファイルの永続化

App Service において共有ストレージは `/home/site/wwwroot` にマウントされますので、永続化したいファイルはこの場所に保存する必要があります。`/home/site/wwwroot` の外にあるファイルの変更は、コンテナが再起動すると失われます。

そのため `/redmine` 内の永続化すべきファイルはその実体が `/home/site/wwwroot` にあるようなシンボリックリンクがアプリコンテナのエントリポイント (`rmops entrypoint`) で作られます。

Redmine サイトのカスタマイズを行うときはこの仕組みを理解しておく必要があります。たとえば Redmine のプラグインは `/home/site/wwwroot/plugins` に、テーマは `/home/site/wwwroot/public/themes` に置いておけば有効化されます。詳細は「コンテナ内のパス」の表を参照してください。

## App Service へのデプロイ

### 手動デプロイの手順

ghcr.io の公開コンテナレジストリから利用可能なコンテナイメージを使って Redmine サイトを App Service にデプロイする手順を示します。

1. まず Redmine アプリのためのデータベースを用意します。 Azure のサービスの場合 Azure Database for MySQL/MariaDB/PostgreSQL がサポートされています。データベース作成権限がある管理者のユーザー名とパスワードをメモしておいてください。
1. App Service のプランおよびアプリを作成してください。種類は Linux のシングルコンテナアプリで、次のコンテナイメージのいずれかを指定してください。
    - `ghcr.io/yaegashi/dx2devops-redmine/redmine` (Redmine)
    - `ghcr.io/yaegashi/dx2devops-redmine/redmica` (RedMica)
1. 次の app settings を設定してください。
    ```
    WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
    RAILS_ENV=production
    RAILS_IN_SERVICE=false
    SECRET_KEY_BASE=<とても長いランダムな文字列>
    DATABASE_URL=<データベース接続設定(後述)>
    ```
1. アプリが起動し、メンテナンスモードに入ります。
1. Azure Portal などを使ってアプリのコンテナに SSH 接続してください。
    - (データベース接続の準備が完了している場合はこの手順は不要です)
      `rmops sql` を実行すると、データベースとそのユーザーを作成するための SQL が出力されます。データベースの種類 (MySQL/PostgreSQL) は `DATABASE_URL` 設定から自動的に判断されます。
        ```console
        root@6228b40cf3a8:~# rmops sql
        -- Database URL: mysql2://test1_7ee9e7cf%40test1-7ee9e7cf:CI8zeEf5r2%24%26%28r%249zQvNSA2%3EYh3lq7%7B7@test1-7ee9e7cf.mariadb.database.azure.com/test1_7ee9e7cf?encoding=utf8mb4&sslverify=true
        -- Run the following command to connect to the database host as an admin user:
        -- mysql -vv -u <admin-user> -p -h test1-7ee9e7cf.mariadb.database.azure.com --ssl
        DROP DATABASE IF EXISTS `test1_7ee9e7cf`;
        DROP USER IF EXISTS 'test1_7ee9e7cf'@'%';
        CREATE USER 'test1_7ee9e7cf'@'%' IDENTIFIED BY 'CI8zeEf5r2$&(r$9zQvNSA2>Yh3lq7{7';
        CREATE DATABASE IF NOT EXISTS `test1_7ee9e7cf` DEFAULT CHARACTER SET `utf8mb4` COLLATE `utf8mb4_unicode_520_ci`;
        GRANT SELECT, LOCK TABLES, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, REFERENCES ON `test1_7ee9e7cf`.* TO 'test1_7ee9e7cf'@'%';
        ```
      この SQL の内容を確認した上で、コメントに書かれている `mysql` `psql` コマンドによりデータベースに接続して実行してください。 `<admin-user>` はデータベース管理者の名前で置き換えてください。
        ```console
        root@6228b40cf3a8:~# rmops sql | mysql -vv -u db_admin@test1-7ee9e7cf -p -h test1-7ee9e7cf.mariadb.database.azure.com --ssl
        Password: ← データベース管理者 (db_admin@test1-7ee9e7cf) のパスワードを入力
        ```
    - `rmops setup` を実行してデータベースのマイグレーションと Redmine の初期セットアップを行ってください。最後に admin ユーザーのパスワードが表示されますので、メモしておいてください。
        ```console
        root@6228b40cf3a8:~# rmops setup
        ...
        I, [2022-06-05T18:52:11.206518 #72]  INFO -- : Enter Redmine at /redmine
        I, [2022-06-05T18:52:31.645480 #72]  INFO -- : Reset password for user "admin"
        I, [2022-06-05T18:52:31.647095 #72]  INFO -- : New password: "tLtLGWFK7VqzZ8TS"
        ```
    - ユーザーのパスワードを忘れてしまったときはいつでも `rmops passwd` でリセットができます。
        ```console
        root@fc06fdbecede:~# rmops passwd admin
        I, [2022-06-14T22:58:39.292157 #306]  INFO -- : Enter Redmine at /redmine
        I, [2022-06-14T22:58:58.152780 #306]  INFO -- : Reset password for user "admin"
        I, [2022-06-14T22:58:58.161336 #306]  INFO -- : New password: "QNFjQkawpuyoBi9F"
        ```
6. app settings で `RAILS_IN_SERVICE=true` を設定してください。
6. アプリが再起動し、Redmine が起動します。
7. Web ブラウザでアプリの web サイトを開き admin ユーザーでログインしてください。

### データベース接続設定 (`DATABASE_URL`)

Redmine のデータベースの接続先は app settings の `DATABASE_URL` で指定します。その記述方法については [Rails ガイド](https://guides.rubyonrails.org/configuring.html#configuring-a-database)を参照してください。

前節で示したように、Redmineサイトごとのデータベース作成・接続ユーザー作成を行う SQL の生成機能 (`rmops sql`) がありますので、 `DATABASE_URL` にはデータベース管理者のユーザーは使用しないことをおすすめします。

Azure Database for MySQL/MariaDB (mysql2 adapter) の例:
```
mysql2://username%40servername:password@servername.mariadb.database.azure.com/dbname?encoding=utf8mb4&sslverify=true
```

Azure Database for PostgreSQL (postgresql adapter) の例:
```
postgresql://username%40servername:password@servername.postgres.database.azure.com/dbname?sslmode=require
```

Azure Database 製品を使用する場合は次の点に注意してください。
- ユーザー名に `username@servername` を使用する (`@` は `%40` にエンコードする)
- `sslverify=true` や `sslmode=require` のような SSL 接続指定を追加する

### Terraform によるデプロイ

App Service での開発・テストを簡単にするために、必要な Azure リソースのデプロイと設定を自動化する Terraform プロジェクトが [terraform/test1](https://github.com/yaegashi/dx2devops-redmine/tree/master/terraform/test1) にありますので参考にしてください。

### サービス運用とメンテナンス

Redmine のサービス運用開始後も随時 SSH 接続を使ってアプリコンテナに接続し、稼働中の Redmine アプリに対して軽微なメンテナンスやトラブルシュートを行うことができます。コンテナではないサーバでサービスを動かす場合とほぼ同じ感覚で作業できると思います。

Redmine アプリからのログは標準出力に出ていますので、Azure Portal のログストリームや `az webapp log tail` コマンドでそれを見ることができます。 [アプリケーションのログ記録](https://docs.microsoft.com/en-us/azure/app-service/troubleshoot-diagnostic-logs#enable-application-logging-linuxcontainer)を設定をしないとログストリームには何も出てきませんので注意してください。

大規模なメンテナンスのために Redmine アプリを止める必要がある場合は、 app settings で `RAILS_IN_SERVICE=false` を設定します。アプリコンテナが再起動してメンテナンスモードに入りますので、SSH 接続して作業してください。作業が完了したら app settings を`RAILS_IN_SERVICE=true` に戻すと、アプリコンテナの再起動の後 Redmine アプリが起動してサービスが再開します。

## ロードマップ

これまでの取り組みの結果 Azure App Service のカスタムコンテナのアプリとしては形になってきましたが、実際の Redmine サービスとして本番運用するにはまだまだ足りない機能があります。今後は次のような機能強化を予定しています。

- Redmineサイトのバックアップ・リストア
- [EasyAuth](https://docs.microsoft.com/en-us/azure/app-service/overview-authentication-authorization) によるサイトアクセス制御と Redmine ユーザー認証との統合
- メール送信サポート: SMTPサーバをコンテナ内で動かし SendGrid などにリレーまたは [ACS email](https://docs.microsoft.com/en-us/azure/communication-services/concepts/email/email-overview) で送信
- ARM テンプレートと Deploy to Azure ボタンの作成
- 運用向け Terraform プロジェクトの作成
- [App Service 可用性ゾーン冗長化](https://docs.microsoft.com/en-us/azure/app-service/how-to-zone-redundancy)の利用

## おわりに

Redmine を Azure App Service で、単に動かすだけでなく運用に耐えうる DevOps プロジェクトの構築について、まだ道半ばではありますがこれまでの取り組みを紹介しました。不明な点の質問や、問題点や改善できる点などのご意見がありましたら、お気軽にご連絡ください。

今後もプロジェクトにアップデートがありしだい紹介記事を書いていきたいと思います。また今回触れなかったアプリコンテナと `rmops` コマンドの実装の詳細やローカル開発環境などのトピックについても順次記事にしますのでよろしくお願いします。