@deployfoundation/foundation-deploy 0.2.11 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/app.js CHANGED
@@ -7,12 +7,12 @@ import {
7
7
  FoundationNetwork,
8
8
  FoundationPipeline,
9
9
  NewsletterStack
10
- } from "../chunk-sxvts8bf.js";
10
+ } from "../chunk-83781ggk.js";
11
11
  import {
12
12
  adminsFor,
13
13
  instanceNames,
14
14
  loadInstanceFile
15
- } from "../chunk-n8qzcdq4.js";
15
+ } from "../chunk-7qn00ny1.js";
16
16
 
17
17
  // bin/app.ts
18
18
  import { execFileSync } from "node:child_process";
@@ -29,7 +29,7 @@ import {
29
29
  stackExists,
30
30
  stackOutput,
31
31
  toolVersion
32
- } from "../chunk-2kqazxx6.js";
32
+ } from "../chunk-e9vs85ah.js";
33
33
  import {
34
34
  BASE_SLACK_EVENTS,
35
35
  CAPABILITY_IDS,
@@ -45,7 +45,7 @@ import {
45
45
  requiredSlackScopes,
46
46
  skillsKey,
47
47
  slackCommandPrefix
48
- } from "../chunk-n8qzcdq4.js";
48
+ } from "../chunk-7qn00ny1.js";
49
49
 
50
50
  // src/deploy/config-sync.ts
51
51
  import { existsSync, mkdirSync, mkdtempSync, readdirSync } from "node:fs";
@@ -1522,8 +1522,7 @@ function spansIngestionPolicy(account, region) {
1522
1522
  Version: "2012-10-17",
1523
1523
  Statement: [
1524
1524
  statement("SpansFromXray", "aws/spans"),
1525
- statement("ApplicationSignalsFromXray", "/aws/application-signals/data"),
1526
- statement("AgentCoreRuntimeSpansFromXray", "/aws/bedrock-agentcore/runtimes/*")
1525
+ statement("ApplicationSignalsFromXray", "/aws/application-signals/data")
1527
1526
  ]
1528
1527
  });
1529
1528
  }
@@ -1538,7 +1537,7 @@ function hasSpansIngestionPolicy(describeOutput) {
1538
1537
  return false;
1539
1538
  return policies.some((p) => {
1540
1539
  const doc = String(p.policyDocument ?? "");
1541
- return doc.includes("aws/spans") && doc.includes("logs:CreateLogStream") && doc.includes("/aws/bedrock-agentcore/runtimes/");
1540
+ return doc.includes("aws/spans") && doc.includes("logs:CreateLogStream");
1542
1541
  });
1543
1542
  }
1544
1543
  function parseDestination(text) {
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import {
3
3
  DEFAULT_RELEASE_BUCKET
4
- } from "../chunk-2kqazxx6.js";
5
- import"../chunk-n8qzcdq4.js";
4
+ } from "../chunk-e9vs85ah.js";
5
+ import"../chunk-7qn00ny1.js";
6
6
 
7
7
  // bin/release-account.ts
8
8
  import * as cdk2 from "aws-cdk-lib";
@@ -572,6 +572,14 @@ var RawProactive = z2.object({
572
572
  channels: z2.array(z2.string().regex(/^[CG][A-Z0-9]{8,}$/)).default([]),
573
573
  maxPerHour: z2.number().int().min(1).max(500).default(20)
574
574
  }).strict().default({ channels: [] });
575
+ var RawRuntime = z2.object({
576
+ idleTimeoutMinutes: z2.number().int().min(5).max(480).default(15),
577
+ maxLifetimeHours: z2.number().int().min(1).max(8).default(8)
578
+ }).strict().default({});
579
+ var RawNetwork = z2.object({
580
+ natGateways: z2.union([z2.literal(1), z2.literal(2)]).default(1),
581
+ endpointAzs: z2.union([z2.literal(1), z2.literal(2)]).default(1)
582
+ }).strict().default({});
575
583
  var RawNaming = z2.object({
576
584
  prefix: z2.string().min(1).max(32).regex(/^[A-Z][A-Za-z0-9]*$/),
577
585
  secretPrefix: z2.string().min(1).max(40).regex(/^[a-z][a-z0-9-]*$/),
@@ -711,6 +719,8 @@ var RawInstance = z2.object({
711
719
  license: RawLicense.optional(),
712
720
  config: z2.string().min(1),
713
721
  proactive: RawProactive,
722
+ runtime: RawRuntime,
723
+ network: RawNetwork,
714
724
  integrations: RawIntegrations,
715
725
  newsletter: RawNewsletter
716
726
  }).strict().superRefine((doc, ctx) => {
@@ -19,12 +19,13 @@ import {
19
19
  scheduledJobBindings,
20
20
  slackCommandPrefixes,
21
21
  webhookJobBindings
22
- } from "./chunk-n8qzcdq4.js";
22
+ } from "./chunk-7qn00ny1.js";
23
23
 
24
24
  // src/stacks/agent-stack.ts
25
25
  import * as cdk from "aws-cdk-lib";
26
26
  import * as ecr from "aws-cdk-lib/aws-ecr";
27
27
  import * as iam from "aws-cdk-lib/aws-iam";
28
+ import * as logs from "aws-cdk-lib/aws-logs";
28
29
  class FoundationAgent extends cdk.Stack {
29
30
  repository;
30
31
  executionRole;
@@ -67,6 +68,7 @@ class FoundationAgent extends cdk.Stack {
67
68
  this.accessPoint = accessPoint;
68
69
  this.executionRole = this.buildExecutionRole(props);
69
70
  this.readOnlyRole = this.buildReadOnlyRole(props);
71
+ this.buildSpansIngestionPolicy();
70
72
  const gateway = this.buildToolGateway(display);
71
73
  this.webSearchUrl = gateway.getAtt("GatewayUrl").toString();
72
74
  this.executionRole.addToPolicy(new iam.PolicyStatement({
@@ -118,8 +120,8 @@ class FoundationAgent extends cdk.Stack {
118
120
  }
119
121
  ],
120
122
  LifecycleConfiguration: {
121
- IdleRuntimeSessionTimeout: 3600,
122
- MaxLifetime: 28800
123
+ IdleRuntimeSessionTimeout: props.instance.runtime.idleTimeoutMinutes * 60,
124
+ MaxLifetime: props.instance.runtime.maxLifetimeHours * 3600
123
125
  },
124
126
  RoleArn: this.executionRole.roleArn,
125
127
  EnvironmentVariables: {
@@ -318,6 +320,27 @@ class FoundationAgent extends cdk.Stack {
318
320
  }));
319
321
  return role;
320
322
  }
323
+ buildSpansIngestionPolicy() {
324
+ return new logs.ResourcePolicy(this, "AgentSpansIngestion", {
325
+ resourcePolicyName: `${this.stackName}-AgentSpansFromXray`,
326
+ policyStatements: [
327
+ new iam.PolicyStatement({
328
+ sid: "AgentSpansFromXray",
329
+ principals: [new iam.ServicePrincipal("xray.amazonaws.com")],
330
+ actions: ["logs:PutLogEvents", "logs:CreateLogStream"],
331
+ resources: [
332
+ `arn:${this.partition}:logs:${this.region}:${this.account}:log-group:/aws/bedrock-agentcore/runtimes/${this.names.runtimeName}-*:*`
333
+ ],
334
+ conditions: {
335
+ StringEquals: { "aws:SourceAccount": this.account },
336
+ ArnLike: {
337
+ "aws:SourceArn": `arn:${this.partition}:xray:${this.region}:${this.account}:*`
338
+ }
339
+ }
340
+ })
341
+ ]
342
+ });
343
+ }
321
344
  buildExecutionRole(props) {
322
345
  const role = new iam.Role(this, "AgentExecutionRole", {
323
346
  assumedBy: new iam.ServicePrincipal("bedrock-agentcore.amazonaws.com"),
@@ -2043,7 +2066,7 @@ class FoundationNetwork extends cdk5.Stack {
2043
2066
  } else {
2044
2067
  this.vpc = new ec2.Vpc(this, "FoundationVpc", {
2045
2068
  maxAzs: 2,
2046
- natGateways: 2,
2069
+ natGateways: props.instance.network.natGateways,
2047
2070
  subnetConfiguration: [
2048
2071
  { name: "ingress", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
2049
2072
  { name: "egress", subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, cidrMask: 24 },
@@ -2088,7 +2111,12 @@ class FoundationNetwork extends cdk5.Stack {
2088
2111
  });
2089
2112
  endpointSg.addIngressRule(ec2.Peer.ipv4(this.vpc.vpcCidrBlock), ec2.Port.tcp(443), "HTTPS from VPC");
2090
2113
  this.endpointSecurityGroup = endpointSg;
2091
- const endpointSubnets = importedVpcId ? { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS } : { subnetGroupName: "app" };
2114
+ const endpointSubnets = importedVpcId ? { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, onePerAz: true } : { subnetGroupName: "app", onePerAz: true };
2115
+ if (props.instance.network.endpointAzs === 1) {
2116
+ const zones = this.vpc.selectSubnets(endpointSubnets).availabilityZones;
2117
+ if (zones.length > 1)
2118
+ Object.assign(endpointSubnets, { availabilityZones: [zones[0]] });
2119
+ }
2092
2120
  const gatewayEndpointSubnets = importedVpcId ? [{ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }] : [{ subnetGroupName: "app" }, { subnetGroupName: "egress" }];
2093
2121
  const sameAccountOnly = new iam6.PolicyStatement({
2094
2122
  effect: iam6.Effect.ALLOW,
@@ -6,7 +6,7 @@ import {
6
6
  manifestKey,
7
7
  releaseCacheDir,
8
8
  verifyManifest
9
- } from "./chunk-n8qzcdq4.js";
9
+ } from "./chunk-7qn00ny1.js";
10
10
 
11
11
  // src/deploy/release.ts
12
12
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
package/dist/src/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  FoundationPipeline,
10
10
  NewsletterStack,
11
11
  deployStatements
12
- } from "../chunk-sxvts8bf.js";
12
+ } from "../chunk-83781ggk.js";
13
13
  import {
14
14
  BUN_VERSION,
15
15
  LAMBDA_ENTRY_POINTS,
@@ -42,7 +42,7 @@ import {
42
42
  skillsKey,
43
43
  skipBundle,
44
44
  verifyManifest
45
- } from "../chunk-n8qzcdq4.js";
45
+ } from "../chunk-7qn00ny1.js";
46
46
  export {
47
47
  BUILD_TIMEOUT,
48
48
  BUN_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deployfoundation/foundation-deploy",
3
- "version": "0.2.11",
3
+ "version": "0.2.14",
4
4
  "description": "Deploys one Foundation instance from a published, signed release: the CDK app and the deploy tool.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -82,11 +82,12 @@ export function spansIngestionPolicy(account: string, region: string): string {
82
82
  Statement: [
83
83
  statement("SpansFromXray", "aws/spans"),
84
84
  statement("ApplicationSignalsFromXray", "/aws/application-signals/data"),
85
- // The agent names its OWN runtime log group in `x-aws-log-group`, and
86
- // X-Ray delivers there too: without this, exports fail with "not
87
- // authorized to perform logs:PutLogEvents on /aws/bedrock-agentcore/
88
- // runtimes/<runtime>-live" (2026-09-07).
89
- statement("AgentCoreRuntimeSpansFromXray", "/aws/bedrock-agentcore/runtimes/*"),
85
+ // NOT the runtime's own log group, which is where the agent's spans
86
+ // actually go (`x-aws-log-group`): that grant is per instance and ships
87
+ // with every deploy as the agent stack's `AgentSpansIngestion` policy.
88
+ // Owning it here made it a one-time `setup` side effect, and an instance
89
+ // that reached its account by `deploy` alone never got it — every export
90
+ // a 400 (Olivia, 2026-09-26).
90
91
  ],
91
92
  });
92
93
  }
@@ -102,11 +103,7 @@ export function hasSpansIngestionPolicy(describeOutput: string): boolean {
102
103
  if (!Array.isArray(policies)) return false;
103
104
  return policies.some((p) => {
104
105
  const doc = String((p as { policyDocument?: unknown }).policyDocument ?? "");
105
- return (
106
- doc.includes("aws/spans") &&
107
- doc.includes("logs:CreateLogStream") &&
108
- doc.includes("/aws/bedrock-agentcore/runtimes/")
109
- );
106
+ return doc.includes("aws/spans") && doc.includes("logs:CreateLogStream");
110
107
  });
111
108
  }
112
109
 
@@ -4,6 +4,7 @@ import type * as ec2 from "aws-cdk-lib/aws-ec2";
4
4
  import * as ecr from "aws-cdk-lib/aws-ecr";
5
5
  import * as iam from "aws-cdk-lib/aws-iam";
6
6
  import type * as kms from "aws-cdk-lib/aws-kms";
7
+ import * as logs from "aws-cdk-lib/aws-logs";
7
8
  import type * as s3 from "aws-cdk-lib/aws-s3";
8
9
  import type * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
9
10
  import type { Construct } from "constructs";
@@ -147,6 +148,7 @@ export class FoundationAgent extends cdk.Stack {
147
148
  this.accessPoint = accessPoint;
148
149
  this.executionRole = this.buildExecutionRole(props);
149
150
  this.readOnlyRole = this.buildReadOnlyRole(props);
151
+ this.buildSpansIngestionPolicy();
150
152
 
151
153
  // The gateway is independent of the runtime: it can (and does) exist
152
154
  // during the phase-one bootstrap deploy.
@@ -227,9 +229,11 @@ export class FoundationAgent extends cdk.Stack {
227
229
  },
228
230
  },
229
231
  ],
232
+ // Idle sessions are billed for their memory: see `runtime` in the
233
+ // instance schema for why the default is 15 minutes, not 60.
230
234
  LifecycleConfiguration: {
231
- IdleRuntimeSessionTimeout: 3600,
232
- MaxLifetime: 28800,
235
+ IdleRuntimeSessionTimeout: props.instance.runtime.idleTimeoutMinutes * 60,
236
+ MaxLifetime: props.instance.runtime.maxLifetimeHours * 3600,
233
237
  },
234
238
  RoleArn: this.executionRole.roleArn,
235
239
  EnvironmentVariables: {
@@ -521,6 +525,48 @@ export class FoundationAgent extends cdk.Stack {
521
525
  return role;
522
526
  }
523
527
 
528
+ /**
529
+ * Let X-Ray write this runtime's spans into the runtime's own log group.
530
+ *
531
+ * The agent posts OTLP to the X-Ray endpoint with `x-aws-log-group` naming
532
+ * `/aws/bedrock-agentcore/runtimes/<runtime>-<endpoint>` (see
533
+ * `packages/agent/src/observability/otel.ts`). X-Ray, not the execution
534
+ * role, then calls `PutLogEvents` — as a role in an AWS service account —
535
+ * so the execution role's `AgentLogs` grant does not apply, and without a
536
+ * CloudWatch Logs RESOURCE policy every export is a 400 "not authorized to
537
+ * perform: logs:PutLogEvents" (Olivia, 2026-09-26). AWS: "If you set
538
+ * OTEL_EXPORTER_OTLP_TRACES_HEADERS to deliver spans to your own log group,
539
+ * you must also add an Amazon CloudWatch Logs resource policy … allow X-Ray
540
+ * (xray.amazonaws.com) to call logs:PutLogEvents on that log group"
541
+ * (bedrock-agentcore devguide, observability-configure).
542
+ *
543
+ * Logs resource policies are account-level (ten per Region), so the name is
544
+ * the stack's own and the grant covers only this runtime's endpoint groups,
545
+ * and only when X-Ray acts for this account. `CreateLogStream` rides along
546
+ * because X-Ray creates the `spans` stream on first delivery.
547
+ */
548
+ private buildSpansIngestionPolicy(): logs.ResourcePolicy {
549
+ return new logs.ResourcePolicy(this, "AgentSpansIngestion", {
550
+ resourcePolicyName: `${this.stackName}-AgentSpansFromXray`,
551
+ policyStatements: [
552
+ new iam.PolicyStatement({
553
+ sid: "AgentSpansFromXray",
554
+ principals: [new iam.ServicePrincipal("xray.amazonaws.com")],
555
+ actions: ["logs:PutLogEvents", "logs:CreateLogStream"],
556
+ resources: [
557
+ `arn:${this.partition}:logs:${this.region}:${this.account}:log-group:/aws/bedrock-agentcore/runtimes/${this.names.runtimeName}-*:*`,
558
+ ],
559
+ conditions: {
560
+ StringEquals: { "aws:SourceAccount": this.account },
561
+ ArnLike: {
562
+ "aws:SourceArn": `arn:${this.partition}:xray:${this.region}:${this.account}:*`,
563
+ },
564
+ },
565
+ }),
566
+ ],
567
+ });
568
+ }
569
+
524
570
  private buildExecutionRole(props: FoundationAgentProps): iam.Role {
525
571
  const role = new iam.Role(this, "AgentExecutionRole", {
526
572
  assumedBy: new iam.ServicePrincipal("bedrock-agentcore.amazonaws.com"),
@@ -572,9 +618,10 @@ export class FoundationAgent extends cdk.Stack {
572
618
  ],
573
619
  }),
574
620
  );
575
- // Spans: the runtime SigV4-posts OTLP to the X-Ray endpoint, which routes
576
- // them into the log group `AgentLogs` above already covers. None of these
577
- // actions take a resource ARN — X-Ray scopes ingestion by account.
621
+ // Spans: the runtime SigV4-posts OTLP to the X-Ray endpoint. These actions
622
+ // only let it call X-Ray (none takes a resource ARN — X-Ray scopes
623
+ // ingestion by account); X-Ray's own write into the runtime log group is
624
+ // authorized by `AgentSpansIngestion`, not by `AgentLogs` above.
578
625
  role.addToPolicy(
579
626
  new iam.PolicyStatement({
580
627
  sid: "TraceIngest",
@@ -70,9 +70,9 @@ export class FoundationNetwork extends cdk.Stack {
70
70
  } else {
71
71
  this.vpc = new ec2.Vpc(this, "FoundationVpc", {
72
72
  maxAzs: 2,
73
- // One NAT per AZ: the Slack ack / chat.update path must survive a
74
- // single-AZ failure.
75
- natGateways: 2,
73
+ // One NAT by default: a second (AZ-failure tolerance for egress) is
74
+ // about $33/month, opted into with `network.natGateways: 2`.
75
+ natGateways: props.instance.network.natGateways,
76
76
  subnetConfiguration: [
77
77
  { name: "ingress", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
78
78
  { name: "egress", subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, cidrMask: 24 },
@@ -82,7 +82,7 @@ export class FoundationNetwork extends cdk.Stack {
82
82
  this.egressSubnets = { subnetGroupName: "egress" };
83
83
  // The addresses everything in the VPC leaves through, so a third party
84
84
  // (an Atlas IP allowlist, say) can name this instance and no other.
85
- // One per AZ, matching `natGateways: 2` above.
85
+ // One per NAT gateway (`network.natGateways`).
86
86
  new cdk.CfnOutput(this, "NatEgressIp", {
87
87
  value: cdk.Fn.join(
88
88
  ",",
@@ -191,9 +191,16 @@ export class FoundationNetwork extends cdk.Stack {
191
191
  // Interface endpoints sit in the isolated app tier when we created the
192
192
  // VPC; on an imported VPC they land in its private subnets. Private DNS
193
193
  // makes them authoritative for the whole VPC either way.
194
+ // One AZ by default (`network.endpointAzs`): the other AZ reaches the
195
+ // same endpoint through private DNS, for a little cross-AZ traffic
196
+ // instead of a second set of endpoint-hours.
194
197
  const endpointSubnets: ec2.SubnetSelection = importedVpcId
195
- ? { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }
196
- : { subnetGroupName: "app" };
198
+ ? { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, onePerAz: true }
199
+ : { subnetGroupName: "app", onePerAz: true };
200
+ if (props.instance.network.endpointAzs === 1) {
201
+ const zones = this.vpc.selectSubnets(endpointSubnets).availabilityZones;
202
+ if (zones.length > 1) Object.assign(endpointSubnets, { availabilityZones: [zones[0]] });
203
+ }
197
204
  // Gateway endpoints attach to every private route table (app + egress) so
198
205
  // S3/DynamoDB traffic from the NAT tier also stays on AWS's network.
199
206
  const gatewayEndpointSubnets: ec2.SubnetSelection[] = importedVpcId