CloudFormationの同一リソース名でのロールバックを回避する方法について調べてみた

CloudFormation 同一リソース名でのロールバック回避方法 3選
2021.10.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは!
AWS事業本部コンサルティング部の繁松です!

CloudFormationで同じテンプレートを使っていると
「同一リソース名のものが既に存在する」といったエラーでロールバックすることありませんか?

こんな感じのエラーログです。

Resource handler returned message: "Resource of type already exists." , HandlerErrorCode: AlreadyExists)

そこで回避する方法を調べてみました。
(ここからはCloudFormationはCFnと表記します。)

回避の為にやってみた方法は以下になります。

  • リソースの名前末尾にランダムな数字を入れる
  • リソースを作成するかどうかを選択できるようにする
  • ロールバックした場合に正常にプロビジョニングされたものは保持する設定

前提

今回はCloudWatch Logsのロググループの作成を例に検証していきたいと思います。

この記事での想定パターンは以下のようになります。

  1. CFnでLambda関数とロググループを作成
    ログは消したくないのでロググループにはDeletionPolicyを設定している。
  2. CFnスタック削除
    ロググループは残る
  3. 再度同じCFnテンプレートを使用
    ロググループが残っている為エラーが出る

3のエラーをどうにかしようという試みです。

リソースの名前末尾にランダムな数字を入れる

名前の末尾などにランダムな数字を入れ、実行するたびに被らないようにする方法です。

CFnのスタックIDをLambda関数名とロググループ名の末尾につけることで回避しました。
以下の内容を追記しランダムな数字を入れました

FunctionName : !Join ['-', [!Sub '${AWS::StackName}-Lambda', !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId' ]]]]]]  
LogGroupName: !Join ['-', [!Sub '/aws/lambda/${AWS::StackName}-Lambda', !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId' ]]]]]]

[!Ref 'AWS::StackId']でStackIdを取得し、[!Select]と[!Split]で一部分だけを抜き出しています。

StackIDは

arn:aws:cloudformation:us-west-2:123456789012:stack/teststack/51af3dc0-da77-11e4-872e-1234567db123

のように返されます。

↓ !Split '/'

["arn:aws:cloudformation:us-west-2:123456789012:stack","teststack","51af3dc0-da77-11e4-872e-1234567db123"]

↓ !Select 2

["51af3dc0-da77-11e4-872e-1234567db123"]

↓ !Split '-'

["51af3dc0","da77","11e4","872e","1234567db123"]

↓ !Select 0

["51af3dc0"]

↓のようになります。、
!Join で

StackName-Lambda-51af3dc0
/aws/lambda/StackName-Lambda-51af3dc0

となります。

関数の詳細についてはこちらをご覧ください
!Select
!Split
!Join

CFnの内容

AWSTemplateFormatVersion: "2010-09-09"  
Description: Creating lambda and loggroup  
  
Resources:  
  Lambdatest:  
    Type: "AWS::Lambda::Function"  
    Properties:  
      Code:  
        ZipFile: |  
          import json  
  
          def lambda_handler(event, context):  
              # TODO implement  
              return {  
                  'statusCode': 200,  
                  'body': json.dumps('Hello from Lambda!')  
              }  
      FunctionName : !Join ['-', [!Sub '${AWS::StackName}-Lambda', !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId' ]]]]]]  
      Handler: "index.lambda_handler"  
      Role: !GetAtt Role.Arn  
      Runtime: "python3.9"  
      Timeout: 10  
  
  LogGrouptest:  
    Type: AWS::Logs::LogGroup  
    DeletionPolicy: "Retain"  
    Properties:  
      LogGroupName: !Join ['-', [!Sub '/aws/lambda/${AWS::StackName}-Lambda', !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId' ]]]]]]  
      RetentionInDays: 30  
  
  Role:  
    Type: "AWS::IAM::Role"  
    Properties:  
      AssumeRolePolicyDocument:  
        Version: "2012-10-17"  
        Statement:  
          Effect: "Allow"  
          Action: "sts:AssumeRole"  
          Principal:  
            Service: "lambda.amazonaws.com"  
      RoleName: !Sub '${AWS::StackName}-Lambda-role'  
      Policies:  
        - PolicyName: !Sub '${AWS::StackName}-IAMonly'  
          PolicyDocument:  
            Version: "2012-10-17"  
            Statement:  
              - Effect: Allow  
                Action:  
                  - logs:CreateLogStream  
                  - logs:PutLogEvents  
                Resource: !Sub 'arn:aws:logs:*:${AWS::AccountId}:*'

これで同一の名前にならず、エラーを回避することができました。

リソースを作成するかどうかを選択できるようにする

次は条件関数を使用し作成するかをパラメータで選択できるようにします。

これにはConditionsと[!Equals]を使います。
パラメータでyesが選択された場合は作成し、noの場合はスキップするようにしています。
以下の内容を追記し選択ができるようにしました。

Parameters:  
  CreateLogGroup:  
    Default: "yes"  
    Type: String  
    AllowedValues: [ "yes", "no" ]  
  
Conditions:  
  CreateProdResources: !Equals  
    - !Ref CreateLogGroup  
    - "yes"  
  
Resources:  
  LogGrouptest:  
    Type: AWS::Logs::LogGroup  
    DeletionPolicy: "Retain"  
    Condition: CreateProdResources  
    Properties:  
      LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-Lambda'  
      RetentionInDays: 30

CFnの内容

AWSTemplateFormatVersion: "2010-09-09"  
Description: Creating lambda and loggroup  
  
Parameters:  
  CreateLogGroup:  
    Default: "yes"  
    Type: String  
    AllowedValues: [ "yes", "no" ]  
  
Conditions:  
  CreateProdResources: !Equals  
    - !Ref CreateLogGroup  
    - "yes"  
  
Resources:  
  Lambdatest:  
    Type: "AWS::Lambda::Function"  
    Properties:  
      Code:  
        ZipFile: |  
          import json  
  
          def lambda_handler(event, context):  
              # TODO implement  
              return {  
                  'statusCode': 200,  
                  'body': json.dumps('Hello from Lambda!')  
              }  
      FunctionName : !Sub '${AWS::StackName}-Lambda'  
      Handler: "index.lambda_handler"  
      Role: !GetAtt Role.Arn  
      Runtime: "python3.9"  
      Timeout: 10  
  
  LogGrouptest:  
    Type: AWS::Logs::LogGroup  
    DeletionPolicy: "Retain"  
    Condition: CreateProdResources  
    Properties:  
      LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-Lambda'  
      RetentionInDays: 30  
  
  Role:  
    Type: "AWS::IAM::Role"  
    Properties:  
      AssumeRolePolicyDocument:  
        Version: "2012-10-17"  
        Statement:  
          Effect: "Allow"  
          Action: "sts:AssumeRole"  
          Principal:  
            Service: "lambda.amazonaws.com"  
      RoleName: !Sub '${AWS::StackName}-Lambda-role'  
      Policies:  
        - PolicyName: !Sub '${AWS::StackName}-IAMonly'  
          PolicyDocument:  
            Version: "2012-10-17"  
            Statement:  
              - Effect: Allow  
                Action:  
                  - logs:CreateLogStream  
                  - logs:PutLogEvents  
                Resource: !Sub 'arn:aws:logs:*:${AWS::AccountId}:*'

CFnのパラメータ画面で、
すでにロググループが存在する場合にはnoを選択するとロググループの作成がスキップされます。

ロールバックした場合に正常にプロビジョニングされたリソースは保持する設定

最後はロールバックした場合に正常にプロビジョニングされたリソースは保持する設定です。

今回のような場合だと、Lambdaは作成に問題がないが、ロググループのみロールバックしているので
この設定をいれることで既に存在するロググループがあってもLambdaを作成することができます。
CFn作成画面で[正常にプロビジョニングされたリソースの保持]にチェックを入れます。

エラーが出ても、LambdaとIAMroleは作成されていることが確認できます。

さいごに

今回はCFnで同一名によるロールバックをした際の回避の方法でした。
どれを選んでも回避出来ると思うので、用途に合わせて、お役に立てれば光栄です!

参考

参考にさせて頂きました!ありがとうございます!