AWS Lambda で遊ぼう(第1話: DynamoDBといっしょ)

AWS Lambda と仲直りしてみる

はじめに

ゴールデンウィーク特別企画!

AWS Lambda と仲良くなろう!!

……はい。


仕事で AWS と関わるようになり早1年が経とうとしているが、未だに使いこなせていないサービスが多い。

AWS Lambda もそのうちのひとつだが、そう遠くない将来、サーバレス案件の比重が増えることが分かっており、なんとかキャッチアップしないといけないと危機感を募らせるようになった。

そこで、Lambda と仲良くなるまでの過程の話を、ここに書き残していきたい。

もともとは 1つの記事に「マネジメントコンソール上で Lambda を作って動かす」→「Cloud9 + AWS SAM CLI で IaC」→「CodeCommit + CodeDeploy + CodePipeline で CI/CD」あたりをまとめて書きたかったが、話があまりにも長くなりすぎたので分割することにした。

今日は、できるだけミニマルなサンプルを通じて、「マネジメントコンソール上で Lambda を作って動かす」レッスンに取り組む話に焦点を絞る。

【練習1】 UUID を生成する Lambda 関数を手作りしてみよう

最低限、いったい何が起こるのかを理解するために、SAM CLI といった便利な自動化ツールの手を借りず、手動で一から Lambda 関数を作成してみよう。

※ いきなり SAM を使いたい方はコチラ:
tercel-tech.hatenablog.com

章題にもある通り、UUID を生成するだけの簡単な Lambda 関数を作ってみることにする。

ちなみに UUID とは絶対に重複しないといわれる便利な乱数である。

関数の作成

AWS マネジメントコンソールで「Lambda」を検索し、ページ右上の「関数の作成」ボタンを押す。

f:id:tercel_s:20210429144750p:plain

関数名に適当な名前をつけ(関数名には半角英数字、-ハイフン_アンダースコアのみ使用可)、ランタイムには「Python 3.8」を選択し、ページ右下の「関数の作成」ボタンを押す。

f:id:tercel_s:20210429144702p:plain

ランタイムのサポートポリシー

下図に示すように、Lambda 関数を作成する画面では、ランタイム(開発言語*1)をいくつかの候補から選択できる。

f:id:tercel_s:20210429134524p:plain

ランタイムを選択する際には、情報の豊富さや AWS Lambda のランタイムサポートにも注目しよう。

ランタイムサポートポリシーによると、Node.js 10系は今年の7月末には非推奨となり(フェーズ1)、さらに8月末には当該ランタイムを使用する Lambda 関数の更新ができなくなる(フェーズ2)とされている。

名前 OS フェーズ1開始 フェーズ2開始
Python 2.7 Amazon Linux 2021/7 /15 2021/9/30
Ruby 2.5 Amazon Linux 2021/7/30 2021/8/30
Node.js 10.x Amazon Linux 2 2021/7/30 2021/8/30
Node.js 8.10 Amazon Linux 2020/3/6
Node.js 6.10 Amazon Linux 2019/8/12
Node.js 4.3 Edge Amazon Linux 2019/4/30
Node.js 4.3 Amazon Linux 2020/3/6
Node.js 0.10 Amazon Linux 2016/10/31
.NET Core 2.0 Amazon Linux 2019/5/30
.NET Core 1.0 Amazon Linux 2019/7/30

ご覧の通り Node.js はそこそこ短命なので、売り物に組み込む際はアップデートを見据えた運用体制を確保できるかどうかがポイントとなる。

Lambda 関数の基本形 (Python 3.8)

しばらく待つと、関数が正常に作成された旨のメッセージが表示される。

f:id:tercel_s:20210429151034p:plain

このままページの中程までスクロールすると、lambda_function.py というファイルが作られ、Hello World 的なサンプルコードが予めセットされていることに気付く。

f:id:tercel_s:20210429150520p:plain

これが、すべての始まりであり基本形でもある。

import json

def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

もう少し一般化すると、Python 3.8 における Lambda 関数の基本形は、eventcontext という 2つの引数をとる以下の形をした関数である。

def xxx_handler(event, context):
    # ... 主処理...
    return 戻り値

ここで、2つの引数がなにものであるか気になったので簡単に触れておく。 気にならないようであれば無視して次節までスキップして構わない。

第1引数event は、いわゆるイベントパラメータである。

たとえば、REST API として Lambda が実行される場合は、HTTP 要求電文に含まれるもろもろのパラメータが event を通して引き継がれてくる。

event には、Lambda 関数を呼び出す側がセットした JSON がそのまま丸ごと入っているため*2、言い換えると event がどのような値を持つかは、Lambda 関数の実行契機となるイベント次第となる。

第2引数context は Lambda 関数自身のメタ情報なので、当面の間あまり意識しなくてよろしい。

context がどのようなプロパティを持っているかについては、公式ドキュメント「Python の AWS Lambda context オブジェクト - AWS Lambda」に詳説があるが、自分自身の関数名や ARN が取得できたりメモリサイズが取得できたりするくらいである。

戻り値も、イベントによって異なる。

REST API ならば HTTP の応答電文を返す必要があるが、単なる定期実行バッチならば 0正常終了9異常終了 かも知れない。

Lambda 関数のコーディング

表示されている lambda_function.py を以下のように書き換えてみよう。

uuid 標準ライブラリからバージョン4 の UUID を生成し、戻り値にセットするだけの簡単な改修である。

import json
import uuid

def lambda_handler(event, context):
    id = str(uuid.uuid4())
    return {
        'statusCode': 200,
        'body': json.dumps({ 'id': id })
    }

Changes not deployed というバッジが表示されるので、「Deploy」ボタンを押す。

f:id:tercel_s:20210429152510p:plain

デプロイに成功すると「Deploy」ボタンが非活性になり、Changes deployed という緑色のバッジが表示される。

動作確認

デプロイした Lambda 関数の動作確認をするため、オレンジ色の「Test」ボタンを押そう。

f:id:tercel_s:20210429154525p:plain

すると、「テストイベントの設定」画面が現れ、動作確認用のパラメータを JSON 形式で設定できる。

ここに設定した内容は、Lambda 関数をテスト実行した際に event 引数にまとめて引き渡されてくる。 ただし今回はパラメータを参照しないため、特に何も変更せず、適当なイベント名を入力してページ右下の「作成」ボタンを押そう。

f:id:tercel_s:20210429153621p:plain

この状態で、再び「Test」ボタンを押すと、Lambda 関数がテスト実行される。

f:id:tercel_s:20210429154911p:plain

しばらく待つと、戻り値や Lambda 関数の実行ログなどが確認できる(標準出力に吐いた文字列などもログとして出力される)。

f:id:tercel_s:20210429155418p:plain

実行結果を見ると、056839e5-552a-44fc-9275-8331a5103386 という、どう考えても被りそうにない UUID が返却されていることが分かる。

𝗥𝗲𝘀𝗽𝗼𝗻𝘀𝗲
{
  "statusCode": 200,
  "body": "{\"id\": \"056839e5-552a-44fc-9275-8331a5103386\"}"
}

(以下略)

ちなみにログは、CloudWatch Logs からも確認できる。

f:id:tercel_s:20210429110506p:plain

Lambda 関数の権限について

ここまでの結果からも分かるとおり、この Lambda 関数は、CloudWatch Logs に対する書き込み権限をもったロール (AWSLambdaBasicExecutionRole) の下で実行されていたことになる。

Lambda 関数を作成する際に、裏でロールが自動的に作成されていたのである。 この正体を確認するには、実際に Lambda 関数の「設定」→「アクセス権限」を選択すればよい。

f:id:tercel_s:20210429163955p:plain

DynamoDB や S3 といったリソースと連携する必要がある場合は、Lambda に対してアクセス権を追加する必要がある。

次の練習で実践してみよう。

【練習2】 UUID を DynamoDB に登録してみよう

続いて、生成した UUID を DynamoDB に蓄積するように改修してみよう。

DynamoDB は AWS の NoSQL データベースである。 データベース操作と聞いて身構える必要はない。

DynamoDB テーブルの作成

マネジメントコンソールから「DynamoDB」を検索しよう。

f:id:tercel_s:20210429165116p:plain

ここで、「テーブルの作成」ボタンを押す。

もし DynamoDB のトップページに「テーブルの作成」ボタンが表示されない場合は、左側のメニューから「テーブル」→「テーブルの作成」ボタンの順にクリックする。

f:id:tercel_s:20210429165322p:plain

テーブル名を適当に入力、プライマリキーは簡単のため「id」と入力し、型は「文字列」のままページ右下の「作成」ボタンを押す。

ここで入力したテーブル名は後で使うので覚えておこう。

f:id:tercel_s:20210429165944p:plain

Lambda 関数の改修

以下のコードに書き換えて、「Deploy」ボタンを押す。

import json
import uuid
import boto3

dynamodb = boto3.resource('dynamodb') # DynamoDB オブジェクト

def lambda_handler(event, context):
    id = str(uuid.uuid4())
    
    table = dynamodb.Table('potato-db')    # 先ほど作成した potato-db テーブル
    table.put_item(Item = { 'id': id })    # id を書き込む
    
    return {
        'statusCode': 200,
        'body': json.dumps({ 'id': id })
    }

f:id:tercel_s:20210429173822p:plain

先ほどと同じ要領で Deploy → Test 実行すると、権限不足で AccessDeniedException が発生してしまう。

f:id:tercel_s:20210429173342p:plain

これは、前述のとおり、Lambda 関数ができることがせいぜい CloudWatch Logs へのログの書き出しくらいだからである。

そこで、次節では DynamoDB の読み書きができるよう、権限を追加してみよう。

アクセス権の編集

再び「設定」タブをクリックし、左側のメニューから「アクセス権限」を選択、表示されている「実行ロール」のリンクをクリックしよう。

f:id:tercel_s:20210429174142p:plain

すると当該ロールの編集ページが開くので、「ポリシーをアタッチします」ボタンをクリックする。

f:id:tercel_s:20210429174823p:plain

「AmazonDynamoDBFullAccess」という権限を選択し、右下の「ポリシーのアタッチ」ボタンをクリックする。

f:id:tercel_s:20210429175603p:plain

成功すると、「ポリシー AmazonDynamoDBFullAccess がアタッチされました」というメッセージが表示される。

f:id:tercel_s:20210429180044p:plain

一応、この画面から AmazonDynamoDBFullAccess がいかなるポリシーかを確認することができる。 単なる DynamoDB への書き込み権限を超えてそこそこえぐい設定が連なっている。

もしも仕事で使うなら、 PoC とはいえもっと権限を絞るべきだろう。

f:id:tercel_s:20210429180338p:plain

動作確認

再び、Lambda 関数に戻り、「Test」ボタンを押す。 プログラムは変更せず、権限を編集しただけなので、再 Deploy は不要である。

うまくいくと、DynamoDB に登録した UUID が戻り値に表示される。

f:id:tercel_s:20210429181146p:plain

では、本当に DynamoDB に登録されているかどうかを確認するため、マネジメントコンソールで「DynamoDB」を検索し、左側のメニューから「テーブル」→(テーブル名)ラジオボタン →「項目」タブの順にクリックしよう。

先ほど表示された UUID が DynamoDB の id に追加されていることが確認できるだろう。

f:id:tercel_s:20210429181655p:plain

ここまでのまとめと課題

実を言うとここまでの手順は、かなりユーザ体験エクスペリエンス(開発者体験?)が悪い

まず、マネジメントコンソールをあちこちったり来たりしなければならず、思考が分断されがちである。

今回は極めて簡単な題材にも拘らず、Lambda と DynamoDB と IAM を切り替えながら作業を進める必要があった。 Lambda から参照するために DynamoDB のテーブル名を確認しに行ったり、権限を確認するため IAM へのリンクに飛んだりといった具合である。

あちこちタブを開いているうちに、自分が何をしようとしているのか忘れて作業漏れが発生したら怖い。

次に、ソースコードの変更管理のしようがない点も気になる。

Web のフォーム上でソースコードを直接編集する方法では、いつ・誰が・なんの目的をもって Lambda を書き換えたかが、後になってトレースできないのである。

そして、最も大きな問題は、リソースが散らかることである。

Lambda と DynamoDB を連携させるため、DynamoDB に設定したテーブル名を Lambda に転記するシーンがあったが、あれはかなりイケてない。

もしも開発の中盤でうっかり DynamoDB の命名にスペルミスが見つかり、急遽設定を変更したとしよう。 関連する夥しい Lambda の横展開修正が怖い。

自分の意思で作った Lambda や DynamoDB はまだしも、暗黙のうちに作られた IAM ロールやポリシーが人知れず溢れかえっていく未来が見える。

開発が大規模化すればするほど、管理できないモノたちがしょうけつを極めるだろう。

だいぶ辛辣ですな。

もしも Lambda の開発方法がコレしかないなら、とうに心が砕け散っていたと思う。

ふむー。

今日の記事は、AWS SAM の話の前置きに過ぎないのだ。

AWS SAM と IaC

そんなわけで AWS SAM というフレームワークを利用することで、これらの問題点の解決に挑みたい。

Lambda 関数と周辺のリソースをまとめて設定ファイルに定義し、コマンドラインツールによってビルド・テスト・デプロイを実現するというものである。

しかしこれはこれで、環境構築や設定ファイルの書き方など最初の難関があったりする。

そこで、できるだけミニマルなサンプルから始めることで、とっかかりの認知負荷を下げつつ Lambda の開発体験を向上させる話を次回ちょっと書くかも。

*1:開発言語とランタイムは厳密には別の意味を持つ言葉だが、話を簡単にするためここでは同義語として扱う。

*2:Lambda 関数の中ではパースされた状態となっている。

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.