前回は、AWS Cloud Development Kit(AWS CDK)で関数を作成しました。

今回は、URL の接続回数をカウントするアプリを作成します。

ワークショップに習って、URL のパスに発行されたリクエストの数をカウントするアプリを作成します。

HitCounter アプリを作成する

まずは、lib フォルダに、hitcounter.ts ファイルを作成します。

import { aws_lambda } from "aws-cdk-lib"
import { Construct } from "constructs"

export interface HitCounterProps {
  downstream: aws_lambda.IFunction
}

export class HitCounter extends Construct {
  constructor(scope: Construct, id: string, props: HitCounterProps) {
    super(scope, id)
  }
}

ワークショップによると、

HitCounterという Construct クラスを定義。

scopeidpropsのコンストラクター引数を定義し、Constructにアクセスする。

propsは、aws_lambda.IFunction型のdownstreamというプロパティを含む、HitCounterProps型の引数である。

とのことです。

HitCounter の Lambda ハンドラーを作成します。

lambda フォルダに、hitcounter.js を作成します。

import { aws_dynamodb, aws_lambda } from "aws-cdk-lib"

export async function handler(event) {
  console.log("request:", JSON.stringify(event, undefined, 2))

  const dynamo = new aws_dynamodb()
  const lambda = new aws_lambda()

  await dynamo
    .updateItem({
      TableName: process.env.HITS_TABLE_NAME,
      Key: { path: { S: event.path } },
      UpdateExpression: "ADD hits :incr",
      ExpressionAttributeValues: { ":incr": { N: "1" } },
    })
    .promise()

  const resp = await lambda
    .invoke({
      FunctionName: process.env.DOWNSTREAM_FUNCTION_NAME,
      Payload: JSON.stringiry(event),
    })
    .promise()

  console.log("downstream response:", JSON.stringify(resp, undefined, 2))

  return JSON.parse(resp.Payload)
}

DynamoDB にデータとして追加する内容と、Lambda 関数を呼び出すコードになっています。

lib フォルダの hitcounter.ts に戻って、hitcounter の handler を呼び出します。

import { aws_lambda, aws_dynamodb } from "aws-cdk-lib"
import { Construct } from "constructs"

export interface HitCounterProps {
  downstream: aws_lambda.IFunction
}

export class HitCounter extends Construct {
  public readonly handler: aws_lambda.Function

  constructor(scope: Construct, id: string, props: HitCounterProps) {
    super(scope, id)

    const table = new aws_dynamodb.Table(this, "Hits", {
      partitionKey: { name: "path", type: aws_dynamodb.AttributeType.STRING },
    })

    this.handler = new aws_lambda.Function(this, "HitCounterHandler", {
      runtime: aws_lambda.Runtime.NODEJS_14_X,
      handler: "hitcounter.handler",
      code: aws_lambda.Code.fromAsset("lambda"),
      environment: {
        DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
        HITS_TABLE_NAME: table.tableName,
      },
    })
  }
}

今回は、environmentに、hitcounter.js で設定した、DOWNSTREAM_FUNCTION_NAMEHITS_TABLE_NAMEを定義しています。

HitCounter の準備ができたので、アプリに実装します。

lib フォルダの test-aws-cdk-stack.ts を開きます。

HitCounter を追加しましょう。

import { Stack, StackProps, aws_lambda, aws_apigateway } from "aws-cdk-lib"
import { Construct } from "constructs"
import { HitCounter } from "./hitcounter"

export class TestAwsCdkStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props)

    const hello = new aws_lambda.Function(this, "HelloHandler", {
      runtime: aws_lambda.Runtime.NODEJS_14_X,
      code: aws_lambda.Code.fromAsset("lambda"),
      handler: "hello.handler",
    })

    const helloWithCounter = new HitCounter(this, "HelloHitCounter", {
      downstream: hello,
    })

    new aws_apigateway.LambdaRestApi(this, "Endpoint", {
      handler: helloWithCounter.handler,
    })
  }
}

HitCounter が hello 関数に中継するようです。

ターミナルでcdk diff を実行し、差分をチェックしましょう。

image2

Hello が HelloHitCounter に変わっていました。

ターミナルでcdk deploy を実行し、デプロイしてみます。

ターミナルで、URL が表示されたので、アクセスすると、

image3

内部エラーが起こっている様です。

Lambda 関数の CloudWatch ログを表示する

なぜ、この様なエラーが起こっているか、HitCounter のログで確認します。

AWS Lambda コンソール( https://console.aws.amazon.com/lambda/home )を開きます。

HitCounter に関する関数をクリックします。

image4

モニタリングをクリックします。

image5

『ClousWatch のログを表示』をクリックします。

image6

『Search log group』をクリック しましょう。

image7

ログを見てみると、

image8

『Cannot use import statement outside a module』というエラーが発生しています。

lambda フォルダの hitcounter.js のimport { aws_lambda, aws_dynamodb } from "aws-cdk-lib";が間違っている様です。

ターミナルで、npm install --save aws-sdk を実行して、aws-sdk をインストールします。

aws_lambdaaws_dynamodbのコードを書き換えます。

さらに、exports.handler にしておきましょう。

const { DynamoDB, Lambda } = require("aws-sdk")

exports.handler = async function (event) {
  console.log("request:", JSON.stringify(event, undefined, 2))

  const dynamo = new DynamoDB()
  const lambda = new Lambda()

  await dynamo
    .updateItem({
      TableName: process.env.HITS_TABLE_NAME,
      Key: { path: { S: event.path } },
      UpdateExpression: "ADD hits :incr",
      ExpressionAttributeValues: { ":incr": { N: "1" } },
    })
    .promise()

  const resp = await lambda
    .invoke({
      FunctionName: process.env.DOWNSTREAM_FUNCTION_NAME,
      Payload: JSON.stringiry(event),
    })
    .promise()

  console.log("downstream response:", JSON.stringify(resp, undefined, 2))

  return JSON.parse(resp.Payload)
}

再びターミナルで cdk deployを実行すると、

image9

エラーが解消されていません。

ログを見てみると、

image10

今度は、『AccessDeniedException』と言われています。

これは、DynamoDB の読み書きの権限が Lambda に与えられていないので、エラーが発生しています。

lib フォルダの hitcounter.ts で、コードを追加修正しましょう。

import { aws_lambda, aws_dynamodb } from "aws-cdk-lib"
import { Construct } from "constructs"

export interface HitCounterProps {
  downstream: aws_lambda.IFunction
}

export class HitCounter extends Construct {
  public readonly handler: aws_lambda.Function

  constructor(scope: Construct, id: string, props: HitCounterProps) {
    super(scope, id)

    const table = new aws_dynamodb.Table(this, "Hits", {
      partitionKey: { name: "path", type: aws_dynamodb.AttributeType.STRING },
    })

    this.handler = new aws_lambda.Function(this, "HitCounterHandler", {
      runtime: aws_lambda.Runtime.NODEJS_14_X,
      handler: "hitcounter.handler",
      code: aws_lambda.Code.fromAsset("lambda"),
      environment: {
        DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
        HITS_TABLE_NAME: table.tableName,
      },
    })

    table.grantReadWriteData(this.handler)
  }
}

三度、ターミナルでcdk deploy を実行して、デプロイします。

image11

エラーのタイプが変わりました。

しかし、ブラウザを確認しても、エラーが解消されていないみたいです。

image12

DynamoDB コンソール( https://console.aws.amazon.com/dynamodbv2/home )のテーブルで確認しましょう。

テーブルは、作成されていました。

image13

image14

image15

lib フォルダの hitcounter.ts に移動します。

呼び出し許可の権限を与えましょう。

import { aws_lambda, aws_dynamodb } from "aws-cdk-lib"
import { Construct } from "constructs"

export interface HitCounterProps {
  downstream: aws_lambda.IFunction
}

export class HitCounter extends Construct {
  public readonly handler: aws_lambda.Function

  constructor(scope: Construct, id: string, props: HitCounterProps) {
    super(scope, id)

    const table = new aws_dynamodb.Table(this, "Hits", {
      partitionKey: { name: "path", type: aws_dynamodb.AttributeType.STRING },
    })

    this.handler = new aws_lambda.Function(this, "HitCounterHandler", {
      runtime: aws_lambda.Runtime.NODEJS_14_X,
      handler: "hitcounter.handler",
      code: aws_lambda.Code.fromAsset("lambda"),
      environment: {
        DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
        HITS_TABLE_NAME: table.tableName,
      },
    })

    table.grantReadWriteData(this.handler)

    props.downstream.grantInvoke(this.handler)
  }
}

四度、ターミナルで cdk deploy を実行します。

image16

やっと、ブラウザに表示されました。

ワークショップのように、何度か、URL の prod/の後ろを変えて実行してみます。

image17

DynamoDB のテーブルを確認すると、

image18

実行した path の回数分、hits がカウントされました。

次回は、テストで作成しているアプリを修正し、テーブルとしてブラウザに表示させます。

ブログ一覧