SAMチュートリアル

2021/08/03に公開

準備

SAM用の IAM を作成

  1. Identity & Access Management (IAM)ページにアクセス
  2. Users をクリックし、Add user をクリック
  3. 名前はなんでも良いが「SAM-Admin」などわかりやすい名前とする
  4. Programmatic access を有効にチェックし Next をクリック
  5. Attach existing policies directly をクリック
  6. AdministratorAccessを検索して選択し Next をクリック(嫌なら必要なポリシー作成すること)
  7. 設定項目を確認の上、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 ..
auth/signUp.js
// 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;
    
};
auth/signIn.js
// 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