AWS SAMでAPIGatewayからLambdaをエイリアス指定で呼び出す

# はじめに
AWS CloudFormationの拡張機能であるAWS SAMを使って、APIGatewayとLambdaを管理するユースケースは多いと思います。SAMのメリットのひとつとしては、少ない記述量で済むことが挙げられると思います。

さて、このSAM。APIGatewayとLambdaを素直に管理するのには良いのですが、プロジェクトが成熟したときに出てくるかもしれない、このような要望には少々難しいところがあります。


私「いやー。AWS謹製のSAM最高。」

SRE「そういえば、Lambdaにエイリアスを作成して、APIGatewayからはエイリアス指定のLambdaを呼び出すようにできたりしますかー？デプロイするタイミングで最新バージョンに向いてしまうのはアレなので。`update-alias`でエイリアスのバージョンを上げるようにしたいです。」

私「なるほどです。できると思います！」

... 数時間後 ...

私「SAMの記述ではできなくないかい...?」

# Versioningなし & AliasなしのLambda

## template.yaml
`GET /hello`というAPIを作成するtemplateを例として書いていきます。

```yaml
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Lambda handler for API Gateway.
Globals:
  Function:
    Runtime: go1.x
    Timeout: 30
Resources:
  ApiGateway:
    Type: AWS::Serverless::Api
    Properties:
      EndpointConfiguration: REGIONAL
      Name: MyAPI
      StageName: prod
  MyLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: myFunction
      CodeUri: functions/hello/
      Handler: app
      Events:
        GetHelloApi:
          Type: Api
          Properties:
            RestApiId: !Ref ApiGateway
            Path: /hello
            Method: GET
```

はい、これだけです。

## template.yamlをvalidationする
```bash
$ sam validate
template.yaml is a valid SAM Template
```
注意点として、`sam validate`は、あくまでテンプレートの構文チェックをしてくれるだけです。つまり、記述内容がデプロイ先のAWS環境と整合性がとれているかといったチェックまではしてくれません。
ですので、初回は、試行錯誤しながら、CloudFormationから吐かれるエラーを潰しながらデプロイしていきました。

## build & deploy

```shell
$ GOOS=linux GOARCH=amd64 go build -o functions/hello/app ./functions/hello
$ sam package --template-file template.yaml --output-template-file packaged.yaml --s3-bucket $DEPLOYMENT_TARGET_BUCKET
$ sam deploy --template-file packaged.yaml --stack-name $STACK_NAME --debug --capabilities CAPABILITY_IAM --no-fail-on-empty-changeset
```

## AWS APIGatewayのマネジメントコンソールで確認
`GET /hello`がmyFunction関数を呼び出すように設定されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156415/b6518118-bfe2-247d-2103-6d12a86cfb3c.png)

# Versioningあり & AliasありのLambda
追加要件はざっくりこのような感じです。

- Lambda関数のバージョニングをする。
- Lambda関数には、開発用と本番用のエイリアスを用意する。
    - 開発用エイリアスは常に最新バージョンを参照する
    - 本番用エイリアスは手動でエイリアスの張替えをする。デプロイする度にバージョン変更されたくない
- APIGatewayからは本番用エイリアスのLambdaを呼び出す。


APIGatewayから呼び出されるLambdaをエイリアス指定しようとすると厳しい現実が突きつけられました。
Type`AWS::Serverless::Function`ではLambda関数を指定するんですが、`FunctionName`にはARNの指定ができないため、エイリアスを指し示すLambda関数の指定ができません。

結論、ほぼSAMの記述（`AWS::Serverless`）は使えずにCloudFormationの記述方法で記載することになりました。

## template.yaml
```yaml
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Lambda handler for API Gateway.
Globals:
  Function:
    Runtime: go1.x
    Timeout: 30
Resources:
#  ApiGateway:
#    Type: AWS::Serverless::Api
#    Properties:
#      EndpointConfiguration: REGIONAL
#      Name: MyAPI
#      StageName: dev
  ApiGateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      EndpointConfiguration:
        Types: [ REGIONAL ]
      Name: MyAPI
  ApiGatewayResourceHello:
    Type: AWS::ApiGateway::Resource
    Properties:
      ParentId: !GetAtt ApiGateway.RootResourceId
      PathPart: 'hello'
      RestApiId: !Ref ApiGateway
  ApiGatewayMethodGetHello:
    Type: AWS::ApiGateway::Method
    Properties:
      AuthorizationType: NONE
      HttpMethod: GET
      ResourceId: !Ref ApiGatewayResourceHello
      RestApiId: !Ref ApiGateway
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Join [ '' , [ 'arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/', !GetAtt MyLambdaFunction.Arn, ':PROD', '/invocations' ] ]
  ApiGatewayDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn: [ ApiGatewayMethodGetHello ]
    Properties:
      RestApiId: !Ref ApiGateway
  ApiGatewayStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      DeploymentId: !Ref ApiGatewayDeployment
      RestApiId: !Ref ApiGateway
      StageName: prod
  MyLambdaFunctionAliasProd:
    Type: AWS::Lambda::Alias
    Properties:
      FunctionName: !GetAtt MyLambdaFunction.Arn
      FunctionVersion: $LATEST
      Name: PROD
  MyLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: myFunction
      CodeUri: functions/hello/
      Handler: app
      AutoPublishAlias: DEV
#      Events:
#        GetHelloApi:
#          Type: Api
#          Properties:
#            RestApiId: !Ref ApiGateway
#            Path: /hello
#            Method: GET
  MyLambdaFunctionPermission:
    Type: AWS::Lambda::Permission
    DependsOn: MyLambdaFunctionAliasProd
    Properties:
      FunctionName: !Join [ '' , [ !GetAtt MyLambdaFunction.Arn, ':PROD' ] ]
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com
      SourceArn: !Join [ '' , [ 'arn:aws:execute-api:', !Ref 'AWS::Region', ':', !Ref 'AWS::AccountId', ':', !Ref ApiGateway, '/*/GET/hello' ] ]
```
結果、あらたな要件に対応したこともあるのですが、step数が27→63stepと倍以上になりました。
いかにSAMが暗黙的に色々なリソースを作成してくれていたか。SAMへのありがたみを感じると同時にCloudFormationの辛みを感じる経験でした。

## build & deploy
さきほどと同様にdeployすると、Cloudformationでエラーになります。
`ApiGatewayResourceHello`リソースを作成する際に、`CREATE_FAILED`になってしまいました。

```
Another resource with the same parent already has this name: hello (Service:AmazonApiGateway; Status Code: 409; Error Code: ConflictException; RequestID: f36f9b8b-8275-4fba-bc73-0d3920203333; Proxy:null)
```
これはつまり、stack内で/helloというリソースが競合しているとのこと。
`MyLambdaFunction`リソースに定義していた`Path: /hello`は削除しているのに競合になってしまうので、一度stackを削除しました。


## AWS APIGatewayのマネジメントコンソールで確認
`GET /hello`がPRODエイリアスのmyFunction関数を呼び出すように設定される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156415/7a04a729-836e-f608-8135-f9bd225ec407.png)

## AWS Lambdaのマネジメントコンソールで確認
PRODエイリアスのLambdaに向けてトリガーが設定されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156415/23cefecf-12eb-ced1-1b05-b6cc2ba4e21b.png)


# まとめ
- 一度stackを削除してデプロイするということは、「リソース削除→新規作成」される挙動になるため、API IDが変わってしまいます。API IDが変わってしまうということは、APIGatewayのエンドポイントに変更が生じてしまうということです。
`https://{API_ID}.execute-api.{REGION}.amazonaws.com/{STAGE}/{PATH}`

- 要件次第ですが、すべてをSAMで対応することはできないと考えたほうが良いです。VersioningやAlias周りを細かく設定したいのであれば、現時点では素のCloudFormationで書くのが良さそうです。

- SAMを書いているのか、CloudFormationを書いているのか迷子になることがありました。そんなときはリソースのTypeを見て、`AWS::Serverless::`と定義していればSAMなので、「今は(SAM|CloudFormation)を書いている」と確認してました。
 


# 参考
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-resource-function.html