銀の弾丸

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

DynamoDB Query APIのパラメータを生成しましょ(プレースホルダをぶっ飛ばせ)

f:id:takamints:20191104181915j:plain
photo credit: Kurayba Micro Machine via photopin (license)

AWS のNoSQLデータベース DynamoDB のテーブルを Query する場合のパラメータを生成するWebアプリをつくりました。

DynamoDBでテーブルの項目を取りだすには Query API を使いますが、このパラメータがちょっと曲者なんですよね。

属性名が予約済みのキーワードにマッチしていると条件式などに直接記述できなくて「属性名のプレースホルダ」を定義して、読み替える必要があるのです。 さらに条件式では具体的な値(文字列や数値)が直接使えず「属性値のプレースホルダ」に変換しないといけません。

実際、慣れるとたいしたことではないのですけど最初はかなり悩みました。 今でも困ってる人はいるんじゃないかと思います。

DynamoDBはよくできたサービスですけど、これ(↑)って参入障壁かもしれない。 かつて悩んで解決したので、このようなソリューションを公開する責任があるのではないだろうか?と鼻息荒く意識は高く、以下に公開する次第ですー・・・


RDB技術者のためのNoSQLガイド
秀和システム (2017-05-09)
売り上げランキング: 45,590

使い方

少しUIがしょぼいですが、徐々に改善するつもりです(ゆるして)。

左にチェックボックスがついているのは省略可能なパラメータ。最初はチェックが外れていますがチェックをつけるとサンプルで入力した内容が復活します。

各テキストボックスを編集すると即座に結果のパラメータが変化します(エラーがあると変化しないので気を付けてください)。

属性名にDynamoDBのキーワードが含まれていると属性名のプレースホルダが生成されるのを確認してみてください。

生成されたパラメータはDynamoDBのQuery APIの第一引数にそのまま指定できるJSONです。[Copy]ボタンでクリップボードにコピーできまーす。

  • TableName は必須です。テーブル名を指定します。
  • KeyConditionExpression も必須で、テーブルのキーに対する抽出条件を指定します。キー以外の属性は使えません。パーティションキーには、= しか使えません。レンジキーには、範囲指定の演算子<,<=,=,>=,>, そして、BETWEEN <s> AND <e> が使えます。両者はAND条件で組み合わせます(ORは無理)。パーティションキーの条件は省略可能です。
  • FilterExpression は、KeyConditionExpressionで抽出した結果に対してフィルターをかける機能。DynamoDBではSCANした量によって課金されますからFilterで絞っても対象外です。でもFilterはAWS側で行われるので、インターネットの転送量は絞ったほうが少なくなります。まあややこしいところですが、そのうち理解できますよ(多分)。
  • Limit は抽出する行数を指定します。
  • ProjectionExpressionSQLのSELECT句。取り出したい属性値の名称をカンマ区切りで指定します。省略すれば属性値全体を取り出します。属性名がキーワードかどうかを気にする必要はありません。

関連サイト

npm aws-node-utilSQL的な構文でDynamoDBを操作する機能を提供していますが、この度こちらにこのページで使っているWebAppを公開してるのでよろしくね。

www.npmjs.com

takamin.github.io

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なんですよ。

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