Skip to content

Commit dea7feb

Browse files
committed
Deploy VPC environment with AgentCore and AgentBuilder enabled
1 parent 8298e9f commit dea7feb

File tree

6 files changed

+718
-19
lines changed

6 files changed

+718
-19
lines changed

packages/cdk/cdk.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
"createGenericAgentCoreRuntime": false,
6969
"agentCoreRegion": null,
7070
"agentCoreExternalRuntimes": [],
71+
"agentCoreNetworkType": "PUBLIC",
72+
"agentCoreVpcId": null,
73+
"agentCoreSubnetIds": null,
7174
"allowedIpV4AddressRanges": null,
7275
"allowedIpV6AddressRanges": null,
7376
"allowedCountryCodes": null,

packages/cdk/lib/agent-core-stack.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export class AgentCoreStack extends Stack {
2222
env: params.env,
2323
createGenericRuntime: params.createGenericAgentCoreRuntime,
2424
createAgentBuilderRuntime: params.agentBuilderEnabled,
25+
agentCoreNetworkType: params.agentCoreNetworkType,
26+
agentCoreVpcId: params.agentCoreVpcId,
27+
agentCoreSubnetIds: params.agentCoreSubnetIds,
2528
});
2629

2730
// Export runtime info for cross-region access via cdk-remote-stack (only if values exist)
@@ -40,6 +43,15 @@ export class AgentCoreStack extends Stack {
4043
});
4144
}
4245

46+
// Output retained security group ID for manual cleanup
47+
if (this.genericAgentCore.retainedSecurityGroupId) {
48+
new CfnOutput(this, 'RetainedSecurityGroupId', {
49+
value: this.genericAgentCore.retainedSecurityGroupId,
50+
description:
51+
'MANUAL CLEANUP REQUIRED: Security Group ID to delete after AgentCore ENI cleanup (check tags: ManualCleanupRequired=true)',
52+
});
53+
}
54+
4355
if (
4456
params.agentBuilderEnabled &&
4557
this.genericAgentCore.deployedAgentBuilderRuntimeArn

packages/cdk/lib/construct/generic-agent-core.ts

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import {
55
Role,
66
ServicePrincipal,
77
} from 'aws-cdk-lib/aws-iam';
8-
import { Stack, RemovalPolicy } from 'aws-cdk-lib';
8+
import { Stack, RemovalPolicy, Tags } from 'aws-cdk-lib';
99
import {
1010
Bucket,
1111
BlockPublicAccess,
1212
BucketEncryption,
1313
} from 'aws-cdk-lib/aws-s3';
14+
import { Subnet, Vpc, SecurityGroup, IVpc, ISubnet } from 'aws-cdk-lib/aws-ec2';
1415
import {
1516
Runtime,
1617
RuntimeNetworkConfiguration,
@@ -37,6 +38,10 @@ export interface GenericAgentCoreProps {
3738
env: string;
3839
createGenericRuntime?: boolean;
3940
createAgentBuilderRuntime?: boolean;
41+
agentCoreNetworkType: 'PUBLIC' | 'PRIVATE';
42+
agentCoreVpcId?: string | null;
43+
agentCoreSubnetIds?: string[] | null;
44+
agentCoreEnvironmentVariables?: Record<string, string>;
4045
}
4146

4247
interface RuntimeResources {
@@ -51,13 +56,20 @@ export class GenericAgentCore extends Construct {
5156
private readonly agentBuilderRuntimeConfig: AgentCoreRuntimeConfig;
5257
private readonly resources: RuntimeResources;
5358

59+
// Security Group ID that requires manual cleanup after AgentCore Runtime deletion
60+
// Used for CloudFormation Output to remind users of manual cleanup tasks
61+
public readonly retainedSecurityGroupId?: string;
62+
5463
constructor(scope: Construct, id: string, props: GenericAgentCoreProps) {
5564
super(scope, id);
5665

5766
const {
5867
env,
5968
createGenericRuntime = false,
6069
createAgentBuilderRuntime = false,
70+
agentCoreNetworkType = 'PUBLIC',
71+
agentCoreVpcId = null,
72+
agentCoreSubnetIds = null,
6173
} = props;
6274

6375
// Create bucket first
@@ -68,10 +80,56 @@ export class GenericAgentCore extends Construct {
6880
this.genericRuntimeConfig = configs.generic;
6981
this.agentBuilderRuntimeConfig = configs.agentBuilder;
7082

83+
// Create security group if VPC mode
84+
let securityGroup: SecurityGroup | undefined;
85+
let vpc: IVpc | undefined;
86+
let subnets: ISubnet[] | undefined;
87+
88+
if (
89+
agentCoreNetworkType === 'PRIVATE' &&
90+
agentCoreVpcId &&
91+
agentCoreSubnetIds
92+
) {
93+
vpc = Vpc.fromLookup(this, 'AgentCoreVpc', { vpcId: agentCoreVpcId });
94+
subnets = agentCoreSubnetIds.map((subnetId, index) =>
95+
Subnet.fromSubnetId(this, `AgentCoreSubnet${index}`, subnetId)
96+
);
97+
securityGroup = new SecurityGroup(this, 'AgentCoreSecurityGroup', {
98+
vpc,
99+
description: 'Security group for AgentCore Runtime',
100+
allowAllOutbound: true,
101+
});
102+
103+
// Add tags for manual cleanup identification
104+
Tags.of(securityGroup).add('ManualCleanupRequired', 'true');
105+
Tags.of(securityGroup).add(
106+
'CleanupReason',
107+
'AgentCore-Managed-ENI-Dependency'
108+
);
109+
Tags.of(securityGroup).add('CreatedBy', `GenU-${env}`);
110+
111+
// Retain security group to prevent deletion errors when changing PRIVATE->PUBLIC or removing AgentCore
112+
// AgentCore Runtime creates managed ENIs that reference this security group
113+
// CloudFormation cannot delete the SG while managed ENIs are using it (even though they're not manually deletable)
114+
// The managed ENIs are automatically cleaned up after AgentCore Runtime deletion, but with a time delay
115+
// Therefore, this SG must be retained and manually deleted after the managed ENIs are cleaned up
116+
//
117+
// Note: Custom Resource with ENI monitoring could solve this, but deletion can take up to 1 hour
118+
// Since security groups incur no cost, RETAIN is the practical solution for better user experience
119+
securityGroup.applyRemovalPolicy(RemovalPolicy.RETAIN);
120+
121+
// Store SG ID for output
122+
this.retainedSecurityGroupId = securityGroup.securityGroupId;
123+
}
124+
71125
// Create all resources atomically
72126
this.resources = this.createResources(
73127
createGenericRuntime,
74-
createAgentBuilderRuntime
128+
createAgentBuilderRuntime,
129+
agentCoreNetworkType,
130+
vpc,
131+
subnets,
132+
securityGroup
75133
);
76134
}
77135

@@ -125,7 +183,11 @@ export class GenericAgentCore extends Construct {
125183

126184
private createResources(
127185
createGeneric: boolean,
128-
createAgentBuilder: boolean
186+
createAgentBuilder: boolean,
187+
agentCoreNetworkType: 'PUBLIC' | 'PRIVATE',
188+
vpc?: IVpc,
189+
subnets?: ISubnet[],
190+
securityGroup?: SecurityGroup
129191
): RuntimeResources {
130192
if (!createGeneric && !createAgentBuilder) {
131193
return { role: this.createExecutionRole() };
@@ -138,15 +200,23 @@ export class GenericAgentCore extends Construct {
138200
resources.genericRuntime = this.createRuntime(
139201
'Generic',
140202
this.genericRuntimeConfig,
141-
role
203+
role,
204+
agentCoreNetworkType,
205+
vpc,
206+
subnets,
207+
securityGroup
142208
);
143209
}
144210

145211
if (createAgentBuilder) {
146212
resources.agentBuilderRuntime = this.createRuntime(
147213
'AgentBuilder',
148214
this.agentBuilderRuntimeConfig,
149-
role
215+
role,
216+
agentCoreNetworkType,
217+
vpc,
218+
subnets,
219+
securityGroup
150220
);
151221
}
152222

@@ -157,20 +227,54 @@ export class GenericAgentCore extends Construct {
157227
private createRuntime(
158228
type: string,
159229
config: AgentCoreRuntimeConfig,
160-
role: Role
230+
role: Role,
231+
agentCoreNetworkType: 'PUBLIC' | 'PRIVATE',
232+
vpc?: IVpc,
233+
subnets?: ISubnet[],
234+
securityGroup?: SecurityGroup
161235
): Runtime {
236+
const networkConfig = this.createNetworkConfiguration(
237+
agentCoreNetworkType,
238+
vpc,
239+
subnets,
240+
securityGroup
241+
);
242+
162243
return new Runtime(this, `${type}AgentCoreRuntime`, {
163244
runtimeName: config.name,
164245
agentRuntimeArtifact: AgentRuntimeArtifact.fromAsset(
165246
path.join(__dirname, `../../${config.dockerPath}`)
166247
),
167248
executionRole: role,
168-
networkConfiguration: RuntimeNetworkConfiguration.usingPublicNetwork(),
249+
networkConfiguration: networkConfig,
169250
protocolConfiguration: ProtocolType.HTTP,
170251
environmentVariables: config.environmentVariables,
171252
});
172253
}
173254

255+
private createNetworkConfiguration(
256+
agentCoreNetworkType: 'PUBLIC' | 'PRIVATE',
257+
vpc?: IVpc,
258+
subnets?: ISubnet[],
259+
securityGroup?: SecurityGroup
260+
): RuntimeNetworkConfiguration {
261+
if (agentCoreNetworkType === 'PRIVATE') {
262+
if (!vpc || !subnets) {
263+
throw new Error(
264+
'VPC and Subnets are required for PRIVATE network type'
265+
);
266+
}
267+
268+
return RuntimeNetworkConfiguration.usingVpc(this, {
269+
vpc,
270+
vpcSubnets: { subnets },
271+
securityGroups: securityGroup ? [securityGroup] : undefined,
272+
});
273+
} else {
274+
return RuntimeNetworkConfiguration.usingPublicNetwork();
275+
}
276+
}
277+
174278
private createExecutionRole(): Role {
175279
const region = Stack.of(this).region;
176280
const accountId = Stack.of(this).account;

packages/cdk/lib/stack-input.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ const baseStackInputSchema = z.object({
160160
})
161161
)
162162
.default([]),
163+
// Agent Core Network Configuration
164+
agentCoreNetworkType: z.enum(['PUBLIC', 'PRIVATE']).default('PUBLIC'),
165+
agentCoreVpcId: z.string().nullish(),
166+
agentCoreSubnetIds: z.array(z.string()).nullish(),
163167
// MCP
164168
mcpEnabled: z.boolean().default(false),
165169
// Guardrail
@@ -202,19 +206,38 @@ const baseStackInputSchema = z.object({
202206
});
203207

204208
// Common Validator with refine
205-
export const stackInputSchema = baseStackInputSchema.refine(
206-
(data) => {
207-
// If searchApiKey is provided, searchEngine must also be provided
208-
if (data.searchApiKey && !data.searchEngine) {
209-
return false;
209+
export const stackInputSchema = baseStackInputSchema
210+
.refine(
211+
(data) => {
212+
// If searchApiKey is provided, searchEngine must also be provided
213+
if (data.searchApiKey && !data.searchEngine) {
214+
return false;
215+
}
216+
return true;
217+
},
218+
{
219+
message: 'searchEngine is required when searchApiKey is provided',
220+
path: ['searchEngine'],
210221
}
211-
return true;
212-
},
213-
{
214-
message: 'searchEngine is required when searchApiKey is provided',
215-
path: ['searchEngine'],
216-
}
217-
);
222+
)
223+
.refine(
224+
(data) => {
225+
// Validate AgentCore network configuration
226+
if (data.agentCoreNetworkType === 'PRIVATE') {
227+
return (
228+
data.agentCoreVpcId &&
229+
data.agentCoreSubnetIds &&
230+
data.agentCoreSubnetIds.length > 0
231+
);
232+
}
233+
return true;
234+
},
235+
{
236+
message:
237+
'VPC ID and Subnet IDs are required when agentCoreNetworkType is PRIVATE',
238+
path: ['agentCoreNetworkType'],
239+
}
240+
);
218241

219242
// schema after conversion
220243
export const processedStackInputSchema = baseStackInputSchema.extend({

0 commit comments

Comments
 (0)