Skip to content

Commit d6d6504

Browse files
authored
support AgentCore runtime infrastructure (#317)
*Issue #, if available:* *Description of changes:* Currently we do not support invoke AgentCore Runtime agent from web UI. This PR adds only infrastructure to support it in the future. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 0b36db5 commit d6d6504

File tree

26 files changed

+3671
-1551
lines changed

26 files changed

+3671
-1551
lines changed

cdk/bin/cdk.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const parseCommaSeparatedList = (envVar: string | undefined): string[] | undefin
2121
const allowedIpV4AddressRanges = parseCommaSeparatedList(process.env.ALLOWED_IPV4_CIDRS);
2222
const allowedIpV6AddressRanges = parseCommaSeparatedList(process.env.ALLOWED_IPV6_CIDRS);
2323
const allowedCountryCodes = parseCommaSeparatedList(process.env.ALLOWED_COUNTRY_CODES);
24+
const enableAgentCore = process.env.ENABLE_AGENT_CORE == 'true';
2425

2526
const virginia = new UsEast1Stack(app, `RemoteSweUsEast1Stack-${targetEnv}`, {
2627
env: {
@@ -31,6 +32,7 @@ const virginia = new UsEast1Stack(app, `RemoteSweUsEast1Stack-${targetEnv}`, {
3132
allowedIpV4AddressRanges,
3233
allowedIpV6AddressRanges,
3334
allowedCountryCodes,
35+
enableAgentCore,
3436
});
3537

3638
const additionalPolicies = parseCommaSeparatedList(process.env.WORKER_ADDITIONAL_POLICIES);
@@ -75,5 +77,6 @@ const props: MainStackProps = {
7577

7678
new MainStack(app, `RemoteSweStack-${targetEnv}`, {
7779
...props,
80+
agentCoreRepository: virginia.agentCoreRepository,
7881
});
7982
// cdk.Aspects.of(app).add(new AwsSolutionsChecks());

cdk/lib/cdk-stack.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { HostedZone } from 'aws-cdk-lib/aws-route53';
1414
import { Auth } from './constructs/auth';
1515
import { AsyncJob } from './constructs/async-job';
1616
import { Webapp } from './constructs/webapp';
17+
import { IRepository } from 'aws-cdk-lib/aws-ecr';
1718

1819
export interface MainStackProps extends cdk.StackProps {
1920
readonly signPayloadHandler: EdgeFunction;
@@ -43,6 +44,7 @@ export interface MainStackProps extends cdk.StackProps {
4344
readonly workerAmiIdParameterName: string;
4445
readonly additionalManagedPolicies?: string[];
4546
readonly initialWebappUserEmail?: string;
47+
readonly agentCoreRepository?: IRepository;
4648
}
4749

4850
export class MainStack extends cdk.Stack {
@@ -128,6 +130,7 @@ export class MainStack extends cdk.Stack {
128130
amiIdParameterName: workerAmiIdParameter.parameterName,
129131
webappOriginSourceParameter: originNameParameter,
130132
additionalManagedPolicies: props.additionalManagedPolicies,
133+
agentCoreRepository: props.agentCoreRepository,
131134
});
132135

133136
const auth = new Auth(this, 'Auth', {
@@ -154,6 +157,7 @@ export class MainStack extends cdk.Stack {
154157
asyncJob,
155158
workerAmiIdParameter,
156159
originNameParameter,
160+
agentCoreRuntimeArn: worker.agentCoreRuntimeArn,
157161
});
158162

159163
new SlackBolt(this, 'SlackBolt', {

cdk/lib/constructs/webapp.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface WebappProps {
3030
workerBus: WorkerBus;
3131
workerAmiIdParameter: IStringParameter;
3232
originNameParameter: IStringParameter;
33+
agentCoreRuntimeArn?: string;
3334

3435
hostedZone?: IHostedZone;
3536
certificate?: ICertificate;
@@ -85,6 +86,7 @@ export class Webapp extends Construct {
8586
EVENT_HTTP_ENDPOINT: props.workerBus.httpEndpoint,
8687
TABLE_NAME: storage.table.tableName,
8788
BUCKET_NAME: storage.bucket.bucketName,
89+
AGENT_RUNTIME_ARN: props.agentCoreRuntimeArn ?? '',
8890
},
8991
memorySize: 1769,
9092
architecture: Architecture.ARM_64,
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha';
2+
import { CfnOutput, CustomResource, Duration, Stack } from 'aws-cdk-lib';
3+
import { ITableV2 } from 'aws-cdk-lib/aws-dynamodb';
4+
import { IRepository, Repository } from 'aws-cdk-lib/aws-ecr';
5+
import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets';
6+
import { IGrantable, IPrincipal, ManagedPolicy, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
7+
import { Runtime } from 'aws-cdk-lib/aws-lambda';
8+
import { IBucket } from 'aws-cdk-lib/aws-s3';
9+
import { IStringParameter } from 'aws-cdk-lib/aws-ssm';
10+
import { Construct } from 'constructs';
11+
import { ContainerImageBuild } from 'deploy-time-build';
12+
import { readFileSync } from 'fs';
13+
import { join } from 'path';
14+
import { WorkerBus } from './bus';
15+
import { DockerImageName, ECRDeployment } from 'cdk-ecr-deployment';
16+
17+
export interface AgentCoreRuntimeProps {
18+
repository: IRepository;
19+
storageTable: ITableV2;
20+
imageBucket: IBucket;
21+
bus: WorkerBus;
22+
slackBotTokenParameter: IStringParameter;
23+
gitHubApp?: {
24+
privateKeyParameterName: string;
25+
appId: string;
26+
installationId: string;
27+
};
28+
gitHubAppPrivateKeyParameter?: IStringParameter;
29+
githubPersonalAccessTokenParameter?: IStringParameter;
30+
loadBalancing?: {
31+
awsAccounts: string[];
32+
roleName: string;
33+
};
34+
accessLogBucket: IBucket;
35+
amiIdParameterName: string;
36+
webappOriginSourceParameter: IStringParameter;
37+
}
38+
39+
export class AgentCoreRuntime extends Construct implements IGrantable {
40+
public grantPrincipal: IPrincipal;
41+
public runtimeArn: string;
42+
43+
constructor(scope: Construct, id: string, props: AgentCoreRuntimeProps) {
44+
super(scope, id);
45+
46+
const repository = props.repository;
47+
48+
const crHandler = new PythonFunction(this, 'CustomResourceHandler', {
49+
runtime: Runtime.PYTHON_3_13,
50+
timeout: Duration.seconds(10),
51+
entry: join(__dirname, 'resources', 'agent-core-runtime-cr'),
52+
});
53+
crHandler.role!.addToPrincipalPolicy(
54+
new PolicyStatement({
55+
actions: [
56+
'bedrock-agentcore:ListAgentRuntimes',
57+
'bedrock-agentcore:CreateAgentRuntime',
58+
'bedrock-agentcore:UpdateAgentRuntime',
59+
'bedrock-agentcore:DeleteAgentRuntime',
60+
'iam:PassRole',
61+
],
62+
resources: ['*'],
63+
})
64+
);
65+
crHandler.role?.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('BedrockAgentCoreFullAccess'));
66+
67+
const role = new Role(this, 'Role', {
68+
assumedBy: ServicePrincipal.fromStaticServicePrincipleName('bedrock-agentcore.amazonaws.com'),
69+
});
70+
this.grantPrincipal = role;
71+
72+
const image = new DockerImageAsset(this, 'WorkerImage', {
73+
directory: '..',
74+
file: join('docker', 'agent.Dockerfile'),
75+
exclude: readFileSync('.dockerignore').toString().split('\n'),
76+
platform: Platform.LINUX_ARM64,
77+
});
78+
const deployDest = new DockerImageName(`${repository.repositoryUri}:${image.imageTag}`);
79+
const deploy = new ECRDeployment(this, 'ImageDeploy', {
80+
src: new DockerImageName(image.imageUri),
81+
dest: deployDest,
82+
imageArch: ['arm64'],
83+
});
84+
85+
role.addToPrincipalPolicy(
86+
new PolicyStatement({
87+
actions: ['ecr:BatchGetImage', 'ecr:GetDownloadUrlForLayer'],
88+
resources: [`${repository.repositoryArn}`],
89+
})
90+
);
91+
role.addToPrincipalPolicy(
92+
new PolicyStatement({
93+
actions: [
94+
'ecr:GetAuthorizationToken',
95+
'xray:PutTraceSegments',
96+
'xray:PutTelemetryRecords',
97+
'xray:GetSamplingRules',
98+
'xray:GetSamplingTargets',
99+
'cloudwatch:PutMetricData',
100+
'logs:DescribeLogStreams',
101+
'logs:DescribeLogGroups',
102+
'logs:CreateLogGroup',
103+
'logs:CreateLogStream',
104+
'logs:PutLogEvents',
105+
'bedrock-agentcore:GetWorkloadAccessToken',
106+
'bedrock-agentcore:GetWorkloadAccessTokenForJWT',
107+
'bedrock-agentcore:GetWorkloadAccessTokenForUserId',
108+
],
109+
resources: ['*'],
110+
})
111+
);
112+
role.addToPrincipalPolicy(
113+
new PolicyStatement({
114+
actions: ['bedrock:InvokeModel'],
115+
resources: ['*'],
116+
})
117+
);
118+
props.storageTable.grantReadWriteData(role);
119+
props.imageBucket.grantReadWrite(role);
120+
props.gitHubAppPrivateKeyParameter?.grantRead(role);
121+
props.githubPersonalAccessTokenParameter?.grantRead(role);
122+
props.slackBotTokenParameter.grantRead(role);
123+
props.bus.api.grantPublishAndSubscribe(role);
124+
props.bus.api.grantConnect(role);
125+
126+
const resource = new CustomResource(this, 'Resource', {
127+
serviceToken: crHandler.functionArn,
128+
resourceType: 'Custom::AgentCoreRuntime',
129+
properties: {
130+
ContainerUri: `${repository.repositoryUri}:${image.imageTag}`,
131+
RoleArn: role.roleArn,
132+
ServerProtocol: 'HTTP',
133+
Env: {
134+
AWS_REGION: Stack.of(this).region,
135+
WORKER_RUNTIME: 'agent-core',
136+
EVENT_HTTP_ENDPOINT: props.bus.httpEndpoint,
137+
GITHUB_APP_PRIVATE_KEY_PARAMETER_NAME: props.gitHubAppPrivateKeyParameter?.parameterName ?? '',
138+
GITHUB_APP_ID: props.gitHubApp?.appId ?? '',
139+
GITHUB_APP_INSTALLATION_ID: props.gitHubApp?.installationId ?? '',
140+
TABLE_NAME: props.storageTable.tableName,
141+
BUCKET_NAME: props.imageBucket.bucketName,
142+
WEBAPP_ORIGIN_NAME_PARAMETER: props.webappOriginSourceParameter.parameterName,
143+
// BEDROCK_AWS_ACCOUNTS: props.loadBalancing?.awsAccounts.join(',') ?? '',
144+
// BEDROCK_AWS_ROLE_NAME: props.loadBalancing?.roleName ?? '',
145+
SLACK_BOT_TOKEN_PARAMETER_NAME: props.slackBotTokenParameter.parameterName ?? '',
146+
GITHUB_PERSONAL_ACCESS_TOKEN_PARAMETER_NAME: props.githubPersonalAccessTokenParameter?.parameterName ?? '',
147+
},
148+
},
149+
serviceTimeout: Duration.seconds(20),
150+
});
151+
resource.node.addDependency(deploy.node.findChild('CustomResource'));
152+
153+
this.runtimeArn = resource.getAttString('agentRuntimeArn');
154+
new CfnOutput(this, 'RuntimeArn', { value: this.runtimeArn });
155+
}
156+
}

cdk/lib/constructs/worker/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import * as logs from 'aws-cdk-lib/aws-logs';
1212
import { WorkerImageBuilder } from './image-builder';
1313
import { readFileSync } from 'fs';
1414
import { Asset, AssetProps } from 'aws-cdk-lib/aws-s3-assets';
15+
import { AgentCoreRuntime } from './agent-core-runtime';
16+
import { IRepository } from 'aws-cdk-lib/aws-ecr';
1517

1618
export interface WorkerProps {
1719
vpc: ec2.IVpc;
@@ -32,13 +34,15 @@ export interface WorkerProps {
3234
amiIdParameterName: string;
3335
webappOriginSourceParameter: IStringParameter;
3436
additionalManagedPolicies?: string[];
37+
agentCoreRepository?: IRepository;
3538
}
3639

3740
export class Worker extends Construct {
3841
public readonly launchTemplate: ec2.LaunchTemplate;
3942
public readonly bus: WorkerBus;
4043
public readonly logGroup: logs.LogGroup;
4144
public readonly imageBuilder: WorkerImageBuilder;
45+
public readonly agentCoreRuntimeArn?: string;
4246

4347
constructor(scope: Construct, id: string, props: WorkerProps) {
4448
super(scope, id);
@@ -343,6 +347,7 @@ StandardOutput=journal
343347
StandardError=journal
344348
SyslogIdentifier=myapp
345349
# Static environment variables
350+
Environment=WORKER_RUNTIME=ec2
346351
Environment=AWS_REGION=${Stack.of(this).region}
347352
Environment=EVENT_HTTP_ENDPOINT=${bus.httpEndpoint}
348353
Environment=GITHUB_APP_PRIVATE_KEY_PATH=${privateKey ? '/opt/private-key.pem' : ''}
@@ -445,6 +450,24 @@ systemctl start myapp
445450
sourceAssetHash,
446451
});
447452

453+
if (props.agentCoreRepository) {
454+
const agentCoreRuntime = new AgentCoreRuntime(this, 'AgentCore', {
455+
repository: props.agentCoreRepository,
456+
storageTable: props.storageTable,
457+
imageBucket: props.imageBucket,
458+
bus: bus,
459+
slackBotTokenParameter: props.slackBotTokenParameter,
460+
gitHubApp: props.gitHubApp,
461+
gitHubAppPrivateKeyParameter: privateKey,
462+
githubPersonalAccessTokenParameter: props.githubPersonalAccessTokenParameter,
463+
loadBalancing: props.loadBalancing,
464+
accessLogBucket: props.accessLogBucket,
465+
amiIdParameterName: props.amiIdParameterName,
466+
webappOriginSourceParameter: props.webappOriginSourceParameter,
467+
});
468+
this.agentCoreRuntimeArn = agentCoreRuntime.runtimeArn;
469+
}
470+
448471
props.webappOriginSourceParameter.grantRead(role);
449472
role.addToPrincipalPolicy(
450473
new iam.PolicyStatement({

0 commit comments

Comments
 (0)