CloudWatch RUM に設定値を渡す仕組み

このページは、RUM の設定値がサーバーからブラウザへ渡り、aws-rum-web が初期化されるまでの流れをコードで説明する。実際の ID やアカウント情報はこのページに含めていない。

1. CDK が設定値をコンテナの環境変数に入れる

App Monitor ID と Identity Pool ID はデプロイ時に決まるため、CDK がリソースの参照をそのまま環境変数に渡す。イメージには値を焼き込まないので、同じイメージを別のアカウントやリージョンでも使える。

// cdk/lib/web-stack.ts (ECS Express Mode service)
primaryContainer: {
  image: image.imageUri,
  containerPort: 3000,
  environment: [
    { name: 'RUM_APP_MONITOR_ID', value: appMonitor.attrId },
    { name: 'RUM_IDENTITY_POOL_ID', value: identityPool.ref },
    { name: 'RUM_REGION', value: this.region },
    { name: 'APP_VERSION', value: image.assetHash.slice(0, 12) },
  ],
},

2. server function が環境変数を読む

createServerFn の処理は常にサーバーで実行される。process.env を読むコードはブラウザ向けのバンドルに入らない。

// src/lib/rum-config.ts
export const getRumConfig = createServerFn({ method: 'GET' }).handler(
  (): RumConfig => ({
    appMonitorId: process.env.RUM_APP_MONITOR_ID ?? '',
    identityPoolId: process.env.RUM_IDENTITY_POOL_ID ?? '',
    region: process.env.RUM_REGION ?? process.env.AWS_REGION ?? 'us-east-1',
    appVersion: process.env.APP_VERSION ?? '0.0.0',
  }),
)

3. ルートの loader が SSR 時に値を HTML へ埋め込む

ルートの loader が server function を呼び、結果を SSR の出力に含める。ブラウザはハイドレーション時にこの値を受け取る。staleTime: Infinity により、画面遷移のたびに再取得することはない。

// src/routes/__root.tsx
export const Route = createRootRoute({
  loader: () => getRumConfig(),
  staleTime: Infinity,
  component: RootComponent,
})

function RootComponent() {
  const rumConfig = Route.useLoaderData()
  return (
    <RootDocument>
      <RumInit config={rumConfig} />
      ...
    </RootDocument>
  )
}

4. ブラウザだけで aws-rum-web を初期化する

aws-rum-web はインスタンス生成時に window と document を参照するので、SSR 中には実行できない。useEffect の中で動的に import することで、ブラウザでだけ評価する。window.__awsRum の確認で二重初期化を防ぎ、初期化に失敗してもアプリは止めない。

// src/components/RumInit.tsx
useEffect(() => {
  if (!config.appMonitorId || !config.identityPoolId || window.__awsRum) return

  let cancelled = false
  import('aws-rum-web')
    .then(({ AwsRum }) => {
      if (cancelled || window.__awsRum) return
      window.__awsRum = new AwsRum(config.appMonitorId, config.appVersion, config.region, {
        sessionSampleRate: 1,
        identityPoolId: config.identityPoolId,
        endpoint: `https://dataplane.rum.${config.region}.amazonaws.com`,
        telemetries: ['errors', 'performance', ['http', { recordAllRequests: true }]],
        allowCookies: true,
        enableXRay: false,
      })
    })
    .catch((err) => console.warn('CloudWatch RUM init failed', err))

  return () => {
    cancelled = true
  }
}, [config])

5. ブラウザは Cognito の一時認証情報で送信する

identityPoolId だけを渡すと、aws-rum-web は Cognito の enhanced flow で未認証 ID の一時認証情報を取得し、PutRumEvents に SigV4 署名を付ける。ゲストロールが許可するのは、この App Monitor への PutRumEvents だけである。

// cdk/lib/web-stack.ts (Cognito guest role)
const guestRole = new iam.Role(this, 'RumGuestRole', {
  assumedBy: new iam.WebIdentityPrincipal('cognito-identity.amazonaws.com', {
    StringEquals: { 'cognito-identity.amazonaws.com:aud': identityPool.ref },
    'ForAnyValue:StringLike': { 'cognito-identity.amazonaws.com:amr': 'unauthenticated' },
  }),
})
guestRole.addToPolicy(
  new iam.PolicyStatement({ actions: ['rum:PutRumEvents'], resources: [appMonitorArn] }),
)

ブラウザに渡る値について

App Monitor ID、Identity Pool ID、リージョン、アプリのバージョンはブラウザが RUM にデータを送るために必要な値であり、ページの HTML に含まれる。これらはシークレットではない。送信できる操作はゲストロールの権限で PutRumEvents に限られる。