ホクホクのイモ on Lambda

2019.07.07

金ホクホクイモ制の導入がクラスメソッド15周年を記念して起きるのではないかと私は推測しています。 きっと短冊に金ホクホクイモ制の願いを書いて、夜空に願いを込める人もいるでしょう。

なので今回はAWS CDKを使って、Lambdaにホクホクのイモをデプロイして、API GW経由で呼び出せるようにしたいと思います。 作成したリポジトリはこちらになります。

コーディング

何はともあれコードを書かなきゃ始まりません。 ホクホクのイモを作るためのコードはこのような中身になりました。

src/hokuimo.js

const shuffle = array => {
  for (let i = array.length - 1; i >= 0; i--) {
    const rand = Math.floor(Math.random() * (i + 1));
    [array[i], array[rand]] = [array[rand], array[i]];
  }
  return array;
};

exports.handler = async () => {
  const hokuimo = ["ホ", "ク", "イ", "モ"];
  const shuffleHokuhoku = shuffle(hokuimo);
  const hokuhoku = shuffleHokuhoku[0] + shuffleHokuhoku[1];
  const shuffleImo = shuffle(hokuimo);
  const imo = shuffleImo[0] + shuffleImo[1];
  return {
    statusCode: 200,
    body: `${hokuhoku}${hokuhoku}の${imo}`,
  };
};

ここで重要になるのは2点です。 ホクイモという文字列をランダムにシャッフルできるかと、API GWとの統合についてです。

Fisher-Yates shuffle

Fisher-Yates shuffleは有限集合からランダムな順列を生成します。 擬似乱数の生成に偏りが生じない限り、完全にランダムなシャッフルを実現できます。

実装部分を見ながら解説をしていきます。

const shuffle = array => {
  for (let i = array.length - 1; i >= 0; i--) {
    const rand = Math.floor(Math.random() * (i + 1));
    [array[i], array[rand]] = [array[rand], array[i]];
  }
  return array;
};

まずはアルゴリズムを語る上で一番大事な計算量です。 渡された有限集合、すなわちarray(配列)の長さを元に、forでループをかけています。 すなわち、計算量はO(n)しかありません。シャッフルするので最低でもn回は確実に入れ替えが必要になりますよね。 なので、Fisher-Yates shuffleアルゴリズムは高速と言われています。

計算量について書いたところで次は、アルゴリズムの中身です。これも非常にシンプルでわかりやすいです。

    const rand = Math.floor(Math.random() * (i + 1));
    [array[i], array[rand]] = [array[rand], array[i]];

1行目では擬似乱数を元に0から配列長-1の長さの数字を決定します。 2行目で先ほど決めた値を元に、配列をシャッフルします。 少し言葉だけだと説明しづらいので図を使います。 インデックスがi、乱数がrand、そして配列に、["ホ", "ク", "イ", "モ"]を使用します。 なので初期状態はこのようになりますね。

インデックス(i) 乱数(rand) 配列(array)
N/A N/A ["ホ", "ク", "イ", "モ"]

それでは1回目の処理を開始します。 インデックスは0になります。乱数は、適当に3としましょう。 この場合の配列への処理はこうなりますね。

[array[0], array[3]] = [array[3], array[0]];

この結果を表に加えましょう。

インデックス(i) 乱数(rand) 配列(array)
N/A N/A ["ホ", "ク", "イ", "モ"]
0 3 ["モ", "ク", "イ", "ホ"]

同様に2回目の処理、3回目の処理を行なっていきます。

インデックス(i) 乱数(rand) 配列(array)
N/A N/A ["ホ", "ク", "イ", "モ"]
0 3 ["モ", "ク", "イ", "ホ"]
1 1 ["モ", "ク", "イ", "ホ"]
2 1 ["モ", "イ", "ク", "ホ"]
3 0 ["ホ", "イ", "ク", "モ"]

ホクホクのイモがマッシュポテトのようにランダムにシャッフルされました。 Fisher-Yatesアルゴリズムについてなんとなくご理解いただけたでしょうか。 また、今回の実装では、与えられた配列に対して破壊的変更を加えています。 そのため、ループ段階で無駄なメモリ確保も必要なくなっています。

API GWとの統合

API GWとLambdaを使用する場合はLambda側でのレスポンスに制約が出ます。 具体的にはLambda側で受け取るcallback関数に、statusCodebodyなどを含んだオブジェクトを返す必要があります。 今回はasyncを使っているので、単純にオブジェクトを返していますが、使わない場合はいくらかコードの記述量が増えます。 今回の実装部分はここです。

exports.handler = async () => {
  const hokuimo = ["ホ", "ク", "イ", "モ"];
  const shuffleHokuhoku = shuffle(hokuimo);
  const hokuhoku = shuffleHokuhoku[0] + shuffleHokuhoku[1];
  const shuffleImo = shuffle(hokuimo);
  const imo = shuffleImo[0] + shuffleImo[1];
  return {
    statusCode: 200,
    body: `${hokuhoku}${hokuhoku}の${imo}`,
  };
};

AWS CDKの実装

さて、今回使うコードについての説明が終わったところで、AWS CDKの設定をしていきましょう。 ディレクトリ構造はこのようになっています。 src配下に先ほどのホクホクのイモを作るコードを入れて、AWS CDKのコードはindex.jsにまとめています。

$ tree -I 'node_modules|cdk.out'
.
├── LICENSE
├── README.md
├── cdk.json
├── index.js
├── package-lock.json
├── package.json
└── src
    └── hokuimo.js

ライブラリのインストール

まずはnpm initと、今回必要になるライブラリのインストールをします。 initに関しては適宜設定をしてください。よくわからなかったら、取り合えずエンターを連打しれ後々整形すれば大丈夫です。

$ npm init
$ npm i @aws-cdk/core @aws-cdk/aws-lambda @aws-cdk/aws-apigateway
$ npm i -D aws-cdk

ここまでできたらpackage.jsonの中身を少し変えて、こんな感じにします。 scriptsの部分にcdkを追加したくらいです。

[package.json

{
  "name": "cdk-template-ts",
  "description": "template repo for AWS CDK",
  "version": "0.37.0",
  "scripts": {
    "cdk": "cdk"
  },
  "author": "37108",
  "license": "MIT",
  "devDependencies": {
    "aws-cdk": "^0.37.0"
  },
  "dependencies": {
    "@aws-cdk/aws-apigateway": "^0.37.0",
    "@aws-cdk/aws-lambda": "^0.37.0",
    "@aws-cdk/core": "^0.37.0"
  }
}

index.jsの設定

AWS CDKプロジェクトの要であるindex.jsを設定します。 今回使用するAWSサービスはAPI GWとLambdaだけなのでこの二種類をスタックの中で作成しています。

index.js

const cdk = require('@aws-cdk/core')
const lambda = require('@aws-cdk/aws-lambda')
const apigateway = require('@aws-cdk/aws-apigateway')

class HokuHokuStack extends cdk.Stack {
  constructor(app, id) {
    super(app, id)
    const hokuhokuLambda = new lambda.Function(this, 'hokuhokuFunction', {
      code: new lambda.AssetCode('src'),
      handler: 'hokuimo.handler',
      runtime: lambda.Runtime.NODEJS_8_10,
    })

    const api = new apigateway.RestApi(this, 'HokuimoApi', {
      restApiName: 'Hokuimo'
    })
    const hokuimo = api.root.addResource('hokuimo')
    const hokuhokuApi = new apigateway.LambdaIntegration(hokuhokuLambda)
    hokuimo.addMethod('GET', hokuhokuApi)
  }
}

const app = new cdk.App()

new HokuHokuStack(app, 'HokuHokuStack')
app.synth()

必要になるので、cdk.jsonも書いていきます。

cdk.json

{
  "app": "node index"
}

ここまで準備ができたらホクホクのイモをデプロイしましょう。

ホクホクのイモをデプロイする

AWS CDKのバージョン0.36.0以降ではデプロイの方法が若干変わりました。 今回使用しているのは0.37.0なので新しいデプロイ方法です。

まずは、cdk bootstrapというコマンドを実行します。 AWS CDKのために必要なS3バケットを作るためのコマンドです。 これをやっておかないと、cdk deployができないのです。

$ npx cdk bootstrap aws://123456789012/ap-northeast-1
 
 ⏳  Bootstrapping environment aws://123456789012/ap-northeast-1...
CDKToolkit: creating CloudFormation changeset...
 0/2 | 11:13:50 AM | CREATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket 
 0/2 | 11:13:52 AM | CREATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket Resource creation Initiated 835987
 1/2 | 11:14:13 AM | CREATE_COMPLETE      | AWS::S3::Bucket | StagingBucket 
 2/2 | 11:14:15 AM | CREATE_COMPLETE      | AWS::CloudFormation::Stack | CDKToolkit

bootstrapの後にある、aws://123456789012/ap-northeast-1の部分で、AWSアカウント(123456789012)と、リージョン(ap-northeast-1)を指定します。 出力に関してはCFnっぽさがすごいあります。 ちなみにbootstrapをやらずにdeployするとこんなエラーが出ます。

 ❌  HokuHokuStack failed: Error: This stack uses assets, so the toolkit stack must be deployed to the environment (Run "cdk bootstrap aws://unknown-account/unknown-region")

AWS CDKプロジェクトの準備ができたので、デプロイしましょう。 作成するスタックでIAM関連のものを作成すると忠告が出ます。 今回は、Lambda関数用のIAM Roleが自動で生成されたため、忠告が出ていますが、問題ないのでそのまま進めます(出力が長いので一部省略しています)。

$ npx 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                                         │
├───┼─────────────────────────────────────────────────┼────────┼───────────────────────┼─────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ + │ ${HokuimoApi/CloudWatchRole.Arn}                │ Allow  │ sts:AssumeRole        │ Service:apigateway.${AWS::URLSuffix}            │                                                   │
├───┼─────────────────────────────────────────────────┼────────┼───────────────────────┼─────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ + │ ${hokuhokuFunction.Arn}                         │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com                │ "ArnLike": {                                      │
│   │                                                 │        │                       │                                                 │   "AWS:SourceArn": "arn:${AWS::Partition}:execute │
│   │                                                 │        │                       │                                                 │ -api:${AWS::Region}:${AWS::AccountId}:${HokuimoAp │
│   │                                                 │        │                       │                                                 │ i7B91A74B}/${HokuimoApi/DeploymentStage.prod}/GET │
│   │                                                 │        │                       │                                                 │ /hokuimo"                                         │
│   │                                                 │        │                       │                                                 │ }                                                 │
│ + │ ${hokuhokuFunction.Arn}                         │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com                │ "ArnLike": {                                      │
│   │                                                 │        │                       │                                                 │   "AWS:SourceArn": "arn:${AWS::Partition}:execute │
│   │                                                 │        │                       │                                                 │ -api:${AWS::Region}:${AWS::AccountId}:${HokuimoAp │
│   │                                                 │        │                       │                                                 │ i7B91A74B}/test-invoke-stage/GET/hokuimo"         │
│   │                                                 │        │                       │                                                 │ }                                                 │
├───┼─────────────────────────────────────────────────┼────────┼───────────────────────┼─────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ + │ ${hokuhokuFunction/ServiceRole.Arn}             │ Allow  │ sts:AssumeRole        │ Service:lambda.${AWS::URLSuffix}                │                                                   │
└───┴─────────────────────────────────────────────────┴────────┴───────────────────────┴─────────────────────────────────────────────────┴───────────────────────────────────────────────────┘
IAM Policy Changes
┌───┬─────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                        │ Managed Policy ARN                                                                      │
├───┼─────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${HokuimoApi/CloudWatchRole}    │ arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs │
├───┼─────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${hokuhokuFunction/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole          │
└───┴─────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────┘
(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
HokuHokuStack: deploying...
Updated: asset.97e6ec938ed6fc40c9106bbf970cbeac58e1971086c68b3dd87a0029328e49a5 (zip)

  0/13 | 11:16:08 AM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata          | CDKMetadata 
  0/13 | 11:16:08 AM | CREATE_IN_PROGRESS   | AWS::IAM::Role              | hokuhokuFunction/ServiceRole (hokuhokuFunctionServiceRole2E30C3EF) 
 11/13 | 11:16:39 AM | CREATE_IN_PROGRESS   | AWS::Lambda::Permission     | hokuhokuFunction/ApiPermission.GET..hokuimo (hokuhokuFunctionApiPermissionGEThokuimoA913B67A) Resource creation Initiated

 ✅  HokuHokuStack

Outputs:
HokuHokuStack.HokuimoApiEndpoint65803AD2 = https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:123456789012:stack/HokuHokuStack/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx

エンドポイントができたので、ホクホクのイモが返ってくるかを確認しましょう。

$ curl https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/hokuimo
イホイホのモホ
$ curl https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/hokuimo
クホクホのイモ

ホクホクのイモは返ってきませんでしたが、レスポンスは問題なさそうなので大丈夫です。

さいごに

これで業務中でもホクホクのイモをできるようになりました。 AWS CDKに関しては、外部ライブラリに依存しないLambda関数とAPI GWの組み合わせの場合はかなり有用なのではないかと感じました。