時限式CloudFormation を作成してみた その2

2023.07.01

こんにちは!AWS 事業本部コンサルティング部のたかくに(@takakuni_)です。

みなさんは、AWSリソースの消し忘れをしたことありませんか。私はたまにあります。

弊社森田の時限式CloudFormationの記事が面白く、 Step Functions でも行けそう?と思ったため、「その2」を作ってみました。

本エントリは「その2」であるため、まずはその1の記事をご覧ください。

構成

今回は AWS Lambda ではなく、 Step Functions で作ってみました。

都度、 CloudFormation で定義するのは面倒ですが、お守りっぽく記載いただけるといいのではないかと思います。

コード

コードは以下になります。作りたいリソースを同じテンプレートに追加していく形になります。

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  Timer:
    Type: "Number"
    Description: "This is the number of seconds to automatically delete the CloudFormation stack. The default is 43200 seconds, which is 12 hours."
    MinValue: 60
    MaxValue: 31536000
    Default: 43200
Resources:
  StepFunctionsRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: "handson-step-functions-role"
      Description: "This is an IAM Role for hands-on use. This Role is used by AWS Step Functions in case a CloudFormation stack has been unintentionally forgotten and not deleted."
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          Effect: "Allow"
          Principal:
            Service:
              - "states.amazonaws.com"
          Action: "sts:AssumeRole"
      ManagedPolicyArns:
          - "arn:aws:iam::aws:policy/AdministratorAccess"
      Tags: 
        - Key: "Name"
          Value: "handson-step-functions-role"

  DeleteStackStateMachine:
    Type: "AWS::StepFunctions::StateMachine"
    Properties:
      StateMachineName: "handson-delete-stack-state-machine"
      StateMachineType: "STANDARD"
      RoleArn: !GetAtt StepFunctionsRole.Arn
      Definition:
        StartAt: "WaitDeletionTimer"
        States:
          WaitDeletionTimer:
            Type: "Wait"
            Seconds: !Ref Timer
            Next: "DeleteStack"
          DeleteStack:
            Type: "Task"
            Resource: "arn:aws:states:::aws-sdk:cloudformation:deleteStack"
            Parameters:
              StackName: !Ref "AWS::StackName"
            End: true

  EventBridgeRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: "handson-eventbridge-role"
      Description: "This is an IAM Role for hands-on use. This Role is used by Amazon EventBridge in case a CloudFormation stack has been unintentionally forgotten and not deleted."
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          Effect: "Allow"
          Principal:
            Service:
              - "events.amazonaws.com"
          Action: "sts:AssumeRole"
      Tags: 
        - Key: "Name"
          Value: "handson-eventbridge-role"

  EventBridgePolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: "handson-eventbridge-policy"
      Description: "This is an IAM policy for hands-on use. This policy is used by Amazon EventBridge in case a CloudFormation stack has been unintentionally forgotten and not deleted."
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "states:StartExecution"
            Resource: !Ref DeleteStackStateMachine
      Roles:
        - !Ref EventBridgeRole

  DeleteStackStateMachineEvents:
    Type: "AWS::Events::Rule"
    Properties:
      Name: "handson-delete-stack-rule"
      Description: "This is a EventBridge rule for hands-on use. This rule is used in case a CloudFormation stack has been unintentionally forgotten and not deleted."
      EventPattern:
        source:
          - "aws.cloudformation"
        detail-type:
          - "CloudFormation Stack Status Change"
        resources: 
          - !Ref "AWS::StackId"
        detail:
          status-details:
            status:
              - "CREATE_COMPLETE"
      Targets:
        - Id: !Sub
            - Id-${UniqueId}
            - UniqueId: !Select [0, !Split ['-', !Select [2, !Split [/, !Ref 'AWS::StackId']]]]
          Arn: !Ref DeleteStackStateMachine
          RoleArn: !GetAtt EventBridgeRole.Arn

Parameter 部分

CloudFormation の Parameter 部分でスタック作成から削除するまでの時間を定義します。

Parameter に指定した値は Step Functions の Wait 部分に定義するため、最大 約 1 年間 (31,536,000 秒) 指定できます。

※ ステートマシンの最大実行時間が 1 年間のため、約 1 年間と表記しています。

1 年。実行が 1 年の最大時間を超える場合、States.Timeout エラーで失敗し、ExecutionsTimedOut CloudWatch メトリクスを出力します。

ステートマシンの実行に関連するクォータ

ちなみにですが、 Wait 時間に対する課金は発生しないため、ご安心ください。デフォルトでは 12 時間を指定するようにしています。

AWS Step Functions の料金

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  Timer:
    Type: "Number"
    Description: "This is the number of seconds to automatically delete the CloudFormation stack. The default is 43200 seconds, which is 12 hours."
    MinValue: 60
    MaxValue: 31536000
    Default: 43200

Resources:
###################################
# 抜粋
###################################

  DeleteStackStateMachine:
    Type: "AWS::StepFunctions::StateMachine"
    Properties:
      StateMachineName: "handson-delete-stack-state-machine"
      StateMachineType: "STANDARD"
      RoleArn: !GetAtt StepFunctionsRole.Arn
      Definition:
        StartAt: "WaitDeletionTimer"
        States:
          WaitDeletionTimer:
            Type: "Wait"
            Seconds: !Ref Timer
            Next: "DeleteStack"
          DeleteStack:
            Type: "Task"
            Resource: "arn:aws:states:::aws-sdk:cloudformation:deleteStack"
            Parameters:
              StackName: !Ref "AWS::StackName"
            End: true
###################################
# 抜粋
###################################

EventBridge 部分

EventBridge Rule では、作成するスタックが CREATE_COMPLETE になったタイミングでトリガー、Step Functions を実行するよう設定されています。

個人的にですが、初めて擬似パラメータの AWS::StackId を使いました。

AWSTemplateFormatVersion: "2010-09-09"
###################################
# 抜粋
###################################
Resources:
###################################
# 抜粋
###################################
  EventBridgeRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: "handson-eventbridge-role"
      Description: "This is an IAM Role for hands-on use. This Role is used by Amazon EventBridge in case a CloudFormation stack has been unintentionally forgotten and not deleted."
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          Effect: "Allow"
          Principal:
            Service:
              - "events.amazonaws.com"
          Action: "sts:AssumeRole"
      Tags: 
        - Key: "Name"
          Value: "handson-eventbridge-role"

  EventBridgePolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: "handson-eventbridge-policy"
      Description: "This is an IAM policy for hands-on use. This policy is used by Amazon EventBridge in case a CloudFormation stack has been unintentionally forgotten and not deleted."
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "states:StartExecution"
            Resource: !Ref DeleteStackStateMachine
      Roles:
        - !Ref EventBridgeRole

  DeleteStackStateMachineEvents:
    Type: "AWS::Events::Rule"
    Properties:
      Name: "handson-delete-stack-rule"
      Description: "This is a EventBridge rule for hands-on use. This rule is used in case a CloudFormation stack has been unintentionally forgotten and not deleted."
      EventPattern:
        source:
          - "aws.cloudformation"
        detail-type:
          - "CloudFormation Stack Status Change"
        resources: 
          - !Ref "AWS::StackId"
        detail:
          status-details:
            status:
              - "CREATE_COMPLETE"
      Targets:
        - Id: !Sub
            - Id-${UniqueId}
            - UniqueId: !Select [0, !Split ['-', !Select [2, !Split [/, !Ref 'AWS::StackId']]]]
          Arn: !Ref DeleteStackStateMachine
          RoleArn: !GetAtt EventBridgeRole.Arn

やってみた

というわけで、 時限式 CloudFormation を使って、スタックを作ってみようと思います。

今回は料金が高めのインスタンスタイプを指定した、 EC2 インスタンスを作成してみようと思います。

template.yaml

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  Timer:
    Type: "Number"
    Description: "This is the number of seconds to automatically delete the CloudFormation stack. The default is 43200 seconds, which is 12 hours."
    MinValue: 60
    MaxValue: 31536000
    Default: 43200
Resources:
  StepFunctionsRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: "handson-step-functions-role"
      Description: "This is an IAM Role for hands-on use. This Role is used by AWS Step Functions in case a CloudFormation stack has been unintentionally forgotten and not deleted."
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          Effect: "Allow"
          Principal:
            Service:
              - "states.amazonaws.com"
          Action: "sts:AssumeRole"
      ManagedPolicyArns:
          - "arn:aws:iam::aws:policy/AdministratorAccess"
      Tags: 
        - Key: "Name"
          Value: "handson-step-functions-role"

  DeleteStackStateMachine:
    Type: "AWS::StepFunctions::StateMachine"
    Properties:
      StateMachineName: "handson-delete-stack-state-machine"
      StateMachineType: "STANDARD"
      RoleArn: !GetAtt StepFunctionsRole.Arn
      Definition:
        StartAt: "WaitDeletionTimer"
        States:
          WaitDeletionTimer:
            Type: "Wait"
            Seconds: !Ref Timer
            Next: "DeleteStack"
          DeleteStack:
            Type: "Task"
            Resource: "arn:aws:states:::aws-sdk:cloudformation:deleteStack"
            Parameters:
              StackName: !Ref "AWS::StackName"
            End: true

  EventBridgeRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: "handson-eventbridge-role"
      Description: "This is an IAM Role for hands-on use. This Role is used by Amazon EventBridge in case a CloudFormation stack has been unintentionally forgotten and not deleted."
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          Effect: "Allow"
          Principal:
            Service:
              - "events.amazonaws.com"
          Action: "sts:AssumeRole"
      Tags: 
        - Key: "Name"
          Value: "handson-eventbridge-role"

  EventBridgePolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: "handson-eventbridge-policy"
      Description: "This is an IAM policy for hands-on use. This policy is used by Amazon EventBridge in case a CloudFormation stack has been unintentionally forgotten and not deleted."
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "states:StartExecution"
            Resource: !Ref DeleteStackStateMachine
      Roles:
        - !Ref EventBridgeRole

  DeleteStackStateMachineEvents:
    Type: "AWS::Events::Rule"
    Properties:
      Name: "handson-delete-stack-rule"
      Description: "This is a EventBridge rule for hands-on use. This rule is used in case a CloudFormation stack has been unintentionally forgotten and not deleted."
      EventPattern:
        source:
          - "aws.cloudformation"
        detail-type:
          - "CloudFormation Stack Status Change"
        resources: 
          - !Ref "AWS::StackId"
        detail:
          status-details:
            status:
              - "CREATE_COMPLETE"
      Targets:
        - Id: !Sub
            - Id-${UniqueId}
            - UniqueId: !Select [0, !Split ['-', !Select [2, !Split [/, !Ref 'AWS::StackId']]]]
          Arn: !Ref DeleteStackStateMachine
          RoleArn: !GetAtt EventBridgeRole.Arn

  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties: 
      ImageId: "{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}"
      InstanceType: "m6i.12xlarge"

AWS CLI で以下のコマンドを実行します。CloudFormation スタックの作成リクエストが送信できました。

[cloudshell-user@ip-10-4-4-169 ~]$ aws cloudformation create-stack --stack-name ec2-stack \
> --template-body file://template.yaml \
> --parameters ParameterKey=Timer,ParameterValue=60 \
> --capabilities CAPABILITY_NAMED_IAM
{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:111111111111:stack/ec2-stack/235d7210-17ff-11ee-83b7-0617954eff63"
}

無事、料金高めのインスタンスが起動していました。

Step Functions を確認すると、 WaitDeletionTimer (今回だと 60 秒経過後) に DeleteStack が実行されていました。

最後に DeleteStack で実行された、アクションによって CloudFormation スタック(およびスタックないで定義されたリソース)が削除されていることが確認できました。

参考

まとめ

以上、「時限式CloudFormation を作成してみた その2」でした。

仕組みとしては単に DeleteStack API を叩いているだけなので、完全にスタックを削除し切るにはまだまだ改善の余地がありますが、 CloudFormation の同一スタックに定義することで、「スタックの削除忘れないようにしないと...!」と、より忘れないような気持ちになるのではないでしょうか。

この記事がどなたかの参考になれば幸いです。AWS 事業本部コンサルティング部のたかくに(@takakuni_)でした!