AWS SAM 覚え書き(2)

[前回書いたメモ](https://qiita.com/iwaiktos/items/345a448ba8c5f746dd0b) で `AutoPublishAlias` のことを書きましたが、もう少し工夫すれば Serverless Framework でやれていたこととほぼ同等のことが実現できるのではないかという目途が立ったので、追加で書きます。前回同様、役に立つ記事とかそういうのはよそを当たってください…。

## AutoPublishAlias で Function URL が使えなくなったワケ

前回

> `FunctionUrlConfig` `AutoPublishAlias` `DeploymentPreference` の 3 つを併用すると Function URL が (定義は残っているにもかかわらず) アクセスできなくなる

と書きましたが、これは自分の理解不足でした。

- `AutoPublishAlias` `DeploymentPreference` の働きによって「古い Lambda バージョンを削除して、新しいバージョンを作成し、 Lambda のエイリアスに最新バージョンを登録する」という動きになる
- `FunctionUrlConfig` の定義はあって Function URL も再発行されるものの、上記の手順の途中でおそらくパーミション (リソースベースポリシー)[^1] が消えてしまう

からであろうと推測しています。

で、おそらくこれは Function URL に限った話ではなく、 S3 や SNS のような「トリガーのための権限を Lambda のリソースベースポリシーで設定しなければならないサービス」ではみんな同じ話になるだろうと思います。(EventBridge ルールであればターゲットに identity ベースポリシーを割り当てられるので、ここの話は当てはまりません)

## Serverless Framework と SAM の違い：エイリアスを意識する

Serverless Framework ではこの辺をキチンと面倒みてくれていたんでしょう。古いバージョンは消さずに新しいバージョンだけを作成し、各種トリガーは非修飾の `$LATEST` に対するものとし、必要なパーミションは再設定もしてくれていた。

SAM で `AutoPublishAlias` `DeploymentPreference` を付けただけでは、新旧バージョンの交代は瞬時にできたとしても「旧バージョンの削除」という要らない動作まで付いてくるので、そこをなんとかしなければなりません。template.yaml で `AWS::Serverless::Function` リソースに `DeletionPolicy: Retain` をつければ旧バージョンの削除は抑制できますが、それをすると今度は `FunctionUrlConfig` の設定が邪魔になります。

ポイントになるのは、Serverless Framework のときは気にしていなかった「エイリアス」を SAM では意識したほうがうまくいく、ということでしょうか。

バージョンがどれだけ変わろうと、呼び出したいのは `AutoPublishAlias` で作ってもらったエイリアスが指しているバージョン。であるならば、 `AWS::Serverless::Function` に `FunctionUrlConfig` を付けるのではなくて、作成されるであろう「エイリアス」に対して明示的に `AWS::Lambda::Url` リソースを作成すればよい。パーミションも「エイリアス」に対して付与すればよい。

あとは、デプロイすればするほど旧バージョンが (Serverless Framework の場合と同様) 増え続けるのを何とかすればよい。残念ながら [serverless-prune-plugin](https://www.npmjs.com/package/serverless-prune-plugin) のような便利ツールは SAM にはなさそうですが、なければ作ればよい。… Copilot くんに尋ねてデバッグさせて、ものの 10 分くらいで使えそうな bash スクリプトもできました。これなら GitHub Actions で自動化することもできそう。

## というわけでこうなった

- 初回のデプロイの template.yaml (抜粋) - 手動で実行

```yaml
Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    DeletionPolicy: Retain
    Properties:
      CodeUri: src/
      Handler: index.handler
      AutoPublishAlias: current
    Metadata: # Manage esbuild properties
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: es2020
        Sourcemap: true
        EntryPoints:
          - index.ts
        External:
          - '@aws-sdk'
```

- 2回目以降のデプロイの template.yaml (抜粋) - CI/CD で実行

```yaml
Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    DeletionPolicy: Retain
    Properties:
      CodeUri: src/
      Handler: index.handler
      AutoPublishAlias: current
      DeploymentPreference:
        Type: AllAtOnce
        Alarms:
          - !Ref AliasErrorMetricGreaterThanZeroAlarm
          - !Ref LatestVersionErrorMetricGreaterThanZeroAlarm
    Metadata: # Manage esbuild properties
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: es2020
        Sourcemap: true
        EntryPoints:
          - index.ts
        External:
          - '@aws-sdk'

  MyFunctionCurrentUrlConfig:
    Type: AWS::Lambda::Url
    DependsOn: MyFunctionAliascurrent # 上記で生成される AWS::Lambda::Alias リソース
    Properties:
      AuthType: AWS_IAM
      Cors:
        AllowOrigins:
          - '*'
      Qualifier: current
      TargetFunctionArn: !GetAtt MyFunction.Arn

  MyFunctionPermission:
    Type: AWS::Lambda::Permission
    DependsOn: MyFunctionAliascurrent # 上記で生成される AWS::Lambda::Alias リソース
    Properties:
      Action: lambda:InvokeFunctionUrl
      FunctionName: !GetAtt MyFunctionAliascurrent.AliasArn
      FunctionUrlAuthType: AWS_IAM
      # Function URL を CloudFront のオリジンとしてぶら下げようと思ってたので、こんな感じ
      Principal: cloudfront.amazonaws.com
      SourceArn: (CloudFront distribution の ARN)
```

- 古くなった Lambda バージョンを掃除する bash スクリプト

```bash
#!/bin/bash

# 設定
REGION="ap-northeast-1"  # リージョンを指定
FUNCTION_NAME="YOUR_FUNCTION_NAME"  # Lambda 関数名を指定
KEEP_VERSIONS=3  # 残すバージョンの数

# すべてのバージョンをリスト
versions=$(aws lambda list-versions-by-function --function-name ${FUNCTION_NAME} --region ${REGION} --query 'Versions[].[Version]' --output text | grep -v "\$LATEST" | sort -n)

# 古いバージョンを削除
count=0
total_versions=$(echo ${versions} | wc -w)
delete_count=$((${total_versions} - ${KEEP_VERSIONS}))
for version in ${versions}; do
  if [ ${count} -lt ${delete_count} ]; then
    echo "Deleting version ${version}"
    aws lambda delete-function --function-name ${FUNCTION_NAME} --qualifier ${version} --region ${REGION}
    count=$((count+1))
  else
    break
  fi
done

echo "Cleanup completed!"
```

~~ここまで苦労するなら最初から CDK 使っちゃえば…~~

### 参考文献

- [AWS SAMでLambdaのバージョン管理をしたい件 - Qiita](https://qiita.com/atw0_0w/items/1fd41907e54db0f1aef7)
- [AWS SAMの「AutoPublishAlias」の使い方を誤るとエイリアス消えちゃうよっていう話 - ヤサイブログ](https://blog.supica.work/entry/2018/12/13/203000)
- [AWS公式ドキュメント](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/automating-updates-to-serverless-apps.html)
- [AWS Lambdaのバージョン管理とエイリアスについて - Zenn](https://zenn.dev/amarelo_n24/articles/158a166d832487)
- [「お前らの AWS Lambda エイリアスは間違っている」と言われた気がしたので備忘録 - Qiita](https://qiita.com/Rikiya_Ota/items/6001f009751f2074fdc3)

[^1]: 追加・削除するときは `aws lambda add-permission` `aws lambda remove-permission` なのに、今の状態を確認するときは `aws lambda get-policy` ... 名前に統一性がないのが残念というかなんというか。