From c9e83f63482976d317adcf917260ef3ca3c55db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Mon, 16 Mar 2026 14:29:56 +0100 Subject: [PATCH 1/2] feat: create grafana component --- src/components/grafana/builder.ts | 35 ++++++++ src/components/grafana/grafana.ts | 136 ++++++++++++++++++++++++++++++ src/components/grafana/index.ts | 2 + 3 files changed, 173 insertions(+) create mode 100644 src/components/grafana/builder.ts create mode 100644 src/components/grafana/grafana.ts diff --git a/src/components/grafana/builder.ts b/src/components/grafana/builder.ts new file mode 100644 index 00000000..00a45124 --- /dev/null +++ b/src/components/grafana/builder.ts @@ -0,0 +1,35 @@ +import * as pulumi from '@pulumi/pulumi'; +import { Grafana } from './grafana'; + +export class GrafanaBuilder { + private name: string; + private prometheusConfig?: Grafana.PrometheusConfig; + private tags?: Grafana.Args['tags']; + + constructor(name: string) { + this.name = name; + } + + public withPrometheus(config: Grafana.PrometheusConfig): this { + this.prometheusConfig = config; + + return this; + } + + public withTags(tags: Grafana.Args['tags']): this { + this.tags = tags; + + return this; + } + + public build(opts: pulumi.ComponentResourceOptions = {}): Grafana { + return new Grafana( + this.name, + { + prometheus: this.prometheusConfig, + tags: this.tags, + }, + opts, + ); + } +} diff --git a/src/components/grafana/grafana.ts b/src/components/grafana/grafana.ts new file mode 100644 index 00000000..7b381c5f --- /dev/null +++ b/src/components/grafana/grafana.ts @@ -0,0 +1,136 @@ +import * as aws from '@pulumi/aws'; +import * as pulumi from '@pulumi/pulumi'; +import * as grafana from '@pulumiverse/grafana'; + +const GRAFANA_CLOUD_AWS_ACCOUNT_ID = '008923505280'; + +export namespace Grafana { + export type PrometheusConfig = { + prometheusEndpoint: pulumi.Input; + region: pulumi.Input; + }; + + export type Args = { + prometheus?: PrometheusConfig; + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; + }; +} + +export class Grafana extends pulumi.ComponentResource { + prometheusDataSource?: grafana.oss.DataSource; + + constructor( + name: string, + args: Grafana.Args, + opts: pulumi.ComponentResourceOptions = {}, + ) { + super('studion:grafana:Grafana', name, {}, opts); + + if (args.prometheus) { + const ampRole = this.createAmpRole(name, args.tags); + this.createPrometheusDataSource(name, args.prometheus, ampRole); + } + + this.registerOutputs(); + } + + private getStackSlug(): string { + const grafanaUrl = process.env.GRAFANA_URL; + + if (!grafanaUrl) { + throw new Error('GRAFANA_URL environment variable is not set.'); + } + + return new URL(grafanaUrl).hostname.split('.')[0]; + } + + private createAmpRole(name: string, tags: Grafana.Args['tags']) { + const stackSlug = this.getStackSlug(); + const grafanaStack = grafana.cloud.getStack({ slug: stackSlug }); + + const ampRole = new aws.iam.Role( + `${name}-amp-role`, + { + assumeRolePolicy: pulumi.jsonStringify({ + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + AWS: `arn:aws:iam::${GRAFANA_CLOUD_AWS_ACCOUNT_ID}:root`, + }, + Action: 'sts:AssumeRole', + Condition: { + StringEquals: { + 'sts:ExternalId': pulumi.output(grafanaStack).id, + }, + }, + }, + ], + }), + tags, + }, + { parent: this }, + ); + + new aws.iam.RolePolicy( + `${name}-amp-policy`, + { + role: ampRole.id, + policy: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: [ + 'aps:GetSeries', + 'aps:GetLabels', + 'aps:GetMetricMetadata', + 'aps:QueryMetrics', + ], + Resource: '*', + }, + ], + }), + }, + { parent: this }, + ); + + return ampRole; + } + + private createPrometheusDataSource( + name: string, + config: Grafana.PrometheusConfig, + ampRole: aws.iam.Role, + ) { + const stackSlug = this.getStackSlug(); + + const plugin = new grafana.cloud.PluginInstallation( + `${name}-prometheus-plugin`, + { + stackSlug, + slug: 'grafana-amazonprometheus-datasource', + version: 'latest', + }, + { parent: this }, + ); + + this.prometheusDataSource = new grafana.oss.DataSource( + `${name}-prometheus-datasource`, + { + type: 'grafana-amazonprometheus-datasource', + url: config.prometheusEndpoint, + jsonDataEncoded: pulumi.jsonStringify({ + sigV4Auth: true, + sigV4AuthType: 'grafana_assume_role', + sigV4Region: config.region, + sigV4AssumeRoleArn: ampRole.arn, + }), + }, + { dependsOn: [plugin], parent: this }, + ); + } +} diff --git a/src/components/grafana/index.ts b/src/components/grafana/index.ts index e549b9ad..4009f0f7 100644 --- a/src/components/grafana/index.ts +++ b/src/components/grafana/index.ts @@ -1 +1,3 @@ export * as dashboard from './dashboards'; +export { Grafana } from './grafana'; +export { GrafanaBuilder } from './builder'; From 1df213564f56ea0b42bb3d52033e0573216a45c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Mon, 16 Mar 2026 14:37:57 +0100 Subject: [PATCH 2/2] docs: add grafana aws account hardcoded value comment --- src/components/grafana/grafana.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/grafana/grafana.ts b/src/components/grafana/grafana.ts index 7b381c5f..e9b90c27 100644 --- a/src/components/grafana/grafana.ts +++ b/src/components/grafana/grafana.ts @@ -2,6 +2,7 @@ import * as aws from '@pulumi/aws'; import * as pulumi from '@pulumi/pulumi'; import * as grafana from '@pulumiverse/grafana'; +// Fixed AWS account ID owned by Grafana Cloud, used to assume roles in customer accounts. const GRAFANA_CLOUD_AWS_ACCOUNT_ID = '008923505280'; export namespace Grafana {