AWS IoT Core に MQTT を送る最小構成 ― まずは作ってみる体験するのが一番早い
書き始めたら長かったので、IoT Core 2回わけます
最小構成で MQTT を Pub/Sub する ところまでやります。「IoT のクラウド連携って何やってるの?」を、最短経路で体感する記事です。
※昔買ったけど家で眠ってる可能性があるRaspberryPi 3B+ と AWS IoT Device SDK for Python を使って、ラズパイのデータを送る、反対に命令を取りに行くなんてことをすると意外と幅が広がります。
はじめに:この連載のコンセプト
「IoT に興味はあるけど、回路図とかハンダごてとか出てきて挫折した」というソフトウェアエンジニア向けに、アナログハードウェアの知識ほぼゼロで Matter / AWS IoT Core を触れるようになる 連載をしています。
| 回 | テーマ | 学べること |
|---|---|---|
| 第1回(本記事) | Pi 3B+ → IoT Core 最小Pub/Sub | Thing / 証明書 / ポリシー / MQTT / Device SDK |
| 第2回 | IoT Core の三種の神器:Rules Engine + Device Shadow + Jobs | クラウド統合、双方向制御、リモート運用 |
| おまけ | Pi 3B+ を Matter Controller 化して IoT Core と繋ぐ | エンドツーエンドの実践サンプル |
第1回のゴールは「デバイスからクラウドにデータが届く」という、IoT のいちばん根っこの体験を最短で踏むこと。第2回以降で「届いたデータの活用」と「クラウドからの制御」を考えます。
自作した場合
色々作る検討する必要がある:
-
- 1: デバイス上のカメラで画像ファイルを作成。センサーから値を取得
- 2: デバイス上の 自前アプリケーション(デーモン化したものを用意)
- 3: ApiGatewayに認証した状態でアクセス(Cognitoでするか書き込みOnlyで行うか悩む)
- 昔やったのはSoracomを通信機器を搭載して、SIMを利用した閉域網状態をつくることでアクセス経路を絞るだった
- 4: 画像ファイルをアップロードする一応秘匿データなので、Transfer Family for SFTP で SSH 鍵を使ってファイル転送する(Apiatewayだと画像ファイルの処理だけでえらく時間がかかってしまうため)
- 5: Tx for SFTP から転送されたファイルは 日付時刻などのフォルダを作りアップロード
- 6: センサーのデータを、Lambdaへ送る
- 7: ファイル、センサーデータの書き込み完了をログで出力(屋外や通信をするので取りこぼしがないかの証跡をとる)
- 8: センサーデータの格納 (Dynamoのキー:DeviceID、Value:センサーデータ{json形式})
- 9: 繰り返す(通信切断時やモジュールの更新を行いたい場合は #10へ)
- 10: raspberrypi 上のアップロードソース更新の場合は現地いくかSSHで更新を行う
楽しかったけど・・・止まった時、そのデバイスをつくったら(東京)、そのデバイスを札幌でうごかすことになるってきいて現地に補助してくれる人がいないと「止まってしまった場合の対処なども考えないといけない」ので、ちょっとリリース前は何重にもとっておかないといけないのが大変な経験でした。
何が動くようになるか(完成イメージ)
最終完成系は同じものを作ること。 アイコンの色を見ていただくとわかるように、IoT Core (GreenGlassもあるけど)、AWS側のIoTのコンソールでほぼほぼ機能と設定が済んじゃうのが楽なところ。
-
-
1: デバイス上のカメラで画像ファイルを作成。センサーから値を取得
-
2: デバイス上の 自前アプリケーション(デーモン化したものを用意:これは実は同じようなもの、ここにAWSのRoleをつかってAWSへのサービスを簡素に実装できるDevice SDK を利用する)
-
3: IoTCore がデバイスの入り口になり、証明書やデバイスのポリシとアクセス時に紐づけることでAWSリソースへのアクセスが可能になる。
-
4: DeviceシャドウというRaspberryPiをAWS上で操作できるインスタンス。オフラインや作業系で止まった時にも使える(ただしこれは作らないでも動作はする)
-
5: カメラで撮った画像のアップロード、読み出したデータの送信をおこなう(一つにまとまってて便利)
-
6: Topcという送信先:送信や受信の向き先を設計検討しやすくなる
-
7: 値の保存:センサーデータの格納 (Dynamoのキー:DeviceID、Value:センサーデータ{json形式})
-
8: チャネルというものでDataの保存先を変更することが可能になる
-
9,10: もちろんS3や他のリソースへのアクセスも可能(デバイスポリシのおかげ)
-
繰り返す(通信切断時やモジュールの更新を行いたい場合は #10へ)
-
※ raspberrypi 上のアップロードソース更新の場合は現地いくかSSHで更新を行う
-
[Raspberry Pi 3B+] [AWS Cloud]
│
│ bridge.py (Python)
│ └ AWS IoT Device SDK v2
│ └ MQTT/TLS over 8883
│
└────────────────────────────────────→ AWS IoT Core
│
├─ MQTT Test Client で受信確認
└─ CloudWatch Logs (任意)
10秒に1回、Pi から hello/raspi トピックに JSON を Publish し、AWS のコンソールで受信できる、というシンプルなゴール。
用意するもの
| 項目 | 推奨スペック | 価格目安 |
|---|---|---|
| Raspberry Pi 3B+ | 既にお持ちのものでOK | — |
| microSD カード | 16GB以上、A1/A2クラス | 1,000円程度 |
| USB電源 | 5V/2.5A以上 | 1,500円程度 |
| 作業用PC | SSH/SCP できればOS問わず | — |
| AWSアカウント | クレジットカード登録済み | 今回の費用はほぼ0円 |
Tips: お財布のことを正直に言うと
本記事の操作だけなら IoT Core の無料利用枠内に余裕で収まります(東京リージョン:接続100万分/月、メッセージ50万件/月まで無料)。検証中の数百件なら誤差です。
Step 0: Raspberry Pi OS を準備する
microSD への書き込み
- Raspberry Pi Imager を作業PCにインストール
- Raspberry Pi OS Lite (64-bit) を選択
- 右下の歯車アイコンで以下を事前設定:
- ホスト名:
raspberrypi - SSH を有効化(パスワード認証 or 公開鍵)
- Wi-Fi の SSID/パスワード
- ロケール(タイムゾーン: Asia/Tokyo、キーボード: jp)
- ホスト名:
Tips: 64bit を選ぶ理由
Pi 3B+ の CPU(Cortex-A53)は ARMv8 で 64bit 対応です。AWSが配布する arm64 パッケージや Python wheel がそのまま使えるので、32bit を選ぶ理由は基本的にありません。
起動 → SSH ログイン
## [SPEC] 作業PCからPiに接続
ssh pi@raspberrypi.local
## [NOTE] 初回ログイン後、OSを最新にする
sudo apt update && sudo apt upgrade -y
sudo apt install -y git python3-pip
Step 1: AWS 側で IoT Core のリソースを作る
ここからが AWS 側の準備本番。Thing、証明書、ポリシー の3点セットを作ります。
1-1. IoT Core の用語整理(超ざっくり)
| 用語 | 役割 | 例えるなら |
|---|---|---|
| Thing | デバイスを表す論理オブジェクト | 社員名簿の「行」 |
| 証明書(X.509) | デバイス認証用の鍵 | 社員証 |
| ポリシー | デバイスができることの定義 | アクセス権限の規程 |
| エンドポイント | MQTT接続先のホスト名 | 会社の住所 |
3つは「証明書 ↔ Thing」「証明書 ↔ ポリシー」の関係で アタッチ されることで初めて使えるようになります。
1-2. 作業PCに AWS CLI を入れる
## [SPEC] AWS CLI v2 (macOSの例)
brew install awscli
## [NOTE] 認証情報を設定
aws configure
## Access Key ID: [IAM ユーザのキーを入力]
## Secret Access Key: [IAM ユーザのシークレットを入力]
## Default region: ap-northeast-1
## Default output: json
1-3. エンドポイントを取得
## [SPEC] アカウント固有のIoTエンドポイントを確認
aws iot describe-endpoint \
--endpoint-type iot:Data-ATS \
--region ap-northeast-1
## [MEMO] 出力例:
## {
## "endpointAddress": "xxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com"
## }
このアドレスは後で Pi 側のコードに埋めるのでメモっておきます。
1-4. Thing を作る
## [SPEC] デバイスを表す Thing オブジェクトを作成
aws iot create-thing --thing-name raspi-bridge
## [MEMO] 出力例:
## {
## "thingName": "raspi-bridge",
## "thingArn": "arn:aws:iot:ap-northeast-1:111122223333:thing/raspi-bridge",
## "thingId": "xxxx-xxxx-xxxx"
## }
1-5. 証明書と鍵ペアを発行する
## [SPEC] 鍵ペアを生成し、証明書をactiveで登録、ローカルにファイル保存
mkdir -p ~/certs && cd ~/certs
aws iot create-keys-and-certificate \
--set-as-active \
--certificate-pem-outfile cert.pem \
--public-key-outfile pub.key \
--private-key-outfile priv.key \
> cert-meta.json
## [NOTE] 出力の certificateArn を控える
cat cert-meta.json | grep certificateArn
Tips: 秘密鍵は今この瞬間しか取得できません
priv.keyはこの create-keys-and-certificate の戻り値でしか入手できず、AWS 側にも保管されません。この場で安全な場所に保存 してください。失くしたら新しい証明書を作り直しです。
1-6. ポリシーを作る
## [SPEC] 最小権限ポリシー定義
cat > policy.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["iot:Connect"],
"Resource": "arn:aws:iot:ap-northeast-1:*:client/raspi-bridge"
},
{
"Effect": "Allow",
"Action": ["iot:Publish"],
"Resource": "arn:aws:iot:ap-northeast-1:*:topic/hello/*"
},
{
"Effect": "Allow",
"Action": ["iot:Subscribe"],
"Resource": "arn:aws:iot:ap-northeast-1:*:topicfilter/hello/*"
},
{
"Effect": "Allow",
"Action": ["iot:Receive"],
"Resource": "arn:aws:iot:ap-northeast-1:*:topic/hello/*"
}
]
}
EOF
## [SPEC] ポリシー作成
aws iot create-policy \
--policy-name raspi-bridge-policy \
--policy-document file://policy.json
Tips: 最初は
Resource: "*"で書きたくなるけど…
検証中こそ最小権限で書く癖をつけると、本番化のときに直す手間がゼロになります。とくにiot:Connectの Resource は client/ の形式で絞り込めるので、別のデバイスが同じ証明書を使い回す事故も防げます。
1-7. アタッチ作業(地味だけど重要)
## [NOTE] 環境変数で証明書ARNをセット
CERT_ARN=$(cat cert-meta.json | jq -r '.certificateArn')
## [SPEC] 証明書 ← ポリシー をアタッチ
aws iot attach-policy \
--policy-name raspi-bridge-policy \
--target $CERT_ARN
## [SPEC] Thing ← 証明書 をアタッチ
aws iot attach-thing-principal \
--thing-name raspi-bridge \
--principal $CERT_ARN
コラム: ここまでで 7コマンド + JSON 1ファイル。「ちょっと多いな」と感じるかもしれませんが、IoT Core では 証明書とポリシーをデバイスごとに紐付ける ことでセキュリティを担保しているので、避けて通れないステップです。なお、台数が10台を超えてくると Fleet Provisioning という仕組みでこの登録作業を自動化できます。
Step 2: 証明書を Pi に転送する
## [SPEC] AmazonのルートCA証明書もダウンロード
curl -o root-CA.pem \
https://www.amazontrust.com/repository/AmazonRootCA1.pem
## [SPEC] Pi へ scp(3ファイル)
scp cert.pem priv.key root-CA.pem pi@raspberrypi.local:~/certs/
## [NOTE] Pi 側で配置確認
ssh pi@raspberrypi.local "mkdir -p ~/certs && ls -la ~/certs/"
Tips: 秘密鍵のパーミッション
Pi 側でchmod 600 ~/certs/priv.keyをしておくと、誤って読まれる事故が減ります。地味だけど大事。
Step 3: Pi に AWS IoT Device SDK を入れる
## [SPEC] Pi 側で実行
ssh pi@raspberrypi.local
## [NOTE] PEP 668対策で --break-system-packages を使う
## (本来は venv を切るのが望ましい。下記Tips参照)
pip3 install awsiotsdk --break-system-packages
Tips: venv で隔離するのが本来は正しい
Bookworm 以降の Raspberry Pi OS は PEP 668 でシステムPython保護がかかっています。本気のプロダクション運用なら:python3 -m venv ~/iot-env source ~/iot-env/bin/activate pip install awsiotsdkただし systemd 化のときに ExecStart のパスを
/home/pi/iot-env/bin/python3に書き換える必要があり、初心者が詰まりやすい部分です。本記事では分かりやすさ優先で--break-system-packagesを使います。
Step 4: Pub/Sub スクリプトを書く
bridge.py を作成
## [SPEC] /home/pi/bridge.py
## [NOTE] 10秒ごとに hello/raspi へJSONをPublishするだけのデーモン
## [REF] https://github.com/aws/aws-iot-device-sdk-python-v2
import time
import json
import socket
from awscrt import mqtt
from awsiot import mqtt_connection_builder
## [SPEC] 接続設定 -- ここを自分の環境に書き換える
ENDPOINT = "xxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com"
CERT_PATH = "/home/pi/certs/cert.pem"
KEY_PATH = "/home/pi/certs/priv.key"
CA_PATH = "/home/pi/certs/root-CA.pem"
CLIENT_ID = "raspi-bridge" ## [MEMO] ポリシーで絞ったclient ID
TOPIC = "hello/raspi"
## [NOTE] 接続イベント時のコールバック
def on_connection_interrupted(connection, error, **kwargs):
print(f"[WARN] Connection interrupted: {error}")
def on_connection_resumed(connection, return_code, session_present, **kwargs):
print(f"[INFO] Connection resumed: code={return_code} session={session_present}")
def on_message_received(topic, payload, **kwargs):
print(f"[RECV] {topic}: {payload.decode()}")
## [SPEC] MQTTコネクション構築 (mTLS / TLSv1.2)
mqtt_conn = mqtt_connection_builder.mtls_from_path(
endpoint=ENDPOINT,
cert_filepath=CERT_PATH,
pri_key_filepath=KEY_PATH,
ca_filepath=CA_PATH,
client_id=CLIENT_ID,
clean_session=False,
keep_alive_secs=30,
on_connection_interrupted=on_connection_interrupted,
on_connection_resumed=on_connection_resumed,
)
print(f"[INFO] Connecting to {ENDPOINT}...")
mqtt_conn.connect().result()
print("[INFO] Connected!")
## [SPEC] 自分の publish 内容を自分でも受信して確認する
subscribe_future, _ = mqtt_conn.subscribe(
topic=TOPIC,
qos=mqtt.QoS.AT_LEAST_ONCE,
callback=on_message_received,
)
subscribe_future.result()
print(f"[INFO] Subscribed to {TOPIC}")
## [SPEC] メインループ -- 10秒ごとにPublish
try:
while True:
payload = json.dumps({
"hostname": socket.gethostname(),
"ts": int(time.time()),
"msg": "hello from Pi 3B+",
})
mqtt_conn.publish(
topic=TOPIC,
payload=payload,
qos=mqtt.QoS.AT_LEAST_ONCE,
)
print(f"[SEND] {payload}")
time.sleep(10)
except KeyboardInterrupt:
print("[INFO] Disconnecting...")
mqtt_conn.disconnect().result()
動かしてみる
## [SPEC] エンドポイントとパスを書き換えたら起動
python3 /home/pi/bridge.py
成功すると、
[INFO] Connecting to xxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com...
[INFO] Connected!
[INFO] Subscribed to hello/raspi
[SEND] {"hostname": "raspberrypi", "ts": 1747...}
[RECV] hello/raspi: {"hostname": "raspberrypi", "ts": 1747...}
[SEND] {"hostname": "raspberrypi", "ts": 1747...}
のように、送信した直後に同じメッセージを受信 します(自分のサブスクライブが拾っているため)。
Step 5: AWS コンソールで受信を確認する
- AWS コンソールで AWS IoT > MQTT テストクライアント を開く
- トピックをサブスクライブする タブ
- トピックフィルター:
hello/raspi - サブスクライブ ボタン
Pi が動いていれば、10秒おきにメッセージが画面に流れてくるはず:
{
"hostname": "raspberrypi",
"ts": 1747459200,
"msg": "hello from Pi 3B+"
}
ここで 「IoT デバイスからクラウドにデータが届いた」 という最初のマイルストーンを達成🎉
Step 6: systemd でデーモン化する
このままだと SSH を切ったら止まる(Ctrl+C や何かしらのエラーでとまると死亡)するため、Linuxの上にデーモンとして起動させる。 [Service]のところが大事で、主に ExecStart でPythonから作ったプログラムを起動させるところ。
systemd unit を書く
## [SPEC] /etc/systemd/system/raspi-bridge.service
[Unit]
Description=Raspi IoT Bridge to AWS IoT Core
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/pi/bridge.py
Restart=on-failure
RestartSec=10
User=pi
WorkingDirectory=/home/pi
## [NOTE] journald にログを集約
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
有効化
sudo nano /etc/systemd/system/raspi-bridge.service ## 上記を貼り付け
sudo systemctl daemon-reload
sudo systemctl enable --now raspi-bridge.service
## [NOTE] 状態確認
sudo systemctl status raspi-bridge.service
## [NOTE] ログ確認
sudo journalctl -u raspi-bridge.service -f
これで Pi の再起動後も自動的にメッセージ送信が再開 される。
ハマりポイント集
第1回の構成で詰まりやすい所をまとめておきます。
ハマり1: RuntimeError: AWS_ERROR_MQTT_UNEXPECTED_HANGUP
- 8割の原因は ポリシー不足
- IoT コンソール > Security > Policies で実際にアタッチされているか確認
- 試しに
Resource: "*"の超ゆるポリシーに置き換えて切り分け(切り分け後は元に戻す)
ハマり2: socket.gaierror: [Errno -2] Name or service not known
- エンドポイント文字列のタイポが多い
- Pi 側
ping xxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.comで疎通確認
ハマり3: ssl.SSLCertVerificationError
- AmazonRootCA1.pem を取得し忘れ or パスが違う
-
wc -l ~/certs/root-CA.pemで行数が出れば取得済み
ハマり4: client_id が衝突する
- 同じ ClientId で複数同時接続すると、後勝ちで前の接続が切断される(これが Hangup の原因にもなりやすい)
- スクリプトを2つ起動してテストしたいなら ClientId を変える + ポリシーも調整
ハマり5: 8883/TCP が出ていけない
- 会社や学校のネットワークだとMQTT用ポート(8883)が塞がれていることがある
- 自宅のWi-Fi(普通のルータ)なら基本問題なし
「振り返って整理」: ここまで何をやったか
| ステップ | 内容 | コマンド/操作数 |
|---|---|---|
| 1 | OS書き込み + SSH設定 | Imager 1回 |
| 2 | AWS CLI 設定 | aws configure |
| 3 | エンドポイント取得 | CLI 1回 |
| 4 | Thing 作成 | CLI 1回 |
| 5 | 証明書発行 | CLI 1回 |
| 6 | ポリシー作成 | CLI 1回 + JSONファイル |
| 7 | ポリシー → 証明書 アタッチ | CLI 1回 |
| 8 | Thing → 証明書 アタッチ | CLI 1回 |
| 9 | 証明書 3ファイル を Pi に scp | scp 1回 |
| 10 | SDK インストール | pip 1回 |
| 11 | bridge.py を書く | コード 60行程度 |
| 12 | systemd unit を書く | 設定ファイル 1個 |
| 13 | enable & start | systemctl 2回 |
ざっと13ステップ、所要時間1〜2時間 で、Pi 3B+ がクラウドに繋がるところまできた!🎉
What's next
ただし! リソースの利用で、IoTからのデータの取り込み方や通信のセキュリティ設計、接続の相互通信のプロトコルを考えイベントの種類などが考慮されたものがIoTCoreの素晴らしいところなので、設計インタフェースの基本を共有モデルでいうところのAWSに任せたことになる。
興味のある、データを集め、データを利用するというビジネスロジック/ビジネスモデルに注力できる。
ここまでで 「IoTデバイスがクラウドに繋がる」 という最大のハードルは越えました。でも、これだけだと「データを送れただけ」で、IoT としては入り口に立ったばかりです。たとえばこんな「次の悩み」が出てきます:
- Question1: 受信したデータを 保存・可視化・通知に流したい → どうつなげる?
- Question2: クラウドから デバイスにON/OFF指示を送りたい (双方向制御) → どうやる?
- Qurstion3: デバイスのコードを リモートから更新したい (毎回SSHは無理) → どう仕組み化する?
この3つの悩みに答えるのが、IoT Core が標準で持つ「3大機能」 それが第2回のテーマ、IoT Core を「ただの MQTT ブローカー」から IoTバックエンド に進化させる3つの機能を扱って実装する:
- Rules Engine — 受信したメッセージを CloudWatch Logs / DynamoDB / Lambda に自動で流す
- Device Shadow — デバイスとクラウドで状態を双方向同期(オフライン耐性つき)
- IoT Jobs — リモートからデバイスにコマンドを送って実行させる(コード更新の運用基本形)
この3つを押さえると 「個人〜業務で数台規模なら、これ以上のサービスは基本要らない」 という状態にたどり着ける。
What's Next with...:
ちょっと前まで分析するにはそれなりの「分析のゴールを決めて、収集し、見えてくる結果を考える」必要があったのだけど
さらにさらに、分析など色々なことできるけど、これと連動させて AmazonQuickで自然言語で分析させるなんてことも考えられる、そんなことできようになる
データをエクスポートして AmazonQuikcから読み出せば良いってところまでできたら素敵!
参考リンク
- IoTCore する時にこのワークショップはやったほうがいい:
- AWS IoT Device SDK for Python v2 (GitHub)
- Tutorial: Connecting a device to AWS IoT Core (AWS Docs)
- AWS IoT Core 料金 (東京リージョン)
- MQTT QoS levels in AWS IoT
この記事は IoT Core 連載シリーズ(全3回)の第1回です。次回「IoT Core の三種の神器:Rules Engine + Device Shadow + Jobs」もぜひ。フィードバックやハマった話があればコメントください🙌

