[Grafana] DynamoDB の時系列データを見るための API を、設定ファイル 1 つで CDK デプロイできるようにしてみました

[Grafana] DynamoDB の時系列データを見るための API を、設定ファイル 1 つで CDK デプロイできるようにしてみました

DynamoDB に溜めたセンサーデータを、どのGrafana環境でも使える汎用的なAPIを通じてダッシュボードで可視化する方法を紹介します。複数テーブルや異なるキー設計に対応し、ダウンサンプリングや最新値取得などの機能を備えた実装をクイックスタートから運用ポイントまで解説します。
2026.11.15

1 はじめに

製造ビジネステクノロジー部の平内(SIN)です。

DynamoDB に溜めたセンサーデータを、Grafana のダッシュボードで見たいという要件はよくあると思います。
しかし、Grafana Labs 公式の DynamoDB データソースは Enterprise プラグインのため、OSS 版の Grafana では使えず、Amazon Managed Grafana や Grafana Cloud でも別途ライセンスが必要です。

https://grafana.com/grafana/plugins/grafana-dynamodb-datasource/

Grafana から DynamoDB を見る方法には、すでにいくつかの選択肢があります。
それぞれが、どの環境で使えるかを表にしてみました。

方法 セルフホストの OSS Grafana Amazon Managed Grafana Grafana Cloud 使える条件・使えない理由
公式プラグイン ❌ ✅ ✅ OSS 版には入れられない。マネージド環境では Enterprise プラグインのライセンスが必要で、5 人で Amazon Managed Grafana は月 270 USD、Grafana Cloud Pro は月 129 USD + 従量
コミュニティ版プラグイン ✅ ❌ ❌ プラグインカタログにないため、マネージド環境には入れられない
プラグインの自作 ✅ ❌ ❌ 署名のないプラグインは、マネージド環境には入れられない
既存の API 版
DevelopersIO、サーバーワークス
✅ ✅ ✅ API を用意すれば、Grafana の環境を問わず使える

今回は、Grafana の環境を問わず使えることを目指し、既存の API 版に次の機能を加え、テーブルの形が違っても使える汎用的な API を作成してみました。

機能 内容
データセット定義 複数のテーブルや用途を、設定ファイル 1 つで公開できる
キー設計と時刻の形式 プレフィックス付きのキー、複合ソートキー、日付で分けたキー、GSI、epoch 秒 / ミリ秒 / ISO 8601 とタイムゾーンを扱える
ダウンサンプリング API 側で集計し、長い期間でも返す点の数を抑えられる
最新値 キーごとの最新値を、わずかな読み込みで返せる(Stat パネルやアラート向け)
ガードレール 期間、件数、ページ数、消費 RCU に上限を設け、Scan はしない
認証 IAM 認証(SigV4)と API キーを選べる
使い始めの補助 既存テーブルから設定の下書きを作るコマンド、接続確認のコマンド、ローカルの Grafana 一式

使い方は 2〜5 章、仕組みは 6 章にまとめました。使うだけであれば、6 章は読み飛ばしてください。

この API は、設定ファイルを 1 つ書くだけで CDK からデプロイでき、Grafana 側は Grafana Labs が保守している Infinity プラグインでつなぎます。
下の画像は、今回用意したサンプルデータ(3 部屋の温度と湿度を 7 日分作った疑似データ)を、この API 経由で表示してみたダッシュボードです。

https://github.com/furuya02/dynamodb-timeseries-api

2 クイックスタート

サンプルのテーブルとデータを使って、ローカルの Grafana にダッシュボードを表示するところまで進めます。
コードは、以下に置きました。

https://github.com/furuya02/timeseries-dataset-api-for-amazon-dynamodb

Node.js 22.23.4、pnpm 10.24.0、Docker(ローカルの Grafana に使用)、AWS CLI v2 と、CDK の bootstrap が済んだ AWS アカウントが必要です。
aws-cdk-lib 2.276.0、Grafana OSS 13.3.1、Infinity プラグイン 4.1.5 で確認しています。

(1) デプロイ

$ git clone https://github.com/furuya02/timeseries-dataset-api-for-amazon-dynamodb.git
$ cd timeseries-dataset-api-for-amazon-dynamodb
$ pnpm install
$ pnpm cdk-deploy -c sample=true -c grafanaUser=true

-c sample=true でサンプルテーブルを、-c grafanaUser=true で Grafana 用の IAM ユーザーを一緒に作ります。
-c の指定は前回の内容を引き継がないので、デプロイのたびに付けます。付け忘れると、そのリソースは削除されます。

この時点で作られるリソースは、次のとおりです。

Endpoint が API の URL です。
Grafana 用の IAM ユーザーには、この API を呼ぶ権限だけが付いています。

(2) サンプルデータの投入

$ pnpm seed

3 部屋分の温度と湿度を、1 分おきに 7 日分書き込みます。30 秒ほどで終わります。
実際のセンサーに近づけるため、日ごとの天気の違い、平日の日中の空調、部屋ごとの振れ幅の違い、ときどきの一時的な高温、短いデータの欠けを入れています。

(3) 接続の確認

Grafana の設定に進む前に、API が応答するかを確かめます。

$ pnpm check

OK で始まる 4 行(データセット一覧、キー一覧、直近 1 時間の集計、最新値)が表示されれば、接続できています。
途中で失敗すると、NG で始まる行に HTTP のステータスと確かめる点が表示され、そこで止まります。

サンプルデータは pnpm seed を実行した時刻までなので、1 時間以上たってから実行すると、直近 1 時間の点の数は 0 になります(API の問題ではありません)。

CloudFormation の出力からエンドポイントを読み、SigV4 で署名して API を呼びます。
Grafana で表示できないとき、API 側の問題かどうかを切り分けるのにも使えます。

(4) Grafana の起動

Grafana 用のアクセスキーを作り、環境変数に入れてから起動します。

$ aws iam create-access-key --user-name timeseries-dataset-api-grafana
$ export TSAPI_AWS_ACCESS_KEY_ID=AKIA...
$ export TSAPI_AWS_SECRET_ACCESS_KEY=...
$ pnpm grafana-config
$ pnpm grafana-up

pnpm grafana-config は、デプロイの出力から Grafana のデータソース設定を作ります。
アクセスキーの値はファイルに書かず、Grafana の起動時に環境変数から読ませています。

pnpm grafana-up は、grafana/docker-compose.yml を使って、手元の Docker に Grafana OSS 13.3.1 のコンテナを起動します(Infinity プラグインは起動時に入ります)。
起動していることは、次のコマンドで確かめられます。

$ docker compose -f grafana/docker-compose.yml ps
NAME                               IMAGE                    COMMAND     SERVICE   CREATED          STATUS          PORTS
timeseries-dataset-api-grafana-1   grafana/grafana:13.3.1   "/run.sh"   grafana   17 minutes ago   Up 17 minutes   127.0.0.1:3000->3000/tcp

127.0.0.1 だけで待ち受けているので、ほかのマシンからは開けません。

ブラウザで http://localhost:3000/ を開くと、Grafana のホーム画面が表示されます。

左のメニューの「Dashboards」から、「timeseries-dataset-api」のフォルダを開きます。

「部屋の温度と湿度(timeseries-dataset-api サンプル)」を選びます。

温度・湿度の推移、最新値のゲージとバーゲージ、日ごとの最高気温の棒グラフを並べたサンプルのダッシュボードが表示されます。

Grafana 13.3.1 では匿名のユーザーを Admin にできないため、閲覧はログインなし、編集は Grafana に最初から用意されている管理者(ユーザー admin、パスワード admin)で、右上の「Sign in」からログインする形にしています。

3 Grafana の設定

ここでは、データソースの設定と、パネルの作り方を説明します。
2 章のローカルの Grafana には、ここで説明する設定がすでに入っているので、画面を開いて確かめていきます。

設定の画面は、ログインしないと表示されません。
右上の「Sign in」から、ユーザー admin、パスワード admin でログインします(パスワードの変更を求められたら「Skip」で進めます)。

(1) データソース

ログインすると、左のメニューに「Connections」が表示されます。

「Connections」→「Data sources」を選ぶと、データソースの一覧が表示されます。

「timeseries-dataset-api」を選び、画面の左のメニューで「Authentication」を開くと、次の画面になります。

設定している項目は、次のとおりです(「タブ」は、画面の左のメニューです)。

タブ 項目 値
URL, Headers & Params Base URL デプロイの出力 Endpoint
Authentication Auth type AWS
Authentication Authentication Provider Access and secret key
Authentication Region / Service デプロイの出力 Region / execute-api
Authentication Access Key ID / Secret Access Key Grafana 用 IAM ユーザーのアクセスキー

既存の Grafana で設定するとき(クイックスタートでは不要です)

先に Infinity プラグインを入れます(「Connections」→「Add new connection」で Infinity を検索してインストール)。
そのあと「Connections」→「Data sources」→「Add new data source」で Infinity を選び、上の表のとおりに入力します。
このとき、Service は monitoring が入った状態で始まるので、execute-api に書き換えます。そのままだと署名が合わず、403 になります。
EC2 上の Grafana であれば、アクセスキーの代わりにインスタンスロールを使う「AWS SDK Default」も選べます。

(2) パネル

サンプルのダッシュボードを開き、「温度(表示間隔ごとの平均)」のパネルにマウスを乗せ、右上のメニュー(︙)から「Edit」を選びます。

パネルの編集画面の下に、クエリの設定が表示されます。

サンプルのパネルには、次のクエリが設定されています(画面の上から順に並べています)。

項目 値
Type JSON
Parser JSONata
Source URL
Format Time Series
Method GET
URL query

Parser は、API の応答(JSON)を読む方法です。
Infinity の Parser には、ブラウザで動くものと、Grafana のサーバーで動くもの(JSONata、JQ)があります。
アラートはブラウザを開いていなくても Grafana のサーバーで評価されるため、サーバーで動く JSONata を選んでいます。

URL のパラメータは、「Headers, Request params」を押すと表示されます。

パラメータ 値 意味
dataset room_temp データセットの名前(config/datasets.yaml に書いたもの)
key ${key:csv} 部屋(画面左上の「部屋」で選んだもの)
from ${__from} 表示している期間の始まり(画面右上の「Last 24 hours」など)
to ${__to} 表示している期間の終わり
interval ${__interval_ms} 集計の間隔(期間とパネルの幅から Grafana が決める)
agg avg 集計の方法(平均)
fields temperature 返す値の列(湿度のパネルでは humidity)

${…} は Grafana の変数で、画面での選択に合わせて、Grafana が実際の値に置き換えてから API を呼びます。

「Parsing options & Result fields」を開くと、応答のどこを読むかが表示されます。

項目 値
Rows/Root rows
Columns time(Time ( UNIX ms ))、key(String)、temperature(Number)

応答は「1 行 = 時刻 × キー」の形で返ります。Format を Time Series にしているので、Infinity が部屋ごとの系列に分けてくれます。
見終わったら、何も変えずに編集を終えます。
パネルの編集画面の右上にある「Back」で、ダッシュボードに戻ります。

ダッシュボードの右上にある「Exit edit」を押します。

「Unsaved changes」と表示されたら、「Discard」を選びます(サンプルのダッシュボードは変更しません)。

(3) よく使う指定

サンプルのダッシュボードのほかのパネルで使っている指定です。

目的 パラメータ パネル
推移 interval と agg(avg / min / max / sum / count / last) Time series
最新値 latest=1 Gauge / Bar gauge / Stat
1 日ごとの集計 interval=1d、tz=Asia/Tokyo Bar chart
複数キーを列で並べる format=wide Table
読み込みの情報 Rows/Root を meta にする Table

部屋のプルダウン(テンプレート変数)は、URL を keys、パラメータを dataset=room_temp にしたクエリで作れます。

(4) アラート

アラートは Grafana のサーバーで評価されるため、ダッシュボードとは使える変数が違います。

変数 ダッシュボード アラート
${__from} / ${__to} ✅ ✅
${__interval_ms} ✅
テンプレート変数(${key:csv}) ✅

アラートのクエリでは、key に値を直接書き、latest=1 か interval=5m のような固定値を使います。
サンプルには「最新の温度が 29℃ を超えた状態が 5 分続いたら」というルールを入れてあり、部屋ごとに評価されます。
ルールは、左のメニューの「Alerting」→「Alert rules」で見られます。
サンプルデータは pnpm seed を実行した時刻までなので、1 時間ほどたつとデータがなくなり、ホーム画面の「Firing alerts」に DatasourceNoData が表示されます(API の問題ではありません)。

4 設定一覧

この章は、クイックスタートのあとに、自分のテーブルや環境に合わせて設定を変えるときのための一覧です。
クイックスタートでは何も変えないので、読み飛ばして 5 章の片付けに進んでも問題ありません。

(1) datasets.yaml

config/datasets.yaml に書く項目です。公開するテーブルとキーの形を、データセットごとに書きます。

項目 既定値 内容
table / index 必須 / なし テーブル名、GSI 名
partitionKey.name / template 必須 / {key} キーの属性名と組み立て方
partitionKey.bucket / timezone none / UTC 日付で分けたキー(daily / monthly)と、その日付のタイムゾーン
sortKey.name / format 必須 時刻の属性名と形式(epoch_s / epoch_ms / iso8601)
sortKey.prefix / timezone 空 / UTC 複合ソートキーのプレフィックス、オフセットのない ISO 8601 の解釈
keys.pattern / values どちらか必須 受け付けるキー
keys.list なし キー一覧(プルダウン)用の登録アイテム
fields(name / alias / type / unit / scale) name は必須 返す属性。ここにない属性は返さない
limits.maxRange / maxKeys 31d / 20 1 回で問い合わせられる期間とキーの数
limits.maxItems / maxPages / maxRcu 50000 / 100 / 1000 読む件数、ページ数、消費 RCU の上限
cacheTtlSeconds 30 応答キャッシュの秒数

(2) デプロイオプション

pnpm cdk-deploy に -c で付ける項目です(2 章の -c sample=true も、その 1 つです)。

-c の名前 既定値 内容
exposure restPublic 公開形態(restPublic / functionUrl / restPrivate)
auth iam 認証(iam / apiKey)
grafanaUser / grafanaRoleTrust なし Grafana 用の IAM ユーザー、Assume Role 用のロール
logRetentionDays 7 ログの保持日数
sample false サンプルテーブルを作る

API のパラメータを含む全項目の説明は、リポジトリの手順書にまとめています。

Github docs/ja

(3) 自分のテーブルで使う

ここまでは clone したままの設定で試しました。自分のテーブルで使うときは、config/datasets.yaml にデータセットを足して、2 章と同じ -c を付けてデプロイし直します。

$ pnpm init-dataset --table <テーブル名>

pnpm init-dataset は、テーブルを読んで datasets.yaml の下書きを表示します(先頭の 100 件だけを読み、ファイルは書き換えません)。
シングルテーブル設計、日付で分けたキー、GSI など、テーブルの形ごとの書き方は、リポジトリの手順書にまとめています。

Github docs/ja/02-datasets.md

5 片付け

この章は、2 章のクイックスタートで作ったもの(AWS のリソース、アクセスキー、ローカルの Grafana)を削除する手順です。
クイックスタートを試し終わったら、ここだけは読み飛ばさずに実行してください。

今回の手順(デプロイ、約 3 万件のサンプルデータの投入、数百回の問い合わせ)で、AWS の費用は $0.1 未満でした(東京リージョン)。
どれも使った分だけの課金なので置いておくだけの費用はほとんどかかりませんが、試し終わったら次の手順で削除してください。

$ pnpm grafana-down
$ aws iam list-access-keys --user-name timeseries-dataset-api-grafana --query 'AccessKeyMetadata[].AccessKeyId' --output text
$ aws iam delete-access-key --user-name timeseries-dataset-api-grafana --access-key-id AKIA...
$ pnpm cdk-destroy -c sample=true -c grafanaUser=true

2 行目のコマンドで、2 章で作ったアクセスキーの ID(AKIA で始まる値)を調べて、3 行目に指定します。
アクセスキーは CloudFormation の管理外で作っているので、先に削除しておく必要があります。残っていると、IAM ユーザーを削除できずにスタックの削除が失敗します。
自分のテーブルは削除されません。CDK は、テーブルを名前で参照しているだけです。

6 仕組み

この章は、API の作りと、実際に測った結果、作る途中でつまずいた点をまとめたものです。
使うだけであれば読まなくても問題ありません。中身を知りたいときや、自分で手を入れるときに読んでください。

(1) 構成

Lambda には、synth の時点で検証した datasets.yaml を JSON にして同梱しています。
環境変数は合計 4KB の上限があり、esbuild の define を使う方法では正規表現の $ がシェルを通るときに壊れたため、この形にしました。

(2) Query API

PartiQL を Grafana からそのまま通す案も考えましたが、Query API に絞りました。
DynamoDB Local で確かめると、パーティションキーの条件がない PartiQL は、エラーにならずに裏で全件を読みます。
Query API はパーティションキーの条件が必須なので、フルスキャンの問い合わせを受け付けずに済みます。

ISO 8601 のソートキーは、保存値の末尾(Z、.000Z、+09:00)がテーブルによって違います。
範囲の上限に最大の文字(U+FFFF)を付けて少し広めに読み、Lambda で時刻を読み直して正確に絞り込んでいます。

(3) 実測

サンプル(3 部屋 × 7 日分、1 分おき)で、問い合わせ方による違いを測りました。

問い合わせ 読んだ件数 消費 RCU 応答時間 応答サイズ
1 キー 1 時間 生データ 57 0.5 219ms 4KB
3 キー 24 時間 5 分平均 4,320 25.5 254ms 64KB
3 キー 7 日 生データ 30,231 177 1,226ms 2,173KB
3 キー 7 日 1 時間平均 30,240 177 1,016ms 43KB
3 キー 7 日 最新値 3 1.5 69ms 0KB

集計しても、DynamoDB から読む件数と消費 RCU は変わりません。減るのは応答の大きさと Grafana の負荷です。
読み込みを減らせるのは、最新値(latest=1)と期間の短縮だけでした。

なお、7 日分の生データで応答が 2.1MB になったことから、上限を 1 つ加えています。
Lambda の同期呼び出しの応答は 6MB までで、超えると API Gateway が理由のわからない 502 を返します。そこで、5.5MB を超える応答は、集計や期間の短縮を案内する 400 にしました。

https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html

(4) つまずいた点

作業中につまずいた点を、原因と対処の形でまとめます。

現象 原因 対処
Grafana に「400 Bad Request」とだけ表示され、理由が出ない Infinity は 400 以上の応答の本文を読まない(4.1.5 のソースで確認) 理由は CloudWatch Logs の構造化ログと pnpm check で確かめる
アラートだけ 400 になる アラートでは ${__interval_ms} とテンプレート変数が置き換わらない その旨を伝えるエラーメッセージを API に追加
init-dataset のキーが 1 つしか見つからない Scan の先頭は 1 つのパーティションに偏る 見つかったキーの形から pattern を作るようにした

Infinity の該当箇所は、pkg/infinity/client.go の応答の処理です。

https://github.com/grafana/grafana-infinity-datasource/blob/v4.1.5/pkg/infinity/client.go

関数 URL の IAM 認証は、同じアカウントの IAM ユーザーであれば、InvokeFunctionUrl の権限だけで呼べました。
AWS CDK の grantInvokeUrl は InvokeFunction も付けるため両方が必要だと考えていましたが、権限の組み合わせごとに IAM ユーザーを作って確かめた結果は次のとおりです。

権限 結果
なし 403
InvokeFunctionUrl だけ 200
InvokeFunction だけ(関数 URL 経由の条件付き) 403
両方 200

なお、最初は 1 人のユーザーのポリシーを切り替えて試しましたが、変更の反映に数分かかって結果が揺れ、判断できませんでした。
権限の組み合わせを比べるときは、組み合わせごとに別の IAM ユーザーを用意するのが確実だと感じました。

7 最後に

実務で使うときに意識しておきたい点を 3 つ挙げます。

  • テーブル設計の段階で、Grafana から見る単位(キーと時刻)を決めておくと、datasets.yaml が素直に書けます。時刻をソートキーにしていないテーブルは、GSI を足すのが近道だと思います
  • ダッシュボードの期間とパネルの数は、そのまま消費 RCU になります。長い期間を見るパネルは集計と組み合わせ、Stat パネルやアラートは latest=1 にするのが良いと思います
  • 理由が Grafana に表示されないエラーがあるため、運用では CloudWatch Logs の構造化ログを Logs Insights で見る習慣を付けておくと安心です

DynamoDB に溜まっている IoT データを、まずは手元の Grafana で眺めてみる、といった用途のたたき台になれば嬉しいです。

この記事で使用したコードは、以下に置きました。

https://github.com/furuya02/timeseries-dataset-api-for-amazon-dynamodb

8 参考リンク

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事