hnwの日記

自宅でAnsibleするなら教科書通りの構成を捨てよう

本稿は「KLab Engineer Advent Calendar 2025」の25日目です。記事を書いてくださった皆さん、お疲れさまでした!

はじめに

私は過去に何度か「自宅のマシン管理にAnsibleを使うぞ」と挑戦しては敗れ去ってきました。定着しなかった一番の理由は「オーバーテクノロジーでコスパが悪いから」です。同じ経験をした人も多いのではないでしょうか。

しかし、試行錯誤の末、自宅のマシン管理に最適な「ホスト指向Ansible」に辿り着きました。本稿ではその概要を紹介します。

なぜ自宅環境だとAnsibleはコスパが悪いのか

Ansibleを知らない人向けに簡単に説明すると、Ansibleは複数マシンのセットアップ手順をコード化する有名OSSです。典型的な教科書通りのAnsibleでは、次のような概念で各ホストを管理します。

graph LR
%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#888', 'edgeLabelBackground':'#bbb' }}}%%
    Host((ホスト)) -->|N:N| Group[グループ]
    Group -->|N:N| Role[Role]
    Role -->|1:N| Task(Task)

ソフトウェア1個をセットアップする手順のかたまりをRoleと呼びます(例:「nginx」や「MySQL」)。グループは「Webサーバ」「DBサーバ」などホストの役割を定義します。

企業のインフラなら「同じ構成のWebサーバを10台作る」ためにグループは必須です。しかし、自宅では同じ役割のマシンが複数必要になることはまずありません。それぞれのマシンが異なる役割を持つ自宅環境で、グループをどう扱うかは悩みの種になります。

また、N:N関係を表現するために設定ファイルの階層が深くなり、冗長性が高くなる点も、個人環境での取っつきにくさの原因と言えるでしょう。

自宅で使うなら「ホスト指向Ansible」

私の出した結論は、自宅ではグループの概念は不要と割り切り、下記のスタイルで運用するというものです。

%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#888', 'edgeLabelBackground':'#bbb' }}}%%
graph LR
    Host((ホスト)) -->|1:N| Role[Role]
    Role -->|1:N| Task(Task)

バッサリと中間層を削り、N:N関係も排除しました。 このアプローチにより、1ホストに対応する設定ファイルは1ファイルのみ(host_vars)となり、見通しが劇的に良くなります。

具体的には、以下のように各ホスト変数ファイル内で「適用したいRoleのリスト」を定義するような方針です。Ansibleに挫折した人でも、このファイルならメンテナンスできるイメージが湧くのではないでしょうか。

---
# host_vars/raspberrypi4.lan/main.yml
apply_roles:
  - system_bootstrap
  - rsyslog
  - prometheus.prometheus.node_exporter
  - name: geerlingguy.docker
    become: true
  - docker_apps

docker_daemon_options:
  log-driver: "journald"
  log-opts:
    tag: "{{ '{{' }}.Name{{ '}}' }}"

docker_apps:
  - dockge
  - alloy

site.yml は滅多に編集しない実行エンジンになる

この構成を実現する最大のポイントは、メインのPlaybookである site.yml に具体的な構成情報を一切持たせないことです。

通常のPlaybookではグループごとに対応するRoleを記述しますが、この構成では全てのホストに対して「変数 apply_roles に書かれたRoleを順番に実行する」という処理だけを行います。

---
- name: Apply dynamic roles to hosts
  hosts: all
  tasks:
    - name: Include roles based on host variable
      ansible.builtin.include_role:
        name: "{{ role_item.name | default(role_item) }}"
        apply:
          become: "{{ role_item.become | default(false) | bool }}"
          tags: "{{ role_item.tags | default([]) }}"
      loop: "{{ apply_roles }}"
      loop_control:
        loop_var: role_item
      tags:
        - always

これにより、新しいサーバーを追加する際も site.yml を編集する必要がなくなり、新しいホスト用の host_vars ファイルを一つ作るだけで作業が完了します。

汎用Roleで省コストなコンテナ配置

私が過去にAnsibleに挫折した頃は、個人のマシン管理にDockerは大げさすぎると思っていました。特に自分しか使わない自作デーモンをコンテナ化するのはコスパが合わないと感じていました。

その考え方を変え、今回はコンテナ化できるものは全てコンテナ化するようにしました。また、コンテナ管理のための汎用のコンテナデプロイRole(docker_app)を用意しました。

この Role は、docker_apps変数で渡されたアプリ名(例: alloy)に基づいて、以下の処理を自動で行います。

  1. 設定ファイルの探索: host_vars または Role 内のテンプレートから docker-compose.yml やその他設定ファイルを探す。
  2. 配置: サーバー上の所定のディレクトリ(例: /opt/stacks/alloy)にファイルを配置。
  3. 起動: docker compose up -d を実行。

ビルド済みコンテナさえあれば、docker-compose.yml を書いて host_vars に1行追加するだけでコンテナを増やせるので、試行錯誤が多い自宅環境に非常に向いています。

まとめ

自宅でのAnsible運用が現実的になったよ、という話を紹介しました。

今回の説明用Ansibleプロジェクトは以下のURLで公開しています。過去に自宅Ansibleに挫折した方は参考にしてみてください。

本稿の取り組みで感じたのが、AI時代になって「コスパ」の考え方が以前と変わっているんじゃないか?ということです。人は楽をしたい生き物なので、優れているけど複雑すぎるものを使い続けることはできません。しかし、AIの助けで入門が楽になったり、楽に運用する方法を一緒に考えたりできるようになり、以前ならコスパが合わなかった技術が今なら採用できる、ということが今後増えていくのではないでしょうか。

Grafana CloudのFree tierでアラート通知に画像を添付する方法

この記事は「KLab Engineer Advent Calendar 2025」の1日目です。初日から少々ニッチな小ネタです。

Grafana CloudのFree tierが大盤振る舞いすぎる

突然ですが、みなさんGrafana Cloud使ってますか?Grafana Cloudは、Grafana Labsが提供するフルマネージドのオブザーバビリティサービスです。私は自宅サーバーやIoTデバイスのモニタリング・アラーティングに活用しています。

例えば、自宅のCO2濃度の推移を可視化し、次のようなグラフを作成することが可能です。

自宅のCO2濃度グラフ

Grafana Cloudの大きな特徴は、Free tierの充実ぶりです。2025年12月現在、クレジットカードの登録不要で以下のリソースを利用できます。

  • Metrics: 10k series(Prometheus等のメトリクス)
  • Logs: 50GB(Loki)
  • Traces: 50GB(Tempo)
  • Users: 3ユーザーまで
  • データ保持期間: 14æ—¥

自宅の環境モニタリングや個人の小規模な開発用途であれば、10k seriesのメトリクスや50GBのログ容量は十分すぎると言えるでしょう。無料で構築できるモニタリング環境として、私の知る限り最強の選択肢の一つだと思います。

アラート通知を設定する

Grafana CloudのFree tierでは最大100個までアラート通知を設定することができます。例えば「CO2濃度が1000ppmを超えたら通知する」という設定は、以下の手順で行います。

  1. Alerting > Alert rules に移動し、「New alert rule」を作成します。
  2. クエリとアラート条件を設定します(例: avg(switchbot_co2{ })、Is above 1000)。
  3. Folder と Evaluation behavior を設定します。
  4. Contact points でSlackなどの通知先を指定します。
  5. Alert ruleの設定画面下部にある「Link dashboard and panel」で、このアラートに対応させるダッシュボードとパネルを選択します。
    • 通知にグラフ画像を添付するには、その元となる描画情報が必要なため、必ずダッシュボードと紐付ける必要があります。

これでアラート通知自体は届くようになります。しかし、デフォルトの状態ではテキストのみの通知となってしまい、グラフ画像は添付されません。

通知に画像を添付するにはサポートへの問い合わせが必要

アラート通知にグラフ画像を添付するには、さらに以下の2つの準備が必要です。

  1. Slack App側で発行したBot Tokenに files:write 権限を付与する。
  2. Grafanaのサポートに連絡し、Image Rendererプラグインを有効化してもらう。

後者のImage Renderer有効化は管理画面では設定できず、サポートチケットを発行して依頼する必要があります1。

私は下記のような問い合わせを出したところ、数時間で有効化されました。

件名: Request to enable "Images in notifications"

本文:

I would like to enable "Images in notifications" for my alerts.
Please enable the image rendering feature for my instance.

Instance Name: [あなたのインスタンス名].grafana.net

Information:
- I am using the Free tier account.
- My Alert Rule is linked to a Dashboard UID/Panel ID correctly.
- I have configured the Slack App with files:write permissions.

チケットのタイプ: Billing/Cancellation (※機能追加の依頼に適した項目が見当たらないため、一番近い選択肢で送ります。)

サポートから「有効化したよ!」と返信が来たら、わざとアラートを発生させて確認しましょう。Slackで下記のようなグラフが確認できるようになります。

Slack通知でグラフ画像が見えている様子

通知画像のタイムゾーンと期間を最適化する

Slackへの画像添付には成功しましたが、デフォルト設定のままだと「時間がずれている(UTC表示)」「グラフの期間が広すぎて直近の変化が分からない」といった点が気になるかもしれません。

ダッシュボードで次の設定を行うことで、見やすい通知画像に調整できます。

1. グラフの標準時をJSTにする

ダッシュボード上部の「Edit」「Settings」「General」から、Timezoneの設定を変更します。

  • Time zone: Default ではなく Asia/Tokyo を指定。

2. デフォルトの時間幅を調整

アラート発生時の状況を把握するには、あまりに長い期間の画像だと直近の変化が潰れて見えなくなってしまいます。

  • ダッシュボード右上の時間選択で、適切な期間(例: Last 3 hours)を選択します。
  • その状態でダッシュボードの「Save」ボタンを押し、「Update default time range」 にチェックを入れて保存します。

これで、アラート通知時に生成される画像が「JST表示」かつ「直近3時間」のグラフになり、Slack上でパッと見ただけで状況が把握できるようになります。

まとめ

Grafana Cloudでアラート画像添付を使いたい場合、サポートチケット経由で依頼する必要があります。Free tierであってもこのリクエストが通る2ことは、意外と知られていない情報だと思うので記事にしてみました。

サポートチケットを発行するのは心理的ハードルがあるかもしれませんが、一度設定してもらうとモニタリングの快適性が大きく向上します。ぜひ、ご自身の環境でも試してみてください。


  1. Use images in notifications を参照
  2. 少なくとも私の場合はリクエストが通りました。公式ドキュメントにはプランによる制限の明記はありませんが、リソースを消費する機能のため、将来的にポリシーが変わる可能性があります。

switchbot-actionsではじめる最小構成のIoT

SwitchBotのセンサーデバイス(人感センサーやCO2センサーなど)は、エンジニア視点で見てもハードウェアとしての完成度が非常に高いガジェットです。コンパクトで省電力、かつデバイス単体であれば価格も手頃です。

しかし、これから試そうと思うエンジニアにとって、一つだけ「心理的なハードル」が存在します。それは、公式アプリでの自動化やクラウド連携を行うために、原則として SwitchBotハブ(定価4,000円程度)が必須となる点です。

「データ取得のためだけに専用ハブを買うのは気が引ける……」 「CO2センサーには興味があるので、まずは少し遊んでみたいな……」

そう考える方におすすめしたいのが、拙作の switchbot-actions です。これを使えば、専用ハブを介さず、デバイス1台からSwitchBotの可能性を引き出すことができます。

switchbot-actionsを使うメリット

switchbot-actions は、SwitchBotデバイスを制御・自動化するためのOSSです。このツールを導入する主なメリットは以下の3点です。

1. ハブ不要でスモールスタートが可能

switchbot-actions は、PCやRaspberry Piに搭載されたBluetooth機能を使い、デバイスと直接BLE通信を行います。つまり、一般的なSwitchBotユーザーが利用する公式の「SwitchBotハブ」を購入する必要がありません。 これはエンジニアにとって大きなメリットと言えるでしょう。

「まずはCO2センサーだけ購入して、手元のRaspberry Pi(ラズパイ)で値を取りたい」という場合でも、最小の手間とコストで始められます。

2. YAMLひとつでロジックを記述できる

公式アプリの自動化機能も進化していますが、複雑な条件分岐(条件Aかつ条件Bの時だけ……等)や、任意のSlack WebhookへのPOST送信といった柔軟な外部連携まではカバーしきれていません。

switchbot-actions では、動作のすべてを1つの config.yaml に記述します。 「温度が28度を超えたら」「開閉センサーのボタンが押されたら」といったトリガー条件や、Webhook、シェルコマンド実行といったアクションを、コードベースで柔軟に定義できます。

3. データを自分のものにできる

センサーの現在値を確認するだけでなく、時系列データとして蓄積・可視化したくなるのがエンジニアの性ではないでしょうか。 このツールでは Prometheus Exporter 機能を実装しています。設定を一行追加するだけで、Grafana等を用いて室温や湿度を簡単にグラフ化できるようになります。

実践:CO2センサーで作る「換気アラート」

では、実際に switchbot-actions を使って、実用的なシステムを作ってみましょう。 テーマは、在宅勤務の生産性を守る「CO2換気アラート」です。

やりたいこと

  • 機材: SwitchBot CO2センサー、Raspberry Pi 3(BLEを搭載していればPCã‚„Macでも動作します)
  • 条件: CO2濃度が1000ppmを超えたら
  • 動作: Slackに「換気してください」と通知を送る

設定ファイル (config.yaml)

やることはシンプルです。config.yaml を作成し、以下のように記述します。

# config.yaml

# 1. デバイスの定義 (MACアドレスはデバイス裏面、またはスキャンコマンドで確認)
devices:
  my-co2-meter:
    address: "AA:BB:CC:DD:EE:FF"

# 2. 自動化ルールの定義
automations:
  - name: "換気アラート"
    # 頻繁に通知が来ないよう、一度発火したら1時間は静かにする
    cooldown: "1h"
    
    # トリガー設定 (IF)
    if:
      source: "switchbot"
      device: "my-co2-meter"
      conditions:
        # CO2濃度が1000ppmを超えたら発火
        co2: "> 1000"
    
    # アクション設定 (THEN)
    then:
      - type: "webhook"
        url: "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"
        method: "POST"
        payload:
          text: "🚨 *換気アラート* 🚨\n現在のCO2濃度は *{co2}ppm* です。窓を開けましょう!"

解説

  • 動作概要: ラズパイが直接CO2センサーのBLEアドバタイズ信号を拾い、YAMLの条件 (co2: "> 1000") に合致した瞬間、インターネット経由でSlack APIを叩きます。
  • プレースホルダー: {co2} と記述するだけで、センサーから取得した実際の値をメッセージに埋め込めます。
  • cooldown: 「1001ppm」と「999ppm」を行き来するたびに通知が来ると、実運用ではノイズになります。 cooldown: "1h" を設定すれば、「一度通知した後は1時間何もしない」といった制御も設定ファイルだけで完結します。

これだけの記述で、自分だけの換気アラートシステムが完成しました。

筆者はほぼ同じ仕組みを実用しています1。在宅勤務中に部屋を閉め切っているとCO2濃度が上がりやすいため、興味を持った方はぜひ試してみてください。 SwitchBot CO2センサーは少々値が張りますが、CO2センサー(NDIR方式)の部品単価自体が高価であることを考えると、完成品としてこの価格はコストパフォーマンスが高いと言えます。

導入手順

導入には Python 3.11 以上 が動く環境が必要です。システム環境を汚さないよう、 pipx でのインストールを推奨します。

1. インストール

# pipxが未インストールの場合はまずインストール
# (Ubuntu/Debian系の場合: sudo apt install pipx)
pipx ensurepath

# switchbot-actionsのインストール
pipx install switchbot-actions

2. デバイスのスキャン

まずは設定ファイルなし、かつデバッグモードで起動し、ラズパイからセンサーが見えているか確認します。

# -vv (verbose x 2) をつけるとDEBUGログが表示され、受信したBLEデータが見えます
# Bluetoothへのアクセス権限が必要な場合があります
switchbot-actions -vv

ログに Received advertisement from... と表示され、手持ちのセンサーのMACアドレスが見つかれば準備完了です。ここで確認したアドレスを config.yaml に記述してください。

3. èµ·å‹•

先ほど作成した config.yaml を保存し、指定して起動します。

switchbot-actions -c config.yaml

これだけでラズパイがスマートホームハブになりました。

本格的に運用する場合は、リポジトリのドキュメントを参考に systemd や Docker で常時稼働させてください。筆者の自宅では半年ほど動き続けています。

まとめ

SwitchBot製品は、ハードウェアとしての完成度だけでなく、ユーザーが自由に遊べる余地が広い点も魅力です。switchbot-actions がそのハードルをさらに下げるツールになればと願っています。

  • ハブを買わずに、センサー1個から始められる
  • YAMLでロジックを記述できる
  • 必要ならGrafanaで可視化できる

まずはデバイスひとつから、手作りIoTの実験をしてみてはいかがでしょうか。特に入門用として、以下の3つは安価でおすすめです。


  1. 筆者はGrafana Cloudで可視化・アラーティングをして、Slack通知しています。設定は面倒ですが、Slackにグラフ画像を出せるのがメリットです。

10年ぶりにPHP勉強会でLT発表してきました

先日、10/29に開催された「第180回PHP勉強会」にてLT発表をさせてもらいました。

なんと、私のPHP勉強会での発表は10年ぶりでした(前回は第94回だったようです)。最近はPHPを直接書く機会は減ってしまいましたが、私にとっても思い入れのある勉強会で、久々に参加できて良かったです。

当日は「業務でAIを使いたい話」というテーマで、AIコーディング支援の理想と現実、そしてAIの能力を引き出すための環境整備についてお話しさせていただきました。

AI活用の実態:アンケート結果「導入9割」vs「活用1割」

当日、会場の参加者の皆さんに簡単なアンケートを実施しました。正確な数字ではないですが、私のパッと見の印象では次のような結果でした。

  • Q1: 会社で有償のAIを導入してますか? → はい:9割
  • Q2: 業務のコード、AIに書かせてますか? → はい:4割
  • Q2b: 9割方AIに書かせてます? → はい:1割

「有償AIの導入率」は9割と非常に高い一方で、そのうち「業務コードの9割をAIに書かせている」と回答したのはわずか1割でした。プロトタイピングや使い捨てのコード作成にAIを活用できていても、業務コードへの適用はまだこれから、という方が多数派と考えて良さそうです。

これに対してどう立ち向かっていくべきかというお話をさせてもらいました。発表資料は下記です。

活用が進んでいる会社の話も聞けた

勉強会では、参加者の方々からリアルなAI活用事例も聞くことができました。

  • 「PHPの中規模プロジェクトでもバリバリAI活用できている。当然ユニットテストは徹底している」
  • 「Devinにコードレビューさせるのが良い、DeepWikiが強力」

といった声が印象的でした。

私自身、AI適性の観点でPHPはNode.jsやPythonに比べて一歩劣る印象を持っていましたが、今回の勉強会を通じて、言語の適性以上に「AIフレンドリーな環境整備」の方が、AI活用の成否に大きく効くのではないかと感じました。勉強会に参加すると生きた話が聞けるのが本当に良いですね。

おわりに

それにしても、PHP勉強会って20年続いてる勉強会なんですよね。これは本当にすごいことだと思いますし、これまでバトンをつないできてくださった運営スタッフの方々には感謝しかありません。

また、勉強会に行くと尖った発表が聞けるのも本当に楽しいですね。私は@nsfisisさんの発表「浮動小数点数の半開区間で一点を指定する」が聞きたくて参加を決めたところもあるのですが、期待以上の内容でした。浮動小数点数のビットパターンが上手い設計になっていてnextUpとnextDownの実装が比較的簡単(要約)という話を発表後に聞けて、とても興味深かったです。

また昔のように勉強会に参加したいな、と思える機会でした。スタッフの皆さま、参加者の皆さま、ありがとうございました!

「毎日が祝日カレンダー」を作りました

みなさん連休を楽しんでいますか?4連休って長いようで短いですよね。「毎日が祝日だったらいいのに…」なんて、小学生の頃はよく考えていました。

そんな子供の頃の夢を、テクノロジーの力で形にしてみました。その名も「毎日が祝日カレンダー」です!

endless-holidays.hnw.jp

このサイトでは世界198の国と地域1の祝日を1つのカレンダーにまとめて表示しています。ぜひサイトを覗いてみてください。カレンダーのほとんどの日が赤く染まっています。

小学生が考える夢のカレンダー

これで毎日がお休み気分…となるかはわかりませんが、世界の祝日をパラパラ眺めているだけでも異文化に触れる面白さがあって、意外と楽しいのでオススメです。

カレンダーを支える技術:date-holidays

このカレンダーの祝日の表示はJavaScriptのdate-holidaysというライブラリで行っています。これは過去の記事「date-holidays という祝日ライブラリが良い意味で狂っていた」でも紹介したもので、世界中の祝日をYAMLで定義している野心的なプロジェクトです。

ただ、このライブラリの素晴らしさにも関わらず協力者が少ない状況で、一部の祝日について不正確だったり最新の法改正に対応できていなかったりします。

私も微力ながら、アルゼンチンやサウジアラビアの祝日についてコントリビュートしていますが、もし海外在住の方や特定の国の祝日に詳しい方がいらっしゃれば、情報の修正や追加にご協力いただけると嬉しいです。

世界の休日・祝日の豆知識

今回のカレンダーを作る中で、いくつか面白い発見があったので紹介します。

①「日曜日=休日」とは限らない

このカレンダーでは、祝日ではない日曜日も赤く表示しています。これは多くの日本人や欧米人にとって自然なことだと思いますが、世界に目を向けると日曜日が平日(労働日)の国も存在します。

例えばサウジアラビアやバングラデシュでは金曜と土曜が休みの週休2日制を採用しています2。こうした国の人々は日曜だけを特別扱いすることに違和感を覚えるかもしれません。

金土が赤いバングラデシュのカレンダー

全世界の人が納得するカレンダーを作るのは意外と難しいですね。

いっそ「世界中の誰かが休んでいる日カレンダー」というコンセプトにして、金・土・日を全部赤く塗りつぶしてしまえば、それはそれで面白いかもしれません。

②独自の暦に紐づく祝日計算が大変すぎる

世界には、私たちが普段使っているグレゴリオ暦とは異なる、独自の暦に基づいて日付が決まる「移動祝日」がたくさんあります。

特に宗教関連の祝日は太陰暦や太陰太陽暦など古い暦に基づいていることが多く、毎年日付が変わります。

例えば、タイなどの上座部仏教国で見られる「ウェーサーカ」(ブッダ生誕、悟り、入滅の日)の祝日は、タイ太陰暦の6月の満月の日に行われます。

こうした移動祝日の日付を将来にわたって正確に計算するには、それぞれの古い暦をプログラムで再現する必要があり、なかなかハードルが高いのが実情です。

date-holidaysライブラリも多くの暦に対応している3のですが、タイ太陰暦の計算ロジックはまだ実装されていません。

また、インドではヒンドゥー教の祝日の基準となる暦が地域ごとに複数4存在するため、インドの祝日すべてに正確に対応しようとするとメチャクチャ大変です。

まとめ

  • 全世界の祝日を1つにまとめた「毎日が祝日カレンダー」を作りました。
  • このカレンダーはJavaScriptライブラリdate-holidaysで実現していますが、祝日情報の更新や複雑な暦の対応に改善の余地があります。
  • 特定の国の祝日事情に詳しかったり、古い暦の知識をお持ちの方がいらっしゃいましたら、date-holidays プロジェクトに情報提供やPull Requestいただけると嬉しいです!

  1. ISO国名コードが割り振られている161の国と37の地域
  2. イスラム圏では金曜日が集団礼拝の日なので、金曜だけ休みだったり、金曜・土曜が休みだったりする国が他にもあるようです
  3. 現時点でイスラム暦、ペルシャ暦、ヘブライ暦、中国旧暦、二十四節気に対応しています
  4. Gemini先生によれば5系統あるそうです

Cloudflare Email Routingのログ確認がGrafana Cloudで快適になった話

Cloudflare Email Routingのダッシュボードの不満点を解決したい

Cloudflare Email RoutingはCloudflareに預けているドメイン宛てのメールを転送してくれるサービスです。無料で提供されているので、独自ドメインの管理者にとってありがたいサービスだと思います。

ただ、Cloudflare Email Routingを利用している中で、私は以下のような不安を感じていました。

  • 転送されなかったメールが本当に不要なものなのか把握できていない
  • 転送を一段はさむことで、DKIM署名の不整合が起きるなど新たなトラブルがあるのではないか

これらの課題に対してCloudflare標準のダッシュボードの機能は十分とは言えません。特にメール転送の成功・失敗のログが過去24時間分しか見えない1のは制約として大きく、何かトラブルがあっても「今週末に調べよう」ということができません。せめて7日間は見せてほしいですね。

このログをGrafana Cloudに転送したら便利になりました、というのが本稿で紹介する内容です。各サービスの無料枠の範囲で実現できますので、私と同じ不満を持っている方は是非お試しください。

Cloudflare Email Routingのダッシュボード

作ったもの

今回作ったものは下記になります。デプロイ手順等はREADMEをご確認ください。

github.com

また、Grafanaのダッシュボードも作りました。こちらもJSONの形でリポジトリに含まれており、コピペで利用できます。

利用しているGrafanaダッシュボード

システム構成

システム概要

今回のログ転送の仕組みはCloudflare Workers上で動作するものです。これをCron Triggers経由で1時間に1回起動させています。いずれもCloudflareユーザーなら無料で使えます2 3。

ログの取得はGraphQL Analytics APIで行っています。Cloudflareの各種ログはGraphQL経由でユーザーに公開されており、Email Routingのログも自分でAPIを叩けば最大31日分取得できます4。

ログの保存にはGrafana CloudのLokiを利用しました。Grafana Cloudは開発元のGrafana Labsが提供しているフルマネージドのGrafana環境です。企業ユーザーが本番環境で使うなら有料プランになると思いますが、個人など小規模であれば無料で利用できます。

Lokiはログ集約・検索を担当するサービスです。Grafana Cloudの無料枠で50GB・14日間のログが保管されるため、私の不満点が解決できるというわけです。

Grafanaを使うメリット

今回の仕組みを作ってみて気づいたんですが、Grafanaは自分でダッシュボードを作れる点が圧倒的なメリットと言えます。

私の場合、転送失敗したメールが本当に問題がないかを確認したかったんですが、Cloudflareのダッシュボードだとパッと見でわかりにくいんですよね。Grafana ダッシュボードでメールサブジェクトも表示するようにしてみて、捨てても問題ないSPAMであることが一目瞭然になりました。

明らかにfrom偽装のSPAMメール群

当然ですが、メールログをドリルダウンで辿るのもGrafanaの方が断然やりやすいですね。

一方で、Grafanaのダッシュボードはあまり直感的に作れない印象を持ちました。かなり複雑なデータ操作もGUIから設定できるんですが、冗長すぎて人間のやる作業ではないように思います。生成AIに頼むとそれっぽいJSONを作ってくれますが、学習量が足りないのかイマイチ参考になりませんでした5。

まとめ

  • Cloudflare Email Routingのログを14日間確認できる仕組みを無料で構築できた
    • Grafana Cloudは無料枠が大きくて素晴らしい
  • Cloudflare Email RoutingはSPFã‚„DMARCを根拠に転送エラーにするが、大半がSPAM
    • 今回の仕組みがなくても大抵の人は困らないはず
  • Grafanaはダッシュボードが自由に作れるのが強み
    • ログやメトリクスをGrafanaに集約すると新たな気づきがあるかも

  1. 期間を指定するメニューに「Previous 7 days」という選択肢があるのですが、これを選ぶと内部的に呼び出しているGraphQLリクエストがエラーを返して機能しません。バグだと思うので、いずれ修正されるとは思います。
  2. Cloudflare Workersの無料枠は10万リクエスト/日です。1時間に1回実行した場合、無料枠の0.024%を消費することになります。
  3. Cron Triggersの無料枠は5です。すでに上限まで使っている場合は他Workerと相乗りして乗り切るか、Workers Paid Plan($5/月)プランの契約が必要になります。
  4. データセット名emailRoutingAdaptiveがメール転送のログです。保管期間についてはCloudflare GraphQL データセットの保管期間を確認するを参照しました。
  5. グラフィカルな設定をJSONで書けるのは生成AI向きなので、将来的には全部生成AIにやらせたいですね。

独自ドメインGmailのメール紛失事件をCloudflare Email Routingで解決した話

この記事は、KLab Engineer Advent Calendar 2024 の25日目の記事です。

はじめに

独自ドメインを長年維持している方々の多くは、現在もGoogle Workspace(旧G Suite無償版)を利用しているのではないでしょうか。筆者もその一人です。独自ドメインのメールを送受信でき、Gmailの便利な機能を無償で利用できるのは本当にありがたいですね。

そんなわけで長年独自ドメインのメールを運用してきたわけですが、最近になって一部のメールが届かないトラブルに見舞われました。その解決策としてCloudflare Email Routingを導入したところ、あっさり問題が解決しました。本記事では、その設定手順および得た知見を紹介します。

メール紛失事件について

今回の取り組みのきっかけになった「メール紛失事件」について説明します。ここ6ヶ月ほど、au IDの二段階認証メールが筆者所有の独自ドメインのメールアドレスに届かない問題が起きていました。この独自ドメインのメールアドレスはGoogle Workspaceで管理しており、このメール以外は特にトラブルなく利用できています。

まずauのサポートに問い合わせましたが、同様の事例報告はないそうで、auサポートでは解決できませんでした。次に、Googleに問い合わせようとしましたが、無償ユーザーのため正式なサポートに頼ることができません。また、Google Workspace管理コンソールの「メールログの検索」も無償ユーザーでは利用できませんでした1。

他サービスからのメールは問題なく届いているため、auが行儀の悪いメールを送っているか、Googleがエラーメール扱いにする条件が厳しすぎるか、どちらかが原因と考えられます。いずれにせよ旧G Suite無償版ではトラブル時の打ち手が少なく、リスクのある環境であることを再認識しました。

やったことの概要

上記の問題を解決するため、Google Workspaceの前段にCloudflare Email Routingを入れることにしました。Cloudflare Email Routingを利用して、問題が起きているメールだけをYahoo! メールなど別サービスに転送すれば問題が解決するのではないか?と考えたためです。

図1:Cloudflare Email Routing導入の概要

手順は以下の通りです。説明上、Google Workspaceで管理しているメールアドレスを[email protected]とします。

  1. ユーザーエイリアスドメインの設定 Google Workspaceの「ユーザーエイリアスドメイン」を利用し、サブドメインのメールアドレスでもメールを受信できるように設定します([email protected])。

  2. Cloudflare Email Routingの導入 元のアドレス宛のメールをCloudflare Email Routingで受信し、サブドメインのアドレスに転送する構成にします。

この方法だと元のGoogle Workspaceのフィルタ設定をそのまま利用可能ですから、他の方法に比べて移行の手間が少なく、万一事故があっても元に戻しやすいという利点があります。

旧G Suite無償版とは

詳細の説明に入る前に、旧G Suite無償版について説明します。

Googleのサービス(Gmail・Google カレンダー・Google Driveなど)を独自ドメインで組織メンバー向けに提供するサービスがGoogle Workspace ですが、以前はG Suite(さらに以前はGoogle Apps)と呼ばれていました。

これが大昔(2012年以前)は10人以下の組織であれば無料で使えるという大盤振る舞いをしていました。当時個人でドメインを所有していた人は全員利用していたように思います(筆者の周りでは本当にそんな印象でした)。2012年に無償版の新規提供は終了しましたが、それ以前からの契約者は引き続き無償提供されています。

2022年にGoogleが「無償版やめるんでお金払うか引っ越して」と言い出したものの、しばらくして「やめるのやめます」となり、移行せずモタモタしていた人(筆者を含む)は再び救われたような経緯があります。

若い人には信じられないような話だと思いますが、古参の人が独自ドメインのメールアドレスを維持できているのはそんなカラクリがあるのです。本当にありがたいことです。

Cloudflare Email Routingとは

Cloudflare Email Routing についても紹介します。

CloudflareはCDNの会社として有名ですが、独自ドメインを維持している人にとっては無償でDNSを提供してくれる会社でもあります。それ以外にも様々なサービスを提供しています。

Cloudflare Email Routingはメールの転送サービスです。1ドメインあたり最大200個の宛先・転送先を管理でき、キャッチオールアドレス(未定義のアドレス全部を受け取るアドレス)も提供しています。大盤振る舞いなことに、利用料金は無料です。

図2:メールの転送状況が可視化されている

図2のように管理画面からメールの転送状況を確認できるのも便利ですね。

設定1:ユーザーエイリアスドメインの設定

まずはGoogle Workspaceでユーザーエイリアスドメインを設定する手順を説明します。ユーザーエイリアスドメインを設定することで、サブドメインのメールアドレスでもメールを受信できるようになります。例えば、[email protected]というアドレスでメールを受信することが可能になります。

手順

  1. Google Workspace管理コンソールにログイン 管理者アカウントでGoogle Workspaceの管理コンソール にログインします。

  2. ドメインの追加 「アカウント」「ドメイン」「ドメインの管理」ページに移動し、「ドメインをを追加」を選択します。ドメイン名を入力し(例:ws.example.com)、「ユーザーエイリアスドメイン」チェックボックスを選択して「ドメインを追加して所有権を証明」ボタンを押します。

  3. ドメインの所有権の確認 ページ内の指示に従ってドメインのDNS設定でTXTフィールドを追加して「確認」ボタンを押します。

  4. ユーザーエイリアスドメインの有効化 「自社ドメインで Gmail の使用を開始する」ボタンを押し、ページ内の指示に従ってMXレコードを設定します。完了後に「確認」ボタンを押すと設定完了です。

  5. メール送信テスト 設定が反映されたか確認するために、サブドメインのメールアドレスにテストメールを送信します。

設定2:Cloudflare Email Routingの導入

次に、Cloudflare Email Routingを導入する方法を説明します。Cloudflare Email Routingを利用することで、独自ドメインのメールを受信し、指定したアドレスに転送することができます。

注意点ですが、対象ドメインのDNS管理にCloudflareを利用していることがEmail Routing利用の前提になります。他社でDNS管理している場合は利用できません。

手順

  1. Cloudflare管理コンソールにログイン Cloudflareの管理コンソールにログインし、ドメイン管理画面に移動します。

  2. 転送ルールの設定 「Email」「Email Routing」ページに移動し、「Routing Rules」タブで転送アドレスの設定を行います。例えば、[email protected] 宛のメールを [email protected] に転送する設定を行います。

  3. 転送先アドレスの確認 転送先として設定したメールアドレスに確認メールが届くので、メールの「Verify email address」ボタンを押します。

  4. Email Routingの有効化 「Overview」タブから「Enable Email Routing」リンクをクリックします。これによりMXレコードが書き換わり、メール転送が開始されます。

  5. メール送信テスト 設定が反映されたか確認するために、テストメールを送信します。メールが正しく転送されることを確認します。

結果:何もしていないのに問題が解決した

上記の設定を行ったところ、それだけでau IDの二段階認証メールがGoogle Workspaceで受け取れるようになりました。

仮に元のメールに何かしら問題があるとすれば、Cloudflare Email Routingを経由したところで問題は変わらないはずですから2、受け取れるようになったのは全く予想外です。

状況が改善したのでいったんは様子見になりますが、メール紛失事件が再発する可能性も否定できません。もしそうなったら改めて対処したいと思います。

今回得られた知見

知見1:ユーザーエイリアスドメインに「gmail」は使えない

Google Workspaceのユーザーエイリアスドメインとして「gmail.example.com」のようなドメインは指定できません。指定しようとすると「無効なドメインです」と怒られますが、なぜ怒られるのかしばらく理解できませんでした。

フィッシング目的で紛らわしいURLを作られないように、という配慮なんでしょうね。

知見2:サービスごとに登録メールアドレスを変えておくと便利

サービスごとに異なるメールアドレスを使用するのは一般的に良いテクニックですが、Cloudflare Email Routingを使う場合は特に有用です。今回のように一部の送信者からのメールだけトラブルがあるような場合に、特定のメールだけ転送先を変えることができます。

知見3:GmailはRFC違反のMessage-IDを書き換える

今回、Google Workspaceで受け取ったメールを別のアカウントに転送しようとしたところ、一部メールがDMARCエラーになりCloudflareが受け取ってくれないことがわかりました。今回の場合、1回目は同じメールを正常として受け取っているはずなので、少々不思議と言えば不思議です。

図3:Gmailからのメール転送時に発生したトラブル

図4:転送時にCloudflare Email Routingで出たエラー

調べたところ、GmailはMessage-IDがRFC違反のメールを受け取るとMessage-IDを書き換えるそうです。一方で、DKIMの署名検証はメールヘッダも対象なので、ヘッダを書き換えたメールが転送されて署名検証で失敗してしまうということなのでしょう3。

RFC違反のメールを送るのが悪いのかGmailがメッセージIDを書き換えてしまうのがやり過ぎなのかCloudflareの実装が悪いのか筆者の知識では判断できませんが、挙動がわかっていれば対処もできるので良しとしましょう。今回の場合は転送先をGoogle Workspaceのサブドメイン宛にすることで転送時もエラーが起きなくなりました。

ちなみに、私が受け取っているメールの中でこのような挙動になるのは「【au ID】2段階認証確認コード通知」「【au PAY】ご利用のお知らせ」などauからのメールの一部のみです。これらのメールのメッセージIDは012.345.678.JavaMail.spluser@c1hsm01aのような形式なのですが、RFCの定義では<と>で囲まれている必要があるようです。

まとめ

  • æ—§G Suite無償版でメール受信トラブルがあった場合、Cloudflare Email Routingを前段に入れる選択肢がある
    • エラーログを確認できる
    • 問題のあるメールだけ転送先を変更できる
    • (記事中では触れませんでしたが)Email Workers で複雑な処理をすることもできる
  • au IDの二段階認証メールのMessage-IDはRFC違反
    • トラブルの原因になりうる
    • 今回の問題の直接の原因とまでは言い切れないが、少なくとも遠因ではありそう

  1. 有償ユーザーであればログからエラー内容が確認できるはずで、もう少し手がかりが得られるはずです。
  2. 転送することで送信元IPアドレスが変わってSPFチェックでsoftfailするので、むしろ状況が悪化しても不思議はありません。
  3. GmailもCloudflare Email RoutingもARC (Authenticated Received Chain) をサポートしており、本来ならこのような事故を防げるはずだと思うのですが、どうにも謎です。
'); $entries_chunk.insertBefore(sections[0]); } else { chunk_id += 1; var $prev_entries_chunk = $entries_chunk; var $read_more_link = $('

これ以前の記事を表示する

'); $read_more_link.on('click', {chunk_id: chunk_id}, function(e){ $(e.target).hide(); $(this).remove(); $('#entries-chunk-' + e.data.chunk_id).fadeIn("slow"); }); $prev_entries_chunk.append($read_more_link); var $entries_chunk = $('
'); $entries_chunk.hide(); $entries_chunk.insertAfter($prev_entries_chunk); } } $(sections[i]).appendTo($entries_chunk); } });