
こんにちは。クラウド事業部の遠見です。
- 本記事は、さくらのクラウドの「モニタリングスイート」を検証したエンジニアが内容をできる限り客観的に共有することを目的に、生成AIを活用して作成したものです。
- 本記事内の見解は執筆者個人のものであり、所属組織を代表するものではありません。
前回の記事では、OpenTelemetry(OTel)デモアプリを使ってモニタリングスイートのアラート運用を検証しました。
今回は、公共システムを想定した自作アプリを題材に、オブザーバビリティの信頼性とPII(Personally Identifiable Information、個人を特定できる情報)対策を検証しました。
目次
検証日時
2026年7月1日
検証環境の構成・概要
題材は、災害時に避難所で使われる「避難者名簿管理システム」です。
氏名・要配慮事項といった機密性の高い個人情報(PII)を扱うシステムを想定しています。
| 分類 | 項目 | 内容 / バージョン | 備考 |
|---|---|---|---|
| インフラ | 実行環境 | さくらのクラウド(石狩第3ゾーン) | - |
| 構成管理 | Terraform (Ubuntu 24.04 / sacloud v3.12) | サーバー・DB・NWをコード化 | |
| アプリ | フレームワーク | FastAPI(避難者登録API) | SQLAlchemy + psycopg2 |
| データベース | データベースアプライアンス(PostgreSQL) | - | |
| 監視基盤 | 計装方式 | OTelによるゼロコード自動計装 | opentelemetry-instrument を使用 |
| 送信・集約 | sacloud-otel-collector |
さくら公式 OTel Collector | |
| バックエンド | モニタリングスイート | ログ / メトリクス / トレース |
構成図

mermaid
graph TD
subgraph zone["さくらのクラウド(石狩第3ゾーン)"]
app["アプリサーバ<br/>hinanjo-app (FastAPI)"]
db["DBアプライアンス<br/>PostgreSQL<br/>(氏名・要配慮事項)"]
app -- "プライベートvSwitch<br/>source_ranges で<br/>アプリIPのみ許可" --> db
end
monitor["モニタリングスイート<br/>ログ/メトリクス/トレース<br/>(グローバルリソース)"]
zone -- "OTLP/HTTP<br/>テレメトリのみ<br/>個人情報(PII)なし" --> monitor
詳しい構築手順(Terraformコード、実行コマンド)などは「【補足】環境構築の詳細」にまとめています。
検証内容
本編では、この環境を使って行った以下2つの検証に絞って進めます。
| 検証項目 | 目的・検証内容 |
|---|---|
| トレース分離 | 1リクエスト=1トレースという単位が、自動計装で正しく守られるか |
| PII対策 | 氏名・要配慮事項のようなPIIを、テレメトリに含めない設計にできるか |
【検証】トレース分離
自動計装の仕組み
opentelemetry-bootstrap がインストール済みのライブラリ(FastAPI、SQLAlchemy等)を自動検出し、対応する計装パッケージをインストールします(scripts/04_app.shに記載)。
opentelemetry-bootstrap -a install
アプリのsystemdサービスは、通常のFastAPIアプリのように uvicorn を直接起動するのではなく、opentelemetry-instrument 経由で起動しています。
実際にはscripts/04_app.shが生成するsystemdサービスファイル内で、Python仮想環境のフルパスを指定した形で実行されますが、概念としては以下と同じです。
opentelemetry-instrument uvicorn main:app --host 0.0.0.0 --port 8000
このコマンドがやっていることは、「Pythonプロセスの起動時に、計装パッケージが各ライブラリにモンキーパッチを当てる」というものです。
モンキーパッチとは、ライブラリのコード自体は変更せず、実行中に処理を差し替える手法です。
これにより、FastAPIやSQLAlchemyの内部処理に対して、スパンの開始・終了・属性付与といった処理が自動的に差し込まれます。
アプリコードは変更しなくていい理由がここにあります。
「ゼロコード自動計装」とは、アプリ開発者はトレースのことを一切気にせず、いつも通りのコードを書くだけで、起動方法を変えるだけでトレースが自動的に取れる、という仕組みのことです。
実際、main.py を見ても tracer.start_as_current_span(...) のような、自分でスパンを作るコードは書いていません。
(opentelemetry.metrics は使っていますが、opentelemetry.trace は使っていません)
すべて自動計装パッケージが、裏側で勝手に生成しているものです。
1リクエストで生成されるスパンを見る
避難者登録エンドポイントは、シンプルなFastAPIのPOSTハンドラです。
@app.post("/evacuees") def register_evacuee(payload: EvacueeRegistration): evacuee_id = str(uuid.uuid4()) with engine.begin() as conn: conn.execute( evacuees_table.insert().values( id=evacuee_id, name=payload.name, needs_support=payload.needs_support, note=payload.note, checked_in_at=time.time(), ) ) _refresh_occupancy_gauges(conn) checkin_counter.add(1) logger.info( f"evacuee registered: id={evacuee_id}, needs_support={payload.needs_support}" ) return {"evacuee_id": evacuee_id}
処理の流れは、
① IDを発番
② DBに登録(トランザクション内)
③ 同じトランザクション内で収容人数を再集計
④ チェックインカウンターを更新
⑤ ログ出力(氏名は含めない)
⑥ IDを返す
というシンプルなものです。
これに対して1件だけ登録してみます。
curl -X POST http://<サーバーIP>:8000/evacuees \
-H "Content-Type: application/json" \
-d '{"name": "単体検証", "needs_support": false}'

モニタリングスイートのLogs画面で見ると、氏名などのPIIがログに含まれていないことが確認できます。

Traces画面で「Span: Root」に切り替えると、1回のリクエストが1行(1トレース)として記録されていることが確認できます。

※一番上のGET /evacueesは、登録が終わった後、ブラウザで一覧画面を確認した際のリクエストです。
該当のTrace IDをクリックすると、1つのトレースの中に10個のスパンがツリー構造で記録されていることが分かります。
(このリクエスト単体のクリック後キャプチャは撮り忘れましたが、構造は次項の複数検証時と同一です)
POST /evacuees ←ルートスパン ├─ POST /evacuees http receive ├─ connect │ └─ SELECT ├─ INSERT hinanjo_db ├─ INSERT ├─ SELECT hinanjo_db ├─ SELECT ├─ POST /evacuees http send └─ POST /evacuees http send
これらのスパンには、http.method、http.route、http.status_code、db.system、db.name といった属性も自動的に付与されています。
すべて自動計装パッケージが裏側で生成しているものです。
生成元の内訳は以下の通りです。
| スパン名 | 生成元パッケージ | フック先 |
|---|---|---|
POST /evacuees、GET /evacuees |
opentelemetry-instrumentation-asgi(FastAPI経由) | ASGIミドルウェア(HTTPリクエストの入口) |
POST /evacuees http receive、http send |
opentelemetry-instrumentation-asgi | ASGIのreceive/sendコールバックのラップ |
connect |
opentelemetry-instrumentation-sqlalchemy | Connection Poolのcheckoutイベント |
SELECT hinanjo_db、INSERT hinanjo_db(db.name付き) |
opentelemetry-instrumentation-sqlalchemy | before_cursor_execute / after_cursor_execute イベント |
SELECT、INSERT(db.nameなし) |
opentelemetry-instrumentation-psycopg2 | ドライバレベルのカーソル実行イベント |
同じ種類のクエリに対して2種類のスパン(db.name付き/なし)が生成される理由は、後述のコラムで説明します。
また、属性の実例(db.statement等)は、後述のPII対策の検証で詳しく見ていきます。
複数回リクエストしても正しく分離されるか
自動計装が「1リクエスト=1トレース」という単位を正しく守っているかは、オブザーバビリティの信頼性そのものに関わる重要な点です。
間隔を空けて5回連続でリクエストを送り、それぞれが独立したトレースとして記録されるかを確認しました。
for i in 1 2 3 4 5; do
curl -s -X POST http://<サーバーIP>:8000/evacuees \
-H "Content-Type: application/json" \
-d "{\"name\":\"複数検証$i\",\"needs_support\": false}"
echo ""
sleep 10
done

Traces画面をRoot表示にすると、5回分の POST /evacuees がそれぞれ別のTrace IDで、時系列に沿って1行ずつ記録されていることが確認できます。

5件のうち1件を確認すると、単体検証と同じ10スパン構成が正しく再現されていました。

OTEL_PYTHON_LOG_CORRELATION=true(scripts/04_app.shのコードで設定)により、アプリが出力するログにも自動的に trace_id と span_id が付与されます。
これにより、あるAPIリクエストで何が起きたかをログとトレースの両面から追えます。
ログで異常に気づいたとき、trace_id でトレース画面に飛んでDBクエリの実行時間まで確認できる、という運用が成立します。
避難所の収容人数・要配慮者数といったビジネスメトリクスは、自動計装ではなくアプリ側で明示的に実装しています。
自動計装では「リクエストの通信経路」は見えますが、「今何人いるか」というドメイン固有の情報までは取得できないためです。
meter = metrics.get_meter("hinanjo-app") occupancy_gauge = meter.create_gauge("hinanjo_occupancy_count") vulnerable_gauge = meter.create_gauge("hinanjo_vulnerable_occupant_count") checkin_counter = meter.create_counter("hinanjo_checkin_total")
5回分の登録テストに対して、メトリクスグラフ(hinanjo_occupancy_count)が階段状に増えていく様子も確認できました。
自動計装(通信の可視化)と手動で作ったビジネスメトリクス(業務の可視化)を組み合わせる、という使い分けは意図通りに機能しています。

コラム:同じクエリなのにスパンが2種類ある理由
先ほどのスパン一覧をよく見ると、SELECT hinanjo_db と SELECT、INSERT hinanjo_db と INSERT のように、同じ種類の操作に対してスパンが2つずつ記録されていることに気づきます。
これは requirements.txt に psycopg2-binary を含めていることが原因です。
opentelemetry-bootstrap -a install を実行すると、インストール済みのライブラリを検出して対応する計装パッケージを自動導入しますが、このとき以下の2つが両方インストールされます。
opentelemetry-instrumentation-sqlalchemy(ORMレベルの計装)→SELECT hinanjo_dbを生成opentelemetry-instrumentation-psycopg2(ドライバレベルの計装)→SELECTを生成
アプリのコードは、以下のようなレイヤー構造でデータベースにアクセスしています。
コード(register_evacuee関数) ↓ SQLAlchemy ← ここでも計装される(① SELECT hinanjo_db) ↓ psycopg2 ← ここでも計装される(② SELECT) ↓ PostgreSQL
SQLAlchemyは内部で必ずpsycopg2を呼び出しています。
つまり「コードが1回クエリを指示する」→「その指示がSQLAlchemy層を通る際に①として記録される」→「同じ指示がpsycopg2層も通る際に②としてもう一度記録される」という、同じ1つの処理が、通り道の2箇所で別々に計装されてしまっている状態です。
これはOTel Python計装の既知の挙動で、open-telemetry/opentelemetry-python-contrib#2826 で報告されています。
If a database client app uses SQLAlchemy (ORM) and mysql-connector (“mysql”)(db driver) to query a MySQL database, and both components are instrumented with opentelemetry-instrumentation-sqlalchemy and opentelemetry-instrumentation-mysql (respectively), the instrumentors produce SELECT spans that are “siblings” instead of parent (sqlalchemy) and child (mysql).
Issue #2826では、SQLAlchemy(ORM)とmysql-connector(ドライバ)を組み合わせた場合に、クエリスパンが本来の親子関係ではなく兄弟関係になってしまうと報告されています。
今回の検証(SQLAlchemy+psycopg2)とはドライバの種類やスパンの細かな親子構造は異なりますが、「ORMとドライバの両方を計装すると重複が生じる」という根本原因は共通しており、メンテナーのコメントでもDBクライアントに限らない一般的な現象として言及されています。
Similar ‘duplicates’ are not unique to db clients, as we also see this with e.g. urllib3 and requests, or FastAPI with ASGI.
なお、この重複を親子関係に統合する対応については、2024年8月時点でOTel開発チーム内での議論があったものの、2026年7月時点でもIssueはOpenのまま、実装に着手された形跡はなく、未解決の状態が続いています。
もしスパンの重複が気になる場合は、OTEL_PYTHON_DISABLED_INSTRUMENTATIONS=psycopg2 のような環境変数でドライバレベルの計装を無効化し、SQLAlchemy計装だけに絞る対処が可能です。
これはメンテナー自身が回避策として提示している方法でもあります。
Users currently have the option to only instrument what they need in-code, or set OTEL_PYTHON_DISABLED_INSTRUMENTATIONS with auto-instrumentation.
今回はゼロコード自動計装の挙動をそのまま見せる目的もあり、あえて標準構成のままにしています。
【検証】PII対策
このシステムの最大の検証ポイントです。
氏名・要配慮事項のようなPIIを、ログ・トレースに誤って出力してしまわないか、意図的に「やってはいけない実装」も試しながら確認しました。
検証を進める中で、PIIがテレメトリに乗るルートは、対策の効き方が異なる2種類に分かれることが分かりました。
1つは「ログの本文(body)に直接書き込んでしまう」ケース(検証A)、もう1つは「自動計装が属性(attributes)に記録してしまう」ケース(検証B)です。
この違いを、実機で再現しながら確認します。
検証A:ログ本文にPIIを書いてしまうと、Collectorでは防げない
まず、わざと「やってはいけない実装」に書き換えます。
# NGな実装例(main.pyのregister_evacuee内、検証用にわざと書き換える) logger.info(f"evacuee registered: name={payload.name}")
main.pyは直接実行されるファイルではなく、systemdサービスとして動いているアプリの一部なので、変更を反映するにはアプリを再起動します。
sudo systemctl restart hinanjo-app sudo systemctl status hinanjo-app
反映できたら、改めてリクエストを送ります。
curl -X POST http://<サーバーIP>:8000/evacuees \
-H "Content-Type: application/json" \
-d '{"name": "PII漏えい検証", "needs_support": false}'

この状態でログストレージを確認すると、text_payload に氏名がそのまま記録されていました。
evacuee registered: name=PII漏えい検証

Collector側で何らかのフィルタを入れていても、これは防げません。
ログの本文は自由記述のテキストであり、Collectorからは「ただの文字列」としか見えないため、属性のように特定のキーを基準に削除する、という対策が原理的に成立しないからです。
検証後、この実装は元に戻しました。
検証B:DBクエリの値は自動計装の仕様で乗らず、SQL文の構造はマスキングで消せる
次に、自動計装が記録する「属性」側を確認します。
SQLAlchemy/psycopg2の自動計装は、DBクエリをトレースのスパン属性(db.statement 等)に記録します。
これがマスキング無しの状態でどう見えるか、sacloud-otel-collector の transform プロセッサ(OpenTelemetry Transformation Language、OTTL)を一時的に外して確認しました。
# otelcol/config.yml(検証用に一時変更) service: pipelines: ... traces: receivers: [otlp] processors: [] # ← ここを一時的に空にする(元は [transform]) exporters: [sacloud]
otelcol/config.ymlはリポジトリ内のファイルなので、サーバー上の実際の設定ファイル(/etc/sacloud-otel-collector/config.yaml)にコピーしてから、Collectorを再起動する必要があります。
sudo cp otelcol/config.yml /etc/sacloud-otel-collector/config.yaml sudo systemctl restart sacloud-otel-collector sudo systemctl status sacloud-otel-collector
反映できたら、改めてリクエストを送ります。
curl -X POST http://<サーバーIP>:8000/evacuees \
-H "Content-Type: application/json" \
-d '{"name": "クエリ確認検証", "needs_support": false}'

トレースストレージで INSERT hinanjo_db スパンの属性を確認すると、以下の7項目が記録されていました。

db.name: "hinanjo_db" db.operation: "INSERT hinanjo_db" db.statement: "INSERT INTO evacuees (id, name, needs_support, note, checked_in_at) VALUES (%(id)s, %(name)s, %(needs_support)s, %(note)s, %(checked_in_at)s)" db.system: "postgresql" db.user: "hinanjo_user" net.peer.name: "192.168.0.11" net.peer.port: 5432
db.statement の中身はSQL文そのものでしたが、値の部分は %(name)s のようなプレースホルダのままで、バインドパラメータの生の値自体は直接埋め込まれていませんでした。
これは偶然ではなく、実装として意図された挙動です。
opentelemetry-instrumentation-psycopg2 のソースコードを確認すると、バインドパラメータの値まで記録するかどうかを制御する capture_parameters オプションが存在し、デフォルトは False になっています。
# opentelemetry-instrumentation-psycopg2 (0.64b0) __init__.py より capture_parameters = kwargs.get("capture_parameters", False)
(参考:opentelemetry-instrumentation-psycopg2のソースコード)
つまり「デフォルト設定では値が埋め込まれない」というのは実装上の仕様であり、明示的に capture_parameters=True を指定しない限り、db.statement にバインドパラメータの生の値が乗ることはありません。
とはいえ、SQL文の構造自体が外部に漏れることはリスクであり、対策が必要です。
transform プロセッサを元に戻します(otelcol/config.ymlのコード)。
ここで、プロセッサのコードを見てみます。
processors: transform: error_mode: ignore ... trace_statements: - context: span statements: - keep_keys(attributes, ["service.name", "http.method", "http.route", "http.status_code", "db.system", "db.name"])
氏名や要配慮事項のような自由記述のPIIは、正規表現での値マッチだけでは漏れが出やすいため、keep_keys() による許可リスト方式を採用しています。
許可リストに明記していない属性キーは、内容を問わずすべて削除されるため、想定していなかった属性経由のPII漏えいにも対応できます。
設定を反映します。
sudo cp otelcol/config.yml /etc/sacloud-otel-collector/config.yaml sudo systemctl restart sacloud-otel-collector
反映できたら、改めてリクエストを送ります。
curl -X POST http://<サーバーIP>:8000/evacuees \
-H "Content-Type: application/json" \
-d '{"name": "マスキング確認検証", "needs_support": false}'

同じ種類のスパン(INSERT hinanjo_db)で、マスキングのON/OFFを前後比較した結果は以下の通りです。
| 状態 | Span Attributes |
|---|---|
transform プロセッサ無効時 |
db.name / db.operation / db.statement / db.system / db.user / net.peer.name / net.peer.port(7項目、SQL文を含む) |
transform プロセッサ有効時 |
db.name / db.system(2項目のみ) |
許可リストに明記していない属性(db.statement 含む)が、有効時には確実に削除されることを実機で確認できました。
検証結果まとめ
| 検証 | 漏えいルート | Collectorでの対策 | 結果 |
|---|---|---|---|
| A | ログ本文への直書き | 効かない | 氏名がそのままログに記録される |
| B-1 | バインドパラメータの値 | 計装側のデフォルト仕様(capture_parameters=False) |
明示設定しない限り値は記録されない |
| B-2 | SQL文の構造(db.statement)・接続情報等 |
keep_keys()による許可リスト |
マスキング有効時は対象外の属性が削除される |
注目すべき点は、Collector側のマスキングが及ぶ範囲は「属性」に限られるという点です。
検証Aで示したログ本文への漏えいは、Collector側のどのプロセッサを使っても防げません。
Collector側でのマスキングは属性経由の漏えいに対する補助的な防波堤であり、本来はアプリ側でPIIをログ・トレースの本文に出さない実装にすることが前提になります。
なお、ログストレージへの送信は取り消せない一方向の操作です。
今回は検証用の架空の名前だったため実害はありませんでしたが、実運用でこの現象が起きた場合、「ログに出てしまったPIIは、後から拾って消すことは容易ではない」という前提に立ち、本文にPIIを書かない実装を最初から徹底することが、確実な対策だと言えます。
まとめ
避難所システムを題材に、オブザーバビリティの信頼性とPII対策を実機検証しました。
- トレース分離
- ゼロコード自動計装でも、1リクエスト=1トレースという単位が正しく守られることを確認しました。
- ただしSQLAlchemyとpsycopg2の両方が計装されることで、同じクエリに対しスパンが2重に記録される、という実装上の癖もありました。
- PII対策
- ログ本文への直書きはCollector側で防げませんが、自動計装が記録する属性は
keep_keys()による許可リスト方式のマスキングで確実に防げることを、実機での前後比較で確認しました。
- ログ本文への直書きはCollector側で防げませんが、自動計装が記録する属性は
リソースの削除
cd terraform terraform destroy
上記でサーバー(ディスク・SSH鍵登録含む)・データベースアプライアンス・プライベートvSwitch・パケットフィルタは削除できます。
以下はTerraform管理外のため、手動での削除が必要です。
- オブジェクトストレージのバケット(
your-tfstate-bucket-name)、そのアクセスキー - モニタリングスイートの各ストレージ(
hinanjo-logs/hinanjo-metrics/hinanjo-traces)、各アクセスキー - GitHub Codespaces Secrets(
SAKURA_ACCESS_TOKEN/SAKURA_ACCESS_TOKEN_SECRET/AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY) - GitHub Codespace本体
モニタリングスイートの各ストレージは「リソースを保持している時間」に対して課金される仕様のため、データ送信を止めるだけでなく、ストレージ自体の削除が必要です。
任意(必須ではない)として、GitHub Personal Access Tokenのrevoke、さくらのクラウドAPIキー(アクセストークン)の無効化も推奨します。
【補足】環境構築の詳細
今回の検証環境(Terraform構築手順・事前準備)の詳細です。
実行環境は、GitHub Codespacesを利用しています。
コードや実行コマンドは項目ごとに折りたたんでいるので、必要な部分だけクリックして展開してください。
ディレクトリ構成(treeコマンドの実行結果)
sakura-hinanjo-demo/ ├─ terraform/ │ ├─ provider.tf │ ├─ variables.tf │ ├─ network.tf │ ├─ database.tf │ ├─ server.tf │ ├─ outputs.tf │ └─ terraform.tfvars.example ├─ app/ │ ├─ main.py │ └─ requirements.txt ├─ otelcol/ │ └─ config.yml ├─ .env.example ├─ scripts/ │ ├─ setup.sh │ ├─ lib/ │ │ └─ common.sh │ ├─ 01_network.sh │ ├─ 02_database.sh │ ├─ 03_otelcol.sh │ └─ 04_app.sh ├─ .devcontainer/ │ └─ devcontainer.json ├─ .gitignore └─ README.md
事前準備
| 取得するもの | 用途 |
|---|---|
| さくらのクラウドAPIキー(アクセストークン/シークレット) | Terraformでのリソース作成 |
オブジェクトストレージのアクセスキー+バケット(your-tfstate-bucket-name) |
Terraform tfstateの保存先(S3互換バックエンド) |
| モニタリングスイートの各ストレージ(ログ/メトリクス/トレース)+アクセスキー | テレメトリの送信先 |
SSH鍵ペア(ssh-keygen -t ed25519) |
サーバーへのログイン |
| GitHub Personal Access Token | プライベートリポジトリのclone |
これらはGitHub Codespaces Secrets(SAKURA_ACCESS_TOKEN / SAKURA_ACCESS_TOKEN_SECRET / AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY)と、サーバー上の .env に分けて設定しています。
ネットワーク設計
今回のネットワーク設計のポイントは、「機密データを持つDBアプライアンスを、アプリサーバ以外から到達不可能にする」ことです。
- アプリサーバとデータベースアプライアンスは、プライベートvSwitchで接続する
- データベースアプライアンス側は
source_rangesで、アプリサーバのプライベートIPからのみ接続を許可する - アプリサーバの共有グローバル側NICには、パケットフィルタで自分のグローバルIPからのSSH・APIアクセスのみを許可する
データベースアプライアンスへの接続は同一ゾーン内のリソース同士でのみ可能なため、アプリサーバとデータベースアプライアンスは必ず同じゾーン(今回は石狩第3ゾーン)に作成しています。
サーバー側は、共有グローバル用とプライベートvSwitch用の2枚のNICを持つ構成です。
terraform/database.tf
resource "sakura_database" "hinanjo_db" { name = "hinanjo-db" database_type = "postgres" plan = "10g" username = var.db_user password_wo = var.db_password password_wo_version = 1 network_interface = { vswitch_id = sakura_vswitch.hinanjo_switch.id ip_address = var.db_ip_address netmask = 24 gateway = cidrhost(var.switch_network_prefix, 1) port = 5432 # アプリサーバのプライベートIPからのみ接続を許可 source_ranges = ["${var.app_server_ip_address}/32"] } }
【補足】
sakura_databaseには初期DB名を指定するパラメータがないため、アプリ用データベースの作成は、後述のセットアップスクリプト(setup.sh)側で行っています。
terraform/server.tf
resource "sakura_disk" "hinanjo_disk" { name = "${var.server_name}-disk" source_archive_id = data.sakura_archive.ubuntu.id plan = "ssd" size = 40 } resource "sakura_ssh_key" "hinanjo_key" { name = "hinanjo-key" public_key = var.ssh_public_key } resource "sakura_server" "hinanjo_server" { name = var.server_name core = 2 memory = 4 disks = [sakura_disk.hinanjo_disk.id] network_interface = [ { # NIC1: 共有グローバルセグメント(SSH・ブラウザからのアクセス用、自分のIPのみ許可) # IPアドレスはさくらのクラウド側のDHCPで自動付与される。 upstream = "shared" packet_filter_id = sakura_packet_filter.filter.id }, { # NIC2: プライベートvSwitch(DBアプライアンスとの通信専用) # user_ip_address は表示用ラベルであり、OS側のNIC設定には反映されない # このNICへIPを設定する処理はsetup.sh内でnetplanを使って行う upstream = sakura_vswitch.hinanjo_switch.id user_ip_address = var.app_server_ip_address }, ] disk_edit_parameter = { disable_pw_auth = true ssh_key_ids = [sakura_ssh_key.hinanjo_key.id] } }
【補足】
sakura_serverのuser_ip_addressは、コンソール上の表示用ラベルでしかなく、OSのNIC設定には反映されません。
実際にIPを設定する処理は、後述のセットアップスクリプト(setup.sh)内でnetplanを使って行っています。
Terraformコード
上記のdatabase.tf・server.tfに加え、以下も含めて構築しています。
terraform/provider.tf
terraform { required_version = ">= 1.11" required_providers { sakura = { source = "sacloud/sakura" version = "~> 3.12" } } backend "s3" { bucket = "your-tfstate-bucket-name" # 作成したバケット名に書き換え key = "hinanjo/terraform.tfstate" region = "jp-north-1" endpoints = { s3 = "https://s3.isk01.sakurastorage.jp" } skip_credentials_validation = true skip_region_validation = true skip_requesting_account_id = true skip_metadata_api_check = true use_path_style = true } } provider "sakura" { zone = "is1c" # 石狩第3ゾーン } data "sakura_archive" "ubuntu" { os_type = "ubuntu2404" }
terraform/variables.tf
variable "server_name" { default = "hinanjo-server" } variable "my_ip" { description = "自分のグローバルIPアドレス" type = string sensitive = true } variable "ssh_public_key" { description = "サーバーにログインするためのSSH公開鍵" type = string } # --- データベース関連 --- variable "db_user" { description = "データベースアプライアンスのユーザー名" type = string default = "hinanjo_user" } variable "db_password" { type = string sensitive = true } variable "db_name" { description = "アプリが使用するPostgreSQLデータベース名" type = string default = "hinanjo_db" } # --- プライベートスイッチ(vSwitch)のネットワーク設定 --- variable "switch_network_prefix" { description = "アプリサーバ-DB間のプライベートネットワーク(例: 192.168.0.0/24)" type = string default = "192.168.0.0/24" } variable "db_ip_address" { description = "データベースアプライアンスのプライベートIP" type = string default = "192.168.0.11" } variable "app_server_ip_address" { description = "アプリサーバのプライベートIP(vSwitch側NIC)" type = string default = "192.168.0.10" }
terraform/network.tf
resource "sakura_vswitch" "hinanjo_switch" { name = "hinanjo-switch" } resource "sakura_packet_filter" "filter" { name = "hinanjo-filter" } resource "sakura_packet_filter_rules" "filter_rules" { packet_filter_id = sakura_packet_filter.filter.id expression = [ { protocol = "tcp" destination_port = "22" source_network = var.my_ip allow = true description = "Allow SSH from my IP only" }, { protocol = "tcp" destination_port = "8000" source_network = var.my_ip allow = true description = "Allow app access from my IP only" }, { protocol = "tcp" destination_port = "32768-61000" allow = true description = "Allow outbound return packets" }, { protocol = "udp" destination_port = "32768-61000" allow = true description = "Allow outbound return packets UDP" }, { protocol = "ip" allow = false description = "Deny ALL" }, ] }
terraform/outputs.tf
output "hinanjo_app_url" { value = "http://${sakura_server.hinanjo_server.ip_address}:8000" } output "hinanjo_db_private_ip" { value = sakura_database.hinanjo_db.network_interface.ip_address }
terraform/terraform.tfvars.example
my_ip = "your_global_ip" # /32を付けない ssh_public_key = "ssh-ed25519 AAAA..." # `cat ~/.ssh/id_ed25519.pub` などで取得した公開鍵の内容 db_password = "your_secure_password"
terraform apply の実行
実行コマンド
cd terraform cp terraform.tfvars.example terraform.tfvars vi terraform.tfvars # my_ip / ssh_public_key / db_password を入力 terraform init terraform plan terraform apply
完了すると、以下のように7リソースが作成され、サーバーIP・DBのプライベートIPが出力されました。
Outputs: hinanjo_app_url = "http://xxx.xxx.xxx.xxx:8000" hinanjo_db_private_ip = "192.168.0.11"
アプリの起動
構築したサーバー内で実行します。
実行コマンド
git clone https://github.com/<GitHubユーザー名>/sakura-hinanjo-demo.git cd sakura-hinanjo-demo cp .env.example .env vi .env # DBパスワード・モニタリングスイートのエンドポイント/トークンを入力 sudo bash scripts/setup.sh
setup.sh は以下4つのスクリプトを順番に呼ぶだけのオーケストレーターです。
- プライベートvSwitch側NICへのIP設定(netplan)
- データベースアプライアンスへの到達確認+アプリ用データベースの作成
sacloud-otel-collectorのインストール・設定- Python仮想環境のセットアップ・アプリのsystemd化
セットアップ完了後、アプリとCollectorの両方が active (running) であることを確認します。
実行コマンド
sudo systemctl status hinanjo-app sudo systemctl status sacloud-otel-collector
アプリ・Collectorのコード
app/main.py
import logging import os import time import uuid from datetime import datetime from html import escape from fastapi import FastAPI, HTTPException from fastapi.responses import HTMLResponse from opentelemetry import metrics from pydantic import BaseModel from sqlalchemy import Boolean, Column, Float, MetaData, String, Table, create_engine from sqlalchemy.exc import OperationalError app = FastAPI() logger = logging.getLogger("hinanjo") logger.setLevel(logging.INFO) meter = metrics.get_meter("hinanjo-app") occupancy_gauge = meter.create_gauge("hinanjo_occupancy_count") vulnerable_gauge = meter.create_gauge("hinanjo_vulnerable_occupant_count") checkin_counter = meter.create_counter("hinanjo_checkin_total") DB_USER = os.environ["DB_USER"] DB_PASSWORD = os.environ["DB_PASSWORD"] DB_HOST = os.environ["DB_HOST"] DB_NAME = os.environ["DB_NAME"] DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:5432/{DB_NAME}" # pool_pre_ping: コネクションプール内の接続を使い回さないようにする engine = create_engine(DATABASE_URL, pool_pre_ping=True) metadata = MetaData() evacuees_table = Table( "evacuees", metadata, Column("id", String, primary_key=True), Column("name", String), Column("needs_support", Boolean), Column("note", String, nullable=True), Column("checked_in_at", Float), ) # アプリ起動直後はDB側の準備が整っていないことがあるため、リトライする for attempt in range(1, 11): try: metadata.create_all(engine) logger.info("database schema is ready") break except OperationalError as e: logger.warning( f"database not ready yet (attempt{attempt}/10):{e.__class__.__name__}" ) if attempt == 10: raise time.sleep(5) class EvacueeRegistration(BaseModel): name: str needs_support: bool = False note: str | None = None def _refresh_occupancy_gauges(conn) -> tuple[int, int]: rows = conn.execute(evacuees_table.select()).fetchall() occupancy_count = len(rows) vulnerable_count = sum(1 for r in rows if r.needs_support) occupancy_gauge.set(occupancy_count) vulnerable_gauge.set(vulnerable_count) return occupancy_count, vulnerable_count @app.post("/evacuees") def register_evacuee(payload: EvacueeRegistration): evacuee_id = str(uuid.uuid4()) with engine.begin() as conn: conn.execute( evacuees_table.insert().values( id=evacuee_id, name=payload.name, needs_support=payload.needs_support, note=payload.note, checked_in_at=time.time(), ) ) _refresh_occupancy_gauges(conn) checkin_counter.add(1) # 氏名(payload.name)はログに出さない。 # 検証Aでは、この行をわざと name=payload.name を含む形に書き換えて、PII漏えいのケースを再現する。 logger.info( f"evacuee registered: id={evacuee_id}, needs_support={payload.needs_support}" ) return {"evacuee_id": evacuee_id} @app.post("/evacuees/{evacuee_id}/checkout") def checkout_evacuee(evacuee_id: str): with engine.begin() as conn: result = conn.execute( evacuees_table.delete().where(evacuees_table.c.id == evacuee_id) ) if result.rowcount == 0: logger.error(f"invalid checkout request: id={evacuee_id}") raise HTTPException(status_code=404, detail="evacuee not found") _refresh_occupancy_gauges(conn) logger.info(f"evacuee checked out: id={evacuee_id}") return {"evacuee_id": evacuee_id} @app.get("/shelter/status") def shelter_status(): with engine.connect() as conn: occupancy_count, vulnerable_count = _refresh_occupancy_gauges(conn) return { "occupancy_count": occupancy_count, "vulnerable_occupant_count": vulnerable_count, } @app.get("/evacuees", response_class=HTMLResponse) def list_evacuees(): # 簡易的な一覧画面 with engine.connect() as conn: rows = conn.execute( evacuees_table.select().order_by(evacuees_table.c.checked_in_at) ).fetchall() rows_html = "".join( "<tr>" f"<td>{escape(r.name)}</td>" f"<td>{'要配慮' if r.needs_support else ''}</td>" f"<td>{escape(r.note or '')}</td>" f"<td>{datetime.fromtimestamp(r.checked_in_at).strftime('%Y-%m-%d %H:%M:%S')}</td>" "</tr>" for r in rows ) html = f"""<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>避難者名簿</title> <style> body{{ font-family: sans-serif; margin: 2rem;}} table{{ border-collapse: collapse; width: 100%;}} th, td{{ border: 1px solid #ccc; padding: 0.5rem; text-align: left;}} th{{ background: #f0f0f0;}} </style> </head> <body> <h1>避難者名簿({len(rows)}名)</h1> <table> <thead><tr><th>氏名</th><th>要配慮</th><th>メモ</th><th>受付時刻</th></tr></thead> <tbody> {rows_html} </tbody> </table> </body> </html>""" return HTMLResponse(content=html) @app.get("/health") def health(): return {"status": "ok"}
app/requirements.txt
# バージョンは検証時点(2026年7月1日)の最新安定版です。 # 実際の構築時はpypi.orgで最新版を再確認してください。 fastapi==0.138.2 uvicorn==0.49.0 opentelemetry-distro==0.64b0 opentelemetry-exporter-otlp==1.43.0 sqlalchemy==2.0.51 psycopg2-binary==2.9.12
otelcol/config.yml
# さくら公式のOTel Collectorカスタムビルド(sacloud-otel-collector)を使用 # https://github.com/sacloud/sacloud-otel-collector # receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: transform: error_mode: ignore log_statements: - context: log statements: - keep_keys(attributes, ["service.name", "http.method", "http.route", "http.status_code", "db.system", "db.name"]) trace_statements: - context: span statements: - keep_keys(attributes, ["service.name", "http.method", "http.route", "http.status_code", "db.system", "db.name"]) exporters: sacloud: # HTTPリクエストのタイムアウト・リトライはsacloud-otel-collectorの # デフォルト値をそのまま明記(READMEのデフォルト値と同じ)。 timeout: 30s retry_on_failure: enabled: true initial_interval: 5s max_interval: 30s max_elapsed_time: 5m logs: endpoint: ${env:MONITORING_LOGS_ENDPOINT} token: ${env:MONITORING_LOGS_TOKEN} metrics: endpoint: ${env:MONITORING_METRICS_ENDPOINT} token: ${env:MONITORING_METRICS_TOKEN} traces: endpoint: ${env:MONITORING_TRACES_ENDPOINT} token: ${env:MONITORING_TRACES_TOKEN} service: pipelines: logs: receivers: [otlp] processors: [transform] exporters: [sacloud] metrics: receivers: [otlp] exporters: [sacloud] traces: receivers: [otlp] processors: [transform] exporters: [sacloud]
env.example
# --- データベース(Terraformで作成したデータベースアプライアンスの値) --- DB_USER=hinanjo_user DB_PASSWORD= DB_HOST=192.168.0.11 DB_NAME=hinanjo_db # --- アプリサーバ自身のプライベートvSwitch側NIC設定 --- APP_PRIVATE_IP=192.168.0.10 PRIVATE_NETWORK_PREFIX=192.168.0.0/24 # --- モニタリングスイート(手動作成したストレージのOTLP/HTTPエンドポイント・アクセスキー) --- MONITORING_LOGS_ENDPOINT= MONITORING_LOGS_TOKEN= MONITORING_METRICS_ENDPOINT= MONITORING_METRICS_TOKEN= MONITORING_TRACES_ENDPOINT= MONITORING_TRACES_TOKEN=
セットアップスクリプト
scripts/setup.sh
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
bash "${SCRIPT_DIR}/01_network.sh"
bash "${SCRIPT_DIR}/02_database.sh"
bash "${SCRIPT_DIR}/03_otelcol.sh"
bash "${SCRIPT_DIR}/04_app.sh"
echo ""
echo "=== セットアップ完了 ==="
echo "アプリの状態確認: sudo systemctl status hinanjo-app"
echo "アプリログ確認: sudo journalctl -u hinanjo-app -f"
echo "Collectorの状態確認: sudo systemctl status sacloud-otel-collector"
echo "Collectorログ確認: sudo journalctl -u sacloud-otel-collector -f"
scripts/lib/common.sh
#!/bin/bash
# 共通処理。各スクリプトの先頭で `source` して使う(直接実行はしない)
# - REPO_DIR の算出とそこへの cd
# - .env の読み込み
# - 必須変数のチェック
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${REPO_DIR}"
if [ ! -f ./.env ]; then
echo "Error: .env が見つかりません(リポジトリのルートディレクトリで実行してください)。.env.example を参考に作成してください。" >&2
exit 1
fi
set -a
source ./.env
set +a
required_vars=(DB_USER DB_PASSWORD DB_HOST DB_NAME APP_PRIVATE_IP PRIVATE_NETWORK_PREFIX MONITORING_LOGS_ENDPOINT MONITORING_LOGS_TOKEN MONITORING_METRICS_ENDPOINT MONITORING_METRICS_TOKEN MONITORING_TRACES_ENDPOINT MONITORING_TRACES_TOKEN)
for v in "${required_vars[@]}"; do
if [ -z "${!v:-}" ]; then
echo "Error:${v} が .env に設定されていません。" >&2
exit 1
fi
done
scripts/01_network.sh
#!/bin/bash
# プライベートvSwitch側NIC(NIC2)にIPを設定する。
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
echo "=== プライベートvSwitch側NICの検出・設定 ==="
PRIMARY_NIC=$(ip -o -4 route show default | awk '{print $5}' | head -n1)
PRIVATE_NIC=""
for iface in $(ls /sys/class/net | grep -v '^lo$' | grep -v "^${PRIMARY_NIC}$"); do
if [ -e "/sys/class/net/${iface}/device" ]; then
PRIVATE_NIC="${iface}"
break
fi
done
if [ -z "${PRIVATE_NIC}" ]; then
echo "Error: プライベートvSwitch側のNICが見つかりません。'ip a'で確認してください。" >&2
exit 1
fi
echo "共有グローバル側NIC:${PRIMARY_NIC} / プライベートvSwitch側NIC:${PRIVATE_NIC}"
PREFIX_LEN="${PRIVATE_NETWORK_PREFIX##*/}"
if ip -4 addr show "${PRIVATE_NIC}" | grep -q "${APP_PRIVATE_IP}/"; then
echo "プライベートNIC(${PRIVATE_NIC})には既に${APP_PRIVATE_IP} が設定済みです。スキップします。"
else
cat <<NETPLANEOF > /etc/netplan/90-hinanjo-private.yaml
network:
version: 2
ethernets:
${PRIVATE_NIC}:
dhcp4: false
addresses:
-${APP_PRIVATE_IP}/${PREFIX_LEN}
NETPLANEOF
chmod 600 /etc/netplan/90-hinanjo-private.yaml
netplan apply
echo "プライベートNIC(${PRIVATE_NIC})に${APP_PRIVATE_IP}/${PREFIX_LEN} を設定しました。"
fi
scripts/02_database.sh
#!/bin/bash
# データベースアプライアンスへの到達確認と、アプリ用データベースの作成。
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
echo "=== データベースアプライアンスへの到達確認 ==="
DB_READY=0
for i in $(seq 1 30); do
if (exec 3<>"/dev/tcp/${DB_HOST}/5432") 2>/dev/null; then
exec 3>&- 3<&- || true
DB_READY=1
break
fi
echo "DB(${DB_HOST}:5432)への到達待ち... (${i}/30)"
sleep 5
done
if [ "${DB_READY}" -ne 1 ]; then
echo "Error: DB(${DB_HOST}:5432)に到達できません。プライベートvSwitch/パケットフィルタ/source_rangesの設定を確認してください。" >&2
exit 1
fi
echo "DBへの到達を確認しました。"
DEBIAN_FRONTEND=noninteractive apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq postgresql-client
echo "--- アプリ用データベース(${DB_NAME})の作成(未作成の場合のみ) ---"
DB_EXISTS=$(PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -U "${DB_USER}" -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" || echo "")
if [ "${DB_EXISTS}" = "1" ]; then
echo "データベース${DB_NAME} は既に存在します。スキップします。"
else
PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -U "${DB_USER}" -d postgres -c "CREATE DATABASE${DB_NAME};"
echo "データベース${DB_NAME} を作成しました。"
fi
scripts/03_otelcol.sh
#!/bin/bash
# sacloud-otel-collectorを.debパッケージでインストールし、設定を反映する。
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
echo "=== sacloud-otel-collectorのセットアップ ==="
SACLOUD_OTELCOL_VERSION="0.7.0"
if ! command -v sacloud-otel-collector >/dev/null 2>&1; then
TMP_DEB="$(mktemp --suffix=.deb)"
curl -fsSL -o "${TMP_DEB}" \
"https://github.com/sacloud/sacloud-otel-collector/releases/download/v${SACLOUD_OTELCOL_VERSION}/sacloud-otel-collector_${SACLOUD_OTELCOL_VERSION}_linux_amd64.deb"
dpkg -i "${TMP_DEB}"
rm -f "${TMP_DEB}"
else
echo "sacloud-otel-collectorは既にインストールされています。スキップします。"
fi
install -o sacloud-otelcol -g sacloud-otelcol -m 0640 "${REPO_DIR}/otelcol/config.yml" /etc/sacloud-otel-collector/config.yaml
cat <<ENVEOF > /etc/default/sacloud-otel-collector
OTELCOL_OPTIONS="--config=/etc/sacloud-otel-collector/config.yaml"
MONITORING_LOGS_ENDPOINT=${MONITORING_LOGS_ENDPOINT}
MONITORING_LOGS_TOKEN=${MONITORING_LOGS_TOKEN}
MONITORING_METRICS_ENDPOINT=${MONITORING_METRICS_ENDPOINT}
MONITORING_METRICS_TOKEN=${MONITORING_METRICS_TOKEN}
MONITORING_TRACES_ENDPOINT=${MONITORING_TRACES_ENDPOINT}
MONITORING_TRACES_TOKEN=${MONITORING_TRACES_TOKEN}
ENVEOF
chmod 600 /etc/default/sacloud-otel-collector
systemctl daemon-reload
systemctl enable --now sacloud-otel-collector
systemctl restart sacloud-otel-collector
scripts/04_app.sh
#!/bin/bash
# アプリ本体をPython仮想環境にセットアップし、systemdサービス化する。
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
echo "=== アプリのセットアップ(systemdサービス) ==="
DEBIAN_FRONTEND=noninteractive apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-venv python3-pip
VENV_DIR="${REPO_DIR}/app/.venv"
python3 -m venv "${VENV_DIR}"
"${VENV_DIR}/bin/pip" install --no-cache-dir --upgrade pip -q
"${VENV_DIR}/bin/pip" install --no-cache-dir -r "${REPO_DIR}/app/requirements.txt" -q
"${VENV_DIR}/bin/opentelemetry-bootstrap" -a install
cat <<SERVICEEOF > /etc/systemd/system/hinanjo-app.service
[Unit]
Description=hinanjo FastAPI app
After=network-online.target sacloud-otel-collector.service
Wants=network-online.target
Requires=sacloud-otel-collector.service
[Service]
Type=simple
WorkingDirectory=${REPO_DIR}/app
EnvironmentFile=${REPO_DIR}/.env
Environment=OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
Environment=OTEL_SERVICE_NAME=hinanjo-app
Environment=OTEL_LOGS_EXPORTER=otlp
Environment=OTEL_PYTHON_LOG_CORRELATION=true
Environment=OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true
ExecStart=${VENV_DIR}/bin/opentelemetry-instrument --service_name hinanjo-app ${VENV_DIR}/bin/uvicorn main:app --host 0.0.0.0 --port 8000
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
SERVICEEOF
systemctl daemon-reload
systemctl enable --now hinanjo-app
systemctl restart hinanjo-app
その他のファイル
.gitignore
# Terraform terraform/.terraform/ terraform/terraform.tfvars *.tfstate *.tfstate.* *.tfplan override.tf override.tf.json *_override.tf *_override.tf.json crash.log crash.*.log # 環境変数(機密情報を含む) .env # Python(setup.shがサーバー上に作るPython仮想環境。ローカルで試す場合も同様) __pycache__/ *.pyc .venv/ venv/ # SSH鍵(リポジトリ内で誤って生成してしまった場合の保険。公開鍵は実害ないが念のため) id_ed25519* id_rsa* *.pem # OS / エディタ .DS_Store *.swp
README.md
# sakura-hinanjo-demo さくらのクラウド上に、避難者名簿管理システム(FastAPI + PostgreSQL)をTerraformで構築し、 モニタリングスイートでオブザーバビリティを検証するためのリポジトリです。 サーバー1台構成のため、Dockerは使わずアプリ・Collectorともsystemdサービスとして直接動かします。 DBはさくらのクラウドのデータベースアプライアンス(別リソース)に分離しています。 ## 構成 \`\`\` terraform/ さくらのクラウドのインフラ定義(サーバー・DBアプライアンス・ネットワーク) app/ アプリ本体(FastAPI。setup.shがPython仮想環境を作りsystemdサービス化する) otelcol/ sacloud-otel-collectorの設定(setup.shが.debパッケージとして導入する) scripts/ ├─ setup.sh … オーケストレーター(以下4本を順番に呼ぶだけ) ├─ lib/common.sh … 共通処理(.env読み込み・必須変数チェック) ├─ 01_network.sh … プライベートvSwitch側NICの検出・netplan設定 ├─ 02_database.sh … データベースアプライアンスへの到達確認・DB作成 ├─ 03_otelcol.sh … sacloud-otel-collectorの.debインストール・設定 └─ 04_app.sh … Python仮想環境のセットアップ・アプリのsystemd化 \`\`\` 各スクリプトは単体でも実行できます(例: DB周りだけやり直したい場合は`sudo bash scripts/02_database.sh`)。 03→04の順序には意味があり、アプリ起動時にOTLPエクスポート先(Collector)が既に立っている状態にするためです。 ## 使い方 ### 1. インフラを構築する \`\`\`bash cd terraform cp terraform.tfvars.example terraform.tfvars vi terraform.tfvars # 値を埋める terraform init terraform apply \`\`\` ### 2. サーバーにアプリをセットアップする プライベートリポジトリのため、`git clone`時にGitHubのユーザー名とPersonal Access Token(パスワード欄)での認証が必要です。 \`\`\`bash ssh ubuntu@<terraform outputのssh_command参照> git clone https://github.com/<GitHubユーザー名>/sakura-hinanjo-demo.git cd sakura-hinanjo-demo cp .env.example .env vi .env # 値を埋める sudo bash scripts/setup.sh \`\`\` ### 3. 動作確認 \`\`\`bash sudo systemctl status hinanjo-app sudo systemctl status sacloud-otel-collector \`\`\` \`\`\`bash curl -X POST http://<サーバーのIP>:8000/evacuees \\ -H "Content-Type: application/json" \\ -d '{"name": "テスト避難者", "needs_support": false}' \`\`\` 登録結果はブラウザで`http://<サーバーのIP>:8000/evacuees`を開くと一覧で確認できます(簡易的なHTML画面です)。 アプリのログは`sudo journalctl -u hinanjo-app -f`で確認できます。 ## ライセンス・注意事項 検証目的のサンプル実装です。要配慮個人情報を扱う想定のシステムですが、 TLS化など本番運用に必要な対応は本リポジトリのスコープ外です。
参考リンク
- モニタリングスイート | さくらのクラウド マニュアル
- アプライアンス「データベース」 | さくらのクラウド マニュアル
- Terraform for さくらのクラウド | さくらのクラウド マニュアル
- 公開鍵認証 | さくらのクラウド マニュアル
- 政府機関、自治体等のためのさくらのクラウド
- 【さくらのクラウド】モニタリングスイートでアラート運用を検証してみた
- 【さくらのクラウド】モニタリングスイートのオブザーバビリティ機能を試してみた
- GitHub - sacloud/sacloud-otel-collector
- さくらのAI EngineをLangfuseで可視化してみた
- open-telemetry/opentelemetry-python-contrib #2826
お知らせ
弊社はさくらインターネットとパートナー契約を締結しています。 www.ap-com.co.jp
また、一緒に働いていただける仲間も募集中です!
www.ap-com.co.jp