Skip to content

Commit d512b10

Browse files
author
Yuriy Bezsonov
committed
feat(infra): add ECS Express Service construct for Spring AI agents deployment
1 parent ff2975f commit d512b10

11 files changed

Lines changed: 1361 additions & 1220 deletions

File tree

infra/README.md

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ infra/
3232
│ │ ├── WorkshopBucket.java # Shared S3 bucket + SSM parameter
3333
│ │ ├── ThreadAnalysis.java # Thread dump Lambda + API Gateway
3434
│ │ ├── AiJvmAnalyzer.java # Pod Identity for AI analyzer
35-
│ │ ├── Unicorn.java # ECR + IAM roles for workshop apps
35+
│ │ ├── Unicorn.java # EventBus + IAM roles for workshop apps
3636
│ │ ├── EcrRegistry.java # ECR create-on-push template
37+
│ │ ├── EcsExpressService.java # ECS Express Mode service for AI agents
3738
│ │ └── CfnPreDeleteCleanup.java # Stack cleanup Lambda
3839
│ ├── src/main/resources/
3940
│ │ ├── userdata.sh # EC2 UserData bootstrap script
@@ -74,7 +75,10 @@ infra/
7475
│ │ ├── eks.sh # EKS cluster configuration
7576
│ │ ├── monitoring.sh # Prometheus + Grafana
7677
│ │ ├── analysis.sh # Thread dump + profiling
77-
│ │ └── unicorn-store-spring.sh # Spring app deployment
78+
│ │ ├── unicorn-store-spring.sh # Spring app deployment
79+
│ │ └── java-spring-ai-agents/ # Spring AI agents setup
80+
│ │ ├── build-and-push.sh # Build and push images to ECR
81+
│ │ └── Dockerfile # Placeholder container image
7882
│ ├── lib/ # Common utilities
7983
│ │ ├── common.sh # Emoji logging, error handling
8084
│ │ └── wait-for-resources.sh # EKS/RDS readiness checking
@@ -92,9 +96,9 @@ infra/
9296
| Template Type | Resources Created |
9397
|---------------|-------------------|
9498
| `base` | VPC, IDE |
95-
| `java-on-aws-immersion-day` | VPC, IDE, CodeBuild, Database, EKS, WorkshopBucket, EcrRegistry, ThreadAnalysis, AiJvmAnalyzer, Unicorn |
99+
| `java-on-aws-immersion-day` | VPC, IDE, CodeBuild, Database, EKS, WorkshopBucket, EcrRegistry, Unicorn, ECR (unicorn-store-spring), ThreadAnalysis, AiJvmAnalyzer |
96100
| `java-on-amazon-eks` | Same as java-on-aws-immersion-day |
97-
| `java-spring-ai-agents` | Same as java-on-aws-immersion-day |
101+
| `java-spring-ai-agents` | VPC, IDE, CodeBuild, Database, EKS, WorkshopBucket, EcrRegistry, Unicorn, 2x EcsExpressService (unicorn-spring-ai-agent, unicorn-store-spring) |
98102

99103
---
100104

@@ -171,16 +175,34 @@ public WorkshopStack(...) {
171175
String prefix = "workshop";
172176
String templateType = getContext("template.type"); // defaults to "base"
173177

178+
boolean isImmersionDay = "java-on-aws-immersion-day".equals(templateType);
179+
boolean isEks = "java-on-amazon-eks".equals(templateType);
180+
boolean isSpringAi = "java-spring-ai-agents".equals(templateType);
181+
boolean isFullTemplate = isImmersionDay || isEks || isSpringAi;
182+
174183
// Always created
175184
Vpc vpc = new Vpc(this, "Vpc", ...);
176185
Ide ide = new Ide(this, "Ide", ...);
177186

178-
// Conditionally created for java-on-aws-immersion-day, java-on-amazon-eks, and java-spring-ai-agents
179-
if ("java-on-aws-immersion-day".equals(templateType) || "java-on-amazon-eks".equals(templateType) || "java-spring-ai-agents".equals(templateType)) {
180-
new CodeBuild(this, "CodeBuild", ...);
181-
new Database(this, "Database", ...);
182-
new Eks(this, "Eks", ...);
183-
// ... additional constructs
187+
// Full template resources (all 3 workshop types)
188+
if (isFullTemplate) {
189+
new CodeBuild(...);
190+
Database database = new Database(...);
191+
Eks eks = new Eks(...);
192+
Unicorn unicorn = new Unicorn(...); // EventBus, EKS/ECS roles, DB setup
193+
194+
// java-on-aws-immersion-day & java-on-amazon-eks only
195+
if (isImmersionDay || isEks) {
196+
new Repository("unicorn-store-spring"); // ECR for manual deployment
197+
new ThreadAnalysis(...);
198+
new AiJvmAnalyzer(...);
199+
}
200+
201+
// java-spring-ai-agents only
202+
if (isSpringAi) {
203+
new EcsExpressService("unicorn-spring-ai-agent", unicorn); // ECR + ECS + ALB
204+
new EcsExpressService("unicorn-store-spring", unicorn); // ECR + ECS + ALB
205+
}
184206
}
185207
}
186208
```

infra/cdk/src/main/java/sample/com/WorkshopStack.java

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package sample.com;
22

3+
import software.amazon.awscdk.RemovalPolicy;
34
import software.amazon.awscdk.Stack;
45
import software.amazon.awscdk.StackProps;
6+
import software.amazon.awscdk.services.ecr.Repository;
57
import software.constructs.Construct;
68
import sample.com.constructs.*;
79
import sample.com.constructs.Ide.IdeProps;
@@ -47,6 +49,12 @@ public WorkshopStack(final Construct scope, final String id, final StackProps pr
4749
gitBranch = "main"; // fallback
4850
}
4951

52+
// Template type flags
53+
boolean isImmersionDay = "java-on-aws-immersion-day".equals(templateType);
54+
boolean isEks = "java-on-amazon-eks".equals(templateType);
55+
boolean isSpringAi = "java-spring-ai-agents".equals(templateType);
56+
boolean isFullTemplate = isImmersionDay || isEks || isSpringAi;
57+
5058
// Core infrastructure (always created)
5159
Vpc vpc = new Vpc(this, "Vpc", Vpc.VpcProps.builder()
5260
.prefix(prefix)
@@ -61,8 +69,8 @@ public WorkshopStack(final Construct scope, final String id, final StackProps pr
6169
.build();
6270
Ide ide = new Ide(this, "Ide", ideProps);
6371

64-
// java-on-aws-immersion-day, java-on-amazon-eks, and java-spring-ai-agents specific resources (CodeBuild for service-linked roles)
65-
if ("java-on-aws-immersion-day".equals(templateType) || "java-on-amazon-eks".equals(templateType) || "java-spring-ai-agents".equals(templateType)) {
72+
// Full template resources (java-on-aws-immersion-day, java-on-amazon-eks, java-spring-ai-agents)
73+
if (isFullTemplate) {
6674
// CodeBuild for workshop setup (service-linked role creation)
6775
new CodeBuild(this, "CodeBuild",
6876
CodeBuild.CodeBuildProps.builder()
@@ -75,7 +83,7 @@ public WorkshopStack(final Construct scope, final String id, final StackProps pr
7583
.buildSpec(buildSpec)
7684
.build());
7785

78-
// Database, EKS, PerformanceAnalysis, Unicorn
86+
// Database
7987
Database database = new Database(this, "Database", Database.DatabaseProps.builder()
8088
.prefix(prefix)
8189
.vpc(vpc.getVpc())
@@ -89,7 +97,7 @@ public WorkshopStack(final Construct scope, final String id, final StackProps pr
8997
.ideInternalSecurityGroup(ide.getIdeInternalSecurityGroup())
9098
.build());
9199

92-
// Shared workshop bucket for thread dumps and profiling data
100+
// Shared workshop bucket
93101
WorkshopBucket workshopBucket = new WorkshopBucket(this, "WorkshopBucket",
94102
WorkshopBucket.WorkshopBucketProps.builder()
95103
.prefix(prefix)
@@ -101,29 +109,61 @@ public WorkshopStack(final Construct scope, final String id, final StackProps pr
101109
.prefix(prefix)
102110
.build());
103111

104-
// Thread Analysis (thread dump Lambda + API Gateway)
105-
new ThreadAnalysis(this, "ThreadAnalysis",
106-
ThreadAnalysis.ThreadAnalysisProps.builder()
107-
.prefix(prefix)
108-
.vpc(vpc.getVpc())
109-
.eksCluster(eks.getCluster())
110-
.eksClusterName(eks.getClusterName())
111-
.workshopBucket(workshopBucket.getBucket())
112-
.build());
113-
114-
// AI JVM Analyzer (Pod Identity role for ai-jvm-analyzer)
115-
new AiJvmAnalyzer(this, "AiJvmAnalyzer",
116-
AiJvmAnalyzer.AiJvmAnalyzerProps.builder()
117-
.workshopBucket(workshopBucket.getBucket())
118-
.build());
119-
120-
// Unicorn construct: Roles + DB Setup (uses unicorn* naming for workshop content compatibility)
121-
new Unicorn(this, "Unicorn", Unicorn.UnicornProps.builder()
112+
// Unicorn construct: EventBus, Roles, DB Setup (uses unicorn* naming for workshop content compatibility)
113+
Unicorn unicorn = new Unicorn(this, "Unicorn", Unicorn.UnicornProps.builder()
122114
.vpc(vpc.getVpc())
123115
.database(database)
124116
.workshopBucket(workshopBucket.getBucket())
125117
.build());
126118

119+
// java-on-aws-immersion-day & java-on-amazon-eks specific resources
120+
if (isImmersionDay || isEks) {
121+
// ECR repository for unicorn-store-spring (manual ECS Express deployment)
122+
Repository.Builder.create(this, "UnicornStoreSpringEcr")
123+
.repositoryName("unicorn-store-spring")
124+
.imageScanOnPush(true)
125+
.removalPolicy(RemovalPolicy.DESTROY)
126+
.emptyOnDelete(true)
127+
.build();
128+
129+
// Thread Analysis (thread dump Lambda + API Gateway)
130+
new ThreadAnalysis(this, "ThreadAnalysis",
131+
ThreadAnalysis.ThreadAnalysisProps.builder()
132+
.prefix(prefix)
133+
.vpc(vpc.getVpc())
134+
.eksCluster(eks.getCluster())
135+
.eksClusterName(eks.getClusterName())
136+
.workshopBucket(workshopBucket.getBucket())
137+
.build());
138+
139+
// AI JVM Analyzer (Pod Identity role for ai-jvm-analyzer)
140+
new AiJvmAnalyzer(this, "AiJvmAnalyzer",
141+
AiJvmAnalyzer.AiJvmAnalyzerProps.builder()
142+
.workshopBucket(workshopBucket.getBucket())
143+
.build());
144+
}
145+
146+
// java-spring-ai-agents specific resources
147+
if (isSpringAi) {
148+
// ECS Express Service for Spring AI Agent
149+
new EcsExpressService(this, "SpringAiAgent",
150+
EcsExpressService.EcsExpressServiceProps.builder()
151+
.appName("unicorn-spring-ai-agent")
152+
.vpc(vpc.getVpc())
153+
.database(database)
154+
.unicorn(unicorn)
155+
.build());
156+
157+
// ECS Express Service for MCP Server
158+
new EcsExpressService(this, "McpServer",
159+
EcsExpressService.EcsExpressServiceProps.builder()
160+
.appName("unicorn-store-spring")
161+
.vpc(vpc.getVpc())
162+
.database(database)
163+
.unicorn(unicorn)
164+
.build());
165+
}
166+
127167
// Pre-delete cleanup (removes VPC endpoints, CloudWatch logs, S3 contents before stack deletion)
128168
new CfnPreDeleteCleanup(this, "CfnPreDeleteCleanup",
129169
CfnPreDeleteCleanup.CfnPreDeleteCleanupProps.builder()
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package sample.com.constructs;
2+
3+
import software.amazon.awscdk.RemovalPolicy;
4+
import software.amazon.awscdk.services.ec2.IVpc;
5+
import software.amazon.awscdk.services.ec2.SubnetSelection;
6+
import software.amazon.awscdk.services.ec2.SubnetType;
7+
import software.amazon.awscdk.services.ecr.Repository;
8+
import software.amazon.awscdk.services.ecs.CfnExpressGatewayService;
9+
import software.amazon.awscdk.services.iam.ManagedPolicy;
10+
import software.amazon.awscdk.services.iam.Role;
11+
import software.constructs.Construct;
12+
13+
import java.util.List;
14+
import java.util.Map;
15+
16+
/**
17+
* ECS Express Mode service construct for Spring AI agents.
18+
* Creates an ECR repository and ECS Express Gateway Service with ALB.
19+
* Reuses Unicorn's ECS roles and adds Bedrock access for AI capabilities.
20+
*/
21+
public class EcsExpressService extends Construct {
22+
23+
private final Repository ecrRepository;
24+
private final CfnExpressGatewayService expressService;
25+
26+
public EcsExpressService(final Construct scope, final String id, final EcsExpressServiceProps props) {
27+
super(scope, id);
28+
29+
String appName = props.getAppName();
30+
31+
// Create ECR Repository
32+
this.ecrRepository = Repository.Builder.create(this, "EcrRepository")
33+
.repositoryName(appName)
34+
.imageScanOnPush(false)
35+
.removalPolicy(RemovalPolicy.DESTROY)
36+
.emptyOnDelete(true)
37+
.build();
38+
39+
// Add Bedrock access to task role for AI capabilities
40+
Role taskRole = props.getUnicorn().getEcsTaskRole();
41+
taskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AmazonBedrockFullAccess"));
42+
43+
// Get private subnets for the service
44+
List<String> privateSubnetIds = props.getVpc().selectSubnets(SubnetSelection.builder()
45+
.subnetType(SubnetType.PRIVATE_WITH_EGRESS)
46+
.build()).getSubnetIds();
47+
48+
// Create ECS Express Gateway Service
49+
this.expressService = CfnExpressGatewayService.Builder.create(this, "ExpressService")
50+
.serviceName(appName)
51+
.infrastructureRoleArn(props.getUnicorn().getEcsInfrastructureRole().getRoleArn())
52+
.executionRoleArn(props.getUnicorn().getEcsTaskExecutionRole().getRoleArn())
53+
.taskRoleArn(taskRole.getRoleArn())
54+
.primaryContainer(CfnExpressGatewayService.ExpressGatewayContainerProperty.builder()
55+
.image(ecrRepository.getRepositoryUri() + ":latest")
56+
.containerPort(8080)
57+
.secrets(List.of(
58+
CfnExpressGatewayService.SecretProperty.builder()
59+
.name("SPRING_DATASOURCE_URL")
60+
.valueFrom(props.getDatabase().getParamDBConnectionString().getParameterArn())
61+
.build(),
62+
CfnExpressGatewayService.SecretProperty.builder()
63+
.name("SPRING_DATASOURCE_PASSWORD")
64+
.valueFrom(props.getDatabase().getDatabaseSecret().getSecretArn() + ":password::")
65+
.build()
66+
))
67+
.build())
68+
.cpu("1024")
69+
.memory("2048")
70+
.healthCheckPath("/actuator/health")
71+
.networkConfiguration(CfnExpressGatewayService.ExpressGatewayServiceNetworkConfigurationProperty.builder()
72+
.subnets(privateSubnetIds)
73+
.build())
74+
.scalingTarget(CfnExpressGatewayService.ExpressGatewayScalingTargetProperty.builder()
75+
.minTaskCount(1)
76+
.maxTaskCount(4)
77+
.autoScalingMetric("CPU")
78+
.autoScalingTargetValue(70)
79+
.build())
80+
.build();
81+
}
82+
83+
public Repository getEcrRepository() {
84+
return ecrRepository;
85+
}
86+
87+
public CfnExpressGatewayService getExpressService() {
88+
return expressService;
89+
}
90+
91+
// Props class
92+
public static class EcsExpressServiceProps {
93+
private final String appName;
94+
private final IVpc vpc;
95+
private final Database database;
96+
private final Unicorn unicorn;
97+
98+
private EcsExpressServiceProps(Builder builder) {
99+
this.appName = builder.appName;
100+
this.vpc = builder.vpc;
101+
this.database = builder.database;
102+
this.unicorn = builder.unicorn;
103+
}
104+
105+
public static Builder builder() {
106+
return new Builder();
107+
}
108+
109+
public String getAppName() {
110+
return appName;
111+
}
112+
113+
public IVpc getVpc() {
114+
return vpc;
115+
}
116+
117+
public Database getDatabase() {
118+
return database;
119+
}
120+
121+
public Unicorn getUnicorn() {
122+
return unicorn;
123+
}
124+
125+
public static class Builder {
126+
private String appName;
127+
private IVpc vpc;
128+
private Database database;
129+
private Unicorn unicorn;
130+
131+
public Builder appName(String appName) {
132+
this.appName = appName;
133+
return this;
134+
}
135+
136+
public Builder vpc(IVpc vpc) {
137+
this.vpc = vpc;
138+
return this;
139+
}
140+
141+
public Builder database(Database database) {
142+
this.database = database;
143+
return this;
144+
}
145+
146+
public Builder unicorn(Unicorn unicorn) {
147+
this.unicorn = unicorn;
148+
return this;
149+
}
150+
151+
public EcsExpressServiceProps build() {
152+
return new EcsExpressServiceProps(this);
153+
}
154+
}
155+
}
156+
}

0 commit comments

Comments
 (0)