AWS Systems Manager: Fleet Management, Patch Automation, and Session Manager in Production

Logging into EC2 instances over SSH with a key file is a security anti-pattern. It requires open port 22, creates long-lived credentials that need rotation, and leaves no structured audit trail. Patching fleets manually requires coordination, downtime windows, and produces inconsistent results across hundreds of instances. AWS Systems Manager solves both problems and more.

In this article I will walk through the four Systems Manager capabilities that matter most in production: Fleet Manager for inventory and visibility, Patch Manager for automated OS patching, Session Manager for SSH-free secure access, and Run Command for executing scripts across your entire fleet.

The SSM Agent and IAM Requirements

Systems Manager works through the SSM Agent, which runs on your EC2 instances. Amazon Linux 2, Amazon Linux 2023, Ubuntu 20.04 and later, and Windows Server 2016 and later all ship with the agent pre-installed. For the agent to communicate with Systems Manager, your instance needs an IAM role with the AmazonSSMManagedInstanceCore policy attached and either a VPC endpoint for SSM or outbound internet access.

resource "aws_iam_role" "ec2_ssm" {
  name = "ec2-ssm-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "ssm_core" {
  role       = aws_iam_role.ec2_ssm.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "ec2_ssm" {
  name = "ec2-ssm-profile"
  role = aws_iam_role.ec2_ssm.name
}

resource "aws_vpc_endpoint" "ssm" {
  vpc_id              = var.vpc_id
  service_name        = "com.amazonaws.${var.region}.ssm"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true
}

resource "aws_vpc_endpoint" "ssm_messages" {
  vpc_id              = var.vpc_id
  service_name        = "com.amazonaws.${var.region}.ssmmessages"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true
}

resource "aws_vpc_endpoint" "ec2_messages" {
  vpc_id              = var.vpc_id
  service_name        = "com.amazonaws.${var.region}.ec2messages"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true
}

Three VPC endpoints are required for Systems Manager to work in a private subnet without internet access: ssm, ssmmessages, and ec2messages. Instances in private subnets with no NAT gateway and only these three endpoints can use all Systems Manager features including Session Manager. This is the correct configuration for production instances that should have no outbound internet access.

Session Manager: SSH Without Keys or Open Ports

resource "aws_ssm_document" "session_preferences" {
  name            = "SSM-SessionManagerRunShell"
  document_type   = "Session"
  document_format = "JSON"

  content = jsonencode({
    schemaVersion = "1.0"
    description   = "Session Manager preferences"
    sessionType   = "Standard_Stream"
    inputs = {
      s3BucketName                = aws_s3_bucket.session_logs.bucket
      s3KeyPrefix                 = "session-logs/"
      s3EncryptionEnabled         = true
      cloudWatchLogGroupName      = "/aws/ssm/sessions"
      cloudWatchEncryptionEnabled = true
      cloudWatchStreamingEnabled  = true
      idleSessionTimeout          = "20"
      kmsKeyId                    = aws_kms_key.ssm.arn
      runAsEnabled                = true
      runAsDefaultUser            = "ssm-user"
    }
  })
}

Configuring session logs to both S3 and CloudWatch gives you a complete audit trail. Every session command is captured, timestamped, and stored. CloudWatch provides real-time streaming for active session monitoring. S3 provides durable long-term storage for compliance audits. The kmsKeyId encrypts session data at rest. runAsDefaultUser = ssm-user means sessions run as a dedicated low-privilege user rather than root or ec2-user.

Patch Manager

resource "aws_ssm_patch_baseline" "amazon_linux" {
  name             = "production-amazon-linux-2023"
  operating_system = "AMAZON_LINUX_2023"
  description      = "Production patch baseline for Amazon Linux 2023"

  approval_rule {
    approve_after_days  = 7
    compliance_level    = "CRITICAL"
    enable_non_security = false

    patch_filter {
      key    = "CLASSIFICATION"
      values = ["Security", "Bugfix"]
    }

    patch_filter {
      key    = "SEVERITY"
      values = ["Critical", "Important"]
    }
  }

  rejected_patches                 = []
  rejected_patches_action          = "BLOCK"
  approved_patches_enable_non_security = false
}

resource "aws_ssm_patch_group" "production" {
  baseline_id = aws_ssm_patch_baseline.amazon_linux.id
  patch_group = "production"
}

resource "aws_ssm_maintenance_window" "weekly_patch" {
  name              = "weekly-patching"
  schedule          = "cron(0 2 ? * SUN *)"
  duration          = 4
  cutoff            = 1
  allow_unassociated_targets = false
}

resource "aws_ssm_maintenance_window_target" "production_instances" {
  window_id     = aws_ssm_maintenance_window.weekly_patch.id
  name          = "production-instances"
  resource_type = "INSTANCE"

  targets {
    key    = "tag:PatchGroup"
    values = ["production"]
  }
}

resource "aws_ssm_maintenance_window_task" "patch_task" {
  window_id        = aws_ssm_maintenance_window.weekly_patch.id
  task_arn         = "AWS-RunPatchBaseline"
  task_type        = "RUN_COMMAND"
  priority         = 1
  max_concurrency  = "20%"
  max_errors       = "10%"

  targets {
    key    = "WindowTargetIds"
    values = [aws_ssm_maintenance_window_target.production_instances.id]
  }

  task_invocation_parameters {
    run_command_parameters {
      document_version    = "$LATEST"
      output_s3_bucket    = aws_s3_bucket.patch_logs.bucket
      output_s3_key_prefix = "patch-logs/"
      service_role_arn    = aws_iam_role.maintenance_window.arn

      parameter {
        name   = "Operation"
        values = ["Install"]
      }

      parameter {
        name   = "RebootOption"
        values = ["RebootIfNeeded"]
      }
    }
  }
}

approve_after_days = 7 means patches released by AWS are automatically approved for installation 7 days after release. This gives AWS time to identify and pull back any problematic patches before they reach your fleet, while keeping your patching lag short enough to satisfy most security requirements. max_concurrency = 20% ensures Patch Manager patches 20 percent of your fleet at a time, leaving 80 percent serving traffic while patching proceeds.

Run Command for Fleet-Wide Operations

import boto3

ssm = boto3.client("ssm", region_name="us-east-1")

def run_command_on_fleet(commands: list, tag_key: str, tag_value: str) -> str:
    response = ssm.send_command(
        Targets=[{
            "Key":    f"tag:{tag_key}",
            "Values": [tag_value]
        }],
        DocumentName    = "AWS-RunShellScript",
        Parameters      = {"commands": commands},
        OutputS3BucketName = "your-ssm-output-bucket",
        OutputS3KeyPrefix  = "run-command-output/",
        MaxConcurrency     = "20%",
        MaxErrors          = "5%",
        Comment            = f"Fleet operation on {tag_key}={tag_value}"
    )
    return response["Command"]["CommandId"]

command_id = run_command_on_fleet(
    commands   = [
        "systemctl status nginx",
        "df -h",
        "free -m"
    ],
    tag_key    = "Environment",
    tag_value  = "production"
)

Run Command executes scripts across every instance matching a tag filter simultaneously. MaxConcurrency and MaxErrors keep a problematic script from cascading across the entire fleet. All output goes to S3, making it auditable and searchable. This is the correct tool for one-off operational tasks like rotating configuration files, flushing caches, or collecting diagnostic information across your fleet without SSH.

Closing Thoughts

Systems Manager eliminates the need for SSH keys, jump boxes, and open port 22. Session Manager gives you audited shell access to every managed instance from the AWS console or CLI. Patch Manager automates OS patching on a schedule that keeps your compliance posture solid without manual coordination. Run Command handles fleet-wide operations safely with built-in concurrency controls.

Remove port 22 from your security groups. Attach the SSM role to every EC2 instance. Set up the VPC endpoints. Tag your instances with a PatchGroup value and configure a maintenance window. These four steps move your fleet management from reactive and manual to proactive and automated.

Enjoy the cloud.

Osama


#AWS #SystemsManager #FleetManagement #PatchManagement #SessionManager #CloudSecurity #Terraform #InfrastructureAsCode #AmazonWebServices #SolutionsArchitect #CloudComputing #DevOps #CloudNative #Automation #TechBlog #CloudInfrastructure #EC2 #Compliance #SRE #Engineering

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.