SAMチュートリアル
準備
SAM用の IAM を作成
- Identity & Access Management (IAM)ページにアクセス
- Users をクリックし、Add user をクリック
- 名前はなんでも良いが「SAM-Admin」などわかりやすい名前とする
- Programmatic access を有効にチェックし Next をクリック
- Attach existing policies directly をクリック
- AdministratorAccessを検索して選択し Next をクリック(嫌なら必要なポリシー作成すること)
- 設定項目を確認の上、Create user をクリック
API Key と Secret を表示しメモしておく。次のステップで必要になる。
Cloud9のワークスペース作成
ここで作業を行う。
とりあえず全てデフォルト設定、無料枠で良い。
AWS Credencial の設定
作成したIAMでCloud9デフォルトのIAMから上書きする / 別の設定を作成して切り替える
aws configure
jq のインストール
curlコマンドの出力結果を綺麗に表示してくれます。
sudo yum install jq
# 使う時はパイプする
# $ curl http://127.0.0.1:3000/hello | jq
AWS SAM の開始
sam init
- 基本デフォルト設定でOK。
- AWS quick start application templates: 3(Quick Start: From Scratch)
プロジェクトのビルド
sam buildコマンドは、アプリケーションの依存関係をビルドし、ソースコードを.aws-sam/build以下のフォルダにコピーします。
# template.yamlがあるディレクトリに移動する
cd {project-name}
sam build
Lambda関数のローカル実行
sam local invoke helloFromLambdaFunction
サンプルイベントオブジェクトの作成
Lambdaをローカル実行する時に便利なイベントオブジェクトのサンプルを作成することができる。
以下は、APIGatewayイベントのサンプルを作成する例。コマンド実行することで .jsonファイルが作成される。
sam local generate-event apigateway aws-proxy > event_file.json
サンプルのLambdaハンドラが、イベントを受け取って出力するように修正する。
/**
* A Lambda function that returns a static string
*/
+ exports.helloFromLambdaHandler = async (event, context) => {
// If you change this message, you will need to change hello-from-lambda.test.js
const message = 'Hello from Lambda!';
// All log statements are written to CloudWatch
+ console.info(`${event.body}`);
return message;
}
作成したサンプルイベントを、Lambda関数に渡すことで、event.bodyがログに出力される。
sam local invoke helloFromLambdaFunction -e event_file.json
ローカルでAPIのテスト
作成したリソースをデプロイすることなくテストすることができます。
start-api
コマンドは、REST APIのエンドポイントを複製しローカルで起動します。裏側ではローカルで関数を実行するためにコンテナをダウンロードしています。
sam local start-api
プロジェクトのデプロイ
ガイドで設定を確認しながらデプロイします。
sam buildコマンドで構築したビルドをパッケージ化して、S3バケットにアップロードされます。さらにAWS CloudFormationスタックが作成され、それに基づいてアプリケーションがデプロイされます。
アプリケーションがHTTPエンドポイントを作成した場合、sam deployが生成する出力には、テストアプリケーションのエンドポイントURLも表示されます。curlを使って、そのエンドポイントURLを使ってアプリケーションにリクエストを送ることができます。例えば、以下のようになります。
sam deploy --guided
CloudFormation events from changeset
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus ResourceType LogicalResourceId ResourceStatusReason
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS AWS::IAM::Role helloFromLambdaFunctionRole -
CREATE_IN_PROGRESS AWS::IAM::Role helloFromLambdaFunctionRole Resource creation Initiated
CREATE_COMPLETE AWS::IAM::Role helloFromLambdaFunctionRole -
CREATE_IN_PROGRESS AWS::Lambda::Function helloFromLambdaFunction -
CREATE_IN_PROGRESS AWS::Lambda::Function helloFromLambdaFunction Resource creation Initiated
CREATE_COMPLETE AWS::Lambda::Function helloFromLambdaFunction -
CREATE_COMPLETE AWS::CloudFormation::Stack sam-app -
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
Successfully created/updated stack - sam-app in ap-northeast-1
DynamoDBの作成
テーブルを定義して作成する。
PK、SK には具体的な名前はつけないで、Prefixをつけ管理できるようにしておく。
Resources:
# ----- DynamoDB -----
HogeHogeTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: fugaTable
AttributeDefinitions:
-
AttributeName: "PK"
AttributeType: "S"
-
AttributeName: "SK"
AttributeType: "S"
KeySchema:
-
AttributeName: "PK"
KeyType: "HASH"
-
AttributeName: "SK"
KeyType: "RANGE"
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
GlobalSecondaryIndexes:
- IndexName: "gsi-pk-sk"
KeySchema:
- AttributeName: "SK"
KeyType: HASH
- AttributeName: "PK"
KeyType: "RANGE"
Projection:
ProjectionType: ALL
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
DynamoDBを操作するLambdaハンドラを作成する
データの作成や取得をするLambdaを作成する。
今回は仮に、組織とそのメンバーを管理するとする。
コンポジットキー
- 組織: ORG#orgId
- メンバー: USER#userId
ハンドラの作成
ファイルを作成する。
mkdir organization
cd organization
touch createUser.js
npm init -y
cd ..
ハンドラを作成する。
// AWS リソースモジュール
const AWS = require("aws-sdk");
AWS.config.update({ region: 'ap-northeast-1'});
// DynamoDB モジュール
const db = new AWS.DynamoDB.DocumentClient();
const TableName = "fugaTable";
// レスポンス
let response;
exports.lambdaHandler = async (event, context) => {
try {
// リクエストボディから情報を取得
const { orgId, userId, username } = JSON.parse(event.body);
if (!orgId) throw new Error('ORGが指定されていません', orgId);
// DBに追加するレコード(アイテム)を作成
const Item = {
PK: "ORG#" + orgId,
SK: "USER#" + userId,
username: username || "",
};
// Putオペレーション
await db.put({ TableName, Item }).promise();
// 成功レスポンス
response = {
'statusCode': 200,
'body': JSON.stringify({
message: 'success',
...Item,
})
};
} catch (err) {
console.info(err);
return err;
}
return response
};
POSTイベントの内容を修正する。
{
"body": "{\"orgId\": \"info@example.co.jp\", \"userId\": \"sls@example.com\", \"password\": \"pass1234\", \"username\": \"加納愼之典\"}",
"resource": "/{proxy+}",
...省略
ハンドラをリソースとして登録する。
(サンプルで入っている helloFromLambdaFunction
を書き換える)
Resources:
CreateUser:
Type: AWS::Serverless::Function
Properties:
CodeUri: organization/
Handler: createUser.lambdaHandler
Runtime: nodejs14.x
MemorySize: 128
Timeout: 100
Description: ユーザーを作成する
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref HogeHogeTable
ビルド -> デプロイ -> local invoke で動作確認する。
API を作成する
Lambdaのリソースを APIGateway に統合する
Resources:
CreateUser:
Type: AWS::Serverless::Function
Properties:
CodeUri: organization/
Handler: createUser.lambdaHandler
Runtime: nodejs14.x
MemorySize: 128
Timeout: 100
Description: ユーザーを作成する
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref HogeHogeTable
+ Events:
+ CreateUserInOrg:
+ Type: Api
+ Properties:
+ Path: /organization
+ Method: post
デプロイして APIGateway確認する
Cognito ユーザープールの作成
ユーザープールを作成する。
デプロイ時のパラメータで諸々設定できるようにすることもできる。
Parameters:
AppName:
Type: String
Description: Name of the application
ClientDomeins:
Type: CommaDelimitedList
Description: Array of domeins alllowed to use the UserPool
Default: 'localhost:8080'
AdminEmail:
Type: String
Description: Email address of admin
AddGroupToScopes:
Type: String
AllowedValues:
- 'true'
- 'false'
Default: 'false'
Conditions:
ScopeGroups:
!Equals [!Ref AddGroupToScopes, 'true']
Resources:
# ----- Cognito -----
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: !Sub ${AppName}-UserPool
Policies:
PasswordPolicy:
MinimumLength: 8
AutoVerifiedAttributes:
- email
UsernameAttributes:
- email
Schema:
- AttributeDataType: String
Name: email
Required: false
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
ClientName: !Sub ${AppName}-UserPoolClient
GenerateSecret: false # set to false for web clients
SupportedIdentityProviders:
- COGNITO
CallbackURLs: !Ref ClientDomeins
AllowedOAuthFlowsUserPoolClient: true
AllowedOAuthFlows:
- code
- implicit # for testing with postman
AllowedOAuthScopes:
- email
- openid
- profile
UserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: !Sub ${AppName}-${AWS::AccountId}
UserPoolId: !Ref UserPool
AdminUserGroup:
Type: AWS::Cognito::UserPoolGroup
Properties:
GroupName: Admins
Description: Admin user group
Precedence: 0
UserPoolId: !Ref UserPool
AdminUser:
Type: AWS::Cognito::UserPoolUser
Properties:
Username: !Ref AdminEmail
DesiredDeliveryMediums:
- EMAIL
ForceAliasCreation: true
UserAttributes:
- Name: email
Value: !Ref AdminEmail
UserPoolId: !Ref UserPool
AddUserToGroup:
Type: AWS::Cognito::UserPoolUserToGroupAttachment
Properties:
GroupName: !Ref AdminUserGroup
Username: !Ref AdminUser
UserPoolId: !Ref UserPool
Cognito ユーザー作成 + 認証のハンドラ作成
ユーザープールに追加したり、認証するためのLamda関数を作成する。
ファイルを作成する。
mkdir auth
cd auth
touch signUp.js
touch signIn.js
npm init -y
cd ..
// AWS リソースモジュール
const AWS = require("aws-sdk");
AWS.config.update({ region: 'ap-northeast-1'});
// Cognito モジュール
const cognito = new AWS.CognitoIdentityServiceProvider();
const ClientId = ''; // ユーザープール > 全般設定 > アプリクライアント で確認する
// レスポンス
let response;
exports.lambdaHandler = async (event, context) => {
try {
const { userId, password } = JSON.parse(event.body);
const params = {
ClientId,
Username: userId,
Password: password,
};
// SignUp実行
const result = await cognito.signUp(params).promise().catch(error => {
// 必要に応じて例外処理を追加する。
// 例えば、IDが重複したときの例外は→「error.code == 'UsernameExistsException'」
throw error;
});
// 成功レスポンス
response = {
'statusCode': 200,
'body': JSON.stringify({
message: 'success',
...result,
})
};
} catch (err) {
console.info(err);
return err;
}
return response;
};
// AWS リソースモジュール
const AWS = require("aws-sdk");
AWS.config.update({ region: 'ap-northeast-1'});
// Cognito モジュール
const cognito = new AWS.CognitoIdentityServiceProvider();
const ClientId = ''; // ユーザープール > 全般設定 > アプリクライアント で確認する
const AuthFlow = 'USER_PASSWORD_AUTH'; // ユーザープール > 全般設定 > アプリクライアント > 認証フローの設定 で指定した認証方法
// レスポンス
let response;
exports.lambdaHandler = async (event, context) => {
try {
const { userId, password } = JSON.parse(event.body);
// ConfirmSignUpのパラメーター
const params = {
ClientId,
AuthFlow,
AuthParameters: {
'USERNAME': userId,
'PASSWORD': password,
},
};
// SignIn実行
const result = await cognito.initiateAuth(params).promise().catch(error => {
// 必要に応じて例外処理を追加する。
// 例えば、パスワード不一致の例外は→「error.code == 'NotAuthorizedException'」
throw error;
});
// 成功レスポンス
response = {
'statusCode': 200,
'body': JSON.stringify({
message: 'success',
...result,
})
};
} catch (err) {
console.info(err);
return err;
}
return response;
};
リソースに追加する
Resource:
CognitoSignUp:
Type: AWS::Serverless::Function
Properties:
CodeUri: auth/
Handler: signUp.lambdaHandler
Runtime: nodejs14.x
Environment:
Events:
CreateOrganization:
Type: Api
Properties:
Path: /auth
Method: post
CognitoSignIn:
Type: AWS::Serverless::Function
Properties:
CodeUri: auth/
Handler: signIn.lambdaHandler
Runtime: nodejs14.x
Environment:
Events:
CreateOrganization:
Type: Api
Properties:
Path: /auth
Method: post
ビルドして確認する。
Discussion