CloudFormationの同一リソース名でのロールバックを回避する方法について調べてみた
こんにちは!
AWS事業本部コンサルティング部の繁松です!
CloudFormationで同じテンプレートを使っていると
「同一リソース名のものが既に存在する」といったエラーでロールバックすることありませんか?
こんな感じのエラーログです。
Resource handler returned message: "Resource of type already exists." , HandlerErrorCode: AlreadyExists)
そこで回避する方法を調べてみました。
(ここからはCloudFormationはCFnと表記します。)
回避の為にやってみた方法は以下になります。
- リソースの名前末尾にランダムな数字を入れる
- リソースを作成するかどうかを選択できるようにする
- ロールバックした場合に正常にプロビジョニングされたものは保持する設定
前提
今回はCloudWatch Logsのロググループの作成を例に検証していきたいと思います。
この記事での想定パターンは以下のようになります。
- CFnでLambda関数とロググループを作成
ログは消したくないのでロググループにはDeletionPolicyを設定している。 - CFnスタック削除
ロググループは残る - 再度同じ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で同一名によるロールバックをした際の回避の方法でした。
どれを選んでも回避出来ると思うので、用途に合わせて、お役に立てれば光栄です!
参考
参考にさせて頂きました!ありがとうございます!