この記事は、Ateam Group U-30のカレンダー | Advent Calendar 2022 - Qiitaの22日目の記事になります。
本日は、@tommy1038が担当いたします。
はじめに
みなさん、AWS Serverless Application Model (AWS SAM)を使ったことがありますか?
AWS Serverless Application Model (AWS SAM)とは、公式サイトによりますと、
AWS Serverless Application Model (AWS SAM) は、AWS でサーバーレスアプリケーションを構築するために使用できるオープンソースのフレームワークです。
とのことです。名称が長いため、以下SAM
と記載します。
今回は、実際に業務で使用している際に対応した、CORS設定の部分を紹介したいと思います。
おことわり
実際に公式のチュートリアルを見ながらデプロイまで進めて、そのあたりも記事に含もうと思っていましたが、そういった記事で参考になるものが多かったことや、業務で詰まった箇所にフォーカスしたかったので、sam init
からsam deploy --guided
の部分は軽く紹介して、その後をメインに本記事を書きたいと思います。
小さく始めていく際に、参考になりそうなサイト・記事
- チュートリアル: Hello World アプリケーションのデプロイ - AWS Serverless Application Model
- [AWS SAM] 概要、Hello World - Qiita
- [AWS]SAMでTypeScriptがサポートされたので5分でAPIを作成する - Qiita
- AWS SAMを使ってみる(nodejs TypeScript版)
事前準備
実行環境としては、以下のバージョンで実施しました。aws cli
は参考までに記載しています。
❯ brew --version
Homebrew 3.6.15
Homebrew/homebrew-core (git revision 32dbb76ab72; last commit 2022-12-17)
Homebrew/homebrew-cask (git revision 54c47be607; last commit 2022-12-17)
❯ sam --version
SAM CLI, version 1.66.0
❯ aws --version
aws-cli/2.9.6 Python/3.11.0 Darwin/21.6.0 source/x86_64 prompt/off
プロジェクトの作成
sam init
からプロジェクトを作成していきます。sam deploy --guided
までしますが、本題は Reactアプリから、APIを呼んでみる からなので、デプロイ済みの方は飛ばしてもらって大丈夫です。
❯ 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 - Serverless Connector Hello World Example
9 - Multi-step workflow with Connectors
10 - Lambda EFS example
11 - 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 - aot.dotnet7 (provided.al2)
2 - dotnet6
3 - dotnet5.0
4 - dotnetcore3.1
5 - go1.x
6 - go (provided.al2)
7 - graalvm.java11 (provided.al2)
8 - graalvm.java17 (provided.al2)
9 - java11
10 - java8.al2
11 - java8
12 - nodejs18.x
13 - nodejs16.x
14 - nodejs14.x
15 - nodejs12.x
16 - python3.9
17 - python3.8
18 - python3.7
19 - ruby2.7
20 - rust (provided.al2)
Runtime: 12
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
Would you like to enable X-Ray tracing on the function(s) in your application? [y/N]: N
Project name [sam-app]: sam-app-typescript
Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment)
-----------------------
Generating application:
-----------------------
Name: sam-app-typescript
Runtime: nodejs18.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-typescript/README.md
Commands you can use next
=========================
[*] Create pipeline: cd sam-app-typescript && sam pipeline init --bootstrap
[*] Validate SAM template: cd sam-app-typescript && sam validate
[*] Test Function in the Cloud: cd sam-app-typescript && sam sync --stack-name {stack-name} --watch
❯ sam build
(中略)
❯ sam deploy --guided
Configuring SAM deploy
======================
Looking for config file [samconfig.toml] : Not found
Setting default arguments for 'sam deploy'
=========================================
Stack Name [sam-app]: sam-app-typescript
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]: 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]: Y
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]: y
HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
Save arguments to configuration file [Y/n]: Y
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]:
Looking for resources needed for deployment:
Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-1eb8qhml3zh2a
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
(中略)
Outputs
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key HelloWorldFunctionIamRole
Description Implicit IAM Role created for Hello World function
Value arn:aws:iam::************:role/sam-app-typescript-HelloWorldFunctionRole-************
Key HelloWorldApi
Description API Gateway endpoint URL for Prod stage for Hello World function
Value https://{restapi_id}.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
Key HelloWorldFunction
Description Hello World Lambda Function ARN
Value arn:aws:lambda:ap-northeast-1:************:function:sam-app-typescript-HelloWorldFunction-************
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Successfully created/updated stack - sam-app-typescript in ap-northeast-1
挙動確認
❯ curl https://{restapi_id}.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message":"hello world"}
❯ curl -i https://{restapi_id}.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
HTTP/2 200
content-type: application/json
content-length: 25
date: Sun, 18 Dec 2022 15:45:55 GMT
x-amzn-requestid:
x-amz-apigw-id:
x-amzn-trace-id:
x-cache: Miss from cloudfront
via:
x-amz-cf-pop:
x-amz-cf-id:
{"message":"hello world"}⏎
Reactアプリから、APIを呼んでみる
さて、ここからが本題です。テンプレートを使用してAPIを作成することができました。
ここで作成したAPIを、自分のWebサイト側から呼び出してみたいと思います。今回は仮で、ReactでWebサイトを作ります。
https://create-react-app.dev/docs/getting-started/ を参考に、Quick Startしてみます。
npx create-react-app my-app
cd my-app
npm start
上記により、自動でReactのアプリが立ち上がり、http://localhost:3000/
で以下のようにアクセスできます。
では、ReactのアプリでAPIを呼び出してみたいと思います。
どんなライブラリでも構いませんが、今回はaxios/axios: Promise based HTTP client for the browser and node.jsを用いてAPIを呼んでみます。
以下、呼び出しのコードを抜粋してみます。
import axios, { AxiosError, AxiosRequestConfig } from "axios";
const url = "https://{restapi_id}.execute-api.ap-northeast-1.amazonaws.com/Prod/"
const options: AxiosRequestConfig = {
url: `${url}/hello`,
method: "GET",
};
axios(options)
.then((res) => {
const { data } = res;
console.log(data);
})
.catch((e: AxiosError) => {
console.log(e.message);
});
npm start
起動して、開発者コンソールを開いてログを確認してみましょう。想定しているのは、{"message":"hello world"}
が確認できることですが、下記のようなエラーが出ていました。
よくある?CORSのエラーですね、、、CORSとはなんでしょうか?
CORS(Cross-Origin Resource Sharing)とは、日本語訳をするとオリジン間リソース共有です。
つまりCORSとは、あるオリジンで動いている Web アプリケーションに対して、別のオリジンのサーバーへのアクセスをオリジン間 HTTP リクエストによって許可できる仕組み (https://qiita.com/att55/items/2154a8aad8bf1409db2b より引用)
今回の場合、Reactのアプリ側からAPIを呼び出そうとした際に、オリジン間のリソース共有を許可していなかったため、エラーになっていました。そのため、CORSを許可してあげることが必要になります。
参考記事
対応方法として、以下の公式サイトを参考にします。
リンクを参考に、Lambda側の修正を行います。returnの中だけ抜粋して記述します。
return {
statusCode: 200,
body: JSON.stringify({
message: 'hello world',
}),
// headersを追加
headers: {
'Access-Control-Allow-Origin': '*', // 全てを許可してしまうので、本来はよろしくない
},
};
headers
の要素に、Access-Control-Allow-Origin
にて、許可するオリジンの文字列を*
で設定しています。開発環境で試しているので、一旦*
ですが、本番でオリジンを指定しましょう。
挙動を確認してみます。
❯ curl -i https://{restapi_id}.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message":"hello world"}
❯ curl -i https://{restapi_id}.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
HTTP/2 200
content-type: application/json
content-length: 25
date: Wed, 21 Dec 2022 15:37:59 GMT
x-amzn-requestid:
access-control-allow-origin: * // ここが追加されている
x-amz-apigw-id:
x-amzn-trace-id:
x-cache:
via:
x-amz-cf-pop:
x-amz-cf-id:
{"message":"hello world"}⏎
では、もう一度、npm start
してReact側での挙動も見てみましょう。
ちゃんと、"hello world"の文字列が受け取れていそうです。これで、Reactアプリから呼び出すことができました。
Lambda プロキシ統合 / 非プロキシ(カスタム)統合
今までのCORS設定の対応で、自分のReactアプリなどからAPIを呼び出すことができますが、CORS設定の対応としてもう一つあるようです。(お恥ずかしながら、記事を書きながら知りました。。。)
それが、Lambda プロキシ統合の使用
の有無です。
普通にCLIベースで作成した場合、プロキシ統合
でAPIが作成されていました。
そのため、返却値として以下のように組むと
{
statusCode: 200,
body: JSON.stringify({
message: 'hello world',
}),
headers: {
'Access-Control-Allow-Origin': '*',
},
}
❯ curl https://{restapi_id}.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message":"hello world"}⏎
と、statusCode
などは得られずに、body
部分だけ得られるという形でした。
業務で使用しているAPIの中には、Lambda 非プロキシ(カスタム)統合
のものもありましたので、そちらの対応もしていこうと思います。
AWS::Serverless::Api リソースのDefinitionBody
に、OpenAPIの定義を用意していきます。
を参考にして、以下のように修正してみます。
template.yaml
に AWS::Serverless::Api
定義を用意します。
MyAPI:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
DefinitionBody: #後述
Stage名はプロパティとして必須ですが、https://{restapi_id}.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
のProd
に対応しています。DefinitionBody
にOpenAPIの定義を用意します。
DefinitionBody:
swagger: 2.0
info:
title:
"sam-app-test"
schemes:
- https
paths:
/hello:
get:
consumes:
- application/json
produces:
- application/json
responses:
'200':
description: '200 response'
headers:
Access-Control-Allow-Origin:
type: string
x-amazon-apigateway-integration:
responses:
default:
statusCode: 200
responseParameters:
method.response.header.Access-Control-Allow-Origin : "'*'"
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations
passthroughBehavior: when_no_match
httpMethod: POST
type: aws
/helloのgetメソッドを定義して、headersを用意します。consumes/producesは、MIME Typeの設定です。レスポンスの設定でAccess-Control-Allow-Origin
を、ヘッダーの要素として定義します。x-amazon-apigateway-integrations
でのレスポンスで、ヘッダーの要素に対して値をセットアップします。
また、Lambda 関数の呼び出しでは、値は POST
である必要があるため、httpMethod
は POST
でOKです。その他の要素は下記リンクを参照ください。
参考: x-amazon-apigateway-integration オブジェクト - Amazon API Gateway
最終的に、下記のようになりました。
さきほどの説明の中で紹介していない点をコメントとして残していますが、AWS::Serverless::Api
の定義として、MyAPI
を作成したので、それを参照するように修正しています。
見やすさのために、git diff
して追記箇所を表してみます。
❯ git diff
diff --git a/template.yaml b/template.yaml
index 25470b9..2253036 100644
--- a/template.yaml
+++ b/template.yaml
@@ -11,6 +11,42 @@ Globals:
Timeout: 3
Resources:
+ MyAPI:
+ Type: AWS::Serverless::Api
+ Properties:
+ StageName: Prod
+ DefinitionBody:
+ swagger: 2.0
+ info:
+ title:
+ "sam-app-test"
+ schemes:
+ - https
+ paths:
+ /hello:
+ get:
+ consumes:
+ - application/json
+ produces:
+ - application/json
+ responses:
+ '200':
+ description: '200 response'
+ headers:
+ Access-Control-Allow-Origin:
+ type: string
+ x-amazon-apigateway-integration:
+ responses:
+ default:
+ statusCode: 200
+ responseParameters:
+ method.response.header.Access-Control-Allow-Origin : "'*'"
+ uri:
+ Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations
+ passthroughBehavior: when_no_match
+ httpMethod: POST
+ type: aws
+
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:
@@ -25,6 +61,7 @@ Resources:
Properties:
Path: /hello
Method: get
+ RestApiId: !Ref MyAPI // 自分で定義したAPIを参照するように
Metadata: # Manage esbuild properties
BuildMethod: esbuild
BuildProperties:
@@ -40,7 +77,7 @@ Outputs:
# 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/"
+ Value: !Sub "https://${MyAPI}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" // 自分で定義したAPIを参照するように
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
挙動を確認してみます。
❯ curl -i https://{restapi_id}.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"statusCode":200,"body":"{\"message\":\"hello world\"}"}
❯ curl -i https://{restapi_id}.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
HTTP/2 200
content-type: application/json
content-length: 57
date: Wed, 21 Dec 2022 16:03:16 GMT
x-amzn-requestid:
access-control-allow-origin: * // ここが追加されている
x-amz-apigw-id:
x-amzn-trace-id:
x-cache:
via:
x-amz-cf-pop
x-amz-cf-id:
{"statusCode":200,"body":"{\"message\":\"hello world\"}"}
同じLambdaですが、プロキシ統合
と返却値が異なっていますね。こちらの方が、API GatewayとLambdaの役割がそれぞれわかりやすいというメリットがありそうですが、さくっとLambdaだけ作って検証したい、というようなときはまとまっていたりすると便利でしょうし、メリット/デメリットを考えて使えればよいのかなと思いました。(参考になりそうな記事を最後にまとめています。)
これで、Reactアプリ側から呼び出してみましょう。
問題なく呼び出せていますね。
まとめ
CORS設定はあるあるネタかなと思ったんですが、SAMでよしなにやってくれていたりする部分を紐解いていくと、API Gateway側なのか、Lambda側なのか、そもそもCloudformationのテンプレートの書き方がダメなのか、とハマってしまいました。同じようなことを試される方の参考になれば幸いです。