Serverless Framework から CloudFormation(SAM) に移行する

# はじめに
Lambda やその実行ログを出力する CloudWatch ロググループ、IAM、API Gateway などを Serverless Framework で管理していたが、V3 がサポートされなくなること、V4 からは有償化されることをきっかけに、CloudFormation(SAM)に移行することとなった。
この時の移行手順についてまとめる。

※基本的には以下を参考にさせていただきました。
https://dev.classmethod.jp/articles/aws-sam-template-import-resources/

# SAM テンプレートで作成された import 用のスタックを準備する
Serverless Framework の実体は CloudFormation なので、CloudFormation の import 機能を使用して SAM に移行していく。
まずは SAM テンプレートで作成された、import 先となるスタックを準備する必要がある。（ここでは適当にリソースを作成して後で削除する）

とりあえず以下のようにしてデプロイする。

```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: import sam tmp stack

Globals:
  Function:
    Timeout: 3

Resources:
  TestFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: lambda-import-tmp
      Handler: app.lambda_handler
      Runtime: python3.13
      Architectures:
        - x86_64
```

```toml:samconfig.toml
version = 0.1

[stg.global.parameters]
stack_name = "charita-import-sam-test"

[stg.build.parameters]
debug    = true
cached   = true
parallel = true

[stg.validate.parameters]
lint = true

[stg.deploy.parameters]
debug = true
capabilities = "CAPABILITY_NAMED_IAM"
confirm_changeset = true
region = "ap-northeast-1"
s3_bucket = "charita-import-sam-test"
s3_prefix = "stg"
disable_rollback = false
```

```zsh
sam build --use-container --config-env stg
sam deploy --config-env stg --profile hogehoge 
```

ちなみに `--use-container` オプションを使用すると、**指定されたランタイムの Lambda 実行環境を厳密に模倣した Docker コンテナ内でビルドプロセスを実行**することができ、以下のような問題に対応できる。（イメージは ECR Public から Pull）

- Python のパッケージの中には、純粋な Python コードだけではなく **C言語で書かれた拡張ライブラリ（`NumPy` や `Pandas` など）** を含むものがあり、実際の Lambda 実行環境で正しく動作するために、**適切な Linux ベースのコンパイル** が必須
- macOS や Windows でコンパイルすると、生成されるバイナリはそれぞれの OS に最適化され、Lambda の実行環境では動かない可能性がある
- 各 OS では標準でインストールされているライブラリやシステムヘッダーが異なるため、同じソースコードでもコンパイル結果が異なり、Lambda で期待通りに動作しない恐れがある

※このオプションを Proxy 接続環境で使用する場合、`sam cli` の実行環境からは Proxy に接続できるが、実際にビルドする Docker コンテナからは Proxy に繋がらないというような問題が発生するため、以下記事のように対応する必要がある模様。

https://qiita.com/komikoni/items/a55e9533bf5e588c85e7

少し話は逸れたが、ここで作成されたスタックに対し、Serverless Framework のテンプレートを import していく。

# import 用の変更セットを作成する
CloudFormation には import 機能があるため、import 用の変更セットを作成せずとも以下のようにして普通にできそうだが、どうやら SAM のテンプレートには対応していないようだった。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/resource-import-existing-stack.html

imoport 対応リソースは以下の通り。

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-supported-resources.html

そのため、import 用の変更セットを作成し適用という手順を追う必要がある。
ここで準備する必要があるのは以下。（命名規則は特になし）

1. 先ほど作成したスタックの `template.yaml`
1. import したいリソースが定義された `import-template.yaml`
1. import したいリソースのリソースタイプ、論理名、識別子の対応が定義された `import.txt`

#### `import-template.yaml`
Serverless Framework で管理されているスタックのテンプレートから、import 対象の `Resource` ブロックをコピーし、1の `template.yaml` に追記する。
以下のようにする。
（今回は CloudWatch ロググループを import 対象とする）

```yaml:import-template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: import sam tmp stack

Globals:
  Function:
    Timeout: 3

Resources:
  TestFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: lambda-import-tmp
      Handler: app.lambda_handler
      Runtime: python3.13
      Architectures:
        - x86_64

  # 以下を追記
  TestLogGroup:
    Type: AWS::Logs::LogGroup
    DeletionPolicy: Retain
    Properties:
      LogGroupName: /aws/lambda/lambda-import-tmp
```

**この時、import 対象のリソースには必ず** `DeletionPolicy` **を付与する必要あり。**
※ `Retain` である必要はなし

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/resource-import.html

https://dev.classmethod.jp/articles/cfn-import-deletionpolicy-not-retain/

#### `import.txt`
import したいリソースのリソースタイプ、論理名、識別子を以下の形式で記述する。

```txt:import.txt
[
    {
        "ResourceType": "AWS::Logs::LogGroup",
        "LogicalResourceId":
            "TestLogGroup"
        ,
        "ResourceIdentifier": {
            "LogGroupName":"/aws/lambda/lambda-import-tmp"
        }
    }
]
```

この時指定する `ResourceIdentifier` は、一度コンソール上からリソースの import を進めてみるとわかる。
例えば以下のようにしてみると、`LogGroupName` というのが `ResourceIdentifier` で指定すべき値だということがわかる。

![スクリーンショット 2025-04-15 15.56.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1131391/c3cab96e-3bb7-4be7-91d4-692d1a1874d0.png)


尚、Lambda を import したい場合、上述の通り SAM のテンプレートには対応していないため、リソースタイプを `Type: AWS::Serverless::Function` のようにすることはできない。
そのため、通常の CloudFormation のテンプレートの通り `Type: AWS::Lambda::Function` とする。（import 完了後に変更可能）

以上で必要なファイルは準備できたが、まだこのままでは import できない。
これは、**CloudFormation は同じリソースを複数のスタックで管理することができない**ためである。
なので、Serverless Framework で管理しているスタックを削除する必要がある。
この時、当然リソースが削除されてしまっては困るため、全てのリソースに対し `DeletionPolicy: Retain` を付与した状態でスタックを削除することで、一度スタックの管理外にしてやる。（以下参考）

スタックを削除する際、実際に `DeletionPolicy: Retain` が効いて削除されずに済むのかが確認できないところが心臓に悪いので、本当に `DeletionPolicy: Retain` が対象リソースに適用できているかというのは、改めてコンソール上からテンプレートを確認しておくことをお勧めする。

https://qiita.com/hubuzo/items/91e27d549fce5c10b258

無事にスタックのみ削除し、リソースをスタックの管理外にできたら、AWS CLI コマンドで import 用の変更セットを作成する。
コマンドは以下のようになる。

```bash
aws cloudformation create-change-set \
  --stack-name charita-import-sam-test \
  --change-set-name import-changeset \
  --resources-to-import file://import.txt \
  --change-set-type IMPORT \
  --template-body file://import-template.yaml \
  --capabilities CAPABILITY_NAMED_IAM \
  --profile hogehoge
```

作成された変更セットは以下のようになり、「アクション」が「import」となっていることを確認し、これを実行してやれば無事 import 完了となる。

![スクリーンショット 2025-04-15 10.46.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1131391/fe826921-5f30-41bd-aa2b-fb8d78e7d078.png)

あとは自由に `samconfig.toml` をいい感じにしてみたり、Lambda のリソースタイプを `Type: AWS::Serverless::Function` から `Type: AWS::Lambda::Function` に変えてみたりと、快適な SAM ライフを楽しむだけ。

Lambda を管理する上では非常に便利な SAM だが、Terraform の import より色々考慮事項が多いし、スタックの削除は本当にやりたくないからもっと簡単にしてほしい、、、