AWS SAM CLIでTypeScriptがネイティブサポートされました (パブリックプレビュー)

2022/2/28時点でパブリックプレビューですが、AWS SAM CLIでTypeScriptがネイティブサポートされました。型のある世界はいいぞ。
2022.02.28

Lambda関数をTypeScriptで書きたい

こんにちは、のんピ(@non____97)です。

皆さんはLambda関数を簡単にTypeScriptで書きたいと思ったことはありますか? 私はあります。

AWS CDKを使うようになってからTypeScriptを触るようになり、型がない世界に戻れなくなりました。そのままAWS CDKだけでなく、Lambda関数もTypeScriptで書きたいと思うようになるのは当然の流れです。しかし、2022/2/28時点でLambda関数はTypeScriptをネイティブサポートしていません。

Lambda関数にTypeScirptで書かれたコードをデプロイするためには、一度トランスコンパイルをしてあげるというワンステップが必要です。

このワンステップが私にとってとてつもなく面倒です。

楽をするのであれば、以下記事のようにwebpackやAWS CDKのaws-lambda-nodejsを使えばトランスコンパイルとパッケージのバンドルを一気に行えます。

しかし、面倒くさがりな私はもう少し簡単にTypeScriptで書かれたコードをLambda関数にデプロイできればと思っていました。

そんなある時、ブログネタに困って What’s Newを眺めていたら、見つけてしまいました。

「 Announcing TypeScript native support in the AWS Serverless Application Model (AWS SAM) CLI (using esbuild) (public preview) 」

つまりは何?

2022/2/28時点でパブリックプレビューですが、AWS SAM CLIでTypeScriptがネイティブサポートされました。

動きとしては、esbuildを使ってTypeScirptで書かれたコードのトランスコンパイルとパッケージのバンドルを行ってくれます。

AWS公式ブログを確認したところ、トランスコンパイル用のコマンドは不要で、今まで通りsam buildsam deployをするだけでLambda関数のデプロイができそうでした。

やってみた

早速、TypeScriptで書かれたコードをAWS SAMを使ってLambda関数にデプロイしてみます。

AWS SAM CLIのバージョンは1.40.0で、Node.jsのバージョンは14.19.0です。

> sam --version
SAM CLI, version 1.40.0

> node --version
v14.19.0

sam initを実行して、TypeScriptのAWSクイックスタートテンプレートでプロジェクトを初期化します。

> sam init

You can preselect a particular runtime or package type when using the `sam init` experience.
Call `sam init --help` to learn more.

Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
	1 - Hello World Example
	2 - Multi-step workflow
	3 - Serverless API
	4 - Scheduled task
	5 - Standalone function
	6 - Data processing
	7 - Infrastructure event management
	8 - Machine Learning
Template: 1

 Use the most popular runtime and package type? (Python and zip) [y/N]: N

Which runtime would you like to use?
	1 - dotnet5.0
	2 - dotnetcore3.1
	3 - go1.x
	4 - java11
	5 - java8.al2
	6 - java8
	7 - nodejs14.x
	8 - nodejs12.x
	9 - python3.9
	10 - python3.8
	11 - python3.7
	12 - python3.6
	13 - ruby2.7
Runtime: 7

What package type would you like to use?
	1 - Zip
	2 - Image
Package type: 1

Based on your selections, the only dependency manager available is npm.
We will proceed copying the template using npm.

Select your starter template
	1 - Hello World Example
	2 - Hello World Example TypeScript
Template: 2

Project name [sam-app]:

Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment)

    -----------------------
    Generating application:
    -----------------------
    Name: sam-app
    Runtime: nodejs14.x
    Architectures: x86_64
    Dependency Manager: npm
    Application Template: hello-world-typescript
    Output Directory: .

    Next steps can be found in the README file at ./sam-app/README.md


    Commands you can use next
    =========================
    [*] Create pipeline: cd sam-app && sam pipeline init --bootstrap
    [*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch

sam init後のディレクトリは以下の通りです。

> tree
.
├── .gitignore
├── README.md
├── events
│   └── event.json
├── hello-world
│   ├── .eslintignore
│   ├── .eslintrc.js
│   ├── .npmignore
│   ├── .prettierrc.js
│   ├── app.ts
│   ├── jest.config.ts
│   ├── package.json
│   ├── tests
│   │   └── unit
│   │       └── test-handler.test.ts
│   └── tsconfig.json
└── template.yaml

4 directories, 13 files

package.jsonを確認すると、ts-nodeやesbuildはもちろん、eslintやprettierなどがdevDependenciesに指定されていました。hello-worldディレクトリ配下で忘れずにnpm installします。

package.json

{
  "name": "hello_world",
  "version": "1.0.0",
  "description": "hello world sample for NodeJS",
  "main": "app.js",
  "repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs",
  "author": "SAM CLI",
  "license": "MIT",
  "dependencies": {
  },
  "scripts": {
    "unit": "jest",
    "lint": "eslint '*.ts' --quiet --fix",
    "compile": "tsc",
    "test": "npm run compile && npm run unit"
  },
  "devDependencies": {
    "@types/aws-lambda": "^8.10.92",
    "@types/jest": "^27.4.0",
    "@types/node": "^17.0.13",
    "@typescript-eslint/eslint-plugin": "^5.10.2",
    "@typescript-eslint/parser": "^5.10.2",
    "esbuild": "^0.14.14",
    "esbuild-jest": "^0.5.0",
    "eslint": "^8.8.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "jest": "^27.5.0",
    "prettier": "^2.5.1",
    "ts-node": "^10.4.0",
    "typescript": "^4.5.5"
  }
}

また、./hello-world/app.tsを確認すると、TypeScriptで書かれたLambdaハンドラーが定義されていました。

import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

/**
 *
 * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
 * @param {Object} event - API Gateway Lambda Proxy Input Format
 *
 * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
 * @returns {Object} object - API Gateway Lambda Proxy Output Format
 *
 */

export const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    let response: APIGatewayProxyResult;
    try {
        response = {
            statusCode: 200,
            body: JSON.stringify({
                message: 'hello world',
            }),
        };
    } catch (err) {
        console.log(err);
        response = {
            statusCode: 500,
            body: JSON.stringify({
                message: 'some error happened',
            }),
        };
    }

    return response;
};

続いて、template.yamlを確認すると、メタデータとしてesbuildのプロパティが設定されていました。ターゲットはes2020です。Node Greenを確認したところ、今回使用しているNode.js v14.19.0は概ねサポートされていそうです。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app
  
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Architectures:
        - x86_64
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get
    Metadata: # Manage esbuild properties
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: "es2020"
        Sourcemap: true
        EntryPoints: 
        - app.ts

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

それでは、

いざ sam build

という気持ちは山々ですが、現在パブリックプレビュー中であるので、TypeScriptを使うために以下のいずれかの下準備が必要です。

  1. 環境変数SAM_CLI_BETA_ESBUILD1を設定する
  2. 以下のパラメータをsamconfig.tomlに追加する
[default.build.parameters]
beta_features = true
[default.sync.parameters]
beta_features = true
  1. sam buildsam syncを実行する際に--beta-featuresを付ける
  2. プロンプトでベータ版の機能の使用について表示されたら、yを選択する

今回は一番簡単な4つ目の方法を使用します。

sam buildを実行すると、ベータ版の機能を有効化するか聞かれました。

> sam build 
Your template contains a resource with logical ID "ServerlessRestApi", which is a reserved logical ID in AWS SAM. It could result in unexpected behaviors and is not recommended.
Using esbuild for bundling Node.js and TypeScript is a beta feature.
Please confirm if you would like to proceed with using esbuild to build your function.
You can also enable this beta feature with 'sam build --beta-features'. [y/N]:

もちろん有効化するのでyを選択します。しばらく待つと、正常にビルドが完了しました。

You can also enable this beta feature with 'sam build --beta-features'. [y/N]: y

Experimental features are enabled for this session.
Visit the docs page to learn more about the AWS Beta terms https://aws.amazon.com/service-terms/.

Building codeuri: /<ディレクトリ名>/sam-app/hello-world runtime: nodejs14.x metadata: {'BuildMethod': 'esbuild', 'BuildProperties': {'Minify': True, 'Target': 'es2020', 'Sourcemap': True, 'EntryPoints': ['app.ts']}} architecture: x86_64 functions: ['HelloWorldFunction']
Running NodejsNpmEsbuildBuilder:CopySource
Running NodejsNpmEsbuildBuilder:NpmCI
Running NodejsNpmEsbuildBuilder:EsbuildBundle

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch
[*] Deploy: sam deploy --guided

ビルドアーティファクトの./.aws-sam/build配下を確認すると、template.yamlとトランスコンパイルされたJavaScriptファイルが出力されていました。

> ls ./.aws-sam/build/
HelloWorldFunction template.yaml

> ls ./.aws-sam/build/HelloWorldFunction/ 
app.js     app.js.map

それでは次にデプロイです。

sam deploy --guidedでガイドを出してデプロイします。今回は全てデフォルトで進めます。

> sam deploy --guided

Configuring SAM deploy
======================

	Looking for config file [samconfig.toml] :  Not found

	Setting default arguments for 'sam deploy'
	=========================================
	Stack Name [sam-app]:
	AWS Region [us-east-1]:
	#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
	Confirm changes before deploy [y/N]:
	#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]:
	#Preserves the state of previously provisioned resources when an operation fails
	Disable rollback [y/N]:
	HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
	Save arguments to configuration file [Y/n]:
	SAM configuration file [samconfig.toml]:
	SAM configuration environment [default]:

	Looking for resources needed for deployment:
Enter MFA code for arn:aws:iam::<AWSアカウントID>:mfa/<IAMユーザー名>:
	Creating the required resources...
	Successfully created!
	 Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-1quh29sgv0k4t
	 A different default S3 bucket can be set in samconfig.toml

	Saved arguments to config file
	Running 'sam deploy' for future deployments will use the parameters saved above.
	The above parameters can be changed by modifying samconfig.toml
	Learn more about samconfig.toml syntax at
	https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

Uploading to sam-app/4915c8283ad33a06ffcffbf42832df65  1353 / 1353  (100.00%)

	Deploying with following values
	===============================
	Stack name                   : sam-app
	Region                       : us-east-1
	Confirm changeset            : False
	Disable rollback             : False
	Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-1quh29sgv0k4t
	Capabilities                 : ["CAPABILITY_IAM"]
	Parameter overrides          : {}
	Signing Profiles             : {}

Initiating deployment
=====================
Uploading to sam-app/571e71e4a3a21168e8bc6cb1f26393e7.template  1336 / 1336  (100.00%)

Waiting for changeset to be created..

CloudFormation stack changeset
---------------------------------------------------------------------------------------------------------------------------------
Operation                        LogicalResourceId                ResourceType                     Replacement
---------------------------------------------------------------------------------------------------------------------------------
+ Add                            HelloWorldFunctionHelloWorldPe   AWS::Lambda::Permission          N/A
                                 rmissionProd
+ Add                            HelloWorldFunctionRole           AWS::IAM::Role                   N/A
+ Add                            HelloWorldFunction               AWS::Lambda::Function            N/A
+ Add                            ServerlessRestApiDeployment47f   AWS::ApiGateway::Deployment      N/A
                                 c2d5f9d
+ Add                            ServerlessRestApiProdStage       AWS::ApiGateway::Stage           N/A
+ Add                            ServerlessRestApi                AWS::ApiGateway::RestApi         N/A
---------------------------------------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:us-east-1:<AWSアカウントID>:changeSet/samcli-deploy1646050506/195d1b69-2364-42b1-8c72-bf30752d0c17


2022-02-28 21:15:19 - Waiting for stack create/update to complete

CloudFormation events from stack operations
---------------------------------------------------------------------------------------------------------------------------------
ResourceStatus                   ResourceType                     LogicalResourceId                ResourceStatusReason
---------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS               AWS::IAM::Role                   HelloWorldFunctionRole           -
CREATE_IN_PROGRESS               AWS::IAM::Role                   HelloWorldFunctionRole           Resource creation Initiated
CREATE_COMPLETE                  AWS::IAM::Role                   HelloWorldFunctionRole           -
CREATE_IN_PROGRESS               AWS::Lambda::Function            HelloWorldFunction               -
CREATE_IN_PROGRESS               AWS::Lambda::Function            HelloWorldFunction               Resource creation Initiated
CREATE_COMPLETE                  AWS::Lambda::Function            HelloWorldFunction               -
CREATE_IN_PROGRESS               AWS::ApiGateway::RestApi         ServerlessRestApi                -
CREATE_IN_PROGRESS               AWS::ApiGateway::RestApi         ServerlessRestApi                Resource creation Initiated
CREATE_COMPLETE                  AWS::ApiGateway::RestApi         ServerlessRestApi                -
CREATE_IN_PROGRESS               AWS::Lambda::Permission          HelloWorldFunctionHelloWorldPe   -
                                                                  rmissionProd
CREATE_IN_PROGRESS               AWS::Lambda::Permission          HelloWorldFunctionHelloWorldPe   Resource creation Initiated
                                                                  rmissionProd
CREATE_IN_PROGRESS               AWS::ApiGateway::Deployment      ServerlessRestApiDeployment47f   -
                                                                  c2d5f9d
CREATE_IN_PROGRESS               AWS::ApiGateway::Deployment      ServerlessRestApiDeployment47f   Resource creation Initiated
                                                                  c2d5f9d
CREATE_COMPLETE                  AWS::ApiGateway::Deployment      ServerlessRestApiDeployment47f   -
                                                                  c2d5f9d
CREATE_IN_PROGRESS               AWS::ApiGateway::Stage           ServerlessRestApiProdStage       -
CREATE_COMPLETE                  AWS::ApiGateway::Stage           ServerlessRestApiProdStage       -
CREATE_IN_PROGRESS               AWS::ApiGateway::Stage           ServerlessRestApiProdStage       Resource creation Initiated
CREATE_COMPLETE                  AWS::Lambda::Permission          HelloWorldFunctionHelloWorldPe   -
                                                                  rmissionProd
CREATE_COMPLETE                  AWS::CloudFormation::Stack       sam-app                          -
---------------------------------------------------------------------------------------------------------------------------------

CloudFormation outputs from deployed stack
-----------------------------------------------------------------------------------------------------------------------------------
Outputs
-----------------------------------------------------------------------------------------------------------------------------------
Key                 HelloWorldFunctionIamRole
Description         Implicit IAM Role created for Hello World function
Value               arn:aws:iam::<AWSアカウントID>:role/sam-app-HelloWorldFunctionRole-15LVMB2PC87I1

Key                 HelloWorldApi
Description         API Gateway endpoint URL for Prod stage for Hello World function
Value               https://hh5nw1npm7.execute-api.us-east-1.amazonaws.com/Prod/hello/

Key                 HelloWorldFunction
Description         Hello World Lambda Function ARN
Value               arn:aws:lambda:us-east-1:<AWSアカウントID>:function:sam-app-HelloWorldFunction-Lgi9nNpOqPbY
-----------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - sam-app in us-east-1

デプロイが完了した後、キーがHelloWorldApiのURLにアクセスすると、ステータスコード200{"message":"hello world"}が返ってきました。

> curl -L https://hh5nw1npm7.execute-api.us-east-1.amazonaws.com/Prod/hello/ -w '\n%{http_code}' -s
{"message":"hello world"}
200

最後に後片付けとしてsam deleteで作成されたリソースを削除します。

> sam delete
	Are you sure you want to delete the stack sam-app in the region us-east-1 ? [y/N]: y
	Are you sure you want to delete the folder sam-app in S3 which contains the artifacts? [y/N]: y
	- Deleting S3 object with key sam-app/4915c8283ad33a06ffcffbf42832df65
	- Deleting S3 object with key sam-app/571e71e4a3a21168e8bc6cb1f26393e7.template
	- Deleting Cloudformation stack sam-app

Deleted successfully

早くGAして欲しい

パブリックプレビューですが、AWS SAM CLIでTypeScriptがネイティブサポートされたことを紹介しました。

型のある生活に入り、抜け出せなくなってしまった私にとっては非常に嬉しいアップデートです。GAされることを楽しみに待っています。

ちなみに、AWS SAM AccelerateもTypeScriptをベータサポートしているので、コードの更新があったら自動でデプロイするといったことも可能です。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!