[AWS SAM] [CloudFormation] 環境ごとに設定値を切り替える方法

# 背景
* [AWS SAM](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/what-is-sam.html) を使って、API Gateway + Lambda のデプロイを自動化しています
* ステークホルダーや各種制約等々の都合上、**複数環境に対してそれぞれ異なる構成**を実現する必要がありました
    * [dev|staging|prd] 環境 × [(無印)|r1|r2] 環境 ＝ **合計9環境**…！
* [パラメータ](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html)や[マッピング](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html)、[条件](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html) などを使うことで実現出来そうなことはすぐに分かりましたが、細かい yaml の書き方などで度々詰まったので、具体的な設定例を共有します

# 実現したこと
* パラメータ、マッピング、条件（と組み込み関数）を併用して、「環境ごとに異なる設定を適用」できるようにしました
  * 名前やタグは全環境で異なる値となるようにしたほか、以下の設定は、要件に従い特定環境だけ設定を変えています

| 環境 | dev | dev-r1 | dev-r2 | staging | staging-r1 | staging-r2 | prd | prd-r1 | prd-r2 |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| アクセス許可IPアドレス | 設定A | 設定A | 設定A | 設定B | 設定B | 設定B | 設定C | 設定C | 設定C |
| Lambda の段階的デプロイ | AllAtOnce | AllAtOnce | AllAtOnce | Linear | Linear | Linear | Linear | Linear | Linear |
| Lambda のプロビジョニング | しない | しない | しない | する | しない | しない | する | しない | しない |

# パラメータ・マッピング・条件の設定
* `template.yaml` 冒頭で、環境変数である「パラメータ」と環境ごとの設定である「マッピング」、及びそれらを用いた「条件」を定義します

## パラメータ ( Parameters )
* [dev|staging|prd] 環境を `EnvProfile`、[(無印)|r1|r2] 環境を `SystemType` としました
      * 空白は設定できない＆分かりにくいので、(無印)環境は `general` としました
* これらの値はデプロイ時に `samconfig.toml` で明示的に指定していますが、指定漏れに備えてデフォルト値は `dev` など安全なものを設定した方が安心です

```yaml:template.yaml
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: test for qiita

# 環境変数
Parameters:
  EnvProfile:
    Type: String
    AllowedValues:
    - dev
    - staging
    - prd
    Default: dev
  SystemType:
    Type: String
    AllowedValues:
    - general # 無印
    - r1      # r1 環境
    - r2      # r2 環境
    Default: general
```

## マッピング ( Mappings )
* 続いて、マッピングを定義します。要は HashMap や連想配列なので、**キーを環境名とします**
    * `EnvProfile` である [dev|staging|prd]、`SystemType` である [general|r1|r2] をキーとして、各環境固有の設定を列挙していきます
    * 例えば「Lambda のデバッグモードは dev では有効だけど、staging と prd では無効」など


```yaml:template.yaml
Mappings:
  # dev / staging / prod 環境ごとの固有値
  EnvProfileMap:
    dev:
      DeploymentPreferenceType: AllAtOnce
      DebugMode: true
      WhiteList:
       - "111.111.111.111/32"
    staging:
      DeploymentPreferenceType: Linear10PercentEvery1Minute
      DebugMode: false
      WhiteList:
       - "222.222.222.222/32"
    prd:
      DeploymentPreferenceType: Linear10PercentEvery1Minute
      DebugMode: false
      WhiteList:
       - "333.333.333.333/32"

  # 無印 / r1 / r2 環境ごとの固有値
  SystemTypeMap:
    general:
      ApiGwName: test-apigw-for-qiita
      ApiGwAccessLogGroupName: /aws/apigateway/test-apigw-for-qiita/
      LambdaName: lambda-handler-for-qiita
      VariablePassToLambda: <lambda variable on general>
    r1:
      ApiGwName: test-apigw-for-qiita-r1
      ApiGwAccessLogGroupName: /aws/apigateway/test-apigw-for-qiita-r1/
      LambdaName: lambda-handler-for-qiita-r1
      VariablePassToLambda: <lambda variable on r1>
    r2:
      ApiGwName: test-apigw-for-qiita-r2
      ApiGwAccessLogGroupName: /aws/apigateway/test-apigw-for-qiita-r2/
      LambdaName: lambda-handler-for-qiita-r2
      VariablePassToLambda: <lambda variable on r1>
```

## 条件 ( Conditions )
* ここでは以下のような条件を定義しています
    * 複数箇所で使われる判定条件
        * 「dev環境かどうか」、「無印環境かどうか」
    * ちょっと複雑な判定が必要な条件
        * 「Lambda のプロビジョニング環境かどうか」＝ここでは「staging 無印環境か prd 無印環境か」
* 後続のリソース設定でも書けますが、ここで定義しておいた方が可読性が良く＆修正しやすくなります

```yaml:template.yaml
Conditions:
  IsDev : !Equals [!Ref EnvProfile, "dev"]
  IsGeneral : !Equals [!Ref SystemType, "general"]
  # Lambda のプロビジョニング環境：staging 無印 と prd 無印のみ
  IsLambdaProvisioningEnv : !And [ !Not [Condition: IsDev], Condition: IsGeneral ]
```

# リソース設定
* API Gateway や Lambda といった AWS リソース設定の内、環境固有の設定を適用している部分を抜粋してご紹介します

## API Gateway のアクセスログ用ロググループ
* ロググループ名を `SystemTypeMap` から `SystemType` をキーとして取得します

```yaml:template.yaml
Resources:
  (略)
  ApiGwAccessLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !FindInMap [ SystemTypeMap, !Ref SystemType, ApiGwAccessLogGroupName ]
      RetentionInDays: 180
```

## API Gateway
* 名前を `SystemTypeMap` から `SystemType` をキーとして取得します
* タグは環境名を付与した形で設定します
    * 例えば dev-r1 環境だと `dev-test-apigw-for-qiita-r1` と設定され、判別しやすくなります
* リソースポリシーによる IP アドレス許可リストを `EnvProfileMap` から `EnvProfile` をキーとして取得します
    * `aws:SourceIp:` を忘れずに

```yaml:template.yaml
  TestApiGatewayForQiita:
    Type: AWS::Serverless::Api
    (略)
    Properties:
      Name: !FindInMap [ SystemTypeMap, !Ref SystemType, ApiGwName ]
      StageName: stage # ステージ名は全環境固定
      TracingEnabled: true
      OpenApiVersion: 3.0.2
      Tags:
        Name: !Sub ${EnvProfile}-test-apigw-for-qiita-${SystemType}
        Env: !Ref EnvProfile
      (略)
      # 公開 API の仕様
      DefinitionBody:
        (略)
        x-amazon-apigateway-policy:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Principal: "*"
              Action: execute-api:Invoke
              Resource:
                - "execute-api:/*"
              Condition:
                IpAddress:
                  aws:SourceIp: !FindInMap [ EnvProfileMap, !Ref EnvProfile, WhiteList ]
```

## Lambda
* 関数名を `SystemTypeMap` から `SystemType` をキーとして取得します
* Lambda の段階的なデプロイ設定 `DeploymentPreference` を `EnvProfileMap` から取得します
* Lambda のプロビジョニング設定は、条件 `IsLambdaProvisioningEnv` を満たす（環境）ならプロビジョニングを実施するよう設定します
    * 条件を満たさない場合、**`!Ref "AWS::NoValue"` を設定して本プロパティを削除**し、プロビジョニングしないようにする必要があります
    * 空白や他の表記だとうまく動かなかったので要注意です
* Lambda に渡す環境変数として `SystemType`、`VariablePassToLambda`、`DebugMode` を設定します
* こちらもタグは環境名を付与した形で設定します
    * 例えば dev-r1 環境だと `dev-lambda-handler-for-qiita-r1` と設定されます

```yaml:template.yaml
  LambdaHandlerForQiita:
    Type: AWS::Serverless::Function
    Dependson: LambdaHandlerForQiitaLogGroup
    Properties:
      FunctionName: !FindInMap [ SystemTypeMap, !Ref SystemType, LambdaName ]
      (略)
      DeploymentPreference:
        Enabled: true
        Type: !FindInMap [ EnvProfileMap, !Ref EnvProfile, DeploymentPreferenceType ]
      ProvisionedConcurrencyConfig:
        !If
          - IsLambdaProvisioningEnv
          - ProvisionedConcurrentExecutions: 100
          - !Ref "AWS::NoValue" # 対象環境以外はプロビジョニングしない
      (略)
      Environment:
        Variables:
          SYSTEM_TYPE: !Sub ${SystemType}
          VARIABLE_PASS_TO_LAMBDA: !FindInMap [ SystemTypeMap, !Ref SystemType, VariablePassToLambda ]
          DEBUG_MODE: !FindInMap [ EnvProfileMap, !Ref EnvProfile, DebugMode]
      Tags:
        Name: !Sub ${EnvProfile}-lambda-handler-for-qiita-${SystemType}
        Env: !Ref EnvProfile
```

# 参考情報
* [パラメータ \- AWS CloudFormation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html)
* [Mappings \- AWS CloudFormation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html)
* [条件 \- AWS CloudFormation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html)
* [擬似パラメータ参照 - AWS::NoValue \- AWS CloudFormation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html#cfn-pseudo-param-novalue)