Spring Cloud AWS EC2: Master Instance Management and Elastic Compute
Learn how to integrate Spring Cloud with AWS EC2 for instance management, elastic compute, and production-ready automation.
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
- ✓Java 17 or later
- ✓Spring Boot 3.x project
- ✓AWS account with EC2 permissions
- ✓Basic knowledge of AWS EC2 concepts (instances, AMIs, security groups)
- Use Spring Cloud AWS EC2 to programmatically manage EC2 instances, including launch, stop, terminate, and describe operations.
- Leverage the
AmazonEC2client bean auto-configured by Spring Cloud AWS for dependency injection. - Implement custom instance tagging strategies for environment isolation and cost tracking.
- Handle rate limiting and pagination when listing instances to avoid throttling.
- Combine with Spring Retry for resilience against transient AWS API failures.
Imagine you have a fleet of delivery trucks. Spring Cloud AWS EC2 is like a remote control that lets you start, stop, and check the status of each truck from your office. You can also tag them (e.g., 'perishable goods' or 'long distance') and automate their schedules. This saves you from manually logging into each truck or driving to the depot.
If you're building microservices on AWS, you've probably automated EC2 instances with scripts or Terraform. But what if your Java application needs to manage instances dynamically—scaling up workers for a batch job, spinning up test environments on demand, or orchestrating a fleet of compute nodes? That's where Spring Cloud AWS EC2 comes in.
I've been using Spring Cloud AWS since the 1.x days, and I've seen teams waste hours reinventing the wheel with raw AWS SDK calls. The Spring Cloud AWS integration gives you auto-configured clients, sensible defaults, and a consistent programming model. But it's not magic—you still need to handle pagination, rate limits, and tagging strategies.
In this article, I'll walk you through real-world patterns for EC2 instance management using Spring Cloud AWS. We'll cover launching instances with custom AMIs, filtering by tags, handling termination protection, and integrating with Spring Retry for resilience. I'll also share a production incident where a misconfigured tag nearly cost a startup $10,000 in orphaned instances. Let's dive in.
Setting Up Spring Cloud AWS EC2
First, add the dependency to your pom.xml. I recommend using the Spring Cloud AWS BOM to avoid version conflicts. As of Spring Cloud AWS 3.0, the artifact is spring-cloud-aws-starter-ec2. Here's the Maven setup:
```xml <dependencyManagement> <dependencies> <dependency> <groupId>io.awspring.cloud</groupId> <artifactId>spring-cloud-aws-dependencies</artifactId> <version>3.1.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
<dependencies> <dependency> <groupId>io.awspring.cloud</groupId> <artifactId>spring-cloud-aws-starter-ec2</artifactId> </dependency> </dependencies> ```
Spring Cloud AWS auto-configures an AmazonEC2 client bean if you have AWS credentials configured (via environment variables, default credential chain, or IAM roles). The client is ready to inject into any service. But here's the gotcha: the default client uses the us-east-1 region unless you set cloud.aws.region.static in your properties. In production, you should always specify the region explicitly.
Pro tip: Use @ConfigurationProperties to externalize region and other settings. Never hardcode region in code.
Launching EC2 Instances with Custom Tags
Launching an instance programmatically is straightforward with RunInstancesRequest. The key is to always tag instances immediately after launch. AWS allows you to specify tags in the launch request itself using TagSpecification. This is critical for cost allocation, automation, and cleanup.
Here's a service that launches an instance with mandatory tags:
```java @Service public class EC2InstanceService {
private final AmazonEC2 amazonEC2;
public EC2InstanceService(AmazonEC2 amazonEC2) { this.amazonEC2 = amazonEC2; }
public String launchInstance(String amiId, String instanceType, String environment) { TagSpecification tagSpec = new TagSpecification() .withResourceType(ResourceType.Instance) .withTags( new Tag("Environment", environment), new Tag("Owner", System.getenv("APP_OWNER")), new Tag("CostCenter", "engineering"), new Tag("CreatedBy", "EC2InstanceService") );
RunInstancesRequest request = new RunInstancesRequest() .withImageId(amiId) .withInstanceType(InstanceType.fromValue(instanceType)) .withMinCount(1) .withMaxCount(1) .withTagSpecifications(tagSpec);
RunInstancesResult result = amazonEC2.runInstances(request); String instanceId = result.getReservation().getInstances().get(0).getInstanceId(); return instanceId; } } ```
Important: The tag specification must be set before the instance is launched. If you tag after launch, there's a race condition where the instance might be terminated by a cleanup script before you apply tags. Always use TagSpecification with ResourceType.Instance.
IllegalArgumentException if any required tag was missing. This prevented instances from being launched without cost tracking. We also added a scheduled job that terminated any instance running for more than 6 hours without the 'Environment' tag.Filtering and Describing Instances with Pagination
Describing instances is a common operation, but naive calls can lead to incomplete results or throttling. The AWS API paginates results with a NextToken. Spring Cloud AWS doesn't automatically handle pagination—you must loop until NextToken is null.
Here's a robust method that retrieves all instances matching a filter:
``java public List<Instance> describeInstancesByTag(String tagKey, String tagValue) { List<Instance> instances = new ArrayList<>(); String nextToken = null; do { DescribeInstancesRequest request = new ``DescribeInstancesRequest() .withFilters(new Filter("tag:" + tagKey, List.of(tagValue))) .withNextToken(nextToken); DescribeInstancesResult result = amazonEC2.describeInstances(request); for (Reservation reservation : result.getReservations()) { instances.addAll(reservation.getInstances()); } nextToken = result.getNextToken(); } while (nextToken != null); return instances; }
Performance tip: If you only need instance IDs or specific attributes, use DescribeInstanceStatus or DescribeInstanceTypes to reduce response size. For large fleets, consider using pagination with a parallel stream, but be mindful of API rate limits.
Rate limiting: The EC2 API has a default throttle of 100 requests per second (adjustable). Use Spring Retry with exponential backoff to handle RequestLimitExceeded exceptions. I'll show you how in the next section.
Filter gotchas: Tag filters are case-sensitive. Also, you can't filter by tag value alone—you must specify the tag key. For example, tag:Environment with value prod works, but a filter on just tag:Name with no value will return all instances with a Name tag (any value). Use tag-key and tag-value filters for more control.
@Retryable with a 200ms delay and a max of 3 attempts. That solved it.Resilience with Spring Retry and Exponential Backoff
AWS API calls can fail transiently—throttling, network issues, or service outages. Spring Retry makes it easy to handle these gracefully. Add spring-retry and spring-boot-starter-aop to your dependencies.
``xml <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``
Enable retry in your configuration:
``java @Configuration @EnableRetry public class RetryConfig {} ``
Now annotate your EC2 methods with @Retryable:
``java @Retryable( value = {AmazonEC2Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2) ) public String launchInstance(String amiId, String instanceType, String environment) { // ... launch logic } ``
This will retry up to 3 times with a 1-second initial delay, doubling each time. You can also define a @Recover method to handle failures after all retries are exhausted.
Production insight: Don't retry on all exceptions. Retry only on transient errors like RequestLimitExceeded or Unavailable. Permanent errors like InvalidAMIID.NotFound should fail fast. You can achieve this by specifying include and exclude on @Retryable.
@Recover method that sent a notification to Slack and queued the request for later processing.Managing Instance Lifecycle: Stop, Start, Terminate
Beyond launching, you'll need to stop, start, and terminate instances. These operations are idempotent, but you must handle state transitions carefully. For example, you can't stop an instance that's already stopped—you'll get an error.
Here's a service that safely manages lifecycle:
``java public void stopInstance(String instanceId) { try { amazonEC2.stopInstances(new ``StopInstancesRequest().withInstanceIds(instanceId)); } catch (AmazonEC2Exception e) { if ("IncorrectInstanceState".equals(e.getErrorCode())) { log.warn("Instance {} is already stopped", instanceId); } else { throw e; } } }
Termination protection: Before terminating an instance, check if it has disableApiTermination enabled. If it does, you must disable it first:
``java public void terminateInstance(String instanceId) { // Check termination protection DescribeInstanceAttributeResult attr = amazonEC2.describeInstanceAttribute( new ``DescribeInstanceAttributeRequest() .withInstanceId(instanceId) .withAttribute(InstanceAttributeName.DisableApiTermination) ); if (Boolean.TRUE.equals(attr.getInstanceAttribute().isDisableApiTermination())) { amazonEC2.modifyInstanceAttribute( new ModifyInstanceAttributeRequest() .withInstanceId(instanceId) .withDisableApiTermination(false) ); } amazonEC2.terminateInstances(new TerminateInstancesRequest().withInstanceIds(instanceId)); }
Warning: Always confirm termination with a human-in-the-loop for production instances. Use a two-step process: first mark for termination, then require a confirmation API call.
What the Official Docs Won't Tell You
The Spring Cloud AWS documentation is decent, but it glosses over several hard-earned lessons. Here are the gotchas I've encountered:
1. The default AmazonEC2 client is not thread-safe. The AWS SDK client is thread-safe, but the auto-configured bean is a singleton. That's fine. However, if you create your own client using AmazonEC2ClientBuilder, make sure you don't create a new instance per request—that will exhaust connections. The docs don't emphasize this enough.
2. Tag propagation to volumes and network interfaces. When you launch an instance with tags, the tags do NOT automatically propagate to the attached EBS volumes or ENIs. You must explicitly add TagSpecification for ResourceType.Volume and ResourceType.NetworkInterface. Otherwise, volumes will be untagged, leading to orphaned resources if the instance is terminated.
3. Instance metadata service (IMDS) v1 vs v2. If you're using IMDSv2 (which you should), the AWS SDK might have issues if your code relies on the default credential chain. Spring Cloud AWS 3.0+ supports IMDSv2, but older versions don't. Upgrade to at least 3.0 if you're on a hardened environment.
4. Rate limits are per-account, per-region. The default limit for DescribeInstances is 100 requests per second. If you have multiple services calling EC2 API, you can hit the limit. Use a shared AmazonEC2 client and consider using a semaphore to limit concurrency.
5. The @Retryable annotation doesn't work on private methods. Spring AOP only applies to public methods called from outside the class. If you call a @Retryable method from within the same class, the retry won't kick in. This is a classic Spring AOP pitfall that the docs don't mention.
6. Spot instance interruptions. If you use spot instances, you must handle interruption notices. The EC2 API provides a 2-minute warning via the instance metadata. Your code should listen for this and gracefully shut down. Spring Cloud AWS doesn't provide a built-in mechanism for this.
Integrating with Spring Cloud Netflix and Beyond
EC2 instance management doesn't exist in a vacuum. In a microservices architecture, you might want to register new instances with Eureka, update load balancers, or trigger configuration refresh via Spring Cloud Bus.
Example: Auto-register with Eureka
After launching an instance, you can register it with Eureka using the EurekaClient:
```java @Autowired private EurekaClient eurekaClient;
public void registerNewInstance(String instanceId, String ipAddress, int port) { InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setInstanceId(instanceId) .setAppName("my-service") .setIPAddr(ipAddress) .setPort(port) .build(); eurekaClient.register(instanceInfo); } ```
Example: Update Route53 DNS
Use the Amazon Route53 client to create a DNS record pointing to the new instance:
```java @Autowired private AmazonRoute53 route53Client;
public void updateDNS(String hostedZoneId, String recordName, String ipAddress) { ResourceRecordSet recordSet = new ResourceRecordSet(recordName, RRType.A) .withResourceRecords(new ResourceRecord(ipAddress)); Change change = new Change(ChangeAction.UPSERT, recordSet); route53Client.changeResourceRecordSets( new ChangeResourceRecordSetsRequest(hostedZoneId, new ChangeBatch(List.of(change))) ); } ```
Spring Cloud Bus integration: If you need to notify other services about a new instance, publish a custom event via Spring Cloud Bus. This is useful for dynamic load balancer updates or cache invalidation.
- [Spring Cloud Eureka](/java/spring-cloud-eureka)
- [Spring Cloud API Gateway](/java/spring-cloud-api-gateway)
- [Spring Cloud Circuit Breaker](/java/spring-cloud-circuit-breaker)
The Orphaned Instance Apocalypse
- Always enforce mandatory tags at the application level—don't rely on external cleanup scripts.
- Use a TagSpecification builder that adds tags from application configuration or environment variables.
- Implement a scheduled audit task that reports untagged instances and optionally terminates them.
- Set up billing alerts to catch cost anomalies early.
- Test your cleanup logic with the exact tag filters you use in production.
DescribeAccountAttributes API to verify limits. Also check if the AMI ID exists and is in the same region.com.amazonaws to see raw API requests.NextToken when listing instances. Consider using a thread pool with bounded queue for concurrent API calls.aws ec2 describe-instances --filters "Name=tag:Environment,Values=prod"Check Spring Cloud AWS logs for filter parameters.| File | Command / Code | Purpose |
|---|---|---|
| EC2Configuration.java | @Configuration | Setting Up Spring Cloud AWS EC2 |
| EC2InstanceService.java | @Service | Launching EC2 Instances with Custom Tags |
| EC2InstanceService.java | public List | Filtering and Describing Instances with Pagination |
| EC2RetryService.java | @Service | Resilience with Spring Retry and Exponential Backoff |
| EC2LifecycleService.java | @Service | Managing Instance Lifecycle |
| VolumeTagSpecification.java | TagSpecification volumeTagSpec = new TagSpecification() | What the Official Docs Won't Tell You |
| EurekaRegistrationService.java | @Service | Integrating with Spring Cloud Netflix and Beyond |
Key takeaways
Interview Questions on This Topic
Explain how to paginate EC2 describe instances results in Java using the AWS SDK. Why is this important?
DescribeInstancesResult includes a NextToken field. You must loop until NextToken is null to retrieve all instances. This is important because the API returns a maximum of 1000 results per call. Without pagination, you'll miss instances beyond the first page.Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
That's Spring Cloud. Mark it forged?
5 min read · try the examples if you haven't