銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

AWS SAMテンプレートでREST APIの複数メソッドをひとつのLambdaに統合するには?

f:id:takamints:20191027090344j:plain photo credit: Andreas Komodromos Urban Nights - Hudson Yards, New York City via photopin (license)

ホントちょっとしたことなんですが、SAMのHelloWorldのチュートリアルをやっただけではハッキリわからなくて、少しだけ試行錯誤が必要でしたので書いておきます。

API Gateway REST APIの複数メソッド(GET,PUT,POST,DELETEなど)を、ひとつのLambda関数に統合(Lambdaプロキシ統合)する場合の AWS SAMのテンプレートの書き方です。

API Gatewayのコンソールでは、各メソッドの設定で、同じLambda関数を指定するだけですが、じゃあSAMのテンプレートではどう書くのでしょうか?ってところです。

目次

みんなのobniz入門
みんなのobniz入門
posted with amazlet at 19.10.27
古籏 一浩
リックテレコム (2019-11-08)
売り上げランキング: 24,382

SAMテンプレートの書き方(例)

以下のYamlでリソースパス /hello に対する GETリクエストとPUTリクエストを、Lambda関数 HelloWorldFunction に統合しています。

sam-app/template.yaml:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        #ココ↓と
        HelloWorldGet:
          Type: Api
          Properties:
            Path: /hello
            Method: get
        #ココ↓
        HelloWorldPut:
          Type: Api
          Properties:
            Path: /hello
            Method: put

一般化すると、AWS::Serverless::FunctionProperties/Events以下にメソッド単位で別々のApiとして宣言すればよいのです。 APIのリソースパスが同じでもメソッドが別なら複数のApi定義が必要ということですね。 SAMテンプレートでは独立したREST APIを宣言しているのではなくて、あくまでもサーバーレス関数の定義の一部なのでこうなるのでしょう(多分)。

今となっては上の定義が理解できていますが、当初「Events 以下のエントリがAPIのリソースパスを一意に表している」と思い込んでいて、Methodを配列にしてみたりして失敗しました。

Lambdaプロキシ統合のパラメータで分岐する

Lambda関数側では、引数のeventのhttpMethod にHTTPメソッドが文字列で格納されていますので、これによって分岐します。

exports.lambdaHandler = async (event) => {
    try {
        const response = { statusCode: 200 };
        switch(event.httpMethod) {
        case "PUT":
            await put(JSON.parse(event.body));
            break;
        case "GET":
            await get(event.queryStringParameters);
            break;
        }
        return response;
    } catch (err) {
        console.log(err);
        return err;
    }
};

リクエストパラメータは、同じくeventのqueryStringParameterbodyに格納されています。 queryStringParameterはObjectですがbodyは文字列(JSON)なので気を付けましょう。 これらパラメータの使い方はメソッドの設計に依存するので一概に「こうしましょう」とは言えません。 他にも pathParametersheaders を利用することがあるかもしれませんね。

Lambdaプロキシ統合されたLambda関数の入力形式については、以下の公式ページに説明があります。 実際に動かしてみて把握する必要はあるかもしれませんよ。

docs.aws.amazon.com

あとがき

普通はメソッドごとにLambda関数を分けることが多いかもしれません。 そのほうがLambdaでの分岐が不要で多少はパフォーマンスが良いはずですし。

しかし、各メソッド間で同じデータモデルを扱うなど共通処理が必要な場合は一つにまとめるとスッキリすると思います。

共通処理についてはLambda Layersを使うのも一つの解です。 ここでは、「とりあえず手軽にやるには?」という観点でサクッと調べた結果を書いておきました。

AWS SAMがAWS CLIを見失う?ならPATH設定を要チェック!

f:id:takamints:20191013125055j:plain
photo credit: dolbinator1000 Over The Hill via photopin (license)

LinuxAWS CLIをインストールしてコンソールから普通に使えていたのに、SAMでサーバーレスアプリのパッケージを作成しようとすると「AWS CLIが見つからない」というエラーが出て失敗しました。ビルドできるのになんでやねんと。

どうやら、AWS CLIのPATH設定と、AWS SAMのPATH検索方法の行き違いみたいなことで発生した問題のようでした。

目次

実行環境

$ sam --version
SAM CLI, version 0.22.0)
$
$ aws --version
aws-cli/1.16.258 Python/3.6.5 Linux/4.19.78-2-lts botocore/1.12.248
$
$ uname -srm
Linux 4.19.78-2-lts x86_64
$

sam package でエラー

sam package でパッケージを作ろうとするとエラーを履いて失敗しました。

$ sam package --s3-bucket <bucket-name> --output-template-file packaged.yaml

Error: Cannot find AWS CLI installation, was looking at executables with names: 
['aws']
$

エラーメッセージは「AWS CLIのインストールが見つからない。コマンド 名 aws を探していましたが・・・」みたいな感じ。

いやしかし、AWS CLI はインストールしており、コンソールから普通に使えている状態。以下のようにPATHも通っていますよ。

$ which aws
/home/<user>/.local/bin/aws

ただ、気になったのは aws がホームディレクトリ以下の自宅の床下みたいな所 にあるということ。 どうやら、pipで自分だけ使えるようにインストールしたら、こうなるようです。 全ユーザーが使えるようにしたらこうはならないらしいです。

とりあえずの対策

Cannot find AWS CLI installation, was looking at executables with names

エラーメッセージで検索すると、Close済のGitのIssueがヒット。 文言が少し違っていますが、同じ問題のように思えます(↓これ)。

github.com

「とりあえず /usr/bin/usr/local/bin に上記 aws コマンドを置けばOK」とありますので、シンボリックリンクを張ってみると、、、

あっさり解決・・・

[~] $ cd /usr/local/bin
[/usr/local/bin] $ sudo ln -S aws /home/<user>/.local/bin/aws
[/usr/local/bin] $ cd ~/sam-app
[~/sam-app] $ sam package --s3-bucket my-sam-app-bucket --output-template-file \
packaged.yaml


Successfully packaged artifacts and wrote output template to file packaged.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /home/<user>/..../packaged.yaml 
--stack-name <YOUR STACK NAME>

しかしこれ、やっぱり aws コマンドの場所が気持ち悪い。 街の通り(/usr/local/bin)から自宅の床下(~/.local/bin)に抜け穴を掘ってるみたいです。

正しい解決法

さらにIssueを読み進めると、ヒントがもひとつ書いてあった。

SAMはawsのありかを自前でチェックしているらしいのです。 なんでそんなことしてるのかは知りませんが、そのおかげで PATH に含まれてる ~ を正しく解釈できないらしい。 ほら言わんこっちゃない。なんで自前でやっちゃうかな。本来それはOSに任せるべきなのでは? とか言ってても仕方がない。

とにかくそういうことらしいので、自分の .bashrc を確認すると、・・・

.bashrc

export PATH="~/.local/bin:$PATH"

普通に ~ を使っていましたね。 結局このせいでAWS SAMはAWS CLIを見つけられず、インストールされていないなんて言っていたわけです。

以下のように ~ を使わないように修正すれば問題解決。 上に書いた /usr/local/bin からのシンボリックリンクも不要です。

export PATH="/home/<user>/.local/bin:$PATH"

結構時間がかかって解決しました。割と大きめの落とし穴にハマった気分でした。

AWS SAM/CloudFormationのテンプレートでDynamoDBテーブル名をLambdaの環境変数に設定するには?

f:id:takamints:20191012145048j:plain
photo credit: SurfaceWarriors 190711-N-WI365-2119 via photopin (license)

AWS SAM/CloudFormationのテンプレートで定義されるDynamoDBのテーブル名を、同じくテンプレート内に定義したLambda関数の環境変数に設定する方法です。

WEB+DB PRESS Vol.113
WEB+DB PRESS Vol.113
posted with amazlet at 19.10.12
野田 奏 小原 一哉 西岡 忍 木村 奈美 澤田 剛 成瀬 允宣 松岡 幸一郎 藤井 謙士朗 井上 真史 山口 慶子 池田 拓司 小山 哲志 末永 恭正 久保田 祐史 鈴木 恭介 牧 大輔 笹田 耕一 松本 宏太 尾形 鉄次 はまちや2 竹原 渡辺 淳 岩井 良太 吉田 健太
技術評論社
売り上げランキング: 1,406

目次

はじめに

SAM/CloudFormationのテンプレートで、テーブル名を指定せずにDynamoDBのテーブルリソースを定義すると、サーバーレスアプリの初回デプロイ時に自動的に命名されます。

このテーブル名には、SAMのアプリ名やリソース名に加えて、デプロイ時にハッシュみたいな一意なIDが使われますので、Lambdaの実装時には不明です。

テンプレートでは、DynamoDBのテーブルリソースを参照することで、まだ作成されていないテーブル名をLambdaの環境変数に設定できます。 Lambda関数では、テーブル名を環境変数から取得すればOKですね。

ということで、その書き方を以下で説明いたします。

テンプレートでLambdaの環境変数を設定する

テンプレートでLambda関数の環境変数を宣言するには、リソースの Properties/Environment/Variables に、 <環境変数名>: <環境変数の値> と記述します。 例ではリソースTypeが AWS::Serverless::Function となっていますが、AWS::Lambda::Function でも同じはず。

template.yaml

Resources:
  #サーバーレス関数の定義(API Gateway REST API+Lambda Function)
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Environment:
        #環境変数の宣言
        Variables:
          <環境変数名>: <環境変数の値>

環境変数にDynamoDBテーブル名を設定する

Lambdaの環境変数にDynamoDBのテーブル名を設定するには、<環境変数の値>!Ref <DyanamoDBリソース名> とします。

※ DynamoDBリソースでテーブル名をProperties/TableNameで明示的に指定していても同様に参照可能です。

template.yaml

Resources:
  #サーバーレス関数の定義(API Gateway REST API+Lambda Function)
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Environment:
        Variables:
          #↓DynamoDBテーブルのテーブル名を参照
          TableName: !Ref DynamoDBTable
   ・
   ・
   ・
  #DynamoDBテーブルリソースの宣言
  DynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties:
  ・
  ・
  ・

デプロイするとLambda関数の設定で、こう(↓)なります。

f:id:takamints:20191012220304p:plain

おまけ:Lambda関数実行時に環境変数を参照する

Node.js の Lambda で実行時に<環境変数の値>を参照するには process.env["<環境変数名>"] とします。

const tableName = process.env["TableName"];

f:id:takamints:20191012215355p:plain

Pythonならば、import os でosモジュールをインポートして os.environ['<環境変数名>'] です。

import os
tableName = os.environ['TableName'];

参考リンク

docs.aws.amazon.com

docs.aws.amazon.com

Git BashやMSYS2の行頭で、Tab押しちゃって固まっちゃった時の対処法

f:id:takamints:20190930203227j:plain
photo credit: verchmarco Close-up of the On / Off Button and Laptop keyboard via photopin (license)

少なくとも、ひと月に1記事は書こうとしていたのですが、2週間前に交通事故に遭っちゃいまして、1週寝込んでさらに1週、痛みと後遺障害の予感に怯えて過ごして全く書けず。気づけば9月最終日。慌てて小ネタを出しておきます。

ごめんじゃすまない! 自転車の事故 (GEIBUN MOOKS 暮らしの落とし穴から守る本)
むさしの森 法律事務所 岡田正樹
芸文社 (2016-07-21)
売り上げランキング: 149,055

ということで、コンソールでたまに頭を悩ませる困ったことの解消法。

環境によるかも知れませんが、 Git Bash (Git for Windows 同梱の MSYS2) の行頭(何も入力していない状態の入力待ち)で Tab を入力してしまうと、結構長時間無反応になっちゃいます。

ハングしているわけではないので、実はしばらく待つと戻ってきます(特に2回目以降はキャッシュされているのか、それなりに早く戻ってきますね)。 Bash のコマンド入力補完機能がPATH上にある全コマンドを列挙していると思っていますが・・・。

これ、即刻復帰させるには、CTRL+Cを押してからコンソールのウィンドウサイズを変えればOKなんですよ。

なんでそうなるのかは全くわかっていませんが・・・(そのうち調べておきます)。

スプレッドシートで行列演算:数式コピペを削減できる

f:id:takamints:20190829170340j:plain
photo credit: j_lai Photo-0141.jpeg via photopin (license)

Googleスプレッドシートの行列演算を上手く使うと、計算式を入力するべきセルの数を非常に少なくできます。 隣り合わせのセルに、ほぼ同じ計算式をズラリとコピーしているようなケースで特に。

セルで行列演算ができることは知っていましたが、特に使う必要がないと思っていました。 しかし、職場の移転をきっかけに、とある情報管理の必然性が出てきまして、使ってみると「これめっちゃ便利じゃないですかー!」とプチ感動。 頭で考える以上に使えるシーンがありそうですので、ここにお知らせしておきます。

おそらくExcelでも同じことができるはずなのですが、今のところはGoogleスプレッドシートでやっています。そのうち調べてみるつもり。

Excel関数リファレンスシリーズ「検索/行列関数」
NOOK出版 (2019-01-26)
売り上げランキング: 32,694
目次

長い前置き(ここは無理に読まなくてもよろしいですよ)

弁当男子の憂鬱話

話は一旦戻りまして、お盆を境に就業場所が変わりました。転職したわけではありません。作業場所が変わっただけです。

が、それによってお昼ごはんの事情が急転、お盆明けから5時半起床で弁当男子をやっております。

以前は職場でまとめて発注する仕出しのお弁当を頂いていたのですが、場所が変わって、そのシステムが無くなって、弁当つくらにゃお昼の費用が跳ね上がるのよ。

月額5500円が心理的な上限だだだ

従来の仕出しのお弁当1食あたりのスペックは、1食350円(ライス無しなら250円)、おかずは360~500kcal。 自分はカラダが小さめ省エネタイプ。さらに通勤が短距離&短時間だったので、ライス無しで問題なし。 月22日勤務で昼食代はなんと破格の5500円でした。

ところが、新しい仕事場では、お弁当システムがなくなりました。 近隣に飲食店やコンビニはたくさんありますが、それでは上記価格水準を保てそうにありません。

レンチンご飯と冷凍食品

昼食代が、ひと月のお小遣いに直接影響する制度な家庭。 もう弁当男子以外に取る手がありませんねということで、こういうときは初動が大事。

分量的に不安定な炊飯ジャーの残りご飯と朝イチ夢の炊きたてご飯のせめぎあい。 朝はパン食、深夜の米とぎ、タイマーセットで日の出の時間にシュシュポポポ。 腕に覚えのだし巻き卵と焦がしタコさんウィンナーと・・・

しかし残念!根本的に平日早朝キッチンを専有するのはどうあっても認められず。 そこでレンチンご飯と冷凍食品で手早く5分で済ませるからって提案(懇願?)により、めでたく許可を得ました有難うヨメ。

実現可能性調査が必要

ということで、さっそく近所の格安スーパーで食材購入。 多少の料理は趣味でかじっておりますが、冷食の価格帯やらお弁当に適正な分量などは全く未知数。 そもそも1食あたり250円程度に抑えて必要なエネルギーを得られるものなのか全く知識がありません。

なるべく低価格で高エネルギーなお弁当を目指したいので、緻密で厳格な実現可能性調査が必要です。 ちなみにお弁当箱は百均で600ccのを買ってきました。テキトーです。

ワークシートで行列演算

さて実現可能性調査のため、やおらGoogleスプレッドシートを取り出して、作ってみたのが以下のシート(見た目は普通の表ですが)。 購入食材とその内容(価格、内容量、熱量)、そして月~金の5日分のお弁当で「どの食材を何個使うか?」を入力し、それぞれ1食あたりの価格とエネルギーを算出しています(文字色が青い部分が一次データの入力セル)。

それほど複雑な演算はしていませんが、「計算式が入力されているのは背景が黄色のセルだけですよ」と言えば少し意外に思われないでしょうか? 全部で6つのセルがありますが、右端の平均値以外ならば3箇所です。

従来から表計算では慣習的に縦一列や横一列に単純で似たような計算式をコピペしていましたが、複数の情報に個数を掛けて積算するところでは「これは行列演算すべきところだ・・・」と気が付きました。

で、この3箇所のセルには行列演算の計算式を入力しており、その式の演算結果は(数学的な)行列です。 Googleスプレッドシートでは、演算結果が行列だと隣接する空白のセルに結果の行列の各要素を展開してくれるんですね。 上のシートでは、背景黄色のセルに隣接する、背景が白以外のセルに、行列の要素が展開されています。

以下でこれら3つの計算式を説明します(上の埋め込みでは式が見えないみたいなので)。

税抜単価(F列)の計算式

セルF5では、各商品の税抜価格(セル範囲D5:D12)を内容数(セル範囲E5:E12)で割っています。

セルF5に入力された計算式:

=ArrayFormula( D5:D12 / E5:E12 )

セルの範囲は行列として扱える

ArrayFormulaの引数にはセル範囲が記述されています。

計算式内のセル範囲は、その範囲に入力されているデータを要素として持つ行列として扱われます。

このため、D5:D12 は、このセル範囲に入力されている8行1列の行列として計算に使えるのです({E5:E12} も同じ)。

ArrayFormula:配列数式を計算して展開する

ArrayFormula は、引数の配列数式の計算結果を返す関数です。

「配列数式の計算」とは、行列の要素単位の計算を行う数式だと理解しています(ちゃんとした定義は不明)。MATLABOctaveのドット演算とほぼ同じ。

ここでは行列で行列を割っています({D5:D12}/{E5:E12})が、配列数式の計算として、個々の要素間で割り算が行われるため、結果として同じ大きさ(8行1列)の行列が返されます。

計算結果の行列は式を起点としたセル範囲に表示される

計算結果が行列である場合、式の入力されたセルを起点とした範囲に各要素が展開されるため、セル範囲 F5:F12 に単価が表示されています。

税込単価(G列)の計算式

ここでも ArrayFomula が使われており、行列の定数倍が行われています。

セルG5に入力された計算式:

=ArrayFormula( F5:F12 * (1+$M$1) )

F列に表示された単価に対して、消費税を適用して税込みの単価を算出し、結果をセル範囲G5:G12へ表示しています。 見た目、なんとなくArrayFormulaは不要なのでは?と思いましたが、結果をセル範囲に展開するためには必要なようです。 ArrayFormulaを外すとセルG5だけに結果が表示されてしまいます。

1食あたりの価格と熱量を一括計算する(セルI15)の計算式

行列演算の真骨頂がこちらです。セルI15のひとつの式(以下)で、3行5列のセル範囲I15:M17の範囲に結果を表示しています。

セルI15に入力された計算式:

= MMULT( TRANSPOSE( F5:H12 ), I5:M12 )

ここでは、MMULTとTRANSPOSEという2つの関数が使われています。 どちらも行列の演算に関しては基本的な関数ですね。

TRANSPOSE:行列の転置

TRANSPOSEは行列の転置を行います。転置とは行列の行と列を入れ替えることです。

TRANSPOSE(F5:H12)は、セル範囲F5:H12が表す8行3列の行列を、3行8列の行列に変換しています。

なぜここで転置を行っているのかは、次のMMULTで説明します。

MMULT:行列の積

MMULTは2つの行列を引数に取り行列の積を行います。 行列演算の基本として、2つの行列AとBがある場合、Aの列数とBの行数が同じでないとA×Bの演算はできません。 このため、第一引数をTRANSPOSEで転置しているのです。

ちなみに、この場合の結果の行列は、「行数がAの列数」で「列数がBの列数」となります。

その他の行列演算にまつわる話題

ArrayFormulaの自動挿入

数式を入力して、[Ctrl]+[Shift]+[Enter]を入力すると、数式全体をArrayFomulaで括ってくれますメチャ便利。

直接要素を指定して行列を初期化する

セル範囲を参照せず直接行列の要素を指定して行列を生成・初期化するには、以下に示す例のように、全要素を区切り文字をつかって行と列を区切って羅列し、全体を波括弧({})で囲みます。列の区切りはカンマ,、行の区切りはセミコロン;です。ちなみに全要素を特定の値で初期化するような関数は無いようです。表計算でそういう需要はあまりないかも知れませんが、全要素を記述するしか無いようです(巨大な行列ではやりたくないが)。

例1) {1,2,3} → \(\begin{bmatrix} 1 & 2 & 3 \end{bmatrix}\)

例2) {1; 2 } → \(\begin{bmatrix} 1 \\ 2 \\ \end{bmatrix}\)

例3) { 1, 2, 3; 4, 5, 6 } → \(\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ \end{bmatrix}\)

単位行列を作る

単位行列は(まあ一般の計算式では使わないかもですが) ワークシート関数 MUNIT を使います。引数には行と列の数です。

例: MUNIT(3) → \(\begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix}\)

行や列の挿入がとても楽だが空白セルは認められない。

表の途中に行や列を挿入する時、行や列ごとに計算式を入れていると計算式をコピペしないといけませんが、行列演算でやってる場合は特に必要ありません。

しかし、通常の計算式では何も入力されていないセルを数値としての 0 として扱ってくれますが、行列として扱っている場合は、計算結果がエラーになります。

あとがき

行や列を操作する関数がもっと増えてくれたら嬉しいな。 行や列の範囲を指定して取り出すとか、行または列の合計とか平均とかね。

ま、使えるところにガンガン使えばきっと便利になるでしょう。エクセル上でディープラーニングとかできると面白いかもね(さすがに無理か)。

とりあえず数学的な行列演算の感覚は思い出しておかないと。