Skip to content

Commit 0d99e05

Browse files
author
Yuriy Bezsonov
committed
feat(java-on-aws-infra): add AI agents support and infrastructure enhancements
1 parent 43021c5 commit 0d99e05

9 files changed

Lines changed: 570 additions & 170 deletions

File tree

infra/cdk/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1212
<maven.compiler.source>25</maven.compiler.source>
1313
<maven.compiler.target>25</maven.compiler.target>
14-
<cdk.version>2.233.0</cdk.version>
14+
<cdk.version>2.235.0</cdk.version>
1515
<constructs.version>10.4.2</constructs.version>
1616
<cdknag.version>2.36.2</cdknag.version>
1717
<junit.version>5.11.3</junit.version>

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

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import software.amazon.awscdk.Stack;
55
import software.amazon.awscdk.StackProps;
66
import software.amazon.awscdk.services.ecr.Repository;
7+
import software.amazon.awscdk.services.iam.ManagedPolicy;
78
import software.constructs.Construct;
89
import sample.com.constructs.*;
910
import sample.com.constructs.Ide.IdeProps;
@@ -69,19 +70,54 @@ public WorkshopStack(final Construct scope, final String id, final StackProps pr
6970
.build();
7071
Ide ide = new Ide(this, "Ide", ideProps);
7172

73+
// CodeBuild for workshop setup (service-linked role creation)
74+
new CodeBuild(this, "CodeBuild",
75+
CodeBuild.CodeBuildProps.builder()
76+
.projectName(prefix + "-setup")
77+
.vpc(vpc.getVpc())
78+
.environmentVariables(Map.of(
79+
"TEMPLATE_TYPE", templateType,
80+
"GIT_BRANCH", gitBranch))
81+
.buildSpec(buildSpec)
82+
.build());
83+
84+
// Shared workshop bucket
85+
WorkshopBucket workshopBucket = new WorkshopBucket(this, "WorkshopBucket",
86+
WorkshopBucket.WorkshopBucketProps.builder()
87+
.prefix(prefix)
88+
.build());
89+
90+
// ECR Registry settings (Repository Creation Template for create-on-push)
91+
new EcrRegistry(this, "EcrRegistry",
92+
EcrRegistry.EcrRegistryProps.builder()
93+
.prefix(prefix)
94+
.build());
95+
96+
// Bedrock logging role (for model invocation logging to CloudWatch)
97+
software.amazon.awscdk.services.iam.Role.Builder.create(this, "BedrockLoggingRole")
98+
.roleName(prefix + "-bedrock-logging-role")
99+
.assumedBy(software.amazon.awscdk.services.iam.ServicePrincipal.Builder.create("bedrock.amazonaws.com")
100+
.conditions(java.util.Map.of(
101+
"StringEquals", java.util.Map.of("aws:SourceAccount", this.getAccount()),
102+
"ArnLike", java.util.Map.of("aws:SourceArn", "arn:aws:bedrock:" + this.getRegion() + ":" + this.getAccount() + ":*")
103+
))
104+
.build())
105+
.description("Role for Bedrock model invocation logging to CloudWatch")
106+
.inlinePolicies(java.util.Map.of("BedrockLogging",
107+
software.amazon.awscdk.services.iam.PolicyDocument.Builder.create()
108+
.statements(java.util.List.of(
109+
software.amazon.awscdk.services.iam.PolicyStatement.Builder.create()
110+
.effect(software.amazon.awscdk.services.iam.Effect.ALLOW)
111+
.actions(java.util.List.of("logs:CreateLogStream", "logs:PutLogEvents"))
112+
.resources(java.util.List.of("arn:aws:logs:" + this.getRegion() + ":" + this.getAccount() + ":log-group:/aws/bedrock/*"))
113+
.build()
114+
))
115+
.build()
116+
))
117+
.build();
118+
72119
// Full template resources (java-on-aws-immersion-day, java-on-amazon-eks, java-spring-ai-agents)
73120
if (isFullTemplate) {
74-
// CodeBuild for workshop setup (service-linked role creation)
75-
new CodeBuild(this, "CodeBuild",
76-
CodeBuild.CodeBuildProps.builder()
77-
.projectName(prefix + "-setup")
78-
.vpc(vpc.getVpc())
79-
.environmentVariables(Map.of(
80-
"TEMPLATE_TYPE", templateType,
81-
"GIT_BRANCH", gitBranch))
82-
.buildSpec(buildSpec)
83-
.build());
84-
85121
// Database
86122
Database database = new Database(this, "Database", Database.DatabaseProps.builder()
87123
.prefix(prefix)
@@ -96,18 +132,6 @@ public WorkshopStack(final Construct scope, final String id, final StackProps pr
96132
.ideInternalSecurityGroup(ide.getIdeInternalSecurityGroup())
97133
.build());
98134

99-
// Shared workshop bucket
100-
WorkshopBucket workshopBucket = new WorkshopBucket(this, "WorkshopBucket",
101-
WorkshopBucket.WorkshopBucketProps.builder()
102-
.prefix(prefix)
103-
.build());
104-
105-
// ECR Registry settings (Repository Creation Template for create-on-push)
106-
new EcrRegistry(this, "EcrRegistry",
107-
EcrRegistry.EcrRegistryProps.builder()
108-
.prefix(prefix)
109-
.build());
110-
111135
// Unicorn construct: EventBus, Roles, DB Setup (uses unicorn* naming for workshop content compatibility)
112136
Unicorn unicorn = new Unicorn(this, "Unicorn", Unicorn.UnicornProps.builder()
113137
.vpc(vpc.getVpc())
@@ -144,6 +168,40 @@ public WorkshopStack(final Construct scope, final String id, final StackProps pr
144168

145169
// java-spring-ai-agents specific resources
146170
if (isSpringAi) {
171+
// AI Agent EKS Pod Identity role with Bedrock access
172+
software.amazon.awscdk.services.iam.Role aiAgentEksRole = software.amazon.awscdk.services.iam.Role.Builder.create(this, "AiAgentEksRole")
173+
.roleName("ai-agent-eks-pod-role")
174+
.assumedBy(software.amazon.awscdk.services.iam.ServicePrincipal.Builder.create("pods.eks.amazonaws.com").build())
175+
.description("EKS Pod Identity role for AI Agent with Bedrock access")
176+
.managedPolicies(java.util.List.of(
177+
ManagedPolicy.fromAwsManagedPolicyName("AmazonBedrockFullAccess")
178+
))
179+
.build();
180+
// Add sts:TagSession for Pod Identity
181+
aiAgentEksRole.getAssumeRolePolicy().addStatements(
182+
software.amazon.awscdk.services.iam.PolicyStatement.Builder.create()
183+
.effect(software.amazon.awscdk.services.iam.Effect.ALLOW)
184+
.principals(java.util.List.of(software.amazon.awscdk.services.iam.ServicePrincipal.Builder.create("pods.eks.amazonaws.com").build()))
185+
.actions(java.util.List.of("sts:TagSession"))
186+
.build()
187+
);
188+
// Grant DB secrets access (same as unicornstore-eks-pod-role)
189+
database.grantSecretsRead(aiAgentEksRole);
190+
191+
// AI Agent Lambda execution role with Bedrock access
192+
software.amazon.awscdk.services.iam.Role aiAgentLambdaRole = software.amazon.awscdk.services.iam.Role.Builder.create(this, "AiAgentLambdaRole")
193+
.roleName("ai-agent-lambda-role")
194+
.assumedBy(software.amazon.awscdk.services.iam.ServicePrincipal.Builder.create("lambda.amazonaws.com").build())
195+
.description("Lambda execution role for AI Agent with Bedrock access")
196+
.managedPolicies(java.util.List.of(
197+
ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"),
198+
ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaVPCAccessExecutionRole"),
199+
ManagedPolicy.fromAwsManagedPolicyName("AmazonBedrockFullAccess")
200+
))
201+
.build();
202+
// Grant DB secrets access
203+
database.grantSecretsRead(aiAgentLambdaRole);
204+
147205
// CodeBuild to push placeholder images (ECS Express needs images at deploy time)
148206
// ECR repos are created automatically via create-on-push (EcrRegistry)
149207
String placeholderBuildSpec = """
@@ -168,10 +226,10 @@ public WorkshopStack(final Construct scope, final String id, final StackProps pr
168226
169227
# Build and push placeholder to both repos (create-on-push creates repos)
170228
docker build -t placeholder /tmp
171-
docker tag placeholder $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/unicorn-spring-ai-agent:latest
172-
docker tag placeholder $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/unicorn-store-spring:latest
173-
docker push $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/unicorn-spring-ai-agent:latest
174-
docker push $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/unicorn-store-spring:latest
229+
for REPO in ai-agent mcp-server1; do
230+
docker tag placeholder $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$REPO:latest
231+
docker push $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$REPO:latest
232+
done
175233
""";
176234

177235
CodeBuild placeholderImageBuild = new CodeBuild(this, "PlaceholderImageBuild",
@@ -184,23 +242,24 @@ public WorkshopStack(final Construct scope, final String id, final StackProps pr
184242
.buildSpec(placeholderBuildSpec)
185243
.build());
186244

187-
// ECS Express Service for Spring AI Agent
188-
new EcsExpressService(this, "SpringAiAgent",
245+
// ECS Express Service for AI Agent
246+
new EcsExpressService(this, "AiAgent",
189247
EcsExpressService.EcsExpressServiceProps.builder()
190-
.appName("unicorn-spring-ai-agent")
248+
.appName("ai-agent")
191249
.vpc(vpc.getVpc())
192250
.database(database)
193-
.unicorn(unicorn)
251+
.configureTaskRole(role ->
252+
role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AmazonBedrockFullAccess")))
194253
.dependsOn(placeholderImageBuild.getCustomResource())
195254
.build());
196255

197256
// ECS Express Service for MCP Server
198257
new EcsExpressService(this, "McpServer",
199258
EcsExpressService.EcsExpressServiceProps.builder()
200-
.appName("unicorn-store-spring")
259+
.appName("mcp-server1")
201260
.vpc(vpc.getVpc())
202261
.database(database)
203-
.unicorn(unicorn)
262+
.configureTaskRole(role -> unicorn.getEventBus().grantPutEventsTo(role))
204263
.dependsOn(placeholderImageBuild.getCustomResource())
205264
.build());
206265
}

infra/cdk/src/main/java/sample/com/constructs/Database.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import software.amazon.awscdk.services.ec2.SubnetSelection;
99
import software.amazon.awscdk.services.ec2.SubnetType;
1010
import software.amazon.awscdk.services.ec2.ISecurityGroup;
11+
import software.amazon.awscdk.services.iam.IGrantable;
1112
import software.amazon.awscdk.services.rds.AuroraPostgresClusterEngineProps;
1213
import software.amazon.awscdk.services.rds.ServerlessV2ClusterInstanceProps;
1314
import software.amazon.awscdk.services.rds.AuroraPostgresEngineVersion;
@@ -140,4 +141,13 @@ public StringParameter getParamDBConnectionString() {
140141
public String getDatabaseSecretString() {
141142
return databaseSecret.secretValueFromJson("password").toString();
142143
}
144+
145+
/**
146+
* Grants read access to database secrets (secret + connection string parameter).
147+
* Use for ECS task execution roles that need to inject secrets at container startup.
148+
*/
149+
public void grantSecretsRead(IGrantable grantee) {
150+
databaseSecret.grantRead(grantee);
151+
paramDBConnectionString.grantRead(grantee);
152+
}
143153
}

infra/cdk/src/main/java/sample/com/constructs/EcsExpressService.java

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,81 @@
77
import software.amazon.awscdk.services.ec2.SubnetType;
88
import software.amazon.awscdk.services.ecs.CfnExpressGatewayService;
99
import software.amazon.awscdk.services.ecs.Cluster;
10+
import software.amazon.awscdk.services.iam.Effect;
1011
import software.amazon.awscdk.services.iam.ManagedPolicy;
12+
import software.amazon.awscdk.services.iam.PolicyStatement;
1113
import software.amazon.awscdk.services.iam.Role;
14+
import software.amazon.awscdk.services.iam.ServicePrincipal;
1215
import software.amazon.awscdk.services.logs.LogGroup;
1316
import software.constructs.Construct;
1417
import software.constructs.IDependable;
1518

1619
import java.util.List;
20+
import java.util.function.Consumer;
1721

1822
/**
19-
* ECS Express Mode service construct for Spring AI agents.
20-
* Creates ECS cluster and ECS Express Gateway Service with ALB.
21-
* ECR repos are created via create-on-push (EcrRegistry construct).
22-
* Reuses Unicorn's ECS roles and adds Bedrock access for AI capabilities.
23+
* Self-contained ECS Express Mode service construct.
24+
* Creates ECS cluster, IAM roles, and ECS Express Gateway Service with ALB.
25+
* ECR repos are created via create-on-push.
26+
*
27+
* All resources use appName as prefix for consistent naming.
28+
*
29+
* Task role has base permissions (CloudWatch, X-Ray) and can be customized
30+
* via configureTaskRole callback for app-specific permissions (Bedrock, EventBridge, etc.)
2331
*/
2432
public class EcsExpressService extends Construct {
2533

2634
private final Cluster ecsCluster;
2735
private final CfnExpressGatewayService expressService;
36+
private final Role infrastructureRole;
37+
private final Role taskExecutionRole;
38+
private final Role taskRole;
2839

2940
public EcsExpressService(final Construct scope, final String id, final EcsExpressServiceProps props) {
3041
super(scope, id);
3142

3243
String appName = props.getAppName();
44+
ServicePrincipal ecsService = ServicePrincipal.Builder.create("ecs.amazonaws.com").build();
45+
ServicePrincipal ecsTasks = ServicePrincipal.Builder.create("ecs-tasks.amazonaws.com").build();
46+
47+
// === Infrastructure Role (for Express Mode) ===
48+
this.infrastructureRole = Role.Builder.create(this, "InfrastructureRole")
49+
.roleName(appName + "-ecs-infrastructure-role")
50+
.path("/service-role/")
51+
.assumedBy(ecsService)
52+
.description("ECS infrastructure role for Express Mode")
53+
.build();
54+
infrastructureRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName(
55+
"service-role/AmazonECSInfrastructureRoleforExpressGatewayServices"));
56+
57+
// === Task Execution Role ===
58+
this.taskExecutionRole = Role.Builder.create(this, "TaskExecutionRole")
59+
.roleName(appName + "-ecs-task-execution-role")
60+
.path("/service-role/")
61+
.assumedBy(ecsTasks)
62+
.description("ECS task execution role for pulling images and injecting secrets")
63+
.build();
64+
taskExecutionRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName(
65+
"service-role/AmazonECSTaskExecutionRolePolicy"));
66+
taskExecutionRole.addToPolicy(PolicyStatement.Builder.create()
67+
.effect(Effect.ALLOW)
68+
.actions(List.of("logs:CreateLogGroup"))
69+
.resources(List.of("*"))
70+
.build());
71+
props.getDatabase().grantSecretsRead(taskExecutionRole);
72+
73+
// === Task Role (app runtime permissions) ===
74+
this.taskRole = Role.Builder.create(this, "TaskRole")
75+
.roleName(appName + "-ecs-task-role")
76+
.path("/service-role/")
77+
.assumedBy(ecsTasks)
78+
.description("ECS task role for application runtime permissions")
79+
.build();
80+
taskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy"));
81+
taskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AWSXrayWriteOnlyAccess"));
82+
if (props.getConfigureTaskRole() != null) {
83+
props.getConfigureTaskRole().accept(taskRole);
84+
}
3385

3486
// Build ECR image URI (repos created via create-on-push)
3587
String imageUri = Fn.sub("${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/" + appName + ":latest");
@@ -46,10 +98,6 @@ public EcsExpressService(final Construct scope, final String id, final EcsExpres
4698
.removalPolicy(RemovalPolicy.DESTROY)
4799
.build();
48100

49-
// Add Bedrock access to task role for AI capabilities
50-
Role taskRole = props.getUnicorn().getEcsTaskRole();
51-
taskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AmazonBedrockFullAccess"));
52-
53101
// Get PUBLIC subnets for the service (Express Mode uses public subnets with ALB)
54102
List<String> publicSubnetIds = props.getVpc().selectSubnets(SubnetSelection.builder()
55103
.subnetType(SubnetType.PUBLIC)
@@ -62,8 +110,8 @@ public EcsExpressService(final Construct scope, final String id, final EcsExpres
62110
this.expressService = CfnExpressGatewayService.Builder.create(this, "ExpressService")
63111
.serviceName(appName)
64112
.cluster(ecsCluster.getClusterName())
65-
.infrastructureRoleArn(props.getUnicorn().getEcsInfrastructureRole().getRoleArn())
66-
.executionRoleArn(props.getUnicorn().getEcsTaskExecutionRole().getRoleArn())
113+
.infrastructureRoleArn(infrastructureRole.getRoleArn())
114+
.executionRoleArn(taskExecutionRole.getRoleArn())
67115
.taskRoleArn(taskRole.getRoleArn())
68116
.primaryContainer(CfnExpressGatewayService.ExpressGatewayContainerProperty.builder()
69117
.image(imageUri)
@@ -116,20 +164,32 @@ public CfnExpressGatewayService getExpressService() {
116164
return expressService;
117165
}
118166

167+
public Role getInfrastructureRole() {
168+
return infrastructureRole;
169+
}
170+
171+
public Role getTaskExecutionRole() {
172+
return taskExecutionRole;
173+
}
174+
175+
public Role getTaskRole() {
176+
return taskRole;
177+
}
178+
119179
// Props class
120180
public static class EcsExpressServiceProps {
121181
private final String appName;
122182
private final IVpc vpc;
123183
private final Database database;
124-
private final Unicorn unicorn;
125184
private final IDependable dependsOn;
185+
private final Consumer<Role> configureTaskRole;
126186

127187
private EcsExpressServiceProps(Builder builder) {
128188
this.appName = builder.appName;
129189
this.vpc = builder.vpc;
130190
this.database = builder.database;
131-
this.unicorn = builder.unicorn;
132191
this.dependsOn = builder.dependsOn;
192+
this.configureTaskRole = builder.configureTaskRole;
133193
}
134194

135195
public static Builder builder() {
@@ -139,21 +199,21 @@ public static Builder builder() {
139199
public String getAppName() { return appName; }
140200
public IVpc getVpc() { return vpc; }
141201
public Database getDatabase() { return database; }
142-
public Unicorn getUnicorn() { return unicorn; }
143202
public IDependable getDependsOn() { return dependsOn; }
203+
public Consumer<Role> getConfigureTaskRole() { return configureTaskRole; }
144204

145205
public static class Builder {
146206
private String appName;
147207
private IVpc vpc;
148208
private Database database;
149-
private Unicorn unicorn;
150209
private IDependable dependsOn;
210+
private Consumer<Role> configureTaskRole;
151211

152212
public Builder appName(String appName) { this.appName = appName; return this; }
153213
public Builder vpc(IVpc vpc) { this.vpc = vpc; return this; }
154214
public Builder database(Database database) { this.database = database; return this; }
155-
public Builder unicorn(Unicorn unicorn) { this.unicorn = unicorn; return this; }
156215
public Builder dependsOn(IDependable dependsOn) { this.dependsOn = dependsOn; return this; }
216+
public Builder configureTaskRole(Consumer<Role> configureTaskRole) { this.configureTaskRole = configureTaskRole; return this; }
157217

158218
public EcsExpressServiceProps build() {
159219
return new EcsExpressServiceProps(this);

0 commit comments

Comments
 (0)