23
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Docker版でバックグラウンドジョブの排他制御環境を構築する

Last updated at Posted at 2025-12-04

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_VER1.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 オプションを使って、特定のインスタンスに対してコマンドを実行できます。

ウォームアップが必要なタイミング:

  • 初回起動時
  • スケールアウトで新しいインスタンスを追加した直後
  • コンテナを再起動した後

ウォームアップを実行しないと、ヘルスチェックが startupunhealthy のままになり、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にアクセスして、各コンテナのログを確認します。

  1. Grafanaにアクセス: http://localhost:3000

  2. ログイン情報:

    • ユーザー名: admin
    • パスワード: .env ファイルで設定した GF_SECURITY_ADMIN_PASSWORD
  3. 左メニューから「Explore」を選択

  4. データソースが「Loki」になっていることを確認

  5. ログブラウザーの「Label browser」から確認したいコンテナを選択:

    • {container_name="/clustering-docker-pleasanter-1"}: プリザンターのログ (複数台の場合は -2, -3 などのサービス名でフィルタ可能)
    • {container_name="/traefik"}: Traefikのアクセスログ
    • {container_name="clustering-docker-db-1"}: PostgreSQLのログ
  6. 「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のヘルスチェック追加
  • データベースのバックアップ設定
  • ログローテーション設定
23
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?