LambdaやDynamoDBを使ったサーバレスアーキテクチャを構築するうえでとても便利なIaaC(Infrastructure as a Code)ツールであるSAM(Serverless Application Model)を使って、Lambda Authorizerを使った認証付きAPIを作成したいと思います。
今回はトークンベースの認証とします。
SAMには、いくつかのテンプレートが用意されています。その中でも最もシンプルなhello-worldテンプレートからスタートしてみます。
sam init
上記のコマンドでプロジェクトの作成を開始します。
今回は、AWS Quick Start Templatesを選び、Zipでデプロイする方式で、ランタイムはnodejs14.x、使用するテンプレートはHello World Exampleとしました。
ちなみに、ここでとってくるSAMのtemplateは、以下のリポジトリからクローンされます。
https://github.com/aws/aws-sam-cli-app-templates
結果として、次のように表示されました。
-----------------------
Generating application:
-----------------------
Name: authorizerDemo
Runtime: nodejs14.x
Dependency Manager: npm
Application Template: hello-world
Output Directory: .
Next steps can be found in the README file at ./authorizerDemo/README.md
これでアプリケーションを開発していくためのベースのテンプレートからスタートできます。
ちょっとディレクトリ構成をいじります。
src <- 新規追加
hello-world <- もともとあったソースをsrcディレクトリ以下に移動
auth <- 新規追加
authディレクトリに移動して、npm init
します。
auth.jsを作成します。これがユーザの資格情報をチェックするコードになりますが、ここでは、簡単な例として、tokenに、mySecret
という文字列が入っているとき、Allowとし、それ以外はDenyとするコードにします。
exports.lambdaHandler = async(event, context) => {
const token = event.authorizationToken;
// このAPIについて拒否したり許可したりしたいので、このAPIを指定するためのリソースARNを取得
const resource = event.methodArn;
// tokenがmySecretであるならば、許可、そうでないなら拒否
if (token == 'mySecret') {
console.log('allow');
return {
principalId: token,
policyDocument: {
Version: "2012-10-17",
Statement: [{
Action: "execute-api:Invoke",
Effect: "Allow",
Resource: resource,
}, ],
},
};
}
else {
console.log('deny')
return {
principalId: token,
policyDocument: {
Version: "2012-10-17",
Statement: [{
Action: "execute-api:Invoke",
Effect: "Deny",
Resource: resource,
}, ],
},
};
}
};
template.yamlを、次のように変更します。
myApiは、API Gatewayを定義します。ここで、Authorizerとして、authFunctionを指定しています。
authFunctionとして、前述のauth.jsが呼ばれます。
また、必須ではないのですが、StageNameをパラメタとして定義して、ほかで使いまわせるようにします。
Parameters:
StageName:
Type: "String"
Default: "dev"
Resources:
myApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref StageName # 上で決めたStageNameを参照します
Auth:
DefaultAuthorizer: authFunction
Authorizers:
authFunction:
FunctionArn: !GetAtt authFunction.Arn
authFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/auth/
Handler: auth.lambdaHandler
Runtime: nodejs14.x
既存のHello-world関数に認証をつけたいと思いますので、以下のようにHello-world関数の定義で、LambdaAuthorizerを呼び出すように変更します。
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/hello-world/
Handler: app.lambdaHandler
Runtime: nodejs14.x
Events:
HelloWorld:
Type: Api
Properties:
RestApiId: !Ref myApi # <- ここで、Authorizerを呼ぶように設定しています
Path: /hello
Method: get
Outputも設定しておきます。
HelloWorldApiのエンドポイントで、上記のmyApiのStageNameを参照するように書き換えています。
Outputs:
ApiWithLambdaAuth:
Description: "API Gateway endpoint URL"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/${StageName}/hello/"
ここまでできたら、あとは、デプロイするだけです。
template.yamlが見えるパスで、以下のコマンドで、ビルドしてデプロイします。
sam build
sam deploy --guided
guidedの対話シェルでは、以下のように入力をしました。ここでsamconfig.tomlファイルを作成しているのですが、このファイルとtemplate.yamlファイルが見えているパスで、--guidedをつけずに、sam deploy
すると、自動的にtomlファイルを読み込んでデプロイしてくれるようになります。便利ですので、せっかくなので作成するようにしましょう。
Setting default arguments for 'sam deploy'
=========================================
Stack Name [sam-app]: lambda-auth-demo
AWS Region [ap-northeast-1]:
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]: y
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: Y
Save arguments to configuration file [Y/n]: Y
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]:
ところが、デプロイは途中でこけてしまいます。以下のエラーが出力されるのですが、何が起きているのでしょうか。
Error: Failed to create changeset for the stack: lambda-auth-demo,
ex: Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state:
For expression "Status" we matched expected path: "FAILED" Status: FAILED.
Reason: Unresolved resource dependencies [ServerlessRestApi] in the Outputs block of the template
Unresolved resource dependencies [ServerlessRestApi] in the Outputs block of the templateは、ServerlessRestApiというリソースが解決されません、ということなのですが、この、ServerlessRestApiというのは、API Gatewayをテンプレートのなかで明示的に指定しなかったときに、SAMによって暗黙的に生成されるAPI Gatewayのリソース名です。
もともとのテンプレートでは、API Gatewayのリソースを明示的に定義していないので、これが使えるのですが、いま、Authorizerを設定するために、API Gatewayのリソースを明示的に定義したので、使えなくなっています。
そこで、自分で定義したmyApiに変えてあげる必要があります。初めてデプロイしたときにはまったので、共有しておきます。
ただ、Outputsは、SAMが作成したエンドポイントを単に出力するだけなので、最悪なくても、動作に問題はないと思います。その場合は、自分でコンソール上でAPIゲートウェイを見に行くか、aws cloudformation --describe-stacks
などを用いてエンドポイントを確認します。
Outputs:
ApiWithLambdaAuth:
Description: "API Gateway endpoint URL"
Value: !Sub "https://${myApi}.execute-api.${AWS::Region}.amazonaws.com/${StageName}/hello/"
さて、これでデプロイできるはずです。やってみましょう。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key ApiWithLambdaAuth
Description API Gateway endpoint URL
Value https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello/
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Successfully created/updated stack - lambda-auth-demo in ap-northeast-1
成功したみたいです!
動作確認をしてみましょう。
1.headerなし
$ curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello/
{"message":"Unauthorized"}
2.不正なトークン
$ curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello/ -H "Authorization: mySecrets"
{"Message":"User is not authorized to access this resource with an explicit deny"}
3.正当なトークン
$ curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello/ -H "Authorization: mySecret"
{"message":"hello world"}
1では401(Unauthorized)、2では403(Forbidden)が返り、3で200(OK)が返ります。
これで期待通りに動作していることが確認できました。