Skip to content

Commit 200e025

Browse files
author
Yuriy Bezsonov
committed
Update infra WIP
1 parent d512b10 commit 200e025

10 files changed

Lines changed: 684 additions & 296 deletions

File tree

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,53 @@ public WorkshopStack(final Construct scope, final String id, final StackProps pr
145145

146146
// java-spring-ai-agents specific resources
147147
if (isSpringAi) {
148+
// CodeBuild to push placeholder images (ECS Express needs images at deploy time)
149+
// ECR repos are created automatically via create-on-push (EcrRegistry)
150+
String placeholderBuildSpec = """
151+
version: 0.2
152+
env:
153+
shell: bash
154+
phases:
155+
build:
156+
commands:
157+
- |
158+
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
159+
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
160+
161+
# Create placeholder Dockerfile
162+
cat > /tmp/Dockerfile << 'EOF'
163+
FROM public.ecr.aws/nginx/nginx:stable-alpine
164+
RUN echo 'server { listen 8080; location /actuator/health { return 200 "OK"; } location / { return 200 "Placeholder"; } }' > /etc/nginx/conf.d/default.conf
165+
EXPOSE 8080
166+
EOF
167+
168+
# Build and push placeholder to both repos (create-on-push creates repos)
169+
docker build -t placeholder /tmp
170+
docker tag placeholder $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/unicorn-spring-ai-agent:latest
171+
docker tag placeholder $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/unicorn-store-spring:latest
172+
docker push $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/unicorn-spring-ai-agent:latest
173+
docker push $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/unicorn-store-spring:latest
174+
""";
175+
176+
CodeBuild placeholderImageBuild = new CodeBuild(this, "PlaceholderImageBuild",
177+
CodeBuild.CodeBuildProps.builder()
178+
.prefix(prefix)
179+
.projectName(prefix + "-placeholder-images")
180+
.vpc(vpc.getVpc())
181+
.privilegedMode(true)
182+
.environmentVariables(Map.of(
183+
"TEMPLATE_TYPE", templateType))
184+
.buildSpec(placeholderBuildSpec)
185+
.build());
186+
148187
// ECS Express Service for Spring AI Agent
149188
new EcsExpressService(this, "SpringAiAgent",
150189
EcsExpressService.EcsExpressServiceProps.builder()
151190
.appName("unicorn-spring-ai-agent")
152191
.vpc(vpc.getVpc())
153192
.database(database)
154193
.unicorn(unicorn)
194+
.dependsOn(placeholderImageBuild.getCustomResource())
155195
.build());
156196

157197
// ECS Express Service for MCP Server
@@ -161,6 +201,7 @@ public WorkshopStack(final Construct scope, final String id, final StackProps pr
161201
.vpc(vpc.getVpc())
162202
.database(database)
163203
.unicorn(unicorn)
204+
.dependsOn(placeholderImageBuild.getCustomResource())
164205
.build());
165206
}
166207

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

Lines changed: 58 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,86 @@
11
package sample.com.constructs;
22

3+
import software.amazon.awscdk.Fn;
34
import software.amazon.awscdk.RemovalPolicy;
45
import software.amazon.awscdk.services.ec2.IVpc;
56
import software.amazon.awscdk.services.ec2.SubnetSelection;
67
import software.amazon.awscdk.services.ec2.SubnetType;
7-
import software.amazon.awscdk.services.ecr.Repository;
88
import software.amazon.awscdk.services.ecs.CfnExpressGatewayService;
9+
import software.amazon.awscdk.services.ecs.Cluster;
910
import software.amazon.awscdk.services.iam.ManagedPolicy;
1011
import software.amazon.awscdk.services.iam.Role;
12+
import software.amazon.awscdk.services.logs.LogGroup;
1113
import software.constructs.Construct;
14+
import software.constructs.IDependable;
1215

1316
import java.util.List;
14-
import java.util.Map;
1517

1618
/**
1719
* ECS Express Mode service construct for Spring AI agents.
18-
* Creates an ECR repository and ECS Express Gateway Service with ALB.
20+
* Creates ECS cluster and ECS Express Gateway Service with ALB.
21+
* ECR repos are created via create-on-push (EcrRegistry construct).
1922
* Reuses Unicorn's ECS roles and adds Bedrock access for AI capabilities.
2023
*/
2124
public class EcsExpressService extends Construct {
2225

23-
private final Repository ecrRepository;
26+
private final Cluster ecsCluster;
2427
private final CfnExpressGatewayService expressService;
2528

2629
public EcsExpressService(final Construct scope, final String id, final EcsExpressServiceProps props) {
2730
super(scope, id);
2831

2932
String appName = props.getAppName();
3033

31-
// Create ECR Repository
32-
this.ecrRepository = Repository.Builder.create(this, "EcrRepository")
33-
.repositoryName(appName)
34-
.imageScanOnPush(false)
34+
// Build ECR image URI (repos created via create-on-push)
35+
String imageUri = Fn.sub("${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/" + appName + ":latest");
36+
37+
// Create ECS Cluster
38+
this.ecsCluster = Cluster.Builder.create(this, "EcsCluster")
39+
.clusterName(appName)
40+
.vpc(props.getVpc())
41+
.build();
42+
43+
// Create CloudWatch Log Group
44+
LogGroup logGroup = LogGroup.Builder.create(this, "LogGroup")
45+
.logGroupName("/aws/ecs/" + appName)
3546
.removalPolicy(RemovalPolicy.DESTROY)
36-
.emptyOnDelete(true)
3747
.build();
3848

3949
// Add Bedrock access to task role for AI capabilities
4050
Role taskRole = props.getUnicorn().getEcsTaskRole();
4151
taskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AmazonBedrockFullAccess"));
4252

43-
// Get private subnets for the service
44-
List<String> privateSubnetIds = props.getVpc().selectSubnets(SubnetSelection.builder()
45-
.subnetType(SubnetType.PRIVATE_WITH_EGRESS)
53+
// Get PUBLIC subnets for the service (Express Mode uses public subnets with ALB)
54+
List<String> publicSubnetIds = props.getVpc().selectSubnets(SubnetSelection.builder()
55+
.subnetType(SubnetType.PUBLIC)
4656
.build()).getSubnetIds();
4757

58+
// Get DB security group ID for network access
59+
String dbSecurityGroupId = props.getDatabase().getDatabaseSecurityGroup().getSecurityGroupId();
60+
4861
// Create ECS Express Gateway Service
4962
this.expressService = CfnExpressGatewayService.Builder.create(this, "ExpressService")
5063
.serviceName(appName)
64+
.cluster(ecsCluster.getClusterName())
5165
.infrastructureRoleArn(props.getUnicorn().getEcsInfrastructureRole().getRoleArn())
5266
.executionRoleArn(props.getUnicorn().getEcsTaskExecutionRole().getRoleArn())
5367
.taskRoleArn(taskRole.getRoleArn())
5468
.primaryContainer(CfnExpressGatewayService.ExpressGatewayContainerProperty.builder()
55-
.image(ecrRepository.getRepositoryUri() + ":latest")
69+
.image(imageUri)
5670
.containerPort(8080)
71+
.awsLogsConfiguration(CfnExpressGatewayService.ExpressGatewayServiceAwsLogsConfigurationProperty.builder()
72+
.logGroup(logGroup.getLogGroupName())
73+
.logStreamPrefix("ecs")
74+
.build())
5775
.secrets(List.of(
5876
CfnExpressGatewayService.SecretProperty.builder()
5977
.name("SPRING_DATASOURCE_URL")
6078
.valueFrom(props.getDatabase().getParamDBConnectionString().getParameterArn())
6179
.build(),
80+
CfnExpressGatewayService.SecretProperty.builder()
81+
.name("SPRING_DATASOURCE_USERNAME")
82+
.valueFrom(props.getDatabase().getDatabaseSecret().getSecretArn() + ":username::")
83+
.build(),
6284
CfnExpressGatewayService.SecretProperty.builder()
6385
.name("SPRING_DATASOURCE_PASSWORD")
6486
.valueFrom(props.getDatabase().getDatabaseSecret().getSecretArn() + ":password::")
@@ -69,19 +91,25 @@ public EcsExpressService(final Construct scope, final String id, final EcsExpres
6991
.memory("2048")
7092
.healthCheckPath("/actuator/health")
7193
.networkConfiguration(CfnExpressGatewayService.ExpressGatewayServiceNetworkConfigurationProperty.builder()
72-
.subnets(privateSubnetIds)
94+
.subnets(publicSubnetIds)
95+
.securityGroups(List.of(dbSecurityGroupId))
7396
.build())
7497
.scalingTarget(CfnExpressGatewayService.ExpressGatewayScalingTargetProperty.builder()
7598
.minTaskCount(1)
7699
.maxTaskCount(4)
77-
.autoScalingMetric("CPU")
100+
.autoScalingMetric("AVERAGE_CPU")
78101
.autoScalingTargetValue(70)
79102
.build())
80103
.build();
104+
105+
// Add dependency on CodeBuild (image must be pushed before service starts)
106+
if (props.getDependsOn() != null) {
107+
this.expressService.getNode().addDependency(props.getDependsOn());
108+
}
81109
}
82110

83-
public Repository getEcrRepository() {
84-
return ecrRepository;
111+
public Cluster getEcsCluster() {
112+
return ecsCluster;
85113
}
86114

87115
public CfnExpressGatewayService getExpressService() {
@@ -94,59 +122,38 @@ public static class EcsExpressServiceProps {
94122
private final IVpc vpc;
95123
private final Database database;
96124
private final Unicorn unicorn;
125+
private final IDependable dependsOn;
97126

98127
private EcsExpressServiceProps(Builder builder) {
99128
this.appName = builder.appName;
100129
this.vpc = builder.vpc;
101130
this.database = builder.database;
102131
this.unicorn = builder.unicorn;
132+
this.dependsOn = builder.dependsOn;
103133
}
104134

105135
public static Builder builder() {
106136
return new Builder();
107137
}
108138

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-
}
139+
public String getAppName() { return appName; }
140+
public IVpc getVpc() { return vpc; }
141+
public Database getDatabase() { return database; }
142+
public Unicorn getUnicorn() { return unicorn; }
143+
public IDependable getDependsOn() { return dependsOn; }
124144

125145
public static class Builder {
126146
private String appName;
127147
private IVpc vpc;
128148
private Database database;
129149
private Unicorn unicorn;
150+
private IDependable dependsOn;
130151

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-
}
152+
public Builder appName(String appName) { this.appName = appName; return this; }
153+
public Builder vpc(IVpc vpc) { this.vpc = vpc; return this; }
154+
public Builder database(Database database) { this.database = database; return this; }
155+
public Builder unicorn(Unicorn unicorn) { this.unicorn = unicorn; return this; }
156+
public Builder dependsOn(IDependable dependsOn) { this.dependsOn = dependsOn; return this; }
150157

151158
public EcsExpressServiceProps build() {
152159
return new EcsExpressServiceProps(this);

infra/cfn/base-stack.yaml

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -676,18 +676,6 @@ Resources:
676676
Fn::GetAtt:
677677
- IdeInstanceLauncherFunction803C5A2A
678678
- Arn
679-
InstanceName: ide
680-
IamInstanceProfileArn:
681-
Fn::GetAtt:
682-
- IdeInstanceProfile61B92038
683-
- Arn
684-
VolumeSize: "50"
685-
SubnetIds:
686-
Fn::Join:
687-
- ""
688-
- - Ref: VpcPublicSubnet1Subnet8E8DEDC0
689-
- ","
690-
- Ref: VpcPublicSubnet2SubnetA811849C
691679
SecurityGroupIds:
692680
Fn::Join:
693681
- ""
@@ -698,8 +686,19 @@ Resources:
698686
- Fn::GetAtt:
699687
- IdeInternalSecurityGroupB0A5D76B
700688
- GroupId
701-
ImageId:
702-
Ref: SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61x8664C96584B6F00A464EAD1953AFF4B05118Parameter
689+
SubnetIds:
690+
Fn::Join:
691+
- ""
692+
- - Ref: VpcPublicSubnet1Subnet8E8DEDC0
693+
- ","
694+
- Ref: VpcPublicSubnet2SubnetA811849C
695+
VolumeSize: "50"
696+
IamInstanceProfileArn:
697+
Fn::GetAtt:
698+
- IdeInstanceProfile61B92038
699+
- Arn
700+
InstanceName: ide
701+
InstanceTypes: m6a.xlarge,m7a.xlarge
703702
UserData:
704703
Fn::Base64:
705704
Fn::Join:
@@ -836,7 +835,8 @@ Resources:
836835
"
837836
exit 1
838837
fi
839-
InstanceTypes: m6a.xlarge,m7a.xlarge
838+
ImageId:
839+
Ref: SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61x8664C96584B6F00A464EAD1953AFF4B05118Parameter
840840
UpdateReplacePolicy: Delete
841841
DeletionPolicy: Delete
842842
IdeEipAssociationDFF81215:

0 commit comments

Comments
 (0)