会場内のCO2濃度をGrafana上に可視化してみた

## はじめに

### 概要

2022年11月21, 22日に開催された[CloudNativeDays Tokyo 2022 (CNDT2022) ](https://event.cloudnativedays.jp/cndt2022)というカンファレンスに運営側として参加しました。PrometheusなどのObservabilityの知見獲得と社外コミュニティを作りたいと思ったことをきっかけに参加して、今回で2回目のカンファレンスになります。その際に会場内のCO2濃度を可視化する企画のメインメンバとして活動したので備忘録として記事に残しておきます。

### 対象読者

私としては、「CO2濃度を取得する記事自体はあるが　取得したデータをグラフ化する部分を書いている記事が少ないので参考になるものを書きたい」のと、「ラズパイやセンサーを初めて触っているので、同じような初心者の勉強になる記事を書きたい」といった思いを込めて書いています。

* 同じようなことを考えている企画者
* Prometheus,Grafanaを勉強したい初級者
* Raspberry Pi, CO2センサーを触ってみたい初級者

といった方々に参考になれば幸いです。

## CO2濃度を取得するまで

### 準備

まず、今回の要件である「会場内の1室の二酸化炭素濃度を取得して既存のPrometheus, Grafanaに連携する」に近しい記事を探してきて、必要になりそうなものを購入しました。

* 必要なもの
    * [Raspberry Pi 4](https://www.amazon.co.jp/Vemico-4GB%E3%83%A9%E3%82%BA%E3%83%99%E3%83%AA%E3%83%BC%E3%83%91%E3%82%A44b-%E3%83%A9%E3%82%BA%E3%83%91%E3%82%A4MicroSD%E3%82%AB%E3%83%BC%E3%83%8932GB-USB-Type-C-HDMI%E3%82%B1%E3%83%BC%E3%83%96%E3%83%AB%E3%83%A9%E3%82%A4%E3%83%B3/dp/B07ZRF5BC1/ref=sr_1_1_sspa?keywords=raspberry+pi+4&qid=1670650030&sprefix=ras%2Caps%2C214&sr=8-1-spons&psc=1&spLa=ZW5jcnlwdGVkUXVhbGlmaWVyPUExNUYxMzFMNFo3Qkw0JmVuY3J5cHRlZElkPUEwNjg4NDYyMk9YMDRESTBQSzEwOSZlbmNyeXB0ZWRBZElkPUEzTlJaTVNYMlhXNVBDJndpZGdldE5hbWU9c3BfYXRmJmFjdGlvbj1jbGlja1JlZGlyZWN0JmRvTm90TG9nQ2xpY2s9dHJ1ZQ==)
        * （昔見た時より高くなっているのにびっくりした。。今後もちょっと勉強用で使えると思って仕方なく購入。。)
    * [MH-Z19C](https://akizukidenshi.com/catalog/g/gM-16142/)
        * 参考記事はMH-Z19Bだったが、今はCの模様
        * mh-z19bで調べるとexporterを作っている人もいるみたいだしできるだろう
        * 秋月電子で購入
    * ジャンパー線（メスメス）
        * ラズパイとセンサーをつなげる線
        * 秋月電子でついでに購入
* 参考記事
    * [Raspberry Pi 4とMH-Z19Bで二酸化炭素濃度を計測してみた](https://dev.classmethod.jp/articles/raspberry-pi-4-b-mh-z19b-co2/)
    * [Raspberry Pi 4 でスマートメータ電気使用量・CO2センサ・温湿度計をグラフ化](https://qiita.com/mikage/items/fe1107ac28d4896eae80#prometheusgrafana%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)

### Raspberry Piのセットアップ

購入してからのセットアップはYoutubeとかで調べれば簡単に出てくるので詳細には説明しませんが、主にやったこととしては

* ヒートシンクをつける
* OSインストール
* Wifiの設定
* SSHの有効化

### CO2センサーのセットアップ

MZ-H19Cにはpythonのライブラリがあって、これを利用すれば簡単にCO2濃度を取得することができます。

https://pypi.org/project/mh-z19/

なので今回はこのライブラリに記載されている下図のようにジャンパー線でRaspberry PiとMH-Z19Cを繋ぎます。
<img width="600" alt="CO2センサー回路" src="https://warehouse-camo.ingress.cmh1.psfhosted.org/5e69358ea376cf9c8460b74610e3cd2ca2f487f9/68747470733a2f2f63616d6f2e67697468756275736572636f6e74656e742e636f6d2f336364346331623438326561393032623765363664636131336434323630313933633833316136332f3638373437343730373333613266326636333631366436663265373136393639373436313735373336353732363336663665373436353665373432653633366636643266333133313332363136343335363636353334333136333338333236313331333633363337333136343332333833383332333033373330333333383334333133303339363333383338333633303633363332663336333833373334333733343337333033373333333336313332363633323636333733313336333933363339333733343336333133323634333633393336363433363331333633373336333533323634333733333337333433363636333733323336333533323635333733333333333333323635333633313336363433363331333736313336363633363635333633313337333733373333333236353336333333363636333636343332363633333330333236363333333433333336333333353333333433333334333236363333333033333338333333323333333833333333333333303333333133333334333236343333333633333338333633343333333233323634333633333333333333363334333633353332363433333331333333363333333433333334333236343333333733363333333333383336333433333339333633323333333733363332333333363333333233363336333633343332363533363631333733303336333533363337">

うまく繋がってセンサーが点滅した状態になればOKです。

<img width="400" alt="ラズパイ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2249551/26229bb2-2949-9ed2-b8d5-b43b12a645a9.jpeg">


ライブラリを実行した結果、CO2濃度の値が返ってくれば無事完了です。

```bash
$ sudo pip install mh_z19
$ sudo python -m mh_z19
{"co2": 851}
```

## Grafana上に表示する

### 構成

今回はPrometheusのPushgatewayを使用しました。Pushgatewayとはいわばメトリクスのキャッシュのようなもので、監視対象がメトリクス情報をPushgatewayにプッシュする中で、最後にプッシュされた情報だけを保持します。PrometheusはPushgatewayをスクレイプしておくことで、Pushgatewayに保持されたメトリクスを取得して内部に保存し、そのメトリクス情報をGrafanaなどに連携することでグラフ化できます。Prometheusは監視対象から定期的にスクレイプするPull型のアーキテクチャのため、継続的に実行されないバッチジョブのようなものには不向きであり、そこを補填するのがPushgatewayです。

最終的に下図のような構成にしました。
<img width="400" alt="概要図" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2249551/e75ffa88-f479-9c93-d5d3-3afa4e76f5bd.png">
※本番のkubernetes構成の場合ingressリソースなどが存在しますが、今回は省略します。

### 環境の構築
実際はkubernetes環境上で動いていますが、ローカルで簡単に試せるように今回はdockerで動かします。

「[Prometheus & Grafanaつかってみた](https://qiita.com/kannkyo/items/b0d1c3565d33919edbb9)」 の記事で必要なコンポーネントが全て書いてあったので今回はこちらを動かします。用意するのは下記2ファイルです。
* docker-compose.yaml
    * prometheus,pushgateway,grafanaのコンテナを使用する
    * 基本設定・ポートはデフォルトのもので良い
    * grafanaのコンテナだけ参考記事とは別のものを使う
* prometheus/prometheus.yaml
    * pushgatewayをスクレイプする設定を追加する

```yaml:docker-compose.yaml
version: '3.1'

services:
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    ports:
      - 9090:9090
    networks:
      - public
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml

  pushgateway:
    image: prom/pushgateway
    container_name: pushgateway
    ports:
      - 9091:9091
    networks:
      - public

  grafana:
    image: grafana/grafana
    container_name: grafana
    ports:
      - 3000:3000
    networks:
      - public

networks:
  public:
```

```yaml:prometheus/prometheus.yml
scrape_configs:
  - job_name: 'pushgateway'
    scrape_interval: 1s
    static_configs:
      - targets: ['pushgateway:9091']
        labels:
          environment: "hoge"
          category: "pushgateway"
```

ファイルの作成が完了したら起動します。
```bash
$ docker-compose up -d
```

必要に応じてRaspberry PiからPushgatewayへアクセスできるように9091ポートを開放します。

### スクリプト作成
* 前述したmh-z19ライブラリを使ってCO2濃度を取得します
* prometheus_clientライブラリを使って取得したメトリクスをpushgatewayへ送信します
```txt:requirement.txt
prometheus_client
mh-z19
```

```python:main.py
import time
import os
import mh_z19

from prometheus_client import CollectorRegistry, Gauge, push_to_gateway

# 送信先と送信間隔を環境変数として読み込む
PUSHGATEWAY_ADDRESS = os.getenv("PUSHGATEWAY_ADDRESS", "localhost:9091")
INTERVAL_SEC        = int(os.getenv("INTERVAL_SEC", "10"))

# メイン関数
def main():
    print(f"PUSHGATEWAY_ADDRESS = {PUSHGATEWAY_ADDRESS}")
    print(f"INTERVAL_SEC        = {INTERVAL_SEC} second")
    while True:
        # mh-z19ライブラリを使ってCO2濃度を取得
        co2_conce = mh_z19.read_all()["co2"]
        send_co2_conce(co2_conce)
        time.sleep(INTERVAL_SEC)

# https://github.com/prometheus/client_python　に従って記載する
def send_co2_conce(co2_conce: int):
    registry = CollectorRegistry()
    g = Gauge("co2_conce", "CO2 Conce", registry=registry)
    g.set(co2_conce)
    push_to_gateway(PUSHGATEWAY_ADDRESS, job="co2", registry=registry)


if __name__ == "__main__":
    main()
```

必要なライブラリをインストールします。
```bash
$ python -m venv venv
$ . venv/bin/activate
$ pip install -r requirements.txt
```

### サービス化する

上記スクリプトの無限ループでCO2濃度を取得し続けることができましたが、Pushgatewayと繋がらない時スクリプトが異常終了します。これでは、気づいたら落ちていたり、失敗したら再起動したりと面倒なオペレーションが発生するので、自動で再起動するようにサービス化します。
（ここは同じチームメンバの方が対応してくれて大変助かりました。。）

systemdの設定ファイルを用意します。
* PUSHGATEWAY_ADDRESSを適宜変更する
* 下記は前述のスクリプト類を/home/observability/observability/co2-sensorというディレクトリに置いた例

```co2-sensor.service
[Unit]
Description=Measure co2 and upload data
After=network.target

[Service]
Environment=PUSHGATEWAY_ADDRESS=<自分の端末IP>:9091
Environment=PYTHONUNBUFFERED=1
WorkingDirectory=/home/observability/observability/co2-sensor
ExecStart=/home/observability/observability/co2-sensor/venv/bin/python -u /home/observability/observability/co2-sensor/main.py
Restart=on-failure
StartLimitBurst=0

[Install]
WantedBy=default.target
```

systemdに登録して、自動起動およびプロセスを開始します。
```bash
$ sudo cp co2-sensor.service /etc/systemd/system/
$ sudo systemctl daemon-reload
$ sudo systemctl enable co2-sensor
$ sudo systemctl start co2-sensor
```

### Grafanaで描画する
CO2濃度を取得して、Prometheusに保存するところができたので最後にGrafanaで表示させます。

すでにコンテナは立ち上がっているので、<http://localhost:3000>をブラウザで開いて、初期ユーザ/Passであるadmin/adminでログインします。パスワード変更を求められるので適宜設定します。

まずはDataSourceを設定します。左タブの「歯車」 > 「add data source」 > 「Prometheus」を選ぶとPrometheusの設定画面に移動します。
* URLでPrometheusを指定する。
    * 今回はdocker環境で作ったので、<http://host.docker.internal:9090>を指定した
* 他はデフォルト値で良い
* 必要に応じてスクレイピングの間隔を変えてもいい 

次に左タブから「+New Dashboard」　> 「Add New Pannel」を開きます。
QueryのMetricをクリックするとco2_conceがあるので、選択してRun queriesを流すと無事グラフが表示されます。
<img width="600" alt="ダッシュボード作成" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2249551/f6594840-2a20-5cc9-630a-d8dcdbef12cc.png">
あとは右タブで、グラフをゲージに変えたり、パネルにタイトルや単位をつけるなど適宜カスタマイズをして保存します。

<img width="600" alt="グラフ表示" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2249551/b4a9e873-2a68-e684-bb47-494c7fc2ff2c.png">

## カンファレンス当日

当日はメインセッションの会場の端に配置しました。

<img width="400" alt="設置場所" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2249551/3f8d0dbe-6acb-2a11-38fb-17338950c0df.jpeg">　<img width="400" alt="設置様子" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2249551/fd365b5a-fcf2-7699-844c-cc8acedc124a.jpeg">

会場設置時にスクショした画面がこちら。
<img width="600" alt="グラフ表示" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2249551/c2accf83-2947-8880-4582-26691578e9d5.png">


上図のグラフでは12時前後で大きく変わっていますが、これはスタッフの準備室の小さい部屋からホールAに移動したことによる変化になります。小さい部屋だと700ppm前後でしたが、大きい部屋だと400ppm程度でした。カンファレンス当日も観察していましたが、設置場所が悪かったのか換気が行き届いていたのか400ppm~500ppmで一定の数値を保っていて面白くなかったので準備時の画像を掲載しました。強いていうならキーノートで混み始めた際に、じわじわとCO2濃度が上昇していくのを観察できました。

## さいごに
まずは3日間ちゃんと動いて安心しました。スクリプトの負荷は少ないので本番と準備で3日間動かしましたが、Raspberry Pi 1台で全く問題なかったです。ただ、ネットワークの通信が悪くなる時が少々あったので、こういった企画にはネットワークの整備およびリトライ処理が重要であることを改めて痛感しました。今後これを広げていくかは分かりませんが、Raspberry Piの価格が高くなっていたのでもう少々コストを抑える施策の検討が必要になるかもしれません。

この企画は自分の興味本位で始めたものでしたが、Prometheus,Grafana,Raspberry Pi,センサーの入門的勉強ができてよかったです。また、自己学習ではなくカンファレンスの一企画なので、目的・締切もはっきりして動け、自己学習の自己満足で終わらなかったのもよかったです。何よりも運営メンバーからいろいろなフィードバックをいただけたのでとても感謝しています。当初の勉強は達成できたので今後は開発周りを手伝えたらいいなと思っています。運営等に興味がある人は気軽に声をかけてください。

最後にお決まりの文言になってしまいますが、本記事は私個人が勉強した内容に関するものです。会社や本カンファレンスとは関係なく個人の意見であることをご了承ください。