【AWS】AmazonLinux2023のEC2をスケジュール起動するCloudformationテンプレート

# 概要
Amazon Linux 2023でEC2(ssh)サーバを平日8:00-20:00の間起動するCloudformationテンプレートを作成しました。

起動テンプレートのUserdataやLambda、autoscalingを使って実装している記事もありましたが、UIからも管理しやすいCloudwatchRuleで運用する方針としています。

### 前提
VPC/Subnet関連は、他のスタックで作成済みの前提です。

また、AL2023のAMIの詳細については以下をご参考ください。

https://docs.aws.amazon.com/ja_jp/linux/al2023/ug/ec2.html

# サンプルテンプレート

```yaml
AWSTemplateFormatVersion: 2010-09-09

################################################################################
# Parameters
################################################################################
Parameters:
  ########################################
  # Common
  ########################################
  SystemTag:
    Type: String
    Default: "sample"
  SampleIP:
    Type: String
    Default: "111.222.333.444/32"

  ########################################
  # EC2
  ########################################
  Ec2InstanceType:
    Type: String
    Default: "t3.micro"
  Ec2InstanceAMI:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
  Ec2EbsVolumeSize1:
    Type: Number
    Default: 10

################################################################################
# Resources
################################################################################
Resources:

  SshRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "ec2.amazonaws.com"
                - "codedeploy.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Path: "/"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEC2FullAccess
        - arn:aws:iam::aws:policy/AmazonRDSFullAccess
        - arn:aws:iam::aws:policy/AWSCodeDeployFullAccess
        - arn:aws:iam::aws:policy/IAMFullAccess
        - arn:aws:iam::aws:policy/AmazonSSMFullAccess  

  SshSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupName: !Sub ${SystemTag}-SSH
      GroupDescription: !Sub ${SystemTag}-SSH
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref SampleIP
      VpcId: !ImportValue　VpcSample # Vpc/Subnet関連は他で作成してインポート
      Tags:
        - Key: Name
          Value: !Sub ${SystemTag}-SSH

  EC2InstanceProfile:
    Type: "AWS::IAM::InstanceProfile"
    Properties:
      Path: "/"
      Roles:
        - !Ref SshRole

  SshEc2Template:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        IamInstanceProfile:
          Arn: !GetAtt EC2InstanceProfile.Arn
        NetworkInterfaces:
            - AssociatePublicIpAddress: true
              DeleteOnTermination: true
              DeviceIndex: 0
              SubnetId: !ImportValue PublicSubnet1 # Vpc/Subnet関連は他で作成してインポート
              Groups: !Ref SshSecurityGroup
        InstanceInitiatedShutdownBehavior: "stop"
        ImageId: !Ref Ec2InstanceAMI
        KeyName: !Sub ${SystemTag}-ssh # KeyPairはマネコンから手動作成
        Monitoring:
          Enabled: false
        CreditSpecification:
          CpuCredits: "standard"
        InstanceType: !Ref Ec2InstanceType
        BlockDeviceMappings:
          - DeviceName: "/dev/xvda"
            Ebs:
              VolumeSize: !Ref Ec2EbsVolumeSize1
              VolumeType: "gp2"
              DeleteOnTermination: false            
        UserData:
          Fn::Base64: |
            #!/bin/bash

            timedatectl set-timezone Asia/Tokyo

            # MySQL Client
            dnf -y localinstall https://dev.mysql.com/get/mysql80-community-release-el7-1.noarch.rpm
            dnf config-manager --disable mysql80-community
            dnf config-manager --enable mysql57-community
            dnf install -y mysql

            if ! command -v aws &> /dev/null
            then
                dnf install -y unzip
                cd /tmp
                curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
                unzip awscliv2.zip
                sudo ./aws/install
            fi

  Ec2Instance:
    Type: AWS::EC2::Instance
    Properties:
      LaunchTemplate:
        LaunchTemplateId: !Ref "SshEc2Template"
        Version: !GetAtt "SshEc2Template.LatestVersionNumber"
      Tags:
        - Key: Name
          Value: !Sub ${SystemTag}-SSH

  ElasticIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
      - Key: Name
        Value: !Sub ${SystemTag}-SSH

  ElasticIPAssociate:
   Type: AWS::EC2::EIPAssociation
   Properties: 
     AllocationId: !GetAtt ElasticIP.AllocationId
     InstanceId: !Ref Ec2Instance
```

もし、このEC2を平日8:00-20:00の間だけ起動したい、という場合は以下を追加すればOKです。Cron式はUTC表記になっているので気を付けてください。

```yaml
  CloudWatchEventsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - 
            Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: "/"
      RoleName: CloudWatchEventsRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMFullAccess
        - arn:aws:iam::aws:policy/AmazonRDSFullAccess
        - arn:aws:iam::aws:policy/AmazonEC2FullAccess

  SSHEventRuleEc2Start:
    Type: AWS::Events::Rule
    Properties:
      Name: EventRuleEc2Start-SSH-sample
      Description: 'Rules for starting sample-SSH ec2 instance'
      ScheduleExpression: cron(0 23 ? * SUN-THU *)
      State: ENABLED
      EventBusName: default
      Targets:
        - Id: !Sub SSHEventRuleEc2Start-${SystemTag}-ssh
          Arn: "arn:aws:ssm:ap-northeast-1::automation-definition/AWS-StartEC2Instance:$DEFAULT"
          RoleArn: !GetAtt CloudWatchEventsRole.Arn
          Input: !Sub '{"InstanceId":["${Ec2Instance}"]}'

  SSHEventRuleEc2Stop:
    Type: AWS::Events::Rule
    Properties:
      Name: EventRuleEc2Stop-SSH-sample
      Description: 'Rules for stopping sample-SSH ec2 instance'
      ScheduleExpression: cron(0 11 * * ? *)
      State: ENABLED
      EventBusName: default
      Targets:
        - Id: !Sub SSHEventRuleEc2Stop-${SystemTag}-ssh
          Arn: "arn:aws:ssm:ap-northeast-1::automation-definition/AWS-StopEC2Instance:$DEFAULT"
          RoleArn: !GetAtt CloudWatchEventsRole.Arn
          Input: !Sub '{"InstanceId":["${Ec2Instance}"]}'
```

## 補足1: AL2023のUserdata

Amazon Linux 2023は、以前のバージョンのAmazon Linuxとは異なり、`yum`ではなく`dnf`パッケージマネージャーを使用します。AL2のように`yum update`としても更新されません。

`dnf`は`yum`の後継であり、より高速で依存関係の解決が改善されているとのこと。
基本的に、従来の`yum`コマンドを`dnf`に置き換えればOKです。

また、AL2023ではデフォルトでAWS CLI v2系がインストールされています。念の為、既にインストールされているかどうかを確認し、インストールされていない場合のみインストールプロセスを実行するようにしています。

## 補足2：セキュリティグループの位置

EC2インスタンスを作成する際に、インスタンスレベルでのセキュリティグループとネットワークインターフェースを同時に指定しようとした場合に以下のエラーが発生しました。

> Resource handler returned message: "Network interfaces and an instance-level security groups may not be specified on the same request

ネットワークインターフェースを明示的に指定する場合、そのネットワークインターフェースにセキュリティグループを割り当てる必要があり、インスタンスレベルで別途セキュリティグループを指定するとエラーになるのです。

上述の通り、`NetworkInterfaces`以下にセキュリティグループの指定があれば問題ありません。

参考：

https://dev.classmethod.jp/articles/aws-cloudformation-networkinterfaces-and-instance-level-error/