Grafana CloudからDSQLへ接続してみた

Grafana CloudからDSQLへ接続してみた

2025.11.07

こんにちは。産業支援グループ製造ビジネステクノロジー部の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の接続情報を定期的に更新するようにしてみます。

aws

  1. LambdaがDSQLからトークンを取得
  2. 取得したトークンをGrafana Cloudのデータソースのパスワードとして設定
  3. LambdaをEventbridge Schedulerで定期的に実行させる

Grafanaに設定するトークンは読み込み専用にするために、カスタムデータベースロールを使います。
以下の記事が参考になります。

Amazon Aurora DSQLのカスタムデータベースロールを使ってLambdaから接続してみた #AWSreInvent

準備と実装

Grafanaの準備

まずPostgreSQL用のデータソースを保存します。この時点では接続できないので名前以外は設定しません。

次にGrafanaのホーム→管理→ユーザーとアクセス→サービスアカウントからサービスアカウントを発行し、AWS側のパラメータストアに値を保存してください。

  • /grafana-dsql/api-url Grafanaの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ではないので注意してください。

動作は大まかに以下の様になっています

  1. Grafanaで指定されたデータソースの現在の設定を取得
  2. DSQLトークンを取得
  3. 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に接続できるようになっているはずです。

chrome_erSl5GwuAc

まとめ

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

以上

この記事をシェアする

FacebookHatena blogX