この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは!
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で同一名によるロールバックをした際の回避の方法でした。
どれを選んでも回避出来ると思うので、用途に合わせて、お役に立てれば光栄です!
参考
参考にさせて頂きました!ありがとうございます!