AWS lambdaでKotlinHelloWorld

Posted on 2020/06/06

TOC

はじめに

先に書いておくと、色々試した結果↓このサイトがかなりわかりやすい。 ServerlessFrameworkでtemplateを使用してプロジェクトを作成でき、gradleやserverless.ymlも作成してくれるので、templateを使うべき。 Kotlinプロジェクトをgradle buildでzip化してServerlessFramework(awsやgcpへのプロビジョニング等を共通化して行うフレームワーク)でaws上にデプロイするのだけど、一連のテンプレートを作成してくれるのは本当に楽。自力でやろうとしてかなり時間がかかった。(理解は深まったが)

https://dev.classmethod.jp/articles/easy-deploy-of-lambda-with-serverless-framework/

以下、templateを使わずに色々と苦戦した経緯を記録する。

Kotlinプロジェクトを生成する

以下コマンドを実行する。自動でKotlinプロジェクトが生成される

gradle init --type=kotlin-application

ちなみに2020/5/23時点でAWSLambdaのJava対応バージョンは8と11のみ。

サンプルの実行(一旦動作確認

プロジェクトをbuild、App.ktのmainメソッドを実行すると’HelloWorld’が表示されることを確認します。

※ちなみにIntelliJで毎回忘れるんですが、以下より使用するSDKのバージョン設定等をする

ProjectStructure -> Project -> Project SDK

build.gradleの修正

作成した案件をIntelliJなどIDEで開き、build.gradleに以下を追加する。依存関係を取り込むにはIntelliJの場合 Gradleビュー -> Reimport All Gradle Projects を押下。

// AWS
    implementation 'com.amazonaws:aws-lambda-java-core:1.2.0'
    implementation 'com.amazonaws:aws-lambda-java-events:2.2.7'
    implementation 'com.amazonaws:aws-java-sdk-s3:1.11.647'

SAM CLIでlambdaのペイロードを確認

AWSのサーバーレスアプリケーションの作成と管理を簡単に行うためにSAM CLIを使用します。 まずはsam local generate-eventを使用してサンプルペイロードを生成します。 特に何か作成されるわけではなく、lambdaが受け取るペイロードのサンプルなので出力された値を参考に実装します。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-local-generate-event.html

sam local generate-event apigateway aws-proxy --method POST

実装

lambda関数のハンドラーはRequestHandleを実装したクラスのhandleRequestメソッドです。 https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/java-handler.html

今回は例として、JSONで受け取りStringを返すメソッドを作ってみます。 まずJSONを扱うために以下をbuild.gradleのdependencyに追加します。

implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'

Gradleでzipを作成

以下の通りgradle build実行時にzipを生成するよう記述します。 以下の記述をしてもしなくてもbuild/distributionにzipファイルは作成されていた。なんでだろう・・・。

task buildZip(type: Zip) {
    from compileKotlin
    from processResources
    into('lib') {
        from configurations.runtime
    }
}
build.dependsOn buildZip

KotlinのGradle記述については以下を参考。

https://kotlinlang.org/docs/reference/using-gradle.html

ちなみにGradleのJavaプラグインについてはGradle公式に記載がある。

http://gradle.monochromeroad.com/docs/userguide/java_plugin.html

構成管理デプロイ(ServerlessFramework)

今回はServerlessFrameworkを使用してAWS上にデプロイします。

AWS CloudFormation

AWSに作成したlambda資源をデプロイするにはAWS CloudFormationの設定が必要です。 AWS CloudFormation は、AWS でのプロビジョニングと管理を単純化する仕組みです。 実態はtemplate.ymlで、色々と設定を書く必要があります。

AWS公式のlambda用CloudFormationサンプル https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-lambda.html

ローカル端末のAWS設定

ローカル端末のAWSの認証設定の確認は以下コマンドです。デプロイ前にデプロイ先があっているか確認しましょう。

cat ~/.aws/credentials

初期設定及び書き換えが必要な場合は以下コマンドによって行います。(書き換え時にはoverwriteオプション -o を付与)

sls config credentials --provider aws --key XXXXXXXXXXXXEXAMPLE --secret XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXEXAMPLEKEY

CLIツール(ServerlessFramework)を使用してデプロイする

AWSアプリケーションをローカルで構築、管理、デプロイするツールは色々あって名前も似ていてややこしいですね。 主要な3つのツールの大まかな概要をを以下にまとめます。

  • AWS CLI(AWS Command Line Interface)はAWSサービスををCLIで管理するツール
  • AWS SAM CLI(AWS Serverless Application Model)はサーバレスアプリケーションをローカルで構築したりデプロイするツール
  • Serverless FrameworkはAWS lambdaやAzure、GCP等にサーバレスアーキテクチャを構築するためのオープンソースフレームワークです

Serverless Frameworkが割と良さそうなので使ってみることにします(slsコマンド)。 実態としてはserverless.ymlを作成し、同ファイルが存在するディレクトリでsls deployを実行すればデプロイされます。

slsコマンドには以下のようなテンプレート(serverless.yml)作成機能もあります。

sls create -t aws-java-gradle -p KotlinWebUpdateChecker     // -t [テンプレート名] -p [パス]

テンプレート作成参考 https://dev.classmethod.jp/articles/easy-deploy-of-lambda-with-serverless-framework/

デプロイ

以下コマンドを実行すればOKです

$ sls deploy

私はこのコマンドで色々エラーが出てかなり時間を食いました。 基本的には権限エラーが多かったです(ServerlessFrameworkの公式サイトにある通りAdministratorAccess権限を付与するのが一番早いけどセキュリティ的に嫌ですね)

Serverless FrameworkでAWS上にデプロイする時、awscliで設定しているcredentialを使用しています。 credentialに設定されたユーザーに権限を付与する必要がありますが、ServerlessFrameworkは開発途上のシステムであるため必要な権限が変化しやすいです。 以下サイトにあるとおりServerlessFramework公式ではAdministratorAccess権限を付与することを推奨しています。) https://dev.classmethod.jp/articles/easy-deploy-of-lambda-with-serverless-framework/

権限与えすぎて怖いので私は使いませんでしたが、設定関連でかなりの時間を費やすことになりました・・・。 エラーと解決策について以下に覚書として書いておきます。

デプロイ時のエラー集

デプロイ時にエラーが多発して苦戦したのでエラー対応時のメモをまとめます。

CloudFormationエラー

sls deployだけでOKだったのですが、以下認証エラーが出ました。

Serverless Error ---------------------------------------
 
  An error occurred: ApiGatewayRestApi - User: arn:aws:iam::07015〜〜〜〜:user/affiliate_user is not authorized to perform: apigateway:POST on resource: arn:aws:apigateway:ap-northeast-1::/restapis (Service: AmazonApiGateway; Status Code: 403; Error Code: AccessDeniedException; Request ID: f0a〜27-2fa6-4f50-add0-cdeb41〜〜〜).

※ IDの一部は「〜」に書き換えています

DescribeStacksの権限をAIM上でユーザーに付与すればOKです。IAM上でポリシーを作成し、ビジュアルエディタで権限を付与するのが簡単です。

S3エラー

S3への書き込み権限がないためエラーになります。 sls deployで成功すると以下のようにS3に書き込んでいることがわかります。

Serverless: Uploading CloudFormation file to S3...

S3への書き込み権限を付与すればOKです。

CloudWatchエラー

Serverless Error ---------------------------------------

An error occurred: MainLogGroup - User: arn:aws:iam::07015~~~~~:user/kotlin_web_update_checker is not authorized to perform: logs:DescribeLogGroups on resource: arn:aws:logs:ap-northeast-1:070153960823:log-group::log-stream: (Service: AWSLogs; Status Code: 400; Error Code: AccessDeniedException; Request ID: 25b~~~f-0025-4ce3-bc11-3b5b8a~~~e).

※ IDの一部は「〜」に書き換えています

CloudWatchLogsのlogs:DescribeLogGroups権限を付与すればOKです。

iam:GetRoleエラー

Serverless Error ---------------------------------------

An error occurred: IamRoleLambdaExecution - API: iam:GetRole User: arn:aws:iam::07015~~~~~:user/kotlin_web_update_checker is not authorized to perform: iam:GetRole on resource: role kotlin-web-update-checker-dev-ap-northeast-1-lambdaRole.

iam:GetRole権限を付与すればOKです。

apigateway:POSTエラー

Serverless Error ---------------------------------------

An error occurred: ApiGatewayRestApi - User: arn:aws:iam::070153960823:user/kotlin_web_update_checker is not authorized to perform: apigateway:POST on resource: arn:aws:apigateway:ap-northeast-1::/restapis (Service: AmazonApiGateway; Status Code: 403; Error Code: AccessDeniedException; Request ID: 497ffdf8-850f-482b-8cb2-5205a60a82bd).

apigateway:POST権限を付与すればOKです。

MainLogGroup already exists

以下エラーが出てリンク先のログを見たところ「UPDATE_ROLLBACK_COMPLETE」というステータスでした。(IAMのスタックのステータス)

Serverless: View the full error output: https://ap-northeast-1.console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stack/detail?stackId=arn%3Aaws%3Acloudformation%3Aap-northeast-1%3A07~~~~~~

Serverless Error ---------------------------------------
 
  An error occurred: MainLogGroup - /aws/lambda/web-update-checker already exists.

UPDATE_ROLLBACK_COMPLETEについて以下公式サイトを見てみると、ロールバック失敗後の作業が必要のようです。 https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-continueupdaterollback.html

AWS CloudFormation にはデータベースが削除されたことがわからないため、データベースインスタンスがまだ存在する前提でそれにロールバックしようとし、更新のロールバックが失敗します。

IAMからスタックの削除をしようとしてもできず、sls removeで削除。加えて、CloudWatchのロググループから/aws/lambda/web-update-checkerを削除することでエラーが取れました。 (CloudWatchだけでも良かったような気が)

CloudFormationのエラー

以下エラーになったらsls remove、sls deployで解決する。(removeされても良いのであれば)

Serverless Error ---------------------------------------
 
  Stack:arn:aws:cloudformation:ap-northeast-1:070153960823:stack/kotlin-web-update-checker-dev/fd8c5260-9c2b-11ea-8d0a-0a949e552770 is in UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS state and can not be updated.

MainLogGroupの削除エラー

ログは消せないみたいなので、初回のデプロイであればsls remove、sls deployで良さそう。運用を初めている場合はログを移行したり。

Serverless Error ---------------------------------------
 
  An error occurred: MainLogGroup - /aws/lambda/web-update-checker already exists.

awsコマンドで以下のように削除できます。

aws logs delete-log-group --log-group-name /aws/lambda/web-update-checker

ちなみに私はAWS上の設定ミス上記コマンドが失敗してしまいました。 IAM Policy SimulatorでCloudWatchLogsのDeleteLogGroupの権限をしても権限がなく、 改めて設定を見直すとユーザーとグループの紐付けが誤っていました。

初めはユーザーとグループAを紐付けていたのですが、途中からユーザーとグループBを紐付けてグループBに紐付いたPolicyを修正していたつもりでしたが、ユーザーとグループの紐付けを更新しておらずグループAの権限でずっと動いていました。 個人で管理する時にはシステム毎にユーザーとグループとPolicyを明確な名前で作って使い分けるべきだと反省。

Forbidden

(連続してsls deployしたからかな?)以下エラーが発生しました。sls removeで解決。

Serverless Error ---------------------------------------
 
  Forbidden

Deploy時にDELETE_FAILEDエラー

ServerlessFrameworkを使ったDeploy時に引き上げエラー。内容を見ると、関連するBucketが削除できないためスタックを削除できないとのこと。 S3は使ってないはずだけど、デプロイ資源がS3に引き上がるっぽい。

Serverless: Validating template...
Serverless: Updating Stack...
 
  Serverless Error ---------------------------------------
 
  Stack:arn:aws:cloudformation:ap-northeast-1:070153960823:stack/kotlinwebupdatechecker-dev/4b3f4220-9dc0-11ea-994b-0e7389934c04 is in DELETE_FAILED state and can not be updated.
The bucket you tried to delete is not empty
The following resource(s) failed to delete: [ServerlessDeploymentBucket].

以下コマンドでスタックを削除できたため開発。 むやみにS3バケットとか削除すると面倒なことになるので避けるのが無難。 以前S3バケットを削除した時は論理削除しかできておらず同様のバケットを作成できず、スタックからはバケットをまだ見ることができてややこしいことになった。

serverless remote -v

再度deployする時に気をつけることまとめ

色々ありましたが以下にまとめました。sls deployで失敗しても中途半端にIAMRoleが作られてたり、CloudFormationのスタックが作成後ロールバックに時間がかかったりで待ち時間がかなり長かったです。セキュリティの問題はあるけどAdministratorAccess権限を使った方が良いですね・・・。

  • IAMRoleが既に作成されていないか
  • CloudFormationのスタックに中途半端な状態のスタックが存在しないか(UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS ステータスのスタックなど)
  • S3のバケットを消してしまうと同じ名前のバケットが1時間作れなくなる。どうしても消したい場合はフォルダは残して中身だけ消す
  • IAMの権限反映は10秒くらいまでばわりとOK

権限をちまちま追加して実行しましたが、結局以下のような感じになりました。以下6つの権限をフルアクセスにすれば間違いなく動きそうです。

  • API Gateway フルアクセス
  • CloudFormation 完全: リスト, 読み込み 制限: 書き込み
  • CloudWatch Logs 制限: リスト, 書き込み
  • IAM 完全: 読み込み 制限: 書き込み, アクセス権限の管理
  • Lambda フルアクセス
  • S3 完全: リスト, 読み込み, 書き込み 制限: アクセス権限の管理

引き上げ後の動作確認

sls deployに成功すると以下のようにURLが表示されます。

endpoints:
  GET - https://l093~~~~6.execute-api.us-east-1.amazonaws.com/dev/users

そこにアクセスすると以下のようにレスポンスが帰ってきました。動作確認完了です。

$ curl https://l093wk8is6.execute-api.us-east-1.amazonaws.com/dev/users

{"message":"Go Serverless v1.x! Your Kotlin function executed successfully!","input":{"resource":"/users","path":"/users","httpMethod":"GET","headers":{"Accept":"*/*","CloudFront-Forwarded-Proto":"https","CloudFront-Is-Desktop-Viewer":"true","CloudFront-Is-Mobile-Viewer":"false","CloudFront-Is-SmartTV-Viewer":"false","CloudFront-Is-Tablet-Viewer":"false","CloudFront-Viewer-Country":"JP","Host":"l093wk8is6.execute-api.us-east-1.amazonaws.com","User-Agent":"curl/7.65.3","Via":"1.1 1a3215a2c48bae3a908a6ecfac43c8f4.cloudfr〜〜〜(省略

ローカルで動作確認・・・できなかった

途中で諦めたのですがメモを残しておきます。 ServerlessFrameworkを使ってローカルで動作確認をするには、まずserverless-offlineをインストール。

$ npm install -D serverless serverless-offline

以下コマンドで実行

$  sls invoke local --docker --function hello  -d '{"key":"value"}'  -e SLS_DEBUG=.serverless/serverless.log

で動けば良いのだけど以下エラー。npmのダウングレードで回避できるという記事もあったけど回避できず。必須ではないので一旦諦めました。

Error --------------------------------------------------
 
  Error: ENOENT: no such file or directory, open '/Users/〜〜/IdeaProjects/kotlinProjects/KotlinWebUpdateChecker/.serverless/invokeLocal/artifact/kotlin/'

ちなみに以下コマンドでリクエストを送り、想定される結果が帰ってくればOK!の予定でした。

$ curl -X POST -d '{"name": "なまえサンプル", age: 98}' --dump-header - http://127.0.0.1:3000

Lambdaのロール設定

ServerlessFrameworkでLambdaに資源を引き上げるとロールが作成される。 そのロールはポリシーが適用されており、ポリシーに設定されたアクセス権限にはS3へのアクセスが含まれていないため付与する必要がある。(CloudWatchLogsへの読み書きのみ付与されている) Lambdaの権限についてはserverless.ymlに記載する。以下のような感じです。

provider:
  iamRoleStatements:
    - Effect: Allow
      Action:
        - "logs:CreateLogStream"
        - "logs:CreateLogGroup"
        - "s3:*"
      Resource:
        - "*"

S3へのバケットポリシー設定

Lambda側からS3へのアクセス制限設定に加えて、S3側でもアクセス制限設定が存在します。 AWSへのアクセスは初期状態で全て拒否です。 バケットを作成しても何かしらアクセス権限を設定しないと外部からアクセスできません。 Lambdaからのアクセスについても同様で、バケットポリシーを設定する必要があります。 DenyポリシーとAllowポリシーを書くことができ、Denyが優先されます。

参考 https://dev.classmethod.jp/articles/learn-aws-policy-documents-with-examples/

LambdaからS3へのアクセス

今回ここでかなり躓きました。 Lambda実行中になぜかS3へのアクセス中に落ちる。 S3上のアクセス制限を緩くしても、Lambda実行時のIAMRoleの権限を強くしても解決できず、結局はLambdaのタイムアウト設定を長くしたら解決しました。 Exceltion吐くはずなのになぜかException結果を出力できなくて標準出力デバッグでかなり苦戦しましたがそもそもタイムアウト設定とは・・・。 実行中に落ちたからException吐かなかったのね、と納得です。 CloudWatchLogsのログの最後によく見たら書いてあるし。AWS不慣れなのげそもそもの原因ですね。

2020-06-05T06:35:56.440Z 916fc3f3-9bba-464f-9eb1-2fbbe29505b5 Task timed out after 6.00 seconds

VPC上のLambdaからインターネットへアクセス(余談)

LambdaからS3へのアクセスができなかった時にAWS上の構成について学び、VPCにLambdaを配置した時の設定について調べたのでついでに書きます。 ちなみに今回のようなケースではVPCを使う必要もないのでNATゲートウェイも必要ありません。

Lambdaからインターネットへアクセスするには以下AWS公式サイトにある通り以下の設定が必要になります。

  • プライベートVPN
  • パブリックVPN
  • NATゲートウェイ(またはNATインスタンス)

参考 https://aws.amazon.com/jp/premiumsupport/knowledge-center/internet-access-lambda-function/

コストを見てみると以下のような感じでした。

コンポーネント コスト
プライベートVPN とくにかからなそう。AWSサイト間VPNや別システムとのプライベート接続をするとコストがかかる
パブリックVPN 同上
NATゲートウェイ 利用可能であった “NAT ゲートウェイ時間” に対して課金。0.045 $/hour(33.48$/Month)

AWSアーキテクチャにおける料金がかかる箇所まとめ

http://blog.serverworks.co.jp/tech/2020/02/05/post-78668/

インターネットゲートウェイとNATをゲートウェイの違い

インターネットゲートウェイはVPCのインスタンスとインターネット間の通信を実現する。 VPCのインスタンスはpublic用のElasticIPを割り振られている必要があり、VPNからインターネットへの通信時にインターネットゲートウェイが送信元IPをElasticIPに変換する。 LambdaにはパブリックIPが付与されないため、インターネットへアクセスするには何らかの方法でパブリックIPを付与する必要がある。 つまりNATをゲートウェイによるパブリックIP付与や、EC2を経由したアクセス(NATインスタンス)などが解決策となる。

メモ: LambdaはAWSの制約でパブリックVPCには置けないみたい。 https://mussyu1204.myhome.cx/wordpress/it/?p=598

定期実行の設定

serverlessに以下のように書くことで定期実行の設定ができます。 ※EventBridgeに設定するため、EventBridgeの権限を付与する必要があります。

functions:
  wuc_update_check:
    handler: wuc.controller.LambdaHandler
    events:
      - schedule: cron(0 6 * * ? *) 

cronの書き方は以下を参照

https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html

総括

Kotlinの実装はそんなに苦ではなかったのですが、AWSへのデプロイがきつかった。私の経験が浅い所なのでやむなしですが・・・ とにかくServerlessFrameworkのtemplateを使うことが一番早い。sls deployとsls removeをとにかく連打w

後はAWS上のクセ。 権限付与関連はServerlessFramework目線で言えば、値の取得、作成、書き込み、上書きなど色々と権限が必要なのでちまちまと権限付与しても何度もエラーにかかって大変。 S3のバケット削除してから1時間は同バケット名が使えないとか、知らないと地味にひっかかる点が多い。 途中でAWS CertificatedSolutionsArchitectAssociateの書籍を読んだが、経験しないとわからない点が多いなと感じた。 黒本は黒本でAWSの仕組みを体系的に学ぶために大変役に立ったがアーキテクト向け知識と実作業知識は異なる。

参考サイト

今から始めるServerless Frameworkで簡単Lambda開発環境の構築 https://dev.classmethod.jp/articles/easy-deploy-of-lambda-with-serverless-framework/

JavaでLambda関数を書いてSAMでデプロイしてみた https://qiita.com/mmclsntr/items/b91097dc0c3697e16f07