Grafana CloudからDSQLへ接続してみた
こんにちは。産業支援グループ製造ビジネステクノロジー部のakkyです。
今回はGrafanaからAmazon Aurora DSQLに接続してみましたのでご紹介します。
今回はGrafana Cloudを使用しましたが、考え方はGrafanaのセルフホスト版でも同じです。
GrafanaからDSQLへ接続する際の課題
Amazon Aurora DSQL(以下DSQL)はPostgreSQL互換のサーバレスRDBです。
GrafanaはデータソースとしてPostgreSQLへ接続することができるので、単純にDSQLを立ち上げてID、パスワードを設定すればそれで良いのではないかと思われるかもしれませんが、実際にやってみると以下のような課題があります。
- DSQLへの接続には、ID/パスワードではなく一時的に使用できるトークンを発行してそれをパスワードとして使用する必要がある。
- トークンの有効期限は最大1週間なので、定期的にトークンを更新する必要がある。
解決策
以下のような構成でGrafana Cloudの接続情報を定期的に更新するようにしてみます。

- LambdaがDSQLからトークンを取得
- 取得したトークンをGrafana Cloudのデータソースのパスワードとして設定
- LambdaをEventbridge Schedulerで定期的に実行させる
Grafanaに設定するトークンは読み込み専用にするために、カスタムデータベースロールを使います。
以下の記事が参考になります。
Amazon Aurora DSQLのカスタムデータベースロールを使ってLambdaから接続してみた #AWSreInvent
準備と実装
Grafanaの準備
まずPostgreSQL用のデータソースを保存します。この時点では接続できないので名前以外は設定しません。
次にGrafanaのホーム→管理→ユーザーとアクセス→サービスアカウントからサービスアカウントを発行し、AWS側のパラメータストアに値を保存してください。
/grafana-dsql/api-urlGrafanaのAPIエンドポイント。https://xxxxxxxx.grafana.net/api/形式/grafana-dsql/service-account-token作成したAPIトークン/grafana-dsql/datasource-uidデータソースのUID(編集ページのアドレスhttps://xxxx.grafana.net/connections/datasources/edit/abcdef01234の末尾abcdef01234の部分)
CDK実装
コンストラクトは以下の様にしました。
Lambdaの環境変数に与えているGRAFANA_APPEND_CONFIG_BODYで追加設定を行います。userプロパティにはDSQLで設定したカスタムデータベースロールのユーザー名を指定してください。
import * as cdk from "aws-cdk-lib";
import { Duration } from "aws-cdk-lib";
import * as dsql from "aws-cdk-lib/aws-dsql";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as aws_lambda_nodejs from "aws-cdk-lib/aws-lambda-nodejs";
import * as ssm from "aws-cdk-lib/aws-ssm";
import { Construct } from "constructs";
export class DsqlStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const dsqlCluster = new dsql.CfnCluster(this, "DsqlCluster", {
deletionProtectionEnabled: false, // 削除保護を無効化(デフォルトは有効)
tags: [
// クラスター名の指定
{
key: "Name",
value: "GrafanaDsqlCluster",
},
],
});
// DSQL クラスターの ID をパラメータストアに保存
new ssm.StringParameter(this, "DsqlClusterIdParameter", {
parameterName: "/grafana-dsql/cluster-id",
stringValue: dsqlCluster.ref,
description: "DSQL Cluster ID for Grafana integration",
});
// DSQL クラスターの ID の出力
new cdk.CfnOutput(this, "DsqlClusterId", {
value: dsqlCluster.ref,
});
const tokenUpdateLambda = new aws_lambda_nodejs.NodejsFunction(this, "TokenUpdateLambda", {
functionName: "grafana-dsql-token-update-lambda",
entry: "../token-update-lambda/src/index.ts",
handler: "handler",
runtime: lambda.Runtime.NODEJS_22_X,
architecture: lambda.Architecture.ARM_64,
environment: {
DSQL_CLUSTER_ID: dsqlCluster.ref,
DSQL_TOKEN_EXPIRES_IN: "172800", // 2日
DSQL_TOKEN_STORAGE_PARAMETER_NAME: "/grafana-dsql/user-auth-token",
GRAFANA_APPEND_CONFIG_BODY: JSON.stringify({
url: `${dsqlCluster.ref}.dsql.${this.region}.on.aws`,
user: "member", // ユーザー名を設定
}),
GRAFANA_API_URL_PARAMETER_NAME: "/grafana-dsql/api-url",
GRAFANA_TOKEN_PARAMETER_NAME: "/grafana-dsql/service-account-token",
GRAFANA_DATASOURCE_UID_PARAMETER_NAME: "/grafana-dsql/datasource-uid",
},
timeout: Duration.seconds(30),
initialPolicy: [
new iam.PolicyStatement({
actions: ["dsql:DbConnect"],
resources: [dsqlCluster.attrResourceArn],
}),
new iam.PolicyStatement({
actions: ["ssm:GetParameter", "ssm:GetParameters", "ssm:PutParameter"],
resources: [`arn:aws:ssm:${this.region}:${this.account}:parameter/grafana-dsql/*`],
}),
],
});
// Lambda Role ARN の出力
new cdk.CfnOutput(this, "LambdaRoleArn", {
value: tokenUpdateLambda.role!.roleArn,
});
// Lambda Role ARN をパラメータストアに保存
new ssm.StringParameter(this, "LambdaRoleArnParameter", {
parameterName: "/grafana-dsql/lambda-role-arn",
stringValue: tokenUpdateLambda.role!.roleArn,
description: "Lambda Role ARN for Grafana integration",
});
// EventBridge Rule: 1日1回Lambda関数を実行
const dailyRule = new events.Rule(this, "DailyTokenUpdateRule", {
ruleName: "grafana-dsql-daily-token-update",
description: "Trigger token update Lambda once per day",
schedule: events.Schedule.cron({
hour: "0",
minute: "0",
}),
});
dailyRule.addTarget(new targets.LambdaFunction(tokenUpdateLambda));
}
}
Lambdaの実装
TypeScriptで実装しました。
トークンの取得には@aws-sdk/dsql-signerライブラリが必要です。@aws-sdk/client-dsqlではないので注意してください。
動作は大まかに以下の様になっています
- Grafanaで指定されたデータソースの現在の設定を取得
- DSQLトークンを取得
- Grafanaへ新たなトークンを設定
最初に現在の設定を取得しているのは、Grafanaのデータソース設定にはversionというプロパティがあり、再設定時にこの値を設定する必要があるためです(値のインクリメントはGrafana側で行われます)
Grafanaのデータソース設定APIのドキュメントには詳細に記載されていませんが、パスワード(トークン)はsecureJsonDataプロパティに設定する必要があります。
import { SSMClient, GetParametersCommand, GetParametersResult, PutParameterCommand } from "@aws-sdk/client-ssm";
import { DsqlSigner } from "@aws-sdk/dsql-signer";
import { Context, EventBridgeEvent } from "aws-lambda";
function findOrThrowException(parameters: GetParametersResult, parameterName: string): string {
const parameter = parameters.Parameters?.find((parameter) => parameter.Name === parameterName);
if (!parameter) {
throw new Error(`Parameter not found in SSM Parameter Store: ${parameterName}`);
}
return parameter.Value as string;
}
async function getParameters(): Promise<{
tokenParameter: string;
apiUrlParameter: string;
datasourceUidParameter: string;
}> {
const tokenParameterName = process.env.GRAFANA_TOKEN_PARAMETER_NAME!;
const apiUrlParameterName = process.env.GRAFANA_API_URL_PARAMETER_NAME!;
const datasourceUidParameterName = process.env.GRAFANA_DATASOURCE_UID_PARAMETER_NAME!;
const ssmClient = new SSMClient();
const command = new GetParametersCommand({
Names: [tokenParameterName, apiUrlParameterName, datasourceUidParameterName],
WithDecryption: true,
});
const response = await ssmClient.send(command);
const tokenParameter = findOrThrowException(response, tokenParameterName);
const apiUrlParameter = findOrThrowException(response, apiUrlParameterName);
const datasourceUidParameter = findOrThrowException(response, datasourceUidParameterName);
return {
tokenParameter,
apiUrlParameter,
datasourceUidParameter,
};
}
async function grafanaGetSingleDatasourceByUid(
grafanaApiUrl: string,
grafanaDatasourceUid: string,
grafanaToken: string,
): Promise<any> {
// データソースをIDで指定するエンドポイントははdeprecatedなので、UIDで指定するバージョンを使う
const grafanaApiEndpoint = `${grafanaApiUrl}datasources/uid/${grafanaDatasourceUid}`;
// まず現在の設定を取得する
// https://grafana.com/docs/grafana-cloud/developer-resources/api-reference/http-api/data_source/#get-a-single-data-source-by-uid
const response = await fetch(grafanaApiEndpoint, {
method: "GET",
headers: {
Authorization: `Bearer ${grafanaToken}`,
Accept: "application/json",
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`fetch GET Error datasources/uid/:uid: ${response.statusText}`);
}
const data = await response.json();
console.log("grafanaGetSingleDatasourceByUid response:", data);
return data;
}
async function grafanaUpdateExistingDatasource(
grafanaApiUrl: string,
grafanaDatasourceUid: string,
grafanaToken: string,
body: any,
): Promise<any> {
// https://grafana.com/docs/grafana-cloud/developer-resources/api-reference/http-api/data_source/#update-an-existing-data-source
const grafanaApiEndpoint = `${grafanaApiUrl}datasources/uid/${grafanaDatasourceUid}`;
const response = await fetch(grafanaApiEndpoint, {
method: "PUT",
headers: {
Authorization: `Bearer ${grafanaToken}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`fetch PUT Error datasources/uid/:uid: ${response.statusText}`);
}
const data = await response.json();
console.log(data);
return data;
}
async function dsqlGetUserAuthToken(
dsqlHostName: string,
dsqlTokenExpiresIn: number,
dsqlTokenStorageParameterName: string,
): Promise<string> {
const signer = new DsqlSigner({
hostname: dsqlHostName,
region: process.env.AWS_REGION,
expiresIn: dsqlTokenExpiresIn,
});
const token = await signer.getDbConnectAuthToken();
const ssmClient = new SSMClient();
const command = new PutParameterCommand({
Name: dsqlTokenStorageParameterName,
Value: token,
Type: "SecureString",
Overwrite: true,
});
await ssmClient.send(command);
return token;
}
export async function handler(event: EventBridgeEvent<string, any>, context: Context): Promise<string> {
const grafanaAppendConfigBody = JSON.parse(process.env.GRAFANA_APPEND_CONFIG_BODY!);
const dsqlClusterId = process.env.DSQL_CLUSTER_ID!;
const dsqlTokenExpiresIn = parseInt(process.env.DSQL_TOKEN_EXPIRES_IN!);
const dsqlTokenStorageParameterName = process.env.DSQL_TOKEN_STORAGE_PARAMETER_NAME!;
const dsqlHostName = `${dsqlClusterId}.dsql.${process.env.AWS_REGION}.on.aws`;
const { tokenParameter, apiUrlParameter, datasourceUidParameter } = await getParameters();
const token = await dsqlGetUserAuthToken(dsqlHostName, dsqlTokenExpiresIn, dsqlTokenStorageParameterName);
// 現在のデータソースの設定を取得(versionプロパティを取得する必要があるため)
const currentSettings = await grafanaGetSingleDatasourceByUid(
apiUrlParameter,
datasourceUidParameter,
tokenParameter,
);
// 新たな設定を生成
const newSettings = { ...currentSettings, secureJsonData: { password: token }, ...grafanaAppendConfigBody };
// データソースの設定を更新
await grafanaUpdateExistingDatasource(apiUrlParameter, datasourceUidParameter, tokenParameter, newSettings);
return "success";
}
DSQLの設定
デプロイしたらマネコンからDSQLへ管理者権限でログインし、前述の記事を参考に、PostgreSQLのスキーマ/テーブル/データ設定を行い、さらにカスタムデータベースロールを設定してください。
AWS IAM GRANT <USERNAME> TOで設定するのはCDKのLambdaRoleArnで出力された値です。
Grafanaデータソースの設定
CDKでデプロイしたら定期的にLambdaが実行され、Grafanaデータソースへパスワードが設定されるようになっているはずです。
実行タイミングの関係で強制的に設定したい場合はパラメータストア(/grafana-dsql/user-auth-token)からトークンをコピペしてきてパスワードに設定します。
【設定ポイント】
- Database nameは
postgresで固定 - TLS/SSL Modeは
require - Max lifetimeはトークンの更新タイミングより短くしたほうがよさそう
ここまでの設定でExploreを使ってGrafanaからDSQLに接続できるようになっているはずです。

まとめ
DSQLへGrafanaから接続するための設定を自動更新する方法をご紹介しました。
DSQLにはRDSとは異なる制限があるものの、サーバレスで従量課金なので、SQLが必要な場面では重宝するはずです。
以上






