Real-User Monitoring for Amazon CloudWatch (RUM) をCDKで設定し、ReactのSPAでページ遷移時のイベントを取得したメモ

## 概要
AWSでGoogle Analyticsのようなログ収集ができる[CloudWatch RUM(Realtime User Monitor)](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch-RUM.html)を試してみる。
CDKで作成したいが、[L2](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_rum-readme.html)はまだ未対応の模様(2022/12/31)。[L1](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_rum.CfnAppMonitor.html)で作成できるか試してみる。

ReactのSPAでのページ遷移時にイベントが送信されるところまで確認する。

[ソースコード:CDK](https://github.com/hibohiboo/aws-cdk-v2/blob/feature/addRUM/cdk/lib/cloudfront-stack.ts)
[ソースコード: client - rum](https://github.com/hibohiboo/aws-cdk-v2/blob/feature/addRUM/client/src/domain/rum/aws-rum.ts)
[ソースコード: client - router](https://github.com/hibohiboo/aws-cdk-v2/blob/feature/addRUM/client/src/router/RoutesApp.tsx)

## CDK構成

```diff:cdk/lib/cloudfront-stack.ts
import * as core from 'aws-cdk-lib'
import * as s3 from 'aws-cdk-lib/aws-s3'
import * as cf from 'aws-cdk-lib/aws-cloudfront'
import * as iam from 'aws-cdk-lib/aws-iam'
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'
import * as cognito from '@aws-cdk/aws-cognito-identitypool-alpha';
import { Construct } from 'constructs'
import { basePath } from '../constants/paths'
import { aws_rum as rum } from 'aws-cdk-lib';

interface Props extends core.StackProps {
  bucketName: string
  identityName: string
  defaultCachePolicyName: string
  distributionName: string
  projectNameTag: string
}

export class AWSCloudFrontStack extends core.Stack {
  constructor(scope: Construct, id: string, props: Props) {
    super(scope, id, props)
    // CloudFront オリジン用のS3バケットを作成
    const bucket = this.createS3(props.bucketName)
    // CloudFront で設定する オリジンアクセスアイデンティティ を作成
    const identity = this.createIdentity(bucket, props.identityName)
    // S3バケットポリシーで、CloudFrontのオリジンアクセスアイデンティティを許可
    this.createPolicy(bucket, identity)
    // CloudFrontディストリビューションを作成
    const distribution = this.createCloudFront(bucket, identity, props)

    // 確認用にCloudFrontのURLに整形して出力
    new core.CfnOutput(this, `${props.distributionName}-top-url`, {
      value: `https://${distribution.distributionDomainName}/${basePath}`,
    })

    new core.CfnOutput(this, `${props.distributionName}-distribution-id`, {
      value: `${distribution.distributionId}`,
    })
+    this.createRUM(distribution.distributionDomainName, props.env!.account!, props.env!.region!);
    core.Tags.of(this).add('Project', props.projectNameTag)
  }

  private createS3(bucketName: string) {
    // s省略
  }

  private createIdentity(bucket: s3.Bucket, identityName: string) {
    // 省略
  }

  private createPolicy(bucket: s3.Bucket, identity: cf.OriginAccessIdentity) {
    // 省略
  }

  private createCloudFront(
    bucket: s3.Bucket,
    identity: cf.OriginAccessIdentity,
    props: {
      defaultCachePolicyName: string
      distributionName: string
    },
  ) {
   // 省略
  }


+  private createRUM(domain: string, accountId: string, region: string) {
+    const { identityPool, role } = this.createIdentityPool();
+    const cfnAppMonitor = new rum.CfnAppMonitor(this, 'MyCfnAppMonitor', {
+      domain,
+      name: `rum-for-croudront-${domain}`,
+      appMonitorConfiguration: {
+        identityPoolId: identityPool.identityPoolId,
+        guestRoleArn: role.roleArn,
+        // サンプルレート。デフォルトは0.1(10%)。
+        // 1にするとセッションの100%をモニタする。高くするほどコストがかかるので注意。
+        // 今回は動作確認のため1を設定
+        sessionSampleRate: 1,
+      },
+    });
+    // CognitoIdのIAMロールにRUMへの送信権限を付与
+    role.addToPrincipalPolicy(new iam.PolicyStatement({
+      effect: iam.Effect.ALLOW,
+      actions: ['rum:PutRumEvents'],
+      resources: [`arn:aws:rum:${region}:${accountId}:appmonitor/${cfnAppMonitor.name}`],
+    }));
+    return cfnAppMonitor
+  }


+  private createIdentityPool(): {
+    identityPool: cognito.IIdentityPool
+    role: iam.IRole,
+  } {
+    const identityPool = new cognito.IdentityPool(this, 'IdentityPool', {
+      allowUnauthenticatedIdentities: true,
+    });
+    return { identityPool, role: identityPool.unauthenticatedRole };
+  }


}
```

## デプロイ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/d4b94ea6-6955-b00c-d958-1d9003345f45.png)

RUMがデプロイされたことをコンソールから確認できた。


## アプリケーションに反映。
コンソールの「JavaScriptを表示」からスニペットを取得する。
[react](https://github.com/aws-observability/aws-rum-web/blob/main/docs/cdn_react.md)を参考に設定。

```ts:client/src/domain/rum/aws-rum.ts
import { AwsRum, AwsRumConfig } from 'aws-rum-web';
let cwr: AwsRum | null = null;
try {
  const config: AwsRumConfig = {
    sessionSampleRate: 1,
    guestRoleArn: "arn:aws:iam::00000000:role/AWSCloudFrontStack-IdentityPoolUnauthenticatedRole-1U7PMD1IH60RX",
    identityPoolId: "ap-northeast-1:00000000-0000-0000-0000-xxxxxx",
    endpoint: "https://dataplane.rum.ap-northeast-1.amazonaws.com",
    telemetries: [],
    allowCookies: false,
    enableXRay: false
  };

  const APPLICATION_ID = '00000000-0000-0000-0000-xxxxxxxxxx';
  const APPLICATION_VERSION = '1.0.0';
  const APPLICATION_REGION = 'ap-northeast-1';


  const awsRum: AwsRum = new AwsRum(
    APPLICATION_ID,
    APPLICATION_VERSION,
    APPLICATION_REGION,
    config
  );
  cwr = awsRum

} catch (error) {
  // Ignore errors thrown during CloudWatch RUM web client initialization
}
export const getRum = () => {
  return cwr
}
```

```diff:client/src/routes/RoutsApp.tsx

const App: React.FC = () => {
+  const location = useLocation()


+  React.useEffect(() => {
+    if (import.meta.env.DEV) return
+    const cwr = getRum()
+    if (!cwr) return
+    cwr.recordPageView(location.pathname)
+  }, [location])

  return (
    <Routes>
      <Route path="/admin" element={<AdminRoute />}>
        <Route path="/admin/top" element={<Top />} />
      </Route>
      <Route path="/private" element={<PrivateRoute />}>
        <Route path="/private/top" element={<Top />} />
      </Route>

      <Route element={<PublicRoute />}>
        <Route path="/" element={<Redirect />} />
        <Route path="/login" element={<Login />} />
        <Route path="/test" element={<Test />} />
      </Route>
    </Routes>
  )
}
```

## デプロイ
### クライアントの確認
RUM用の通信が行われていることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/a0c74c69-ca14-7498-92a3-6a9bbb4f147e.png)

### コンソールの確認
反映までは少し時間がかかる。
イベントが送信されることを確認できた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/a58ca8d9-6dea-3013-8c9f-7ce0e2f031b8.png)




## 参考

[AWS 公式 - CloudWatch RUM を使用する](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch-RUM.html)
[新機能 – Amazon CloudWatch RUM をご紹介](https://aws.amazon.com/jp/blogs/news/cloudwatch-rum/)
[One Observability Workshop - 新しい CloudWatch RUM アプリケーションを作成する](https://catalog.workshops.aws/observability/ja-JP/rum/create)
[CloudWatch新機能増えすぎ問題。Synthetics？ RUM？ Evidently？？](https://qiita.com/takuyayone/items/0a3d605eaf38dd39c2f8) ... Next.jsでの設定例
[One Observability Workshop をやってみた](https://qiita.com/kyamamoto9120/items/6d88196a1b46672c3aa7)
[アプリケーションのクライアント側のパフォーマンスをモニタリングする Amazon CloudWatch RUM の紹介](https://qiita.com/yu-yama-sra/items/c7f7ca0e45d75ba3908c)
[CloudWatch RUMでReactアプリのパフォーマンスやエラーのモニタリングを試してみた](https://zenn.dev/teketek/articles/cloudwatch-rum-react)

[CloudWatch RUM App Monitor with AWS CDK](https://deepdive.codiply.com/cloudwatch-rum-app-monitor-with-aws-cdk) ... cfnCognito sample
[cdk - CfnAppMonitor ](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_rum.CfnAppMonitor.html)
[AWS CDKでデプロイ時の値を使ってフロントエンドとバックエンドを一発でデプロイできるようになったので試してみる](https://zenn.dev/winteryukky/articles/5e5353ae72ab5c)
[AWS CDKでWebフロントエンドをデプロイする3つの方法](https://tmokmss.hatenablog.com/entry/20220515/1652623112)
[github - cdk-rfc - RFC L2 AppMonitor](https://github.com/aws/aws-cdk-rfcs/pull/408/commits) 
[github - cdk - L2 AppMonitor](https://github.com/aws/aws-cdk/compare/main...WinterYukky:aws-cdk:feat/add-RUM-AppMonitor-L2-Construct) ... v1 sample