OSSのノーコード・ローコード開発ツール「プリザンター」のアドベントカレンダー の5日目の記事です。
注意点
本エントリはバックグラウンドジョブの排他制御を手軽に試すことができる検証環境例を示したものです
本番運用を想定したものではありませんのでご注意下さい
記事内容の一部にGitHub Copilotの提案を利用しています
コマンド例は Bash を想定しています。
PowerShell や他のシェルでも同じように実行できますが、ログの確認時の grep などは適宜読み替えてください。
はじめに
2025年11月11日リリースのプリザンター ver.1.4.22.0 には「バックグラウンドジョブの複数インスタンス間の排他制御機能」が追加されました。
プリザンターの直近のバージョンアップ情報 | Pleasanter
今回はこの機能を試してみます。
Docker版の場合はパラメータを変更するためには工夫が必要ですが、以下の手順で設定を行うことができます。
本エントリはこちらの手順に設定を追加していきますので、先に動く状態にしておいてください。
Dockerイメージを使用しパラメータを既定値から変更して起動する | Pleasanter
今回のエントリでやること
本エントリでは、次回の排他制御動作確認に向けた環境構築を行います。
- 複数インスタンスでの動作に必要なパラメータを設定します。
- ロードバランサー(Traefik)を使った負荷分散環境を構築します。
- システムログをGrafanaで可視化します。
次回のエントリでは、この環境を使ってバックグラウンドジョブの排他制御が正しく動作することを確認します。
最終的なファイルの配置
.
├── compose.yml
├── .env # 環境変数ファイル
├── appsettings.json # システムログをJSON形式で出力する設定
├── app_data_parameters/
│ ├── Quartz.json # バックグラウンドサービス排他制御の設定
│ ├── Security.json # ヘルスチェックの設定
│ ├── Service.json # 言語・タイムゾーンの設定
│ └── SysLog.json # システムログの設定
│
├── Pleasanter/
│ └── Dockerfile
│
├── CodeDefiner/
│ └── Dockerfile
│
├── traefik/ # ロードバランサーの設定
│ └── traefik.yml
│
├── fluentd/
│ ├── Dockerfile
│ └── conf/
│ └─ fluent.conf
│
└── grafana/
└── provisioning/
└── datasources/
└─ loki.yml
環境変数 ( .env )
PLEASANTER_VER を 1.4.22.0 に変更します。
Grafana の管理者パスワードの設定が追加となっています。(適宜設定してください)
GF_SECURITY_ADMIN_PASSWORD
プリザンターの設定
- 以降のパラメータの例は該当部分の抜粋です。
- 他の設定はデフォルト値のままです。必要に応じて調整してください。
Quartz.json
排他制御を有効にします。
{
"Clustering": {
"Enabled": true
}
}
詳細は公式ドキュメントを参照してください。
Security.json
複数インスタンスで動作させるため、ヘルスチェックを有効にします。
現状のプリザンターはウォームアップ機能が不十分なため、いつまでも unhealthy のままになる場合があります。
ウォームアップについては後述の「ウォームアップについて」セクションを参照してください。
{
"HealthCheck": {
"Enabled": true,
"EnableDatabaseCheck": true,
}
}
詳細は公式ドキュメントを参照してください。
Service.json
検証環境ですので初期化を簡単にするため、以下のように設定します。
{
"TimeZoneDefault": "Asia/Tokyo",
"DefaultLanguage": "ja"
}
詳細は公式ドキュメントを参照してください。
SysLog.json
テキスト出力を有効にします。
{
"EnableLoggingToFile": true,
"OutputErrorDetails": true
}
詳細は公式ドキュメントを参照してください。
appsettings.json
システムログをJSON形式で出力する設定をします。
昨年 2024年のアドベントカレンダーで Docker版のシステムログをコンテナの外に出す と題してシステムログをテキスト形式で出力する方法を紹介しました。
こちらを流用してください。
Pleasanter/Dockerfile
Pleasanter/Dockerfile は以下のようにします。
ARG VERSION=latest
FROM implem/pleasanter:${VERSION} AS build
RUN apt-get update; \
apt-get install -y --no-install-recommends curl ca-certificates; \
rm -rf /var/lib/apt/lists/*
FROM build AS final
WORKDIR /app
COPY app_data_parameters/ App_Data/Parameters/
COPY appsettings.json .
ENTRYPOINT [ "dotnet", "Implem.Pleasanter.dll" ]
ウォームアップ手段に curl を使います。そのためのインストールも含めます。
CodeDefiner/Dockerfile
変更はありません。
ロードバランサー Traefik
今回はDockerと相性がいいとされる Traefik を使用します。
Traefikは、マイクロサービス環境に最適化された最新のHTTPリバースプロキシおよびロードバランサーです。Dockerコンテナを自動的に検出し、設定ファイルを書き換えることなく動的にルーティングを行うことができます。今回は複数のプリザンターインスタンスへの負荷分散とセッション維持を担います。
# Traefik Static Configuration
# API and Dashboard
api:
dashboard: true
insecure: true # 開発環境用
# EntryPoints
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":44391"
metrics:
address: ":8082"
# Providers
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: pleasanter_network
# Logging
log:
level: INFO
format: json
# Access Logs
accessLog:
format: json
# Metrics (Prometheus統合用)
metrics:
prometheus:
entryPoint: metrics
Fluentd
Fluentdは、オープンソースのデータコレクターで、ログの収集・変換・転送を統一的に行うことができます。様々なデータソースからログを収集し、複数の出力先に振り分けることができるため、集中ログ管理の中核を担います。今回はDockerコンテナから出力されるログを受け取り、Lokiに転送する役割を果たします。
ログの受け取り用のFluentdは、今回は他のコンテナと同一のホストで動かします。
<source>
@type forward
port 24224
bind 0.0.0.0
</source>
# Lokiへの転送
<match **>
@type loki
url http://loki:3100
flush_interval 10s
<label>
container_name $.container_name
source $.source
</label>
</match>
Grafanaで一括参照するので全てをLokiに転送します。
もし、同時にディスクにも保存したい場合は追加の設定が必要です。
Fluentdのドキュメントはこちらを参照してください。
Loki
Lokiは、Grafana Labsが開発したログ集約システムです。Prometheusに触発されたアーキテクチャを持ち、ログをインデックス化せずにラベルのみでメタデータを管理するため、軽量かつ低コストで運用できます。今回はFluentdから送信されたログを保存し、Grafanaからのクエリに応答します。
今回の構成では、Lokiのデフォルト設定を使用してログを保存します。
Grafana
Grafanaは、メトリクスやログを可視化するためのオープンソースのダッシュボードツールです。様々なデータソースに対応しており、リアルタイムでのモニタリングやアラート設定が可能です。今回はLokiに保存されたログを検索・可視化し、システム全体のログを一元的に確認できるようにします。
データソースにLokiを指定します。
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
editable: true
jsonData:
maxLines: 1000
compose.yml
とても長いですが、完成版を示します。
traefik, loki, grafana に関してはGitHub Copilotにお任せしています。
動作確認はしましたが検証目的のため深堀りしていません。
services:
pleasanter:
build:
context: .
dockerfile: ./Pleasanter/Dockerfile
args:
- VERSION=${PLEASANTER_VER}
environment:
Implem.Pleasanter_Rds_SaConnectionString: ${Implem_Pleasanter_Rds_PostgreSQL_SaConnectionString}
Implem.Pleasanter_Rds_OwnerConnectionString: ${Implem_Pleasanter_Rds_PostgreSQL_OwnerConnectionString}
Implem.Pleasanter_Rds_UserConnectionString: ${Implem_Pleasanter_Rds_PostgreSQL_UserConnectionString}
healthcheck:
test:
[
"CMD-SHELL",
'curl -f "http://localhost:8080/healthz" || exit 1',
]
interval: 1m30s
timeout: 10s
retries: 3
restart: unless-stopped
depends_on:
db:
condition: service_healthy
networks:
- default
labels:
- "traefik.enable=true"
- "traefik.http.routers.pleasanter.rule=PathPrefix(`/`)"
- "traefik.http.routers.pleasanter.entrypoints=websecure"
- "traefik.http.services.pleasanter.loadbalancer.server.port=8080"
- "traefik.http.services.pleasanter.loadbalancer.sticky.cookie=true"
- "traefik.http.services.pleasanter.loadbalancer.sticky.cookie.name=pleasanter_session"
- "traefik.http.services.pleasanter.loadbalancer.healthcheck.path=/healthz"
- "traefik.http.services.pleasanter.loadbalancer.healthcheck.interval=90s"
- "traefik.http.services.pleasanter.loadbalancer.healthcheck.timeout=10s"
- "traefik.http.routers.pleasanter.middlewares=pleasanter-headers,pleasanter-buffering"
- "traefik.http.middlewares.pleasanter-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.pleasanter-buffering.buffering.maxrequestbodybytes=104857600"
logging:
driver: "fluentd"
options:
fluentd-address: localhost:24224
tag: "pleasanter.app"
codedefiner:
profiles:
- rds
build:
context: .
dockerfile: ./CodeDefiner/Dockerfile
environment:
Implem.Pleasanter_Rds_SaConnectionString: ${Implem_Pleasanter_Rds_PostgreSQL_SaConnectionString}
Implem.Pleasanter_Rds_OwnerConnectionString: ${Implem_Pleasanter_Rds_PostgreSQL_OwnerConnectionString}
Implem.Pleasanter_Rds_UserConnectionString: ${Implem_Pleasanter_Rds_PostgreSQL_UserConnectionString}
depends_on:
db:
condition: service_healthy
networks:
- default
traefik:
image: traefik:v3.0
container_name: traefik
ports:
- "80:80"
- "44391:44391"
- "8080:8080" # ダッシュボード
- "8082:8082" # メトリクス
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
networks:
- default
depends_on:
- pleasanter
restart: unless-stopped
logging:
driver: "fluentd"
options:
fluentd-address: localhost:24224
tag: "traefik.access"
fluentd:
build:
context: ./fluentd
dockerfile: Dockerfile
container_name: fluentd
ports:
- "24224:24224"
volumes:
- ./fluentd/conf:/fluentd/etc
- ./fluentd/logs:/fluentd/log
networks:
- default
healthcheck:
test:
[
"CMD-SHELL",
'ruby -e ''require "socket"; TCPSocket.new("localhost", 24224).close'' || exit 1',
]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
restart: unless-stopped
loki:
image: grafana/loki:latest
container_name: loki
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
networks:
- default
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD}
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
networks:
- default
depends_on:
- loki
restart: unless-stopped
db:
image: postgres:${POSTGRES_VER:-16}
volumes:
- type: volume
source: pg_data
target: ${PGDATA}
networks:
- default
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-postgres}
POSTGRES_HOST_AUTH_METHOD: ${POSTGRES_HOST_AUTH_METHOD}
POSTGRES_INITDB_ARGS: ${POSTGRES_INITDB_ARGS}
PGDATA: ${PGDATA}
user: postgres
ports:
- "5432:5432"
depends_on:
fluentd:
condition: service_healthy
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U $${POSTGRES_USER:-postgres} -d $${POSTGRES_DB:-postgres} || exit 1",
]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
logging:
driver: "fluentd"
options:
fluentd-address: localhost:24224
tag: "postgres.db"
volumes:
pg_data:
name: ${COMPOSE_PROJECT_NAME:-default}_pg_data
grafana-data:
networks:
default:
name: pleasanter_network
ヘルスチェック
Docker Composeのヘルスチェック機能は、コンテナが正常に動作しているかを定期的に確認し、他のサービスの起動タイミングを制御できます。healthcheckセクションでテストコマンドや間隔を設定することで、依存関係のあるサービスを適切な順序で起動できます。
今回は各サービスにヘルスチェックを設定しました。
サービスの healthcheck セクションで設定しています。
各コンテナの初期化時間がまちまちなので、依存関係をはっきりさせるためにもヘルスチェックを利用しています。
なお、検証目的のため Traefik と Grafana、Loki にはヘルスチェックを設定していません。
本番環境等では適切なヘルスチェックの追加を検討してください。
services.pleasanter
コンテナ内で curl でヘルスチェックを行っています。
プリザンターのヘルスチェックエンドポイントの /healthz に対してリクエストを送っています。このエンドポイントは Security.json で有効化したヘルスチェック機能により提供されます。
services.fluentd
コンテナのベースイメージにRubyランタイムが含まれているため、Rubyのソケット機能を使ってポート24224への接続確認を行っています。
services.traefik
今回の構成ではヘルスチェックを設定していません。
Traefikには /ping エンドポイントが用意されており、ヘルスチェックが必要な場合は以下のような設定を追加できます。
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 10s
timeout: 5s
retries: 3
または、curlを使用する場合:
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/ping"]
interval: 10s
timeout: 5s
retries: 3
services.loki
今回の構成ではヘルスチェックを設定していません。
必要に応じて/readyエンドポイントを使用したヘルスチェックを追加できます。
services.db
PostgreSQLの pg_isready コマンドでヘルスチェックを行っています。このコマンドはPostgreSQLサーバーが接続を受け入れる準備ができているかを確認します。
起動する
イメージのビルドと初回起動時としてデータベースの初期化を行います。
docker compose --profile rds build --pull
docker compose --profile rds run --rm codedefiner _rds
services.codedefiner 以外のコンテナ群を一気に起動します。
docker compose up -d
起動の確認をします。
各インスタンスの状態確認:
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
出力例:
NAMES STATUS PORTS
clustering-docker-pleasanter-1 Up 9 minutes (healthy) 8080/tcp
traefik Up 8 minutes 0.0.0.0:80->80/tcp, [::]:80->80/tcp, 0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp, 0.0.0.0:8082->8082/tcp, [::]:8082->8082/tcp, 0.0.0.0:44391->44391/tcp, [::]:44391->44391/tcp
grafana Up 17 minutes 0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp
clustering-docker-db-1 Up 16 minutes (healthy) 0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp
fluentd Up 17 minutes (healthy) 0.0.0.0:24224->24224/tcp, [::]:24224->24224/tcp
loki Up 17 minutes 0.0.0.0:3100->3100/tcp, [::]:3100->3100/tcp
プリザンターが起動したら、ウォームアップを実行して動作確認を行います。
ウォームアップについて
コンテナを利用した環境においては、IIS の Application Initialization モジュールは使用できません。
暫定ではありますが、コンテナ内で curl コマンドによりログイン画面へ一度アクセスすることでウォームアップを行う方法があります。
docker compose exec --index=1 pleasanter curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/users/login
--index オプションを使って、特定のインスタンスに対してコマンドを実行できます。
ウォームアップが必要なタイミング:
- 初回起動時
- スケールアウトで新しいインスタンスを追加した直後
- コンテナを再起動した後
ウォームアップを実行しないと、ヘルスチェックが startup や unhealthy のままになり、Traefikが該当インスタンスにリクエストを振り分けない可能性があります。
スケールアウト時は、インスタンス数分のウォームアップが必要です。
例えば3台にスケールした場合:
docker compose exec --index=1 pleasanter curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/users/login
docker compose exec --index=2 pleasanter curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/users/login
docker compose exec --index=3 pleasanter curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/users/login
プリザンターにログイン
http://localhost:44391/users/login にアクセスしてログインしてください。
初回パスワードの変更ダイアログが表示されますので、指示に従ってパスワードを変更してください。
まっさらなプリザンターの環境が整いました。
動作確認
複数インスタンスで動作していることを確認する
プリザンターが複数インスタンスで動作しているかを確認します。
現在起動しているインスタンス数を確認:
docker compose ps pleasanter
初回起動時は1台のみ起動しています。Traefikのダッシュボードでも確認できます。
Traefikダッシュボードにアクセス: http://localhost:8080/dashboard/
「HTTP Routers」→「pleasanter@docker」から、プリザンターへのルーティング設定と接続されているインスタンス数を確認できます。
Grafanaでログを確認する
Grafanaにアクセスして、各コンテナのログを確認します。
-
Grafanaにアクセス: http://localhost:3000
-
ログイン情報:
- ユーザー名:
admin - パスワード:
.envファイルで設定したGF_SECURITY_ADMIN_PASSWORD
- ユーザー名:
-
左メニューから「Explore」を選択
-
データソースが「Loki」になっていることを確認
-
ログブラウザーの「Label browser」から確認したいコンテナを選択:
-
{container_name="/clustering-docker-pleasanter-1"}: プリザンターのログ (複数台の場合は-2,-3などのサービス名でフィルタ可能) -
{container_name="/traefik"}: Traefikのアクセスログ -
{container_name="clustering-docker-db-1"}: PostgreSQLのログ
-
-
「Run query」ボタンをクリックしてログを表示
これにより、主要コンテナのログを一元的に参照できます。
また、左メニューの「Drilldown」→「Logs」からもログを一元的に参照できます。
Fluentdコンテナ自身のログは、Fluentdログドライバーを使用していないため、container_name ラベルが設定されず {service_name="unknown_service"} として記録されます。
Fluentdのログを確認する場合は、以下のいずれかの方法を使用してください:
- Dockerコマンド:
docker logs fluentd - Grafanaで
{service_name="unknown_service"}を指定して検索
スケール数を変更する
# 5台に増やす
docker compose up -d --scale pleasanter=5
# 2台に減らす
docker compose up -d --scale pleasanter=2
スケールダウン時は、指定した数を超えるコンテナが自動的に停止・削除されます。
スケール後の確認
スケールアウト後、各インスタンスの状態を確認:
docker compose ps pleasanter
新しく追加されたインスタンスにもウォームアップを実行してください。
Traefikが複数インスタンスに負荷分散していることを確認:
# Traefikのログから負荷分散の様子を確認
docker compose logs traefik | grep pleasanter
複数のインスタンス名が表示されれば、負荷分散が機能しています。
環境の停止
検証を終了する場合は、以下のコマンドで環境を停止できます。
# コンテナを停止
docker compose down
# データベースも含めて完全に削除する場合
docker compose down -v
down に -v オプションを付けると、データベースの内容も 削除 されます。
さいごに
本エントリでは、プリザンターの複数インスタンス環境を構築し、ロードバランサーによる負荷分散とログの一元管理を実現しました。
次回のエントリでは、この環境を使って以下を確認する予定です:
- バックグラウンドジョブの排他制御が正しく動作すること
- 複数インスタンス間でジョブが重複実行されないこと
- Quartz.NETによるクラスタリングの動作
また、本環境は検証目的のため、本番運用には以下の点を追加で検討してください:
- Traefikの認証設定とダッシュボードの保護
- Grafanaのセキュリティ強化(パスワード変更、HTTPS化)
- Loki、Grafanaのヘルスチェック追加
- データベースのバックアップ設定
- ログローテーション設定