DEV Community

Automating EC2 Instance Resizing with AWS Systems Manager (SSM) Document

Managing EC2 instance sizes manually can be time-consuming, especially when scaling resources across multiple instances. In this guide, we’ll deploy an AWS Systems Manager (SSM) document using AWS CloudFormation (CFN) to automate the resizing of EC2 instances in bulk. This setup allows users to resize instances across different Availability Zones with a single click.

Overview

Our goal is to create a CloudFormation stack that:

  • Deploys multiple EC2 instances in different Availability Zones
  • Creates an SSM document to automate the resizing of EC2 instances
  • Allows resizing based on Availability Zone, Tags, and Instance Type
  • Enables automation through AWS Systems Manager

Instead of manually stopping, resizing, and restarting instances, this solution streamlines the entire process into an automated workflow.

Architecture

The architecture consists of:

  • EC2 Instances - Launched with CloudFormation, spread across different Availability Zones
  • AWS SSM Document - Defines the automation process for resizing instances
  • IAM Role for SSM - Grants permissions to modify EC2 instance attributes

Image description

Step-by-Step Guide

1. Create a CloudFormation Template for EC2 Instances

We first create an EC2 CloudFormation (CFN) template (ec2.yaml) to launch instances in different Availability Zones.

ec2.yaml

AWSTemplateFormatVersion: 2010-09-09
#Description: EC2 with user data from CloudFormation.
Parameters:
  InstanceType:
    Description: WebServer EC2 instance type.
    Type: String
    Default: t2.micro
    AllowedValues:
      - t1.micro
      - t2.nano
      - t2.micro
      - t2.small
      - t2.medium
      - t2.large
      - m1.small
      - m1.medium
      - m1.large
      - m1.xlarge
      - m2.xlarge
      - m2.2xlarge
      - m2.4xlarge
      - m3.medium
      - m3.large
      - m3.xlarge
      - m3.2xlarge
      - m4.large
      - m4.xlarge
      - m4.2xlarge
      - m4.4xlarge
      - m4.10xlarge
      - c1.medium
      - c1.xlarge
      - c3.large
      - c3.xlarge
      - c3.2xlarge
      - c3.4xlarge
      - c3.8xlarge
      - c4.large
      - c4.xlarge
      - c4.2xlarge
      - c4.4xlarge
      - c4.8xlarge
      - g2.2xlarge
      - g2.8xlarge
      - r3.large
      - r3.xlarge
      - r3.2xlarge
      - r3.4xlarge
      - r3.8xlarge
      - i2.xlarge
      - i2.2xlarge
      - i2.4xlarge
      - i2.8xlarge
      - d2.xlarge
      - d2.2xlarge
      - d2.4xlarge
      - d2.8xlarge
      - hi1.4xlarge
      - hs1.8xlarge
      - cr1.8xlarge
      - cc2.8xlarge
      - cg1.4xlarge

  # Ec2Name:
  #   Description: Ec2 Resource name.
  #   Type: String

  # BucketName:
  #   Description: BucketName.
  #   Default: ahmedsalem-testbuckettt
  #   Type: String

  # ObjectPrefix:
  #   Description: idex.html prefix,for ex / 
  #   Type: String
  #   Default: /

  # KeyName:
  #   Description: Name of an existing EC2 KeyPair to enable SSH access to the instance
  #   Type: 'AWS::EC2::KeyPair::KeyName'
  #   ConstraintDescription: must be the name of an existing EC2 KeyPair.

  # SubnetId:
  #  Description: Subnet ID which will run the web server instanc into.
  #  Type: 'AWS::EC2::Subnet::Id'

  LatestAmiId:
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'

  SSHLocation:
    Description: The IP address range that can be used to SSH to the EC2 instance.
    Type: String
    MinLength: '9'
    MaxLength: '18'
    Default: 0.0.0.0/0
    AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})'
    ConstraintDescription: Must be a valid IP CIDR range of the form x.x.x.x/x

Resources:
  WebServerInstance1:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: !Ref LatestAmiId
      NetworkInterfaces: 
      - AssociatePublicIpAddress: "true"
        DeviceIndex: "0"
        SubnetId: subnet-ed2f8486
        GroupSet:
            - !GetAtt "WebServerSecurityGroup.GroupId"
      Tags:
       - Key: "Name"
         Value: "server_1"
       - Key: "ssm-tag"
         Value: "test-ssm"
      # SecurityGroupIds:
      #       - !GetAtt "WebServerSecurityGroup.GroupId"
      #IamInstanceProfile: !Ref IAMInstanceProfile

  WebServerInstance2:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: !Ref LatestAmiId
      NetworkInterfaces: 
      - AssociatePublicIpAddress: "true"
        DeviceIndex: "0"
        SubnetId: subnet-0556b778
        GroupSet:
            - !GetAtt "WebServerSecurityGroup.GroupId"
      Tags:
       - Key: "Name"
         Value: "server_2"
       - Key: "ssm-tag"
         Value: "test-ssm"
      # SecurityGroupIds:
      #       - !GetAtt "WebServerSecurityGroup.GroupId"
      #IamInstanceProfile: !Ref IAMInstanceProfile

  WebServerInstance3:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: !Ref LatestAmiId
      NetworkInterfaces: 
      - AssociatePublicIpAddress: "true"
        DeviceIndex: "0"
        SubnetId: subnet-34531b78
        GroupSet:
            - !GetAtt "WebServerSecurityGroup.GroupId"
      Tags:
       - Key: "Name"
         Value: "server_3"
      #  - Key: "ssm-tag"
      #    Value: "test-ssm"
      # SecurityGroupIds:
      #       - !GetAtt "WebServerSecurityGroup.GroupId"
      #IamInstanceProfile: !Ref IAMInstanceProfile

  WebServerInstance4:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: !Ref LatestAmiId
      NetworkInterfaces: 
      - AssociatePublicIpAddress: "true"
        DeviceIndex: "0"
        SubnetId: subnet-ed2f8486
        GroupSet:
            - !GetAtt "WebServerSecurityGroup.GroupId"
      Tags:
       - Key: "Name"
         Value: "server_4"

  WebServerInstance5:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: !Ref LatestAmiId
      NetworkInterfaces: 
      - AssociatePublicIpAddress: "true"
        DeviceIndex: "0"
        SubnetId: subnet-ed2f8486
        GroupSet:
            - !GetAtt "WebServerSecurityGroup.GroupId"
      Tags:
       - Key: "Name"
         Value: "server_5"

  WebServerInstance6:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: !Ref LatestAmiId
      NetworkInterfaces: 
      - AssociatePublicIpAddress: "true"
        DeviceIndex: "0"
        SubnetId: subnet-ed2f8486
        GroupSet:
            - !GetAtt "WebServerSecurityGroup.GroupId"
      Tags:
       - Key: "Name"
         Value: "server_6"

  WebServerInstance7:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: !Ref LatestAmiId
      NetworkInterfaces: 
      - AssociatePublicIpAddress: "true"
        DeviceIndex: "0"
        SubnetId: subnet-ed2f8486
        GroupSet:
            - !GetAtt "WebServerSecurityGroup.GroupId"
      Tags:
       - Key: "Name"
         Value: "server_7"

  WebServerSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: Enable HTTP access via port 80 and SSH Access port 22.
      # VpcId: vpc-0078f41e6b5568b6e
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: !Ref SSHLocation

  # IAMInstanceProfile:
  #  Type: AWS::IAM::InstanceProfile
  #  Properties:
  #    Roles:
  #      - !Ref IAMRole
  #    InstanceProfileName: ec2-access-s3

  # IAMRole:
  #  Type: AWS::IAM::Role
  #  Properties:
  #    RoleName: ec2-s3-access
  #    AssumeRolePolicyDocument:
  #      Version: '2012-10-17'
  #      Statement:
  #        - Effect: Allow
  #          Principal:
  #            Service: ec2.amazonaws.com
  #          Action: sts:AssumeRole
  #    Path: '/'
  #    Policies:
  #      - PolicyName: 'EC2Access'
  #        PolicyDocument:
  #          Version: '2012-10-17'
  #          Statement:
  #            - Effect: 'Allow'
  #              Action:
  #                - 's3:GetObject'
  #              Resource: !Sub 'arn:aws:s3:::${BucketName}/index.html'
Enter fullscreen mode Exit fullscreen mode

What This Template Does:

• Launches multiple EC2 instances with different tags
• Ensures instances are deployed in multiple Availability Zones
• Assigns instances a security group for controlled access

2. Create a CloudFormation Template for the SSM Document

We now create another CloudFormation template (ssm.yaml) that defines the SSM document for resizing EC2 instances.

ssm.yaml

AWSTemplateFormatVersion: 2010-09-09

# Description: SSM Document for resizing list of EC2 instances

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

  document: 
    Type: AWS::SSM::Document
    Properties:
      DocumentType: Automation
      Content:
        schemaVersion: '0.3'
        description: 'Resize EC2 instances.'
        assumeRole: '{{ AutomationAssumeRole }}'
        parameters:
          AvailabilityZone:
            type: String
            description: "(Required) The impacted Availability."
            default: 'us-east-2a'

          TagName:
            type: String
            description: "(Required) The Tag Name."
            default: 'ssm-tag'  

          TagValue:
            type: String
            description: "(Required) The Tag Value."
            default: 'test-ssm' 

          InstanceType:
            type: String
            description: "(Required) The Desired Instance Type."
            default: 't2.small'  

          # If I need to specify another IAMRole
          AutomationAssumeRole:
            type: String
            description: "(Required) The Arn of the role that allows the automation."
            default: !GetAtt SSMDocumentRole.Arn

        mainSteps:
        - name: listInstances
          action: aws:executeAwsApi
          timeoutSeconds: '60'
          inputs:
            Service: ec2
            Api: DescribeInstances
            Filters:
              - Name: availability-zone
                Values: ["{{ AvailabilityZone }}"]

              - Name: instance-state-name
                Values: ["running"]

              - Name: tag:{{ TagName }}
                Values: ["{{ TagValue }}"]
          outputs:
          - Name: InstanceIds
            Selector: "$.Reservations..Instances..InstanceId"
            Type: StringList

        - name: stopInstances
          action: aws:changeInstanceState
          onFailure: Continue
          inputs:
            InstanceIds:
              - "{{ listInstances.InstanceIds }}"
            DesiredState: stopped

        - name: ResizeInstances
          action: "aws:executeScript"
          inputs: 
            Runtime: "python3.8"
            Handler: resizeInstance
            InputPayload: 
              InstanceIds:
                - "{{ listInstances.InstanceIds }}"
            Script: |-
              def resizeInstance(events, context):
                import boto3
                client = boto3.client('ec2')
                for instance_id in events['InstanceIds']:
                  #client.modify_instance_attribute(InstanceId=instanceId, InstanceType={'Value': 't2.small',},)
                  print(instance_id)
                  print(type(instance_id))
                  client.modify_instance_attribute(InstanceId=instance_id, Attribute='instanceType', Value='t2.small')
                return True
          onFailure: Continue

        - name: startInstances
          action: aws:changeInstanceState
          inputs: 
            InstanceIds:
                - "{{ listInstances.InstanceIds }}"
            DesiredState: running
          isEnd: true
Enter fullscreen mode Exit fullscreen mode

What This Template Does:

• Creates an SSM document that:
• Identifies EC2 instances based on Availability Zone and Tags
• Stops the instances before resizing
• Modifies the instance type using boto3
• Restarts the instances after resizing
• Defines an IAM Role for SSM to interact with EC2

3. Deploy the CloudFormation Templates

  1. Launch the EC2 Stack
    • Deploy the ec2.yaml template to provision instances.
    • Ensure the Tag Name and Tag Value are correctly set.
  2. Launch the SSM Stack
    • Deploy the ssm.yaml template to create the SSM document.
    • Verify that the IAM role is correctly attached.

4. Execute the SSM Document to Resize Instances

Once both CloudFormation stacks are deployed:

  1. Go to AWS Systems Manager → Shared Documents
  2. Click on “Shared with me”
  3. Select your custom SSM document and click “Execute document”
  4. Enter the required parameters (Availability Zone, Tag Name, New Instance Type)
  5. Click Execute

The SSM automation will now:

• Identify instances in the specified Availability Zone & Tag
• Stop instances before resizing
• Modify instance types dynamically
• Restart instances

You should see all EC2 instances updated successfully!

5. Clean Up Resources

To remove the deployed resources:

  1. Delete the SSM CloudFormation Stack
  2. Delete the EC2 CloudFormation Stack This ensures that no unintended costs are incurred.

Conclusion

By leveraging AWS Systems Manager (SSM) Documents, we can automate EC2 instance resizing across multiple environments with minimal effort. This solution eliminates manual resizing, ensuring faster and more efficient instance modifications.

Start implementing this in your AWS environment to save time and improve scalability!

Top comments (0)