IoT自作プログラムのLinuxサービス化

# はじめに
今までの記事でIoTのプログラムをいくつか作成してきました。一方で、シェル上でプログラムを動かすことはできますが、何かの際に終了されたり、立ち上げを忘れたりすると **安定したサービスを提供できないという課題** があります。そこで、作成したプログラムをOS(Linux)のサービスとして登録し、安定して稼働させるまでをやってみようと思います。
なお、AWSを使っているので、IoT CoreからSQSへ送ったりして構成できると思いますが、LinuxオンリーでやりたかったのでAWSの構成ではやっていません。（クラウドに頼らない実装をしてみたかった）
私の備忘録的な意味合いが強い記事ですが、よろしければご覧ください。
また、認識不足の箇所が多くあると思いますので、知見ある方からご指摘いただけますとありがたいです。

## 環境
- Amazon Linux 2023 (ssh)
  - Python3インストールとMQTT用の仮想環境構築済みの状態
- Mac OS Sonoma 14.4.1 (こちらではzshを使います)

なお、Python3とPostgreSQLはインストール済みを想定します。
(さすがにこれらをいれると記事が膨大になりすぎるので)


# 概要
手順としては下記の流れです。
1. プログラムの作成
1. Shellスクリプトの作成
1. サービスファイルの作成
1. SCPでサーバへファイルを送信
1. systemdへの登録


# サービス作成詳細

## 1. プログラムの作成
まずは何はともあれプログラムがないと始まりません。
Pythonファイルと設定値を入れたファイルを作成します。

### Pythonプログラム
[以前作成した下記のプログラム](https://qiita.com/f-mio/items/e07d09de9cec5b20649c)をベースにしましょう。(若干変えてます)
ファイル名は、iot_core_sub.pyとしておき、下記の内容を記述します。

```python
import os
import json
from paho.mqtt.client import Client
from dotenv import load_dotenv
import psycopg2

# .envのデータ読み取り
load_dotenv()

# AWS IoT Core connection　information
CLIENT_ID = 'subscribe_id'
TOPIC = 'test/aaa'
MQTT_HOST = YYYYYYYYYY # AWS IoT Coreエンドポイント名を入れてください
MQTT_PORT = 8883
ROOT_CERT_FILE_PATH = "/home/ec2-user/project/cert/root-CA.crt"
DEVICE_CERT_FILE_PATH = "/home/ec2-user/project/cert/test20240214.cert.pem"
PRIVATE_KEY_FILE_PATH = "/home/ec2-user/project/cert/test20240214.private.key"

# クライアントインスタンスの作成
client = Client(client_id=CLIENT_ID)

# コールバック関数
def on_connect(client, userdata, flags, rc):
    print("Connected with result code "+str(rc))
    client.subscribe(TOPIC)

def on_message(client, userdata, msg):
    """
    メッセージ受信時の処理
    """
    insert_record_to_database(msg)

def insert_record_to_database(message):
    """
    jsonデータをパースし、適切にデータベースへの書き込む関数
    """

    try:
        res = json.loads(message.payload)
        payload = res['payload']
    except Exception as e:
        print(f"jsonデコード失敗. error message: {e}")
        return

    # クエリを作成
    try:
        query: str = f"""
            INSERT INTO light_value
            (
                topic_name,
                light_val,
                pub_timestamp
            )
            VALUES
            (
                '{message.topic}',
                '{payload['lightVal']}',
                '{payload['timestamp']}'
            );
        """
    except Exception as e:
        print(f"jsonからの値取り出し失敗. error message: {e}")
        return


    # DBとのconnectionを作成
    try:
        connection = connect_database()
    except Exception as e:
        print(f"DB接続失敗. error message: {e}")
        return

    # クエリを実行
    try:
        execute_query(connection, query)
    except Exception as e:
        # closeするためにreturnしない。
        print(f"DB書き込み失敗. error message: {e}")

    # connectionクローズ
    try:
        connection.close()
    except Exception as e:
        print(f'DB切断失敗. error message: {e}')


def connect_database():
    connection = psycopg2.connect(
        host = os.environ['iot_db_host'],
        dbname = os.environ['iot_db_name'],
        port = os.environ['iot_db_port'],
        user = os.environ['iot_db_user_name'],
        password = os.environ['iot_db_user_pass']
    )
    return connection

def execute_query(connection, query) -> None:
    with connection.cursor() as cur:
        cur.execute(query)
        connection.commit()

# TLS接続の設定
client.tls_set(
    ca_certs=ROOT_CERT_FILE_PATH,
    certfile=DEVICE_CERT_FILE_PATH,
    keyfile=PRIVATE_KEY_FILE_PATH
)
client.on_connect = on_connect
client.on_message = on_message

# 接続
client.connect(MQTT_HOST, MQTT_PORT)
# 永久ループで待機
client.loop_forever()
```

### 環境変数ファイル.env
環境変数格納用のファイル.envを作成します。
```
iot_db_host = "DBホスト"
iot_db_name = "DB名"
iot_db_port = "DBのポート番号"
iot_db_user_name = "DB接続用ユーザ名"
iot_db_user_pass = "DBユーザのパスワード"
```

## 2. Shellスクリプトの作成

iot_core_subscribe.shという名前でファイルを作成し、下記の内容を記述します。
```shell
#!/bin/bash
source /home/ec2-user/project/venvs/mqtt/bin/activate
cd /home/ec2-user/project/mqtt_subscribe/
python iot_core_sub.py
```
:::note info
ここでは、EC2インスタンスの/home/ec2-user/project/venvs/にmqttという仮想環境を作成していることを仮定しています。
:::


## 3. サービスファイルの作成
iot_core_subscribe.serviceというファイルを作成し、下記の内容を書き込みます。
```
[Unit]
Description = iot core sub subscribe

[Service]
ExecStart = /opt/mqtt_subscribe/iot_core_subscribe.sh
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target
```
細かい内容が気になる方は、参照サイトの項目をご確認ください。RedHat社のページが丁寧かな、と思います。


## 4. SCPでサーバへファイルを送信

### 4-1. ディレクトリの作成
まずはファイルの置き場を作成します。EC2上で下記のコマンドを実行します。
```bash
$ cd
$ mkdir ./work/
$ mkdir -p ./project/cert/
$ mkdir ./project/mqtt_subscribe/
$ sudo mkdir /opt/mqtt_subscribe/
```


### 4-2. 証明書の送付

#### クライアントPCからのファイル送付
クライアントPCからSCPでファイルを送信します。
```zsh
% scp -i /path/to/XXX.pem /any/path/to/root-CA.crt ec2-user@XX.XX.XX.XX:/home/ec2-user/projects/certs/
% scp -i /path/to/XXX.pem /any/path/to/test20240214.cert.pem ec2-user@XX.XX.XX.XX:/home/ec2-user/projects/certs/
% scp -i /path/to/XXX.pem /any/path/to/test20240214.private.key ec2-user@XX.XX.XX.XX:/home/ec2-user/projects/certs/
```
:::note info
IPアドレスをXX.XX.XX.XXと記載します。ご自身の環境に合うものを入れてください。
:::


#### 権限変更
証明書なので、安全のため所有者にのみ読み取り権限を与えておきましょう。
```bash
$ chmod -R 400 /home/ec2-user/project/cert/
```

### 4-3. プロジェクトファイルの送付
クライアントPCからPythonソースコードを下記のコマンドで送付します。
```zsh
% scp -i /path/to/XXX.pem /any/path/to/iot_core_sub.py ec2-user@XX.XX.XX.XX:/home/ec2-user/project/mqtt_subscribe/
% scp -i /path/to/XXX.pem /any/path/to/.env ec2-user@XX.XX.XX.XX:/home/ec2-user/project/mqtt_subscribe/
```

### 4-4. シェルスクリプトの送付
#### クライアントPCからの送付
クライアントPCからscpでEC2のworkに入れます。
```zsh
% scp -i /path/to/XXX.pem /any/path/to/iot_core_subscribe.sh ec2-user@XX.XX.XX.XX:/home/ec2-user/work/
```

#### 起動確認
送付したシェルスクリプトがエラーなく実行できることを確認します。
```bash
$ cd /home/ec2-user/work/
$ chmod a+x ./iot_core_subscribe.sh
$ ./iot_core_subscribe.sh
```
上記でエラーなくプログラムが実行できればOK。

#### 置き場変更
実行ファイルを別の場所に保管します。(rootユーザ管理のディレクトリに移動)
```bash
$ sudo mv ./iot_core_subscribe.sh /opt/mqtt_subscribe/
```

### 4-3. サービスファイルの送付

#### クライアントPCからの送付
一旦、クライアントPCからscpでEC2のworkに入れます。
```zsh
% scp -i /path/to/XXX.pem /any/path/to/iot_core_subscribe.service ec2-user@XX.XX.XX.XX:/home/ec2-user/work/
```
次にEC2で下記のコマンドを実行します。
```bash
$ sudo mv ~/work/iot_core_subscribe.service /etc/systemd/system/
$ chmod 644 /etc/systemd/system/iot_core_subscribe.service
```


## 5.systemdへの登録

### 5-1.サービスの登録
下記コマンドを実行してLinuxサービスへの登録を行います。
```bash
$ sudo systemctl daemon-reload
$ sudo systemctl enable iot_core_subscribe.service
$ sudo systemctl start iot_core_subscribe.service
```

無事にサービス登録し、起動しているかを下記コマンドで確認します。
```bash
$ systemctl status iot_core_subscribe.service
```

### 5-2.再起動して確認

再起動コマンドを実行します。
```bash
$ sudo shutdown -r now
```

再起動後にサービスが起動しているのかを下記コマンドで確認します。
```bash
$ systemctl status iot_core_subscribe.service
```

statusがrunningとなっており、テストでメッセージを送ってみて受け取れたらOKです。
・・・だいぶ、記事がマニアックな内容になってきました。(需要あるのか？）

# 参考サイト
- https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/7/html/system_administrators_guide/sect-managing_services_with_systemd-unit_files
- https://qiita.com/DQNEO/items/0b5d0bc5d3cf407cb7ff


# 最後に
IoT用に作成したプログラムをLinuxのサービス化するということをやってみました。
ただ、せっかくクラウドというサービスが存在するのでこちらを有効活用するということもやってみたいです。
今後、MQTTの書き込み〜DB保管までをAWSサービスに置き換えた形の検証もしてみたいと思います。