echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Serverless Framework】API Gatewayを複数プロジェクト(serverless.yml)で共有する(Share API Gateway and API Resources)

今回の内容

Serverless Frameworkを使用して、API Gatewayを複数プロジェクト(serverless.yml)で共有する(=あるプロジェクトで作成したAPI Gatewayを別のプロジェクトでも使いまわす)方法です。

Serverless Framework公式ページ

詳細

Serverless Frameworkでは、デフォルトではプロジェクト(serverless.yml)ごとに「functions」セクションで定義したLambdaのエンドポイントとなるAPI Gatewayが自動で作成されるため*1、「既存のAPI Gatewayを共有する」ことはできません。

しかし、状況によっては「既存のAPI Gatewayを共有したい(=今回もエンドポイントとして使用したい)」ケースが発生します。

それをServerless Frameworkで実現する方法です。

API Gatewayを共有する必要性

始めのうちはともかく、だんだんプロジェクトが大きくなると、デプロイ時に下記問題が発生します。

  • デプロイにとても時間がかかる
  • 1リソース(Lambdaなど)を追加するだけで、全リソースをフルデプロイしなきゃならない
    • 必要最小限だけデプロイすればいい
    • (例) Lambdaをデプロイするだけなら、S3やDynamoDBのデプロイは不要
  • リソース数が多くなると、CloudFormationの制限に引っかかる
    • CloudFormationでは、1スタックのリソースは最大200個まで

そのため、上記の解決策として下記のような「スタック分割」が行われます。

  • スタックのネスト
  • ライフサイクル(≒更新頻度)によるスタック分割
  • (マイクロサービス的な)一定のサービス単位によるリソース分割

この場合、

  • API GatewayとLambdaを別スタック(serverless.yml)に定義する
  • 別スタックで作成したAPI GatewayをLambdaのエンドポイントにする

ということがあります。(この場合「共有」ではないですが、「既存のAPI Gatewayを使いまわす」という意味では同じになります)

そのため、「既存のAPI Gatewayを共有する」必要が発生してくるわけです。

※この「スタック分割」については、別の機会に書こうと思ってます。

参考サイト

API Gatewayの共有の仕方

具体的なAPI Gatewayの共有の仕方ですが、これは実際のserverless.ymlを見ながら説明します。

ここでは「splitdeployfirst」「splitdeploysecond」という、2つのプロジェクト(=スタック)を使用します。(各serverless.ymlの内容を、末尾に掲載してあります)

「splitdeployfirst」(=先にデプロイする方のserverless.yml)

先にデプロイする「splitdeployfirst」ですが、こちらは特にAPI Gateway共有だからといって、大きく変わる部分はないと思います。(「apiGateway.restApiId」や「apiGateway.restApiRootResourceId」の定義も不要です)

ただ一点、大きく違うのは「resouces.Outputs」で、「apiGatewayRestApiId」「apiGatewayRestApiRootResourceId」という2つの項目のValueにそれぞれ

  • Ref: ApiGatewayRestApi
  • 'Fn::GetAtt': [ApiGatewayRestApi, RootResourceId]

を指定しています。(実際には必須ではないですが、こうしたほうが便利です」)

こうすることで、作成されるAPI Gatewayの「restApiId」及び「restApiRootResourceId」の値を取得できるので、それを「出力」に表示し「splitdeploysecond」プロジェクトから参照できるようにしています。

「ApiGatewayRestApi」について

※「apiGatewayRestApiId」及び「apiGatewayRestApiRootResourceId」のValueの指定で「ApiGatewayRestApi」という、serverless.ymlのどこにも定義していないキーを指定しています。

これなのですが、Serverless FrameworkではAPI Gatewayを作成する際に、「Type=AWS::ApiGateway::RestApi」のリソースを「ApiGatewayRestApi」という項目名で作成します。
ですのでこれらは「こう書くものなんだ」と思ってください。

※「apiGatewayResourcesSplitDeploy」については、後述します。

「splitdeploysecond」(=後でデプロイする方のserverless.yml)

後でデプロイする(=既存のAPI Gatewayを共有する)「splitdeploysecond」ですが、こちらで「restApiId」「restApiRootResourceId」など、API Gatewayの共有に必要な「provider.apiGateway」項目の定義を行います。

「restApiId」及び「restApiRootResourceId」の値について、「${cf:[スタック名].[キー名]}」という変数を使用しています。
Serverless Frameworkでは、この記載をすることで、CloudFormationスタックの「出力」タブ内の「キー」に該当する「値」を取得することができます。

※実際に取得できるのは「resources.Outputs」内の各項目の「Value」の値

こうしておくと、値を直書きした場合と違い、下記のメリットがあります。(splitdeployfirstで「resources.Outputsの定義は必須ではないが、こうしたほうが便利」と書いたのは、そのためです)

  • 環境を変えても(dev/stg/prodなど)、値を書き直す必要がない
  • spritdeployfirstをremove→再度デプロイしても、値を書き直す必要がない

【参考】https://www.serverless.com/framework/docs/providers/aws/guide/variables/#reference-cloudformation-outputs

※ちなみに「restApiId」及び「restApiRootResourceId」の値は、AWSコンソールからも確認できます。
API Gatewayから該当のAPI Gatewayを選択し、左のリストの「リソース」を選択すると、画面左上に表示されます。(下の画像では消してますが)

f:id:Makky12:20200901195527p:plain

restApiResourcesの指定

ところで、プロジェクト「spritdeployfirst」「spritdeploysecond」に定義したLambda(hello, guten)は、共にルートパス「splitdeploy」を参照しています。

この場合、それぞれパス「splitdeploy/hello」及び「splitdeploy/guten」のリソースがなんの問題もなくAPI Gatewayに作成されて欲しいところです。

しかし、Serverless Framework公式ページにも下記の記載があることからわかるように、実際は「spritdeploysecond」のデプロイ時にエラーになります。

The above example services both reference the same parent path /posts. However, Cloudformation will throw an error if we try to generate an existing path resource.

これは「spritdeploysecond」の「splitdeploy」を「spritdeployfirstで作成した「splitdeployと同じである」ことを認識してくれないため、下記の現象が発生し、エラーとなります。

  • 「spritdeploysecond」のデプロイ時に、ルートパス「splitdeploy」を持つリソースを新規に作成しようとする
  • しかしルートパス「splitdeploy」を持つリソースはすでに存在する
  • すでにルートパス「splitdeploy」を持つリソースは存在するのに、ルートパス「splitdeploy」を持つリソースを新規作成しようとしている
  • 重複エラー

そのため、既存のAPI Gatewayを共有する場合、パスの一部が共通のLambdaを作成する場合「パスsplitdeployは、既存のパスsplitdeployの事だよ」と教えてあげる必要があります。

そして、それを行っているのが下記の部分で、「restApiResources」に「/splitdeploy」、及びそのrestApiResourceIdを指定することで「このserverless.ymlのfunctionsのパスで指定している/splitdeployは、既存の/splitdeployの事だよ」と教えています。

こうすることで、既存のパス「/splitdeploy」を持つLambda(guten)があっても、正常にデプロイできるようになります。

provider:  
  apiGateway:
    restApiResources:
      /splitdeploy: ${cf:${self:custom.parentStackName}.apiGatewayResourcesSplitDeploy}
apiGatewayResourcesSplitDeployの内容

ところで、上記「/splitdeploy」の値を見て「splitdeployfirst(のserverless.yml)のresources.Outputs.SplitDeployFirstAuthorizerArn.Valueの値を取得している」ことは何となく分かると思います。

しかし、実際にそこに指定されている「ApiGatewayResourceSplitdeploy」という項目は、serverless.ymlのどこにもありません。

では、どこに指定されているのでしょうか?

結論を言いますと、Serverless Frameworkでは、定義内容をCloudFormation構文に変換する(serverless-state.jsonに書き込む)際、「events=http」であるLambdaのpathの内容について、区切り(/)ごとに「Type=AWS::ApiGateway::Resource」のリソースを作成します。
そしてそのキー名は「ApiGatewayResource + ルートパスからのpathの区切りごとの名前」となります。(先頭のみ大文字)

例えば、pathが「/splitdeploy/hello」の場合、キー名が下記の2つのリソースが作成されます。

  • ApiGatewayResourceSplitdeploy
  • ApiGatewayResourceSplitdeployHello

なので、それを見越して「Ref:ApiGatewayResourceSplitdeploy」と記載しているわけです。

Lambdaオーサライザの指定

API Gatewayにオーソライザを設定する場合があると思います。

Serverless Frameworkでは、Lambdaオーソライザを使用する場合、各Lambdaの「event.http」の子要素として「authorizer」を設定します。(splitdeploysecondのserverless.ymlを参照)

そしてここでは、splitdeployfirstにオーサライザ用のLambdaのみ設定し(AuthorizerLambda)、gutenでは「authorizer」にそのARNを指定しています。

この場合、今までのようにsplitdeployfirstのリソースの値(今回はARN)を参照する場合、resources.Outputsについつい下記定義をしようとしてしまいます。

resources:
  Outputs.
    SplitDeployFirstAuthorizerArn:
      Value:
        'Fn::GetAtt': [AuthorizerLambda, Arn]
      Export:
        Name: ${self:service}-SplitDeployFirstAuthorizerArn-${self:provider.stage}

しかし上記定義だと、デプロイ時に「AuthorizerLambdaなんて項目ねえよ」とエラーになってしまいます。ちなみに、Cognitoをオーソライザにした場合は、このエラーは発生しません。*2
なぜでしょうか?

これまたServerless Frameworkの仕様なのですが、Serverless Frameworkでは、定義内容をCloudFormation構文に変換する際、Lambda関数のキー名には、末尾に「LambdaFunction」というサフィックスを付けてserverless-state.jsonに書き込みます。

ですので「AuthorizerLambda」はserverless-state.jsonには「AuthorizerLambdaLambdaFunction」というキー名で書き込まれます。*3

「ApiGatewayRestApi」しかり「ApiGatewayResourceSplitdeploy」しかり、こういうServerless Framework独自の仕様を知らないと、思わぬところでハマるので注意が必要です。(serverless-state.jsonを追っていけばそのうち分かりますけどね)

まとめ

以上、長くなってしまいましたが、API Gatewayを共有する方法(別プロジェクトで使いまわす方法)でした。

始めのうちは気にする必要はないかもしれませんが、アプリが大きくなったり、運用面を考えると「スタックのネスト」「デプロイ分割」を考える必要があり、その際にAPI Gatewayの共有を考える必要が出てきます。

スタック(=serverless.yml)の再構成というのは、アプリがそれなりの規模になればなるほど困難になってしまうので、たとえ面倒でも始めのうちにしっかり考えて、最適な構成にしておきたいものです。

てか次回はその「スタックのネスト」「デプロイ分割」あたりについて書こうかな、と考えています。

それでは、長くなってしまいましたが、今回はこの辺で。

備考:「splitdeployfirst」「splitdeploysecond」のserverless.ymlの内容

# splitdeployfirstのserverless.yml
service: splitdeployfirst
  
provider:
  name: aws
  runtime: nodejs12.x
  
# you can overwrite defaults here  
  stage: dev
  region: ap-northeast-1
  profile: default
  
functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: /splitdeploy/hello
          method: get
  
  AuthorizerLambda:
    handler: handler.authorizer
  
resources:
  # Resources:
  Outputs:
    apiGatewayRestApiId:
      Value:
        Ref: ApiGatewayRestApi
      Export:
        Name: ${self:service}-restApiId-${self:provider.stage}
    apiGatewayRestApiRootResourceId:
      Value:
        'Fn::GetAtt': [ApiGatewayRestApi, RootResourceId]
      Export:
        Name: ${self:service}-rootResourceId-${self:provider.stage}
    apiGatewayResourcesSplitDeploy:
      Value:
        Ref: ApiGatewayResourceSplitdeploy
      Export:
        Name: ${self:service}-ResourcesSplitDeploy-${self:provider.stage}
    SplitDeployFirstAuthorizerArn:
      Value:
        'Fn::GetAtt': [AuthorizerLambdaLambdaFunction, Arn]
      Export:
        Name: ${self:service}-SplitDeployFirstAuthorizerArn-${self:provider.stage}
#splitdeploysecondのserverless.yml 
service: splitdeploysecond
custom:
  parentStackName: splitdeployfirst-dev
  
provider:
  name: aws
  runtime: nodejs12.x
  
  apiGateway:
    restApiId: ${cf:${self:custom.parentStackName}.apiGatewayRestApiId} 
    restApiRootResourceId: ${cf:${self:custom.parentStackName}.apiGatewayRestApiRootResourceId} 
    restApiResources:
      /splitdeploy: ${cf:${self:custom.parentStackName}.apiGatewayResourcesSplitDeploy}
 
# you can overwrite defaults here
  stage: dev
  region: ap-northeast-1
  profile: default
    
functions:
  guten:
    handler: handler.guten
    events:
      - http:
          path: /splitdeploy/guten
          method: get  
          authorizer: ${cf:${self:custom.parentStackName}.SplitDeployFirstAuthorizerArn} 

*1:event=httpのLambdaがある場合

*2:その場合Cognitoはresources.Resourcesに指定すると思いますが、resources.Resourcesに指定したキー名はserverless-state.jsonにもそのままのキー名で書き込まれるため

*3:明らかに「Lambda」がかぶってるのはどうなの?という感じですね...