【コピペで使える】リソース削除忘れを防止する自動削除 CloudFormation を作ってみた

2023.01.29

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

こんにちは、森田です。

みなさんは、AWSリソースちゃんと削除できていますか?

私は、特に CloudFormation で構築した場合に、忘れてしまうことがあります。

本記事では、リソース削除忘れを防止する方法として、自動で CloudFormation スタックを削除する方法をご紹介します。

アーキテクチャ

以前、以下のような 時限式CloudFormation をご紹介しましたが、それよりシンプルな構成となっております。

CloudFormationテンプレートの内部に EventBridge Scheduler を埋め込み、特定の時間になったらスタックの削除を行います。

事前準備

なお、事前準備として EventBridge Scheduler で利用する IAMロール を CloudFormation で用意する必要があります。

iam.yml

Description: EventBridge Scheduler IAM Role
Resources:
  Role:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - scheduler.amazonaws.com
            Action:
              - "sts:AssumeRole"
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/PowerUserAccess'
      Path: "/"

Outputs:
  DeleteSchedulerRoleARN:
    Value: !GetAtt Role.Arn
    Export:
      Name: DeleteSchedulerRoleARN

 

コピペして使えるテンプレート

あとは、お好きなCloudFormationテンプレートに以下のパラメータとリソースを追加するだけで自動で削除してくれます。

追加するパラメータとリソース

Parameters:
  Hour:
    Description: Please type Delete Hour.
    Type: String
    Default: "13"
  Minute:
    Description: Please type Delete Minute.
    Type: String
    Default: "00"
  
Resources:
  EventSchedule:
    Type: AWS::Scheduler::Schedule
    Properties:
      Description: 'Delete Schedule'
      ScheduleExpression: !Sub "cron(${Minute} ${Hour} ? * * *)"
      ScheduleExpressionTimezone: "Asia/Tokyo"
      FlexibleTimeWindow:
        Mode: 'OFF'
      State: ENABLED
      Target:
        Arn: "arn:aws:scheduler:::aws-sdk:cloudformation:deleteStack"
        Input: !Sub "{ \"StackName\": \"${AWS::StackName}\" }"
        RoleArn: !ImportValue DeleteSchedulerRoleARN

パラメータについては、削除する時間の HourMinute を入力します。

  • Hour
    • 削除する時間の時
  • Minute
    • 削除する時間の分

実際にやってみる

以下の記事のテンプレートに対して実際に適用させてみます。

 

なお、EventBridge Scheduler で利用する IAMロールは作成済みとします。

作成していない方は、事前準備を実施してください。

テンプレートの準備

以下のようにテンプレートを用意します。元のテンプレートに削除用のテンプレートを追加します。

alb.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: ALB and Nginx on EC2
Parameters:
  Hour:
    Description: Please type Delete Hour.
    Type: String
    Default: "13"
  Minute:
    Description: Please type Delete Minute.
    Type: String
    Default: "00"
  
Resources:
  EventSchedule:
    Type: AWS::Scheduler::Schedule
    Properties:
      Description: 'Delete Schedule'
      ScheduleExpression: !Sub "cron(${Minute} ${Hour} ? * * *)"
      ScheduleExpressionTimezone: "Asia/Tokyo"
      FlexibleTimeWindow:
        Mode: 'OFF'
      State: ENABLED
      Target:
        Arn: "arn:aws:scheduler:::aws-sdk:cloudformation:deleteStack"
        Input: !Sub "{ \"StackName\": \"${AWS::StackName}\" }"
        RoleArn: !ImportValue DeleteSchedulerRoleARN
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-vpc

  PublicSubnet0:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: ap-northeast-1a
      CidrBlock: 10.0.0.0/24
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-public-1a-subnet
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: ap-northeast-1c
      CidrBlock: 10.0.1.0/24
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-public-1c-subnet

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-igw
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-public-rt
  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  
  PublicSubnet0RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet0
      RouteTableId: !Ref PublicRouteTable
  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  ALBSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
        GroupDescription: "ALB SG"
        GroupName: !Sub ${AWS::StackName}-alb-sg
        VpcId: !Ref VPC
        Tags:
          - Key: Name
            Value: !Sub ${AWS::StackName}-alb-sg
        SecurityGroupIngress:
          - IpProtocol: tcp
            FromPort: 80
            ToPort: 80
            CidrIp: 0.0.0.0/0
          - IpProtocol: tcp
            FromPort: 443
            ToPort: 443
            CidrIp: 0.0.0.0/0

  EC2SecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
        GroupDescription: "EC2 SG"
        GroupName: !Sub ${AWS::StackName}-ec2-sg
        VpcId: !Ref VPC
        Tags:
          - Key: Name
            Value: !Sub ${AWS::StackName}-ec2-sg
        SecurityGroupIngress:
          - IpProtocol: tcp
            FromPort: 80
            ToPort: 80
            SourceSecurityGroupId: !Ref ALBSecurityGroup

  EC2InstanceProfile: 
    Type: AWS::IAM::InstanceProfile
    Properties: 
      Path: "/"
      Roles: 
        - !Ref EC2Role
      InstanceProfileName: !Sub ${AWS::StackName}-ec2-profile
  EC2Role:
    Type: "AWS::IAM::Role"
    Properties:
        Path: "/"
        RoleName: !Sub ${AWS::StackName}-ec2-role
        Tags: 
        - Key: Name
          Value: !Sub ${AWS::StackName}-ec2-role
        AssumeRolePolicyDocument: 
          Version: 2012-10-17
          Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
        ManagedPolicyArns: 
          - "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  KeyPair:
    Type: 'AWS::EC2::KeyPair'
    Properties:
      KeyName: !Sub ${AWS::StackName}-keypair

  WebServer:
    Type: AWS::EC2::Instance
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-web
      InstanceType: t3.nano
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeType: gp3
            VolumeSize: 8
            DeleteOnTermination: true
            Encrypted: true
      NetworkInterfaces: 
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          GroupSet: 
            - !Ref EC2SecurityGroup
          SubnetId: !Ref PublicSubnet0
      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2}}'
      IamInstanceProfile: !Ref EC2InstanceProfile
      KeyName: !Ref KeyPair
      DisableApiTermination: false
      EbsOptimized: true
      UserData: 
        Fn::Base64: |
          #!/bin/bash
          sudo amazon-linux-extras install nginx1
          sudo systemctl enable nginx
          sudo systemctl start nginx

  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Type: "application"
      Scheme: "internet-facing"
      Name: !Sub ${AWS::StackName}-alb
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-alb
      IpAddressType: ipv4
      Subnets: 
        - !Ref PublicSubnet0
        - !Ref PublicSubnet1
      SecurityGroups: 
        - !Ref ALBSecurityGroup

  ListenerHTTP:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP
  
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub ${AWS::StackName}-tg
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-tg
      Port: 80
      Protocol: HTTP
      Matcher:
        HttpCode: '200'
      VpcId: !Ref VPC
      TargetType: instance
      Targets:
        - Id: !Ref WebServer

Outputs:
  ALBURL:
    Description: ALB endpoint URL
    Value: !Join
        - ""
        - - http://
          - !GetAtt ALB.DNSName

スタックの作成

あとは、上記のテンプレートでスタックを作成します。以下の場合は、14:00に削除するようにパラメータを与えています。

スタック作成後は、しばらく待つと通常通り CREATE_COMPLETE になります。

スタックの自動削除

パラメータに指定した時間までしばらく待ちます。

すると、以下のようにスタックが自動的に削除されます。

 

まとめ

本記事では、自動でCloudFormationのスタックを削除する方法をご紹介しました。

EventBridge Scheduler のおかげで結構シンプルに実現できたと思います。

皆さんもリソースの削除忘れがないようこのテンプレートをぜひ利用してみてください。