@ito_tetsushi

東京・神奈川のサーバーレス / Web / モバイルアプリエンジニア

PinpointのイベントをKinesis Data StreamからLambdaへ流す

概要

Amazon PinpointにはイベントをAmazon Kinesisに流す機能があります。 これを使って、JavaScriptクライアントで発生したイベントをLambdaで処理するアーキテクチャーを作ることができます。 JavaScriptクライアントは、AWS AmplifyのJavaScriptフレームワークを使って構築します。

新規Angularプロジェクトを作成する

フロントエンドはAngularを使って構築します。 Angular CLIをインストールして、$ ng new で新規プロジェクトを構築します。

# Angular CLIをインストール
$ npm install -g @angular/cli

# Angularプロジェクトを新規作成
$ ng new PinpointKinesisLambda

# Angular Routingは使わない
? Would you like to add Angular routing? No

# スタイルシートはCSSで書く
? Which stylesheet format would you like to use? CSS

# ディレクトリ移動
$ cd PinpointKinesisLambda

AWS Amplify JavaScriptフレームワークを準備する

# Amplify CLIをインストール
$ npm install -g @aws-amplify/cli

# Amplify CLIを使って初期設定を行う
$ amplify init
Note: It is recommended to run this command from the root of your app directory

# プロジェクト名を指定
? Enter a name for the project PinpointKinesisLam

# 環境名を指定
? Enter a name for the environment pro

# エディタを指定
? Choose your default editor: Visual Studio Code

# 開発対象を指定(iOS or Android or JavbaScript)
? Choose the type of app that you're building javascript

Please tell us about your project

# JavaScriptフレームワークの種類を指定
? What javascript framework are you using angular

# ソースディレクトリを指定
? Source Directory Path:  src

# ビルド成果物のディレクトリを指定
? Distribution Directory Path: dist

# ビルドコマンドを指定
? Build Command:  npm run-script build

# ローカルサーバー開始コマンドを指定
? Start Command: ng serve
Using default provider  awscloudformation
For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html

# AWS CLIのプロファイルを使う
? Do you want to use an AWS profile? Yes

# AWS CLIのプロファイルを指定
? Please choose the profile you want to use <プロファイル名>

Amplify CLIを使ってAWS上にユーザー認証基盤を構築する

Amazon Pinpointに対してクライアントで発生したイベントを送信するには、必要な権限が付与されたAWSの認証情報が必要になります。 クライアントアプリケーションに対してAWSの認証情報を発行するには、Amazon Cognitoフェデレーテッドアイデンティティを使います。

Amazon Cognitoフェデレーテッドアイデンティティでは、認証されたクライアントと未認証のクライアントそれぞれに付与するIAMロールを指定することができます。 それぞれのロールに、Amazon Pinpointにイベントを送信するために必要な以下のポリシーを追加します。

  • mobiletargeting:GetUserEndpoints
  • mobiletargeting:UpdateEndpoint
  • mobiletargeting:PutEvents

Amplify CLIを使うことで、このようなAWS上のリソースを意識することなくバックエンドを構築することができます。 もちろんIAMロールの生成や、適切なポリシーのアタッチも自動で行ってくれます。

# Amplify CLIで「Auth」機能を追加する
$ amplify add auth
Using service: Cognito, provided by: awscloudformation

 The current configured provider is Amazon Cognito.

# 既定の設定を利用する
 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections.

# 認証に使う情報を指定
 How do you want users to be able to sign in? Username

# その他の設定はお任せ
 Do you want to configure advanced settings? No, I am done.

Amplify CLIを使ってAmazon Pinpointを構築する

Amazon PinpointもAmplify CLIで構築することができます。

# Amplify CLIで「Analytics」機能を追加する
$ amplify add analytics
Using service: Pinpoint, provided by: awscloudformation

# リソース名を指定
? Provide your pinpoint resource name: pinpointkinesislam
Adding analytics would add the Auth category to the project if not already added.

# 未認証でのアクセスを許可する
? Apps need authorization to send analytics events. Do you want to allow guests and unauthenticated users to send analytics events? (we
 recommend you allow this when getting started) Yes

バックエンドの変更を適用する

$ amplify push

Current Environment: pro

| Category  | Resource name              | Operation | Provider plugin   |
| --------- | -------------------------- | --------- | ----------------- |
| Auth      | pinpointkinesislam42083a71 | Create    | awscloudformation |
| Analytics | pinpointkinesislam         | Create    | awscloudformation |

# 変更を確認したら実行
? Are you sure you want to continue? Yes

これでCloudFormationが起動してバックエンドのリソースが構築される。

AngularアプリケーションでAmplifyが使えるように設定する

AngularアプリケーションでAmplify JavaScriptフレームワークを使うにはいくつか設定が必要です。 まず、必要なパッケージを追加します。

$ npm install aws-amplify aws-amplify-angular 

次に、src/polyfills.tsに以下のコードを追記します。

(window as any).global = window;
(window as any).process = {
  env: { DEBUG: undefined },
};

続いて、src/tsconfig.app.jsoncompilerOptions"types": ["node"]を指定します。

src/app/app.module.ts を以下のように変更します。

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";

import { AppComponent } from "./app.component";

// この行を追加
import { AmplifyService } from "aws-amplify-angular";

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [AmplifyService], // providersにAmplifyServiceを追加
  bootstrap: [AppComponent]
})
export class AppModule {}

最後に、src/main.tsに以下のコードを追加してAmplifyを初期化します。

import Amplify from "aws-amplify";
import awsconfig from "./aws-exports";
Amplify.configure(awsconfig);

Angularアプリケーションに、Pinpointへイベントを送信する機能を追加する

src/app/app.component.html を以下のように変更します。

<button (click)="onButtonClicked()">イベント送信</button>

src/app/app.component.ts を以下のように変更します。

import { Component } from "@angular/core";
import { AmplifyService } from "aws-amplify-angular";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  constructor(private amplifyService: AmplifyService) {}

  onButtonClicked() {
    this.amplifyService.analytics().record({
      name: "MyEvent",
      attributes: {
        date: new Date().getTime().toString()
      }
    });
  }
}

これでJavaScriptクライアントの準備が整いました。

Amazon Kinesis Data Streamを構築する

今回、Amazon Pinpointはオレゴンリージョンに立ち上がりました。 Kinesis Data Streamも同じオレゴンリージョンにセットアップしないと、Pinpointのイベントを流すことができません。

AWSのマネジメントコンソールにログインして、Kinesisの管理画面を開きます。

「今すぐ始める」をクリックします。

f:id:ito_tetsushi:20190906231125p:plain

「データストリームの作成」をクリックします。

f:id:ito_tetsushi:20190906231126p:plain

ストリーム名とシャード数を指定して、ストリームを作成します。

f:id:ito_tetsushi:20190906231128p:plain

f:id:ito_tetsushi:20190906231649p:plain

これでKinesis Data Streamは準備完了です。

Amazon PinpointからKinesis Data Streamにデータを流す

マネジメントコンソールにログインして、Amazon Pinpointの管理画面を開きます。 該当プロジェクトを選択して、メニューの「Settings」→「event stream」と進みます。 「Edit」をクリックします。

f:id:ito_tetsushi:20190906231704p:plain

Kinesisの情報とIAMロール名を指定して設定を保存すれば完了です。

f:id:ito_tetsushi:20190906231717p:plain

Kinesisをトリガーにして起動するLambda関数を作成する

マネジメントコンソールにログインして、AWS Lambdaの管理画面を開き、新しくLambda関数を作成します。 Lambda関数のランタイムはNode.JS 10.Xを選び、本体は以下のようなシンプルなコードにしました。

exports.handler = async (event) => {
    console.log(JSON.stringify(event));
};

また、AWS LambdaからKinesis Data Streamのストリームにアクセスするために、Lambda関数にアタッチされているIAMロールに以下の権限をアタッチします。

  • kinesis:ListStreams
  • kinesis:GetShardIterator
  • kinesis:GetRecords
  • kinesis:DescribeStream

Lambda関数のトリガーにKinesis Data Streamをセットする

Lambda関数の編集画面で、「トリガーを追加」をクリックします。

f:id:ito_tetsushi:20190906232519p:plain

先ほど作成したKinesis Data Streamを指定します。

f:id:ito_tetsushi:20190906232546p:plain

これで準備完了です。

CloudWatch Logsを眺めながら、イベントを送信して検証する

実際にLambda関数が動かないとCloudWatch Logsにログストリームもロググループも作られないので、Angularアプリケーションのイベント送信ボタンを何度か押してみます。

Pinpointに送信するイベントはAmplify JavaScriptフレームワークである程度バッファリングされているようです。 また、PinpointからKinesisへとデータが流れて、Lambda関数が実行されるまでも多少時間がかかります。

肌感覚ですが、だいたいイベントが発生してから2分ほどでLambda関数が実行されます。

しばらく待つと、CloudWatch Logsに以下のようにログが記録されていました。

f:id:ito_tetsushi:20190906233254p:plain

データの構造を確認する

Kinesisから受け取ったデータは以下のような形をしています。 データはバッチ処理されるので、Recordsフィールドに配列で入っています。

実際にPinpointから送信されたデータは、kinesis.dataにBase64形式にエンコードされて格納されています。

{
  "Records": [
    {
      "kinesis": {
        "kinesisSchemaVersion": "1.0",
        "partitionKey": "-1667896638",
        "sequenceNumber": "49599259535682552348557865618060497301418154171654209538",
        "data": "<Base64形式のデータ>",
        "approximateArrivalTimestamp": 1567780307.056
      },
      "eventSource": "aws:kinesis",
      "eventVersion": "1.0",
      "eventID": "shardId-000000000000:49599259535682552348557865618060497301418154171654209538",
      "eventName": "aws:kinesis:record",
      "invokeIdentityArn": "arn:aws:iam::XXXXXXXX:role/service-role/PinpointKinesisLambda-20190906-role-m33cg6kb",
      "awsRegion": "us-west-2",
      "eventSourceARN": "arn:aws:kinesis:us-west-2:XXXXXXXX:stream/pinpointkinesislam-pro"
    }
  ]
}

Pinpointから送られてきたBase64をデコードして形式を確認する

kinesis.dataフィールドのBase64エンコードデータをデコードすると以下のようなJSONが格納されていることがわかります。 Amplify JavaScriptフレームワークを使うことでこれほど多くの情報が自動的に収集されています。

{
  "event_type": "_session.stop",
  "event_timestamp": 1567780272608,
  "arrival_timestamp": 1567780274582,
  "event_version": "3.1",
  "application": {
    "app_id": "7f79ec844d644e669ac818fecbc156dc",
    "cognito_identity_pool_id": "ap-northeast-1:4e815606-ddf2-4bc1-bbc5-74141b826dfc",
    "sdk": {},
    "version_name": "Chrome/76.0.3809.132"
  },
  "client": {
    "client_id": "c3cc0c40-d0ad-11e9-b963-3d4f07405ba8",
    "cognito_id": "ap-northeast-1:d473ddb6-e26b-4cfc-8caf-5f3c3790587a"
  },
  "device": {
    "make": "Gecko",
    "model": "Chrome",
    "platform": {
      "name": "macintel"
    }
  },
  "session": {
    "session_id": "f843cda1-d0b2-11e9-ad52-7fe1bc8d928d",
    "start_timestamp": 1567780269946,
    "stop_timestamp": 1567780272608
  },
  "attributes": {},
  "endpoint": {
    "EndpointStatus": "ACTIVE",
    "OptOut": "ALL",
    "RequestId": "d47ed32e-947a-417b-a679-592744f33466",
    "Location": {},
    "Demographic": {
      "Make": "Gecko",
      "Model": "Chrome",
      "ModelVersion": "76.0.3809.132",
      "AppVersion": "Chrome/76.0.3809.132",
      "Platform": "macintel"
    },
    "EffectiveDate": "2019-09-06T14:31:14.577Z",
    "Attributes": {},
    "Metrics": {},
    "User": {
      "UserId": "ap-northeast-1:d473ddb6-e26b-4cfc-8caf-5f3c3790587a"
    },
    "ApplicationId": "7f79ec844d644e669ac818fecbc156dc",
    "Id": "c3cc0c40-d0ad-11e9-b963-3d4f07405ba8",
    "CohortId": "77",
    "CreationDate": "2019-09-06T13:53:54.730Z"
  },
  "awsAccountId": "XXXXXXXXXXX"
}

まとめ

JavaScriptフロントエンドアプリケーション(今回はAngular)で発生したイベントを、Amplify JavaScriptフレームワークを使ってAmazon Pinpointに送信し、それをKinesis Data Streamを経由してAWS Lambdaを起動するアーキテクチャーを構築しました。

イベントを随時分析用のデータベースに流し込むようなアーキテクチャーはこれでいけるかと思います。

フロントエンドはAWS Amplfyで構築して、バックエンドはAWSのマネージドサービスを組み合わせて構築すれば、スケーラブルでセキュアなシステムがスピーディに構築することができそうです。

AWS CDKでCloudWatch EventsでLambdaを定期実行(TypeScript版)

概要

AWS CDK(Cloud Development Kit)は、AWS上のリソースをPython / Java / .NET / TypeScriptを使って構築することができるフレームワークです。 CloudWatch Eventsは、AWS上のサービスの変更に関するイベントをトリガーにして他のアクションを起動できる仕組みを提供します。 また、cronのように定期的にイベントを発行する機能もサポートされています。

動作確認

  • aws-cdk@1.6.1

AWS CDKのCLIをインストール

$ npm i -g aws-cdk

AWS CDKのプロジェクトを作成

# ディレクトリを作成
$ mkdir events

# ディレクトリに移動
$ cd events

# AWS CDKプロジェクトを作成
$ cdk init app --language=typescript

この時点でのディレクトリは以下のようになっています。

$ tree . -I node_modules
.
├── README.md
├── bin
│   └── cognito.ts
├── cdk.json
├── lib
│   └── cognito-stack.ts
├── package-lock.json
├── package.json
└── tsconfig.json

必要なパッケージをインストールする

AWS CDKでは、AWSの各リソースを構築するためのクラスはそれぞれ別パッケージとなっています。 次のコマンドでCloudWatch Events / Lambda / イベントソースとLambda関数の関連付け を構築するためのパッケージをインストールします。

$ npm i @aws-cdk/aws-iam @aws-cdk/aws-lambda @aws-cdk/aws-events @aws-cdk/aws-events-targets

イベントをトリガーにして起動するLambda関数本体を作成する

resources/lambda-function/index.js を以下の内容で作成します。 シンプルにログを出力するだけの関数です。

exports.handler = async events => {
  console.log(event);
  return {};
};

リソースを生成するコードを追加する

定期的にイベントを発行するCloudWatch Eventsの設定と、イベントをトリガーにして起動するLambda関数を作成します。 lib/events-stack.ts を以下のように変更します。

時刻を指定する場合、GMT時刻で指定しなければいけないことに注意してください。 日本だと意図した時間の9時間前の時刻をコードに書けばOKです。

import cdk = require("@aws-cdk/core");
import iam = require("@aws-cdk/aws-iam");
import lambda = require("@aws-cdk/aws-lambda");
import events = require("@aws-cdk/aws-events");
import eventsTargets = require("@aws-cdk/aws-events-targets");

export class EventsStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda関数に付与するIAMロールを生成
    const role = new iam.Role(this, "SampleFunctionRole", {
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      path: "/service-role/",
      inlinePolicies: {
        CloudWatchWritePolicy: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
              ],
              resources: ["*"]
            })
          ]
        })
      }
    });

    // Lambda関数を生成
    const lambdaFunction = new lambda.Function(this, "SampleFunction", {
      code: lambda.Code.asset("resources/sample-function"),
      handler: "index.handler",
      runtime: lambda.Runtime.NODEJS_10_X,
      role: role
    });

    // 毎日19:00に発火するイベントを生成
    new events.Rule(this, "ScheduleEvent", {
      // 注意:GMTで指定
      schedule: events.Schedule.cron({
        minute: "0",
        hour: "10",
        day: "*",
        month: "*",
        year: "*"
      }),
      targets: [new eventsTargets.LambdaFunction(lambdaFunction)]
    });
  }
}

AWS CDKで構築したリソースをデプロイする

# TypeScriptをコンパイル
$ npx tsc

# リソースをデプロイ
$ npx cdk deploy

リソースを削除する

# リソースを削除する
$ npx cdk destroy

AWS CDKでCognitoユーザープールを構築する方法(TypeScript)

概要

AWS CDK(Cloud Development Kit)は、AWS上のリソースをPython / Java / .NET / TypeScriptを使って構築することができるフレームワークです。 Amazon CognitoはAWS上でユーザーの認証基盤を構築できるマネージドサービスです。

動作確認

  • aws-cdk@1.6.1

AWS CDKのCLIをインストール

$ npm i -g aws-cdk

AWS CDKのプロジェクトを作成

# ディレクトリを作成
$ mkdir cognito

# ディレクトリに移動
$ cd cognito

# AWS CDKプロジェクトを作成
$ cdk init app --language=typescript

この時点でのディレクトリは以下のようになっています。

$ tree . -I node_modules
.
├── README.md
├── bin
│   └── cognito.ts
├── cdk.json
├── lib
│   └── cognito-stack.ts
├── package-lock.json
├── package.json
└── tsconfig.json

Amazon Cognitoのコンポーネント

Amazon Cognitoはユーザープールとフェデレーテッドアイデンティティで構成されます。

  • Cognito User Pools(ユーザープール):ユーザーの情報を格納 / ユーザーの認証 / JWTトークン生成
  • Cognito Identity Pools(フェデレーテッドアイデンティティ):クライアントに対してAWSの認証情報を付与

また、Cognitoユーザープールにアクセスするユーザープールクライアントという概念もあります。 CDKでは、これらを別々のコンストラクタで生成します。

フェデレーテッドアイデンティティでは、未認証のクライアントに対して付与するIAMロール(Unauth Role)と認証済みのクライアントに付与するロール(Auth Role)を指定する必要があります。 これらのIAMロールは事前に生成しておき、フェデレーテッドアイデンティティの生成時に結びつける必要があります。

Cognitoのコンポーネントを構築するためのパッケージを追加する

AWS CDKでは、AWSの各リソースを構築するためのクラスはそれぞれ別パッケージとなっています。 次のコマンドでAmazon Cognitoを構築するためのパッケージをインストールします。

$ npm i @aws-cdk/aws-cognito @aws-cdk/aws-iam

Cognitoユーザープールを構築するコードを書く

UserPoolクラスの3つ目の引数に指定するオプションに必須項目は無いので、一行でCognitoユーザープールを生成できる。

import cdk = require("@aws-cdk/core");
// 以下のコードを追加
import cognito = require("@aws-cdk/aws-cognito");

export class CognitoStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 以下のコードを追加
    new cognito.UserPool(this, "UserPool");
  }
}

AWS CDKで構築したリソースをデプロイする

# TypeScriptをコンパイル
$ npx tsc

# リソースをデプロイ
$ npx cdk deploy

リソースを削除する

# リソースを削除する
$ npx cdk destroy

AWS CDKでAWS Lambda関数を構築する方法(TypeScript版)

概要

AWS CDK(Cloud Development Kit)は、AWS上のリソースをPython / Java / .NET / TypeScriptを使って構築することができるフレームワークです。 Amazon LambdaはAWS上でコードを実行できるFaaS(Function as a Service)です。

動作確認

  • aws-cdk@1.6.1

AWS CDKのCLIをインストール

$ npm i -g aws-cdk

AWS CDKのプロジェクトを作成

# ディレクトリを作成
$ mkdir lambda

# ディレクトリに移動
$ cd lambda

# AWS CDKプロジェクトを作成
$ cdk init app --language=typescript

この時点でのディレクトリは以下のようになっています。

$ tree . -I node_modules
.
├── README.md
├── bin
│   └── lambda.ts
├── cdk.json
├── lib
│   └── lambda-stack.ts
├── package-lock.json
├── package.json
└── tsconfig.json

AWS Lambdaを構築するためのパッケージを追加する

AWS CDKでは、AWSの各リソースを構築するためのクラスはそれぞれ別パッケージとなっています。 次のコマンドでAWS Lambdaを構築するためのパッケージをインストールします。

$ npm i @aws-cdk/aws-lambda @aws-cdk/aws-iam

AWS Lambdaの関数本体のコードを作成する

今回は入力をログに出力するだけのシンプルなLambda関数をNode.jsを使って書きます。 resources/sample-function/index.js として、以下の内容を作成します。

exports.handler = async events => {
  console.log(event);
  return {};
};

AWS Lambdaを構築するコードを追加する

lib/lambda-stack.ts に以下のコードを追加します。

import cdk = require("@aws-cdk/core");
// 以下のコードを追加
import iam = require("@aws-cdk/aws-iam");
import lambda = require("@aws-cdk/aws-lambda");

export class LambdaStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 以下のコードを追加(IAMロール作成)
    const role = new iam.Role(this, "SampleFunctionRole", {
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      path: "/service-role/",
      inlinePolicies: {
        CloudWatchWritePolicy: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
              ],
              resources: ["*"]
            })
          ]
        })
      }
    });

    // 以下のコードを追加(Lambda関数作成)
    new lambda.Function(this, "SampleFunction", {
      code: lambda.Code.asset("resources/sample-function"), // パスはプロジェクトのルートからのパス
      handler: "index.handler",
      runtime: lambda.Runtime.NODEJS_10_X,
      role
    });
  }
}

Lambda関数を生成する前に、Lambdaに設定するIAMロールを生成しています。 付与する権限はCloudWatch Logsへの書き込みです。

Lambda関数本体をアップロードするためのS3バケットを作成する

AWS Lambdaの関数をAWS CDKでデプロイするにはいくつか方法がありますが、Amazon S3を経由する方法を使います。 AWSアカウントで初めてS3にファイルを配置するようなデプロイを行う際は、まず以下のコマンドでS3バケットを作成する必要があります。

$ npx cdk bootstrap

 ⏳  Bootstrapping environment aws://XXXXXXXXX/ap-northeast-1...
CDKToolkit: creating CloudFormation changeset...
 0/2 | 21:15:08 | CREATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket
 0/2 | 21:15:10 | CREATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket Resource creation Initiated
 1/2 | 21:15:32 | CREATE_COMPLETE      | AWS::S3::Bucket | StagingBucket
 2/2 | 21:15:33 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | CDKToolkit
 ✅  Environment aws://XXXXXXXXX/ap-northeast-1 bootstrapped.

これで準備完了です。

AWS CDKで構築したリソースをデプロイする

IAMロールの変更を伴う場合、変更点が表示されて確認が求められます。

# TypeScriptをコンパイル
$ npx tsc

# リソースをデプロイ
$ cdk deploy

This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬───────────────────────────────────┬────────┬───────────────────────────────────┬───────────────────────────────────┬───────────┐
│   │ Resource                          │ Effect │ Action                            │ Principal                         │ Condition │
├───┼───────────────────────────────────┼────────┼───────────────────────────────────┼───────────────────────────────────┼───────────┤
│ + │ ${SampleFunctionRole.Arn}         │ Allow  │ sts:AssumeRole                    │ Service:lambda.amazonaws.com      │           │
├───┼───────────────────────────────────┼────────┼───────────────────────────────────┼───────────────────────────────────┼───────────┤
│ + │ *                                 │ Allow  │ logs:CreateLogGroup               │ AWS:${SampleFunctionRole}         │           │
│   │                                   │        │ logs:CreateLogStream              │                                   │           │
│   │                                   │        │ logs:PutLogEvents                 │                                   │           │
└───┴───────────────────────────────────┴────────┴───────────────────────────────────┴───────────────────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)

Do you wish to deploy these changes (y/n)? y
LambdaStack: deploying...
Updated: asset.cdc666b01549182a49a7205d6dccaa8f295349e048a070ed4c3c2cfba0f724a9 (zip)
LambdaStack: creating CloudFormation changeset...
 0/4 | 21:18:21 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata    | CDKMetadata
 0/4 | 21:18:22 | CREATE_IN_PROGRESS   | AWS::IAM::Role        | SampleFunctionRole (SampleFunctionRoleE8514E0F)
 0/4 | 21:18:22 | CREATE_IN_PROGRESS   | AWS::IAM::Role        | SampleFunctionRole (SampleFunctionRoleE8514E0F) Resource creation Initiated
 0/4 | 21:18:23 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata    | CDKMetadata Resource creation Initiated
 1/4 | 21:18:24 | CREATE_COMPLETE      | AWS::CDK::Metadata    | CDKMetadata
 2/4 | 21:18:35 | CREATE_COMPLETE      | AWS::IAM::Role        | SampleFunctionRole (SampleFunctionRoleE8514E0F)
 2/4 | 21:18:38 | CREATE_IN_PROGRESS   | AWS::Lambda::Function | SampleFunction (SampleFunction7DB1D36A)
 2/4 | 21:18:39 | CREATE_IN_PROGRESS   | AWS::Lambda::Function | SampleFunction (SampleFunction7DB1D36A) Resource creation Initiated
 3/4 | 21:18:39 | CREATE_COMPLETE      | AWS::Lambda::Function | SampleFunction (SampleFunction7DB1D36A)
 4/4 | 21:18:41 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | LambdaStack

 ✅  LambdaStack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:854529464916:stack/LambdaStack/e5755240-ce44-11e9-8c4f-0efdab6a43a0

AWS Lambdaの関数をAWS CDK経由でデプロイすることができました。

f:id:ito_tetsushi:20190903212104p:plain

リソースを削除する

# リソースを削除する
$ npx cdk destroy

DynamoDBテーブルへのアクセス権限をスマートに付与する

AWS CDKには、Cfn で始まるコンストラクタ(ex:CfnTable , CfnFunction)とそれ以外のコンストラクタの2種類があります。 Cfn付きコンストラクタはAWS CloudFormationで用意されているフィールドがそのまま反映された形の仕様となっています。

一方、 Cfnなしコンストラクタの方は、CloudFormationの仕様からさらに抽象化されて、よりシンプルに、より便利にリソースを構築できるようになっています。

DynamoDBテーブルとLambda関数にはCfnなしコンストラクタが用意されているので、相互に連携して便利に権限付与を行うことができます。

必要な部分だけ抜粋したものですが、Lambdaの関数からDynamoDBテーブルへアクセスできるような権限を付与するには、以下のように書きます。

// Lambda関数を生成
const lambdaFunction = new lambda.Function(this, "SampleFunction", {
  code: lambda.Code.asset("resources/sample-function"), // パスはプロジェクトのルートからのパス
  handler: "index.handler",
  runtime: lambda.Runtime.NODEJS_10_X,
  role
});

// DynamoDBテーブルを作成
const dynamodbTable = new dynamodb.Table(this, "PostTable", {
  partitionKey: {
    name: "id",
    type: dynamodb.AttributeType.STRING
  }
});

// 読み取り権限を追加
dynamodbTable.grantReadData(lambdaFunction);

// 書き込み権限を追加
dynamodbTable.grantWriteData(lambdaFunction);

// 読み書き権限を追加
dynamodbTable.grantReadWriteData(lambdaFunction);

Cfnなしコンストラクタが提供されていないリソースについては、この書き方ができないので、iam.Role クラスを使って地道にリソース間の権限付与を行なっています。 Cfnなしコンストラクタも徐々に増えていくかと思うので、キャッチアップすることでどんどんCDKのプロジェクトをシンプルに、より安全にすることができそうです。

AWS CDKでDynamoDBテーブルを構築する方法(TypeScript版)

概要

AWS CDK(Cloud Development Kit)は、AWS上のリソースをPython / Java / .NET / TypeScriptを使って構築することができるフレームワークです。 Amazon DynamoDBはAWS上にNoSQLデータベースを構築することができるマネージドサービスです。

動作確認

  • aws-cdk@1.6.1

AWS CDKのCLIをインストール

$ npm i -g aws-cdk

AWS CDKのプロジェクトを作成

# ディレクトリを作成
$ mkdir dynamodb

# ディレクトリに移動
$ cd dynamodb

# AWS CDKプロジェクトを作成
$ cdk init app --language=typescript

この時点でのディレクトリは以下のようになっています。

$ tree . -I node_modules
.
├── README.md
├── bin
│   └── dynamodb.ts
├── cdk.json
├── lib
│   └── dynamodb-stack.ts
├── package-lock.json
├── package.json
└── tsconfig.json

Amazon DynamoDBを構築するためのパッケージを追加する

AWS CDKでは、AWSの各リソースを構築するためのクラスはそれぞれ別パッケージとなっています。 次のコマンドでAmazon DynamoDBを構築するためのパッケージをインストールします。

$ npm i @aws-cdk/aws-dynamodb

Amazon DynamoDBを構築するコードを追加する

lib/dynamodb-stack.ts に以下のコードを追加します。

import cdk = require("@aws-cdk/core");

// 以下のコードを追加
import dynamodb = require("@aws-cdk/aws-dynamodb");

export class DynamodbStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 以下のコードを追加
    new dynamodb.Table(this, "PostTable", {
      partitionKey: {
        name: "id",
        type: dynamodb.AttributeType.STRING
      }
    });
  }
}

AWS CDKで構築したリソースをデプロイする

# TypeScriptをコンパイル
$ npx tsc

# リソースをデプロイ
$ cdk deploy

DynamodbStack: deploying...
DynamodbStack: creating CloudFormation changeset...
 0/3 | 21:18:55 | CREATE_IN_PROGRESS   | AWS::DynamoDB::Table | PostTable (PostTable197E3F68)
 0/3 | 21:18:55 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata   | CDKMetadata
 0/3 | 21:18:55 | CREATE_IN_PROGRESS   | AWS::DynamoDB::Table | PostTable (PostTable197E3F68) Resource creation Initiated
 0/3 | 21:18:57 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata   | CDKMetadata Resource creation Initiated
 1/3 | 21:18:57 | CREATE_COMPLETE      | AWS::CDK::Metadata   | CDKMetadata
 2/3 | 21:19:26 | CREATE_COMPLETE      | AWS::DynamoDB::Table | PostTable (PostTable197E3F68)
 3/3 | 21:19:27 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | DynamodbStack

 ✅  DynamodbStack

マネジメントコンソールにてAmazon DynamoDBのテーブルIが出来上がっているのが確認できます。

f:id:ito_tetsushi:20190902212229p:plain

リソースを削除する

# リソースを削除する
$ npx cdk destroy

AWS CDKでAmazon DynamoDBのGSI(Global Secondary Index)を作成する

Amazon DynamoDBでは、Hash Key / Sort Keyとして定義した属性でしかアイテムを検索することができません。 GSI(Global Secondary Index)を使うことで、メインのテーブルとは別の属性でアイテムを検索できるようになります。

GSIを作成するには、lib/dynamodb-stack.ts を以下のように変更します。

import cdk = require("@aws-cdk/core");
import dynamodb = require("@aws-cdk/aws-dynamodb");

export class DynamodbStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const table = new dynamodb.Table(this, "PostTable", {
      partitionKey: {
        name: "id",
        type: dynamodb.AttributeType.STRING
      }
    });

    // Hash Key = name, Sort Key = createdAt のGSIを作成
    table.addGlobalSecondaryIndex({
      indexName: "name-createdAt_index",
      partitionKey: {
        name: "name",
        type: dynamodb.AttributeType.STRING
      },
      sortKey: {
        name: "createdAt",
        type: dynamodb.AttributeType.NUMBER
      }
    });
  }
}

Table クラスのインスタンスメソッドである addGlobalSecondaryIndex メソッドを使うことでGSIを作成することができます。 GSIはいつでも追加・削除することができます。

AWS CDKでAmazon DynamoDBのLSI(Local Secondary Index)を作成する

メインのテーブルと同じHash Keyを持ち、Sort Keyのみ異なるインデックスを作成したい場合はLSI(Local Secondary Index)を利用することができます。 LSIを作成するには、lib/dynamodb-stack.ts を以下のように変更します。

import cdk = require("@aws-cdk/core");
import dynamodb = require("@aws-cdk/aws-dynamodb");

export class DynamodbStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const table = new dynamodb.Table(this, "PostTable", {
      partitionKey: {
        name: "id",
        type: dynamodb.AttributeType.STRING
      }
    });

    // Hash Key = id, Sort Key = createdAt のLSIを作成
    table.addLocalSecondaryIndex({
      indexName: "createdAt_index",
      sortKey: {
        name: "createdAt",
        type: dynamodb.AttributeType.NUMBER
      }
    });
  }
}

Table クラスのインスタンスメソッドである addLocalSecondaryIndex メソッドを使うことでLSIを作成することができます。 LSIはテーブルの作成時にしか作成できないので、AWS CDKで管理するプロジェクトでLSIを作成・変更・削除するような変更をデプロイしようとするとDynamoDBテーブルの作り直しになってしまうのでご注意ください。

メインのテーブルと同じHash Keyを持つGSIを作ることも可能なので、一度作ってしまったテーブルに対してLSI的なインデックスを追加したい場合、GSIを使う選択肢もありかなと思います。

AWS CDKでAmazon DynamoDBのPITR(Point In Time Recovery)を設定する

Amazon DynamoDBでポイントインタイムリカバリ (PITR)を有効にすると、有効にした瞬間から最大 35日前までの任意の時点でのバックアップからそのテーブルを復元することができます。 AWS CDKを使ってPITRを有効化するには、lib/dynamodb-stack.ts を以下のように変更します。

import cdk = require("@aws-cdk/core");
import dynamodb = require("@aws-cdk/aws-dynamodb");

export class DynamodbStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new dynamodb.Table(this, "PostTable", {
      partitionKey: {
        name: "id",
        type: dynamodb.AttributeType.STRING
      },
      pointInTimeRecovery: true // PITRを有効化
    });
  }
}

AWS CDKでAWS AppSyncを構築する方法(TypeScript版)

概要

AWS CDK(Cloud Development Kit)は、AWS上のリソースをPython / Java / .NET / TypeScriptを使って構築することができるフレームワークです。 AWS AppSyncはAWS上にGraphQLサーバーを構築することができるマネージドサービスです。 Amazon API Gatewayを使うことでREST APIを構築できますが、GraphQLのAPIを立てたい場合はAWS AppSyncを使います。

GraphQLには以下のような特徴があります。

  • 型指定されたスキーマ
  • 必要なフィールドのみ取得
  • サーバー側からの情報のプッシュ

動作確認

  • aws-cdk@1.6.1

AWS CDKのCLIをインストール

$ npm i -g aws-cdk

AWS CDKのプロジェクトを作成

# ディレクトリを作成
$ mkdir appsync

# ディレクトリに移動
$ cd appsync

# AWS CDKプロジェクトを作成
$ cdk init app --language=typescript

この時点でのディレクトリは以下のようになっています。

$ tree . -I node_modules
.
├── README.md
├── bin
│   └── appsync.ts
├── cdk.json
├── lib
│   └── appsync-stack.ts
├── package-lock.json
├── package.json
└── tsconfig.json

AWS AppSyncを構築するためのパッケージを追加する

AWS CDKでは、AWSの各リソースを構築するためのクラスはそれぞれ別パッケージとなっています。 次のコマンドでAWS AppSyncを構築するためのパッケージをインストールします。

$ npm i @aws-cdk/aws-appsync

AWS AppSyncを構築するコードを追加する

lib/appsync-stack.ts に以下のコードを追加します。

import cdk = require("@aws-cdk/core");

// 以下の行を追加
import appsync = require("@aws-cdk/aws-appsync");

export class AppsyncStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 以下の行を追加
    new appsync.CfnGraphQLApi(this, "AppSyncAPI", {
      name: "MyAppSyncAPI",
      authenticationType: "API_KEY"
    });
  }
}

AWS CDKで構築したリソースをデプロイする

# TypeScriptをコンパイル
$ npx tsc

# リソースをデプロイ
$ cdk deploy

マネジメントコンソールにてAWS AppSyncのAPIが出来上がっているのが確認できます。

f:id:ito_tetsushi:20190902000056p:plain

リソースを削除する

# リソースを削除する
$ cdk destroy