時限式CloudFormation を作成してみた

2022.10.12

こんにちは、森田です。

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

私は、結構消し忘れをしてしまいます...笑

本記事では、AWSリソースの消し忘れを防止するべく時限式CloudFormationを考えてみましたので、そのご紹介をいたします。

時限式CloudFormationとは

時限式爆弾のように、ある時間経過後自動で削除するCloudFormationスタックを作成します。

こちらを実現するために、削除を行うコンポーネントをInclude Transformを用いて作成し、事前にS3バケットに保存しておきます。
削除を行うコンポーネントでは、以下のように、カスタムリソース用の AWS Lambda を作成します。

その処理としては、動的な EventBridge ルールの作成を行います。

また、実行元がカスタムリソースからではなく、EventBridge ルールからであれば、自身のスタックを削除するように構成します。

あとは、時限式CloudFormationとしたいテンプレート中にコンポーネントを追加するだけで自動で削除してくれるようにします。

実際にやってみた

では、実際に時限式CloudFormationをやっていきます。
今回使用するテンプレートやコマンドなどは以下のリポジトリにも置いてあります。

(初回のみ) Delete Component のアップロード

まずは、Delete Component を事前にS3バケットに格納しておく必要があります。
以下のテンプレートを用意します。

delete.yml(ここを押すとコードが展開されます)
TimeSetupper:
    Type: Custom::SetupLambda
    Properties:
      ServiceToken: 
        Fn::GetAtt: 
          - Lambda
          - Arn
  
Lambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: 
        Fn::Sub: ${AWS::StackName}-stack-deleter
      Runtime: python3.9
      Handler: index.handler
      Role: 
        Fn::ImportValue: DeleteLambdaRoleARN
      Environment:
        Variables:
          StackName: 
            Ref: AWS::StackName
          AccountID:
            Ref: AWS::AccountId
          Timer: 
            Ref: Timer
      Code:
        ZipFile: |
          from datetime import datetime, timedelta, timezone
          import cfnresponse
          import os
          import boto3

          JST = timezone(timedelta(hours=+9), 'JST')

          StackName = os.environ.get('StackName') 
          account_id = os.environ.get('AccountID')
          Timer = int(os.environ.get('Timer', "3") )


          def handler(event, context):
            if event.get('RequestType') == 'Create':
              create_eb()
              cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                  {'Response': 'Success'})
            elif event.get('RequestType') == 'Delete':
              delete_eb()
              cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                  {'Response': 'Success'})
            elif event.get('RequestType') == 'Update':
              print('Update')
              cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                  {'Response': 'Success'})
            else:    
              client = boto3.client('cloudformation')
              response = client.delete_stack(
                  StackName=StackName
              )
              print(response)
              
              jp_time = datetime.now(JST)
              print(jp_time)
              return { 'statusCode': 200, 'exec_date': jp_time.strftime('%Y%m%d') }


          def create_eb():
              client = boto3.client('events')

              event_name = "{}-deleterule".format(StackName)
              now = datetime.now() + timedelta(minutes=Timer)

              event_time = "cron({} {} * * ? *)".format(now.minute, now.hour)
              print(event_time)

              description = "CloudFormation {} Stack Delete Rule".format(StackName)

              response = client.put_rule(
                                  Name=event_name,
                                  ScheduleExpression= event_time,
                                  EventPattern='',
                                  State="ENABLED",
                                  Description=description)
              
              rule_arn = response["RuleArn"]
              lambda_arn = 'arn:aws:lambda:ap-northeast-1:{}:function:{}-stack-deleter'.format(account_id, StackName)

                  
              response = client.put_targets(
                  Rule=event_name,
                  Targets=[
                      {
                          'Id': "lambada-target",
                          'Arn':  lambda_arn,
                      }
                  ]
              )
              client = boto3.client('lambda')
              response = client.add_permission(
                  FunctionName="{}-stack-deleter".format(StackName),
                  StatementId='{}-delete'.format(StackName),
                  Action='lambda:InvokeFunction',
                  Principal='events.amazonaws.com',
                  SourceArn=rule_arn
              )
              
              
          def delete_eb():
              client = boto3.client('events')

              event_name = "{}-deleterule".format(StackName)

              response = client.remove_targets(
                  Rule=event_name,
                  Ids=["lambada-target"]
              )
              response = client.delete_rule(Name=event_name)

以下のコマンドを実行しバケットの作成、ファイルのアップロードを行います。

# バケットの作成
aws s3 mb バケット名 

# バケットにファイルを格納する
aws s3 cp delete.yml s3://バケット名

(初回のみ) 削除用IAMロールの作成

続いてDelete Component内で利用するIAMロールを事前に作成しておきます。
以下のテンプレートを用いてロールの作成を行います。

delete_iam.yml(ここを押すとコードが展開されます)
Description: Time Delete
Resources:
  LambdaRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/AdministratorAccess'
      Path: "/"

Outputs:
  DeleteLambdaRoleARN:
    Value: !GetAtt LambdaRole.Arn
    Export:
      Name: DeleteLambdaRoleARN

以上で、事前準備は終わりです!

時限式CloudFormationとするテンプレートの作成

では、実際に時限式CloudFormationを作成します。
今回はシンプルにS3バケットの作成を行うテンプレートに対してDelete Componentを追加して時限式CloudFormationとします。
Delete Componentとして以下のコードを記述します。

テンプレートに追加するコード

Fn::Transform:
    Name: AWS::Include
    Parameters:
        Location: s3://バケット名/delete.yml

また、パラメータとして削除する時間(Timer)を指定する必要があります。
下記の例では、スタック作成後3分経過すると、スタックの削除が行われることになります。

example.yml

Description: Time Delte Item
Parameters:
  BucketName:
    Type: String
  Timer:
    Type: Number
    MinValue: 3
    Default: 3

Resources:
  Fn::Transform:
    Name: AWS::Include
    Parameters:
      Location: s3://バケット名/delete.yml
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName:
        Ref: BucketName

あとは、作成したテンプレートをAWSコンソールやAWS CLIなどで展開してスタックの作成を行います。

作成されたスタックの確認

まずは、リソースが作成されていることを確認します。以下のように、S3バケット、削除用のLambdaが作成されていることを確認します

そして、スタックが自動で削除されるかを確認するため、一定時間経過(今回であれば3分)するのを待ちます。

すると、正常に動作していると以下のように対象のリソース、CloudFormationスタックが削除されていることが確認できます!

 

補足

削除のタイミングとしては、厳密には、スタックの作成から3分後ではなく、 上記のように、Delete Component内のカスタムリソース実行後から3分後となります。

最後に

時限式CloudFormationを利用することでCloudFormationで作ったリソースの削除忘れを防止することができます。
また、利用方法としても事前準備を1回行うだけであとは、コンポーネントを追加するだけでお手軽に利用できますので、ぜひ利用してみてください!