フロントエンド(JavaScript) から Kinesis Data Streams に Cognito ID プール (未認証) を使ってデータを送付してみた

# はじめに

フロントエンドの JavaScript で、定期的に AWS 側にデータを送信したい時があります。例えば、Web ブラウザを操作するユーザーのクリック数を取得したい、といったことです。これを実現する方法は色々方法はあるのですが、今回は Cognito Identity Pool と AWS SDK for JavaScript v3 を使っていく方法を検証していきます。フロントエンドで生成したデータを JavaScript を使って AWS に送付します。

整理するとこんな内容を確かめていきます。

- フロントエンドから、Kinesis Data Streams にデータを送る
- AWS SDK for JavaScript v3 を利用して、未認証ユーザーとして Cognito Identity Pool から一時的なクレデンシャル情報を取得する



今回は、Cognito Identity Pool を使って未認証ユーザーでも AWS にデータを送る方法を確認していきます。AWS SDK for JavaScript v3 を使ってAWS サービスと連携するときには、何らかのクレデンシャル情報が必要です。IAM の Secret Key を生成して JavaScript のコードに埋め込むことは技術的にはできるのですが、セキュリティ的に気になります。そこで、Cognito Identity Pool を使うことで、一時的なクレデンシャル情報を払い出すことが出来ます。さらに、紐づける権限を最小限とすることで、セキュリティを考慮することも可能です。

このあたりの手順を確かめるために、検証していきます。



# 検証の構成図

こんな感じの構成図です。フロントエンドは、シンプルは HTML ページとします。React や Vue.js のようなフレームワークは使いません。

![image-20220919184021024.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/4325b3ad-36bf-2240-90fb-e638ab774aff.png)



# Cognito Identity Pool を作成

まず、Cognito の Identity Pool を作成していきます。これは、未認証のユーザーでも一時的な AWS のクレデンシャル情報を払い出すために利用します。Cognito のページを開いて、Manage Identity Pools を選択します。

![image-20220917012106359.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/04f3d5d4-65c2-20c2-0455-dd65a20bbd85.png)



未認証ユーザーに対しても、Identity 払い出しを許可する設定を入れます。

- Enable access to unauthenticated identities を ON にする

![image-20220917012211600.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/5d96d24e-6327-ad45-068e-84899cbd56d2.png)



自動的に作成される IAM Role の名前を確認して、Allow を押します。この IAM Role に関する権限設定はあとから変更します。

![image-20220917012424742.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/064e43fc-4942-c582-eb5c-d69956e3e832.png)



Identity Pool が作成されました。

![image-20220917012452242.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/b9377b03-bab3-2bc7-abd4-40805ff16cf1.png)



# Kinesis Data Streams の作成

次に、フロントエンド側からデータを送信するために、Kinesis Data Streams を作成します。

![image-20220917012734068.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/0dc79360-8d42-09ec-a996-56a40f6bacbc.png)



今回は、オンデマンドを指定します。

![image-20220917012835104.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/3dd10355-9b64-baac-a8ea-4b32050eee04.png)



Create を押します

![image-20220917012850781.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/5635ecc1-b385-b9da-ff91-dba243399051.png)



Kinesis Data Streams が作成されました。ARN をコピーしておきます。

![image-20220917012918942.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/e2db2a14-991d-32f7-cedd-4faa8451b8b3.png)





# IAM Policy 変更

Cognito の未認証ユーザーに紐づけるための、必要最小限の権限設定をしていきます。Cognito Identity Pool を作成したとき、自動的に未認証用の IAM Role が作成されています。

![image-20220917013258894.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/9aa13842-e908-d81e-d3d1-a3268a4fccc5.png)



この IAM Role に、次の IAM Policy を付与します。未認証ユーザーが実行できるので、必要最低限の権限にするのは必須です。例えば Administrator 権限といった強力な権限を付与するのは検証環境でもやめましょう。

- Kinesis Data Strems に対する Put を許可
- ARN で、対象のストリームを制限

```
{
   "Version": "2012-10-17",
   "Statement": [
      {
         "Effect": "Allow",
         "Action": [
            "mobileanalytics:PutEvents",
            "cognito-sync:*"
         ],
         "Resource": [
            "*"
         ]
      },
      {
         "Effect": "Allow",
         "Action": [
            "kinesis:Put*"
         ],
         "Resource": [
            "arn:aws:kinesis:ap-northeast-1:xxxxxxxxxxxx:stream/jp-contents-hub-click-count-stream"
         ]
      }
   ]
}
```



# AWS CLI で動作確認

ここまでの設定が正しいか、AWS CLI で動作確認をしていきます。Cognito Identity Pool から get-id で `IdentityId` を取得します。(Identity とか ID とか、アイディーがゲシュタルト崩壊しそうです)

```
aws cognito-identity get-id \
--identity-pool-id "ap-northeast-1:8d946ee4-f20a-4523-9940-84ab30411110"
```



実行例

```
> aws cognito-identity get-id \
      --identity-pool-id "ap-northeast-1:8d946ee4-f20a-4523-9940-84ab30411110"
{
    "IdentityId": "ap-northeast-1:4a00bd47-7a62-403e-8a8f-5a111111111"
}
```



上記で取得した `IdentityId` を使って、一時的な AWS に対するクレデンシャル情報を取得できます。

- 内容を省略

```
aws cognito-identity get-credentials-for-identity \
      --identity-id "ap-northeast-1:4a00bd47-7a62-403e-8a8f-5a111111111"
```



実行例

- クレデンシャル情報として、アクセスキー、シークレットキー、セッショントークンを取得できます。

```
> aws cognito-identity get-credentials-for-identity \
            --identity-id "ap-northeast-1:4a00bd47-7a62-403e-8a8f-5a111111111"
{
    "IdentityId": "ap-northeast-1:4a00bd47-7a62-403e-8a8f-5a111111111",
    "Credentials": {
        "AccessKeyId": "xxxxxx",
        "SecretKey": "yyyyyy",
        "SessionToken": "xxxxxxx",
        "Expiration": "2022-09-19T21:24:55+09:00"
    }
}
```





取得してきたクレデンシャル情報を、変数に格納します。これによって、AWS CLI の実行権限が、こちらの未認証ユーザーとして切り替わります。

```
export AWS_ACCESS_KEY_ID=xxxxxx
export AWS_SECRET_ACCESS_KEY=yyyyyy
export AWS_SECURITY_TOKEN=xxxxxxx
```



切り替わっていることを確認します。`aws sts get-caller-identity` で、Arn を確認し、`Unauth_Role` が利用されていることが確認できます。

```
> aws sts get-caller-identity
{
    "UserId": "AROA5LRB5APILSZNV5HFV:CognitoIdentityCredentials",
    "Account": "xxxxxxxxxxxx",
    "Arn": "arn:aws:sts::xxxxxxxxxxxx:assumed-role/Cognito_jpcontentshubclickcountUnauth_Role/CognitoIdentityCredentials"
}
```



このまま、Kinesis Data Streams に Put してみましょう。

```
aws kinesis put-record --stream-name jp-contents-hub-click-count-stream --partition-key 1 --data testdata
```



実行例

- 正常に Put が出来ている様子が確認できます。

```
> aws kinesis put-record --stream-name jp-contents-hub-click-count-stream --partition-key 1 --data testdata
{
    "ShardId": "shardId-000000000003",
    "SequenceNumber": "49633368714284114532611740693726025006294304841680289842"
}
```



# フロントエンド側の JavaScript を作成

シンプルな HTML ファイルと、その中で活用する JavaScript を作っていきます。適当な作業用ディレクトリに移動します。

```
cd ~/temp/aws-sdk-javascript-v3/
```



npm init で、依存関係を管理するための package.json を作成します。

```
npm init
```



実行例

- 全部デフォルト

```
package name: (aws-sdk-javascript-v3) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /home/ec2-user/temp/aws-sdk-javascript-v3/package.json:

{
  "name": "aws-sdk-javascript-v3",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this OK? (yes) yes
```



JavaScript をパッケージングするために Webpack を install します。

```
npm install --save-dev webpack
npm install --save-dev path-browserify
```



package.json を修正します。

- `"build": "webpack"` と追記します。

```
{
  "name": "aws-sdk-javascript-v3",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "path-browserify": "^1.0.1",
    "webpack": "^5.74.0"
  }
}
```



webpack.config.js を作成します。

```
var path = require("path");
module.exports = {
    entry: [path.join(__dirname, "clickcount.js")],
    output: {
        path: __dirname,
        filename: 'clickcount_webpack.js'
    },
    resolve: {
        fallback: { path: require.resolve("path-browserify") }
    }
};
```



AWS SDK など、必要なパッケージを install します。

- Cognito
- Kinesis
- など

```
npm install @aws-sdk/client-cognito-identity
npm install @aws-sdk/credential-provider-cognito-identity
npm install @aws-sdk/client-kinesis
npm install @aws-sdk/util-utf8-browser
npm install date-fns-timezone
```



AWS SDK を使って Cognito や Kinesis と連携するために `clockcount.js` ファイルを作成します。

```javascript
// AWS SDK などのライブラリを読みこむ
const { KinesisClient, PutRecordCommand } = require('@aws-sdk/client-kinesis');
const { CognitoIdentityClient } = require("@aws-sdk/client-cognito-identity");
const {
    fromCognitoIdentityPool,
} = require("@aws-sdk/credential-provider-cognito-identity");
const { Buffer } = require("buffer");
const { formatToTimeZone } = require("date-fns-timezone");

// Kinesis Client を生成する。Cognito Identity Pool を利用して、未認証のブラウザ上のユーザーでも Kinesis へ Put 出来るようにしている
const REGION = 'ap-northeast-1';
const client = new KinesisClient({
    region: REGION,
    credentials: fromCognitoIdentityPool({
        client: new CognitoIdentityClient({ region: REGION }),
        identityPoolId: "ap-northeast-1:8d946ee4-f20a-4523-9940-84ab30411110" // IDENTITY_POOL_ID
    })
});


// send 関数
const send = async (command) => {
    try {
        const data = await client.send(command);
        console.log(data);
    } catch (error) {
        console.log(error);
    } finally {
        console.log('done');
    }
};

// Kinesis にデータ送付をつかさどる関数
function sendClickCount(url, contentsName) {
    // 現在時刻を取得
    const FORMAT = 'YYYY-MM-DD HH:mm:ss';
    const TIME_ZONE_TOKYO = 'Asia/Tokyo';

    const nowDate = new Date();
    const now = formatToTimeZone(nowDate, FORMAT, { timeZone: TIME_ZONE_TOKYO });

    const params = {
        Data: Buffer.from(JSON.stringify({ type: "clickEvent", date: now, url: url, contentsName: contentsName })),
        PartitionKey: 'partitionKey',
        StreamName: 'jp-contents-hub-click-count-stream',
    };

    // PutRecordコマンドの実行
    const command = new PutRecordCommand(params);
    send(command)
}

// 作成した関数を window 関数に登録することで、webpack した後にWebpack 外の JavaScript から呼び出せるようにする
window.sendClickCount = sendClickCount;
```



重要なところをいくつかピックアップします。

まず、以下の部分です。Kinesis Client を生成しています。Kinesis Client を生成する際に、AWS のクレデンシャル情報が必要です。今回は、Cognito の未認証ユーザーを使った取得になるので、Cognito の Identity Pool ID を指定しています。この指定をすることで、細かいクレデンシャルの取得は裏側で自動的にやってくれます。

```javascript
// Kinesis Client を生成する。Cognito Identity Pool を利用して、未認証のブラウザ上のユーザーでも Kinesis へ Put 出来るようにしている
const REGION = 'ap-northeast-1';
const client = new KinesisClient({
    region: REGION,
    credentials: fromCognitoIdentityPool({
        client: new CognitoIdentityClient({ region: REGION }),
        identityPoolId: "ap-northeast-1:8d946ee4-f20a-4523-9940-84ab30411110" // IDENTITY_POOL_ID
    })
});
```



次に、実際に Kinesis にデータを送信している部分です。`params` で定義したデータを `send(command)` で送っています。`params` では、ParitionKey を固定で `partitionKey` としています。ParitionKey は、本番環境ではパフォーマンスを考慮して適切なパーティションキーを指定する必要があります。順不同で構わないなら、ランダムな値を使うことで、シャード間での負荷の分散化を期待できます。

```javascript
// Kinesis にデータ送付をつかさどる関数
function sendClickCount(url, contentsName) {
    // 現在時刻を取得
    const FORMAT = 'YYYY-MM-DD HH:mm:ss';
    const TIME_ZONE_TOKYO = 'Asia/Tokyo';

    const nowDate = new Date();
    const now = formatToTimeZone(nowDate, FORMAT, { timeZone: TIME_ZONE_TOKYO });

    const params = {
        Data: Buffer.from(JSON.stringify({ type: "clickEvent", date: now, url: url, contentsName: contentsName })),
        PartitionKey: 'partitionKey',
        StreamName: 'jp-contents-hub-click-count-stream',
    };

    // PutRecordコマンドの実行
    const command = new PutRecordCommand(params);
    send(command)
}
```



こちらも重要な部分です。Webpack で1個のファイルにパッケージングすると、その関数を外部から呼び出すのが難しいです。そのため、window に `sendClickCount` を与えることで、外部から呼び出しやすくしています。

```javascript
// 作成した関数を window 関数に登録することで、webpack した後にWebpack 外の JavaScript から呼び出せるようにする
window.sendClickCount = sendClickCount;
```



上記ファイルを作成したので、Webpack でパッケージングします。以下のコマンドを実行します。

```
cd /home/ec2-user/temp/aws-sdk-javascript-v3
npm run build
```



これによって、`clickcount_webpack.js` が生成されました。

![image-20220917211519972.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/094e7aaa-df5b-83e7-048f-c6315a11933c.png)



# HTML を作成

シンプルな HTML を作成します。

- Webpack でパッケージングした JavaScript を宣言
- Webpack で作成した `sendClickCount` を呼びだす

```html
<!DOCTYPE html>
<html>
    <head>
        <title>test title</title>
    </head> 
    <body>
        <script type="text/javascript" src="./clickcount_webpack.js"></script>
        <script>
          sendClickCount("http://test.com", "テストファイル");
        </script>
    </body>
</html>
```



この HTML と Webpack でパッケージングした `./clickcount_webpack.js`　を同じディレクトリに格納します。

![image-20220919210148462.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/68595763-918e-9145-398f-72d2e903b3a4.png)



そして、index.html を開き、Console 画面開くと、いろいろメッセージが流れています。`done` と表示されれば、正常に JavaScript から Kinesis にデータを送信出来ているはずです。

![image-20220919205425490.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/68ceacc3-3c7e-eaf1-8a2c-f05db9d4bb20.png)



# AWS CLI で Kinesis Data Streams の中身を確認

AWS CLI で実際に Kinesis Data Streams にデータが格納されているか確認します。まず、Shard Iterator を取得します。

- shard-id は、Console のログに表示されているものを指定。

```
aws kinesis get-shard-iterator \
--stream-name jp-contents-hub-click-count-stream \
--shard-id shardId-000000000002 \
--shard-iterator-type LATEST \
--profile jch
```



実行例

```
{
    "ShardIterator": "AAAAAAAAAAGMgNhSNZ48CG4WBC/ehTAGqs7fXSnhKvOsNkHXvSfS0JCRYgZVGXo5WQGP0OU6+UCwkqLsV3wqi71hHSMMPzlFoDifdGDJ1kyu5Rwpsm07tTinC0yhg/D7/uSMt+2HT5rHRDzhHGy7ezISnq8ZVdLo26DmfnHIm7iM0nrXqvix+uvFCrorQI2Uwow3FQO1VAilV4YTRVz2m4gzvFxXLKmwLw/bfxt9zszInOQEMW2b9BsrQHzoz2CTpO5tqJog0/8="
}
```



この Iterator を変数に格納します (Shell は fish です。Bash などの場合は適宜変更してください。)

```
set shardIterator (aws kinesis get-shard-iterator \
            --stream-name jp-contents-hub-click-count-stream \
            --shard-id shardId-000000000002 \
            --shard-iterator-type LATEST \
            --profile jch | jq -r .ShardIterator)
```



変数の中身を確認

```
echo $shardIterator
```



このあとに、HTML ファイルを再度開き、ふたたび Kinesis にデータを送信します。



実際にデータの取得を行う

```
aws kinesis get-records \
--shard-iterator $shardIterator \
--profile jch
```



取得したデータは、Base 64 でエンコードされています。デコードすると、投入したデータを確認できます。

```
> echo -n "eyJ1cmwiOiJodHRwOi8vdGVzdC5jb20yIiwiY29udGVudHNOYW1lIjoi44GZ44GU44GE44OP44Oz44K644Kq44Oz44Kz44Oz44OG44Oz44OEMiJ9" | base64 -d
{"url":"http://test.com","contentsName":"テストファイル"}
```



# 参考 URL

https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/kinesis-page-scrolling-browser-script.html

https://awstut.com/2022/03/27/aws-sdk-for-javascript-v3-in-browser/

https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javascriptv3/example_code/kinesis/src

https://qiita.com/hinaqiita/items/bf7e5f53a4b20cced211#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97