Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,24 +177,26 @@ on the next deployment.
}
```

| Field | Required | Description |
| ------------------------- | -------- | --------------------------------------------------------------- |
| `name` | Yes | Agent name (1-48 chars, alphanumeric + underscore) |
| `build` | Yes | `"CodeZip"` or `"Container"` |
| `entrypoint` | Yes | Entry file (e.g., `main.py` or `main.py:handler`) |
| `codeLocation` | Yes | Directory containing agent code |
| `runtimeVersion` | Yes | Runtime version (see below) |
| `networkMode` | No | `"PUBLIC"` (default) or `"VPC"` |
| `networkConfig` | No | VPC configuration (subnets, security groups) |
| `protocol` | No | `"HTTP"` (default), `"MCP"`, or `"A2A"` |
| `envVars` | No | Custom environment variables |
| `instrumentation` | No | OpenTelemetry settings |
| `authorizerType` | No | `"AWS_IAM"` or `"CUSTOM_JWT"` |
| `authorizerConfiguration` | No | JWT authorizer settings (for `CUSTOM_JWT`) |
| `requestHeaderAllowlist` | No | Headers to forward to the agent |
| `lifecycleConfiguration` | No | Runtime session lifecycle settings (idle timeout, max lifetime) |
| `executionRoleArn` | No | ARN of an existing IAM execution role (skips CDK-managed role) |
| `tags` | No | Agent-level tags |
| Field | Required | Description |
| ------------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `name` | Yes | Agent name (1-48 chars, alphanumeric + underscore) |
| `build` | Yes | `"CodeZip"` or `"Container"` |
| `entrypoint` | Yes | Entry file (e.g., `main.py` or `main.py:handler`) |
| `codeLocation` | Yes | Directory containing agent code (and the `Dockerfile` for Container builds) |
| `runtimeVersion` | Yes | Runtime version (see below) |
| `networkMode` | No | `"PUBLIC"` (default) or `"VPC"` |
| `networkConfig` | No | VPC configuration (subnets, security groups) |
| `protocol` | No | `"HTTP"` (default), `"MCP"`, or `"A2A"` |
| `envVars` | No | Custom environment variables |
| `instrumentation` | No | OpenTelemetry settings |
| `authorizerType` | No | `"AWS_IAM"` or `"CUSTOM_JWT"` |
| `authorizerConfiguration` | No | JWT authorizer settings (for `CUSTOM_JWT`) |
| `requestHeaderAllowlist` | No | Headers to forward to the agent |
| `lifecycleConfiguration` | No | Runtime session lifecycle settings (idle timeout, max lifetime) |
| `executionRoleArn` | No | ARN of an existing IAM execution role (skips CDK-managed role) |
| `tags` | No | Agent-level tags |
| `buildContextPath` | No | **Container only.** Docker build context directory (replaces `codeLocation` as context). |
| `customDockerBuildArgs` | No | **Container only.** Key/value pairs forwarded as `--build-arg` flags. |

### Runtime Versions

Expand Down
67 changes: 67 additions & 0 deletions docs/container-builds.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,73 @@ All other fields work the same as CodeZip agents.
> must also add a `Dockerfile` and `.dockerignore` to the agent's code directory. The easiest way is to create a
> throwaway container agent with `agentcore add agent --build Container` and copy the generated files.

### Advanced: Shared Dockerfile (monorepo)

When multiple agents share the same build logic, you can point them all at a single `Dockerfile` using two optional
fields:

| Field | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `buildContextPath` | Docker build context directory. Replaces `codeLocation` as the `docker build` context and as the root the `dockerfile` path is resolved against. |
| `customDockerBuildArgs` | Key/value pairs forwarded as `--build-arg` flags, allowing a shared Dockerfile to branch per agent. Keys must be valid identifiers. |

Both fields are honored identically by local `agentcore dev` / `agentcore package` and by `agentcore deploy` (which
builds in CodeBuild). The `dockerfile` field is resolved relative to the build context, so with `buildContextPath: "."`
the default `Dockerfile` refers to `./Dockerfile` at the project root. `dockerfile` may also be a relative subpath (e.g.
`docker/Dockerfile` or `.docker/Dockerfile`); absolute paths, `..` traversal, and directory-shaped values (a trailing
slash) are rejected.

**Example — two agents, one shared `Dockerfile` at the project root:**

```json
{
"name": "agent-one",
"build": "Container",
"entrypoint": "main.py",
"codeLocation": "app/agent-one/",
"buildContextPath": ".",
"customDockerBuildArgs": { "AGENT_NAME": "agent-one" }
},
{
"name": "agent-two",
Comment thread
tejaskash marked this conversation as resolved.
"build": "Container",
"entrypoint": "main.py",
"codeLocation": "app/agent-two/",
"buildContextPath": ".",
"customDockerBuildArgs": { "AGENT_NAME": "agent-two" }
}
```

The shared `Dockerfile` can then branch on the build arg:
Comment thread
tejaskash marked this conversation as resolved.

```dockerfile
ARG AGENT_NAME
COPY app/${AGENT_NAME}/ ./app/
```

**When to use `buildContextPath`:** use it when your `Dockerfile` needs to `COPY` files that live outside of
`codeLocation` (e.g. shared libraries at the project root). Without it, Docker only sees the `codeLocation` directory as
its build context.

> **Secrets and build junk are excluded from the upload — always.** On `agentcore deploy`, the build context uploaded to
> CodeBuild (whether it's `codeLocation` or a `buildContextPath`) always has a baseline of secret/junk patterns excluded
> — `.env`, `.env.*`, `.git`, `.venv`, `node_modules`, `__pycache__`, `.pytest_cache`, `.DS_Store` — at every depth
> (nested `app/agent-one/.env` and `app/agent-one/node_modules` are excluded too, not just the top level). So a stray
> `.env` is never persisted to the assets bucket or baked into the deployed image, regardless of whether you have a
> `.dockerignore`. Your own `.dockerignore` at the build-context root is honored on top of this baseline (exclude more,
> or re-include a baseline path with a `!pattern` line), and the `Dockerfile` itself is always kept even under an
> allowlist-style (`*`) `.dockerignore`.
>
> **A `.dockerignore` is also generated for local builds when you set `buildContextPath`.** The whole of that directory
> is the build context — for `buildContextPath: "."` that's your entire project — and a local `docker build` honors only
> a real `.dockerignore`. So if none exists at that root, `agentcore dev` / `agentcore package` creates one (the same
> baseline above, listed both bare and `**/`-prefixed, plus `agentcore/`) so your local image matches the deploy upload.
> It's an ordinary file — commit it, and edit it to include anything you intentionally want in the context (e.g. a
> non-secret `.env.production`, by adding a `!.env.production` line).

**When to use `customDockerBuildArgs`:** use it to parameterise a shared `Dockerfile` so each agent produces a different
image (different entry point, bundled code, etc.) without duplicating the file.

## Local Development

```bash
Expand Down
87 changes: 70 additions & 17 deletions src/cli/operations/deploy/__tests__/preflight-container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,38 @@ import { validateContainerAgents } from '../preflight.js';
import { existsSync, readFileSync } from 'node:fs';
import { afterEach, describe, expect, it, vi } from 'vitest';

const { ensureMock } = vi.hoisted(() => ({ ensureMock: vi.fn() }));

vi.mock('node:fs', () => ({
existsSync: vi.fn(),
readFileSync: vi.fn(),
}));

vi.mock('../../../../lib', () => ({
DOCKERFILE_NAME: 'Dockerfile',
getDockerfilePath: (codeLocation: string, dockerfile?: string) => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const p = require('node:path') as typeof import('node:path');
return p.join(codeLocation, dockerfile ?? 'Dockerfile');
},
resolveCodeLocation: vi.fn((codeLocation: string, configBaseDir: string) => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const p = require('node:path') as typeof import('node:path');
const repoRoot = p.dirname(configBaseDir);
return p.resolve(repoRoot, codeLocation);
}),
// Stub other exports that the module may pull in
ConfigIO: vi.fn(),
requireConfigRoot: vi.fn(),
}));
vi.mock('../../../../lib', () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const p = require('node:path') as typeof import('node:path');
const resolveCodeLocation = (codeLocation: string, configBaseDir: string) =>
p.resolve(p.dirname(configBaseDir), codeLocation);
const getDockerfilePath = (buildContext: string, dockerfile?: string) =>
p.join(buildContext, dockerfile ?? 'Dockerfile');
return {
DOCKERFILE_NAME: 'Dockerfile',
getDockerfilePath,
resolveCodeLocation,
// Mirror the real shared resolver so existsSync/ensure are called with the same paths.
resolveBuildContext: (
spec: { codeLocation: string; buildContextPath?: string; dockerfile?: string },
baseDir: string
) => {
const buildContext = resolveCodeLocation(spec.buildContextPath ?? spec.codeLocation, baseDir);
return { buildContext, dockerfilePath: getDockerfilePath(buildContext, spec.dockerfile) };
},
ensureBuildContextDockerignore: ensureMock,
// Stub other exports that the module may pull in
ConfigIO: vi.fn(),
requireConfigRoot: vi.fn(),
};
});

const mockedExistsSync = vi.mocked(existsSync);
const mockedReadFileSync = vi.mocked(readFileSync);
Expand Down Expand Up @@ -135,6 +145,49 @@ describe('validateContainerAgents', () => {
expect(() => validateContainerAgents(spec, CONFIG_ROOT)).toThrow(/Dockerfile\.gpu not found/);
});

it('resolves the Dockerfile against buildContextPath when set', () => {
mockedExistsSync.mockReturnValue(true);
mockValidDockerfile();

const spec = makeSpec([
{ name: 'mono', build: 'Container', codeLocation: dir('agents/mono'), buildContextPath: dir('.') },
]);

expect(() => validateContainerAgents(spec, CONFIG_ROOT)).not.toThrow();
// The Dockerfile is resolved from the build context (repo root), not the agent's codeLocation.
const calledPath = mockedExistsSync.mock.calls[0]?.[0] as string;
expect(calledPath).not.toContain('agents/mono');
});

it('generates a build-context .dockerignore (and logs) when buildContextPath is set and none exists', () => {
mockedExistsSync.mockReturnValue(true);
mockValidDockerfile();
// Simulate "created" — return the path so the preflight logs it.
ensureMock.mockReturnValue('/project/.dockerignore');
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);

const spec = makeSpec([
{ name: 'mono', build: 'Container', codeLocation: dir('agents/mono'), buildContextPath: dir('.') },
]);

expect(() => validateContainerAgents(spec, CONFIG_ROOT)).not.toThrow();
// Ensured against the resolved build context (repo root), not the agent's codeLocation.
const ensuredPath = ensureMock.mock.calls[0]?.[0] as string;
expect(ensuredPath).not.toContain('agents/mono');
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('created'));
warnSpy.mockRestore();
});

it('does not touch .dockerignore when buildContextPath is unset', () => {
mockedExistsSync.mockReturnValue(true);
mockValidDockerfile();

const spec = makeSpec([{ name: 'plain', build: 'Container', codeLocation: dir('agents/plain') }]);

validateContainerAgents(spec, CONFIG_ROOT);
expect(ensureMock).not.toHaveBeenCalled();
});

it('warns when Dockerfile uses deprecated bookworm base image', () => {
mockedExistsSync.mockReturnValue(true);
mockedReadFileSync.mockReturnValue(
Expand Down
32 changes: 27 additions & 5 deletions src/cli/operations/deploy/preflight.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { ConfigIO, DOCKERFILE_NAME, getDockerfilePath, requireConfigRoot, resolveCodeLocation } from '../../../lib';
import {
ConfigIO,
DOCKERFILE_NAME,
ensureBuildContextDockerignore,
requireConfigRoot,
resolveBuildContext,
} from '../../../lib';
import { StaleCdkConstructError, ValidationError } from '../../../lib/errors/types';
import type { AgentCoreProjectSpec, AwsDeploymentTarget } from '../../../schema';
import { validateAwsCredentials } from '../../aws/account';
Expand Down Expand Up @@ -198,15 +204,31 @@ export function validateContainerAgents(projectSpec: AgentCoreProjectSpec, confi
const errors: string[] = [];
for (const agent of projectSpec.runtimes || []) {
if (agent.build === 'Container') {
const codeLocation = resolveCodeLocation(agent.codeLocation, configRoot);
const dockerfilePath = getDockerfilePath(codeLocation, agent.dockerfile);
// Build context + Dockerfile via the shared resolver (identical to how ContainerSourceAsset
// uploads the context and how the CodeBuild buildspec resolves the -f path).
const { buildContext, dockerfilePath } = resolveBuildContext(agent, configRoot);

if (!existsSync(dockerfilePath)) {
errors.push(
`Agent "${agent.name}": ${agent.dockerfile ?? DOCKERFILE_NAME} not found at ${dockerfilePath}. Container agents require a Dockerfile.`
);
} else {
warnDeprecatedBaseImage(dockerfilePath, agent.name);
}
warnDeprecatedBaseImage(dockerfilePath, agent.name);

// Dockerfile validated. buildContextPath widens the docker build context (e.g. the whole repo);
// ensure a .dockerignore at that root so secrets/junk (.env, .git, agentcore/) are never baked
// into the local image. Both local and deploy honor this file; it is created only when absent
// (never overwrites the user's) and only AFTER validation — so a failing deploy leaves no stray
// file and this never masks the friendly "Dockerfile not found" error above.
if (agent.buildContextPath) {
const created = ensureBuildContextDockerignore(buildContext);
Comment thread
tejaskash marked this conversation as resolved.
if (created) {
console.warn(
`Agent "${agent.name}": created ${created} with default secret exclusions because buildContextPath is ` +
`set (the entire context is sent to Docker/CodeBuild). Review and commit it; edit to include any ` +
`intentionally-bundled files.`
);
}
}
}
}
Expand Down
71 changes: 71 additions & 0 deletions src/cli/operations/dev/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,77 @@ describe('getDevConfig', () => {
expect(config).not.toBeNull();
expect(config?.dockerfile).toBe('Dockerfile.gpu');
});

it('threads buildContextPath from Container agent spec to DevConfig (resolved absolute)', () => {
const project: AgentCoreProjectSpec = {
name: 'TestProject',
version: 1,
managedBy: 'CDK' as const,
runtimes: [
{
name: 'ContainerAgent',
build: 'Container',
runtimeVersion: 'PYTHON_3_12',
entrypoint: filePath('main.py'),
codeLocation: dirPath('./agents/container'),
protocol: 'HTTP',
buildContextPath: dirPath('.'),
},
],
memories: [],
knowledgeBases: [],
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
agentCoreGateways: [],
policyEngines: [],
configBundles: [],
abTests: [],
harnesses: [],
datasets: [],
payments: [],
};

const config = getDevConfig(workingDir, project, '/test/project/agentcore');
expect(config).not.toBeNull();
// resolveCodeDirectory('.', '/test/project/agentcore') => dirname('/test/project/agentcore') + '/' + '.' => '/test/project'
expect(config?.buildContextPath).toBe('/test/project');
});

it('threads customDockerBuildArgs from Container agent spec to DevConfig', () => {
const project: AgentCoreProjectSpec = {
name: 'TestProject',
version: 1,
managedBy: 'CDK' as const,
runtimes: [
{
name: 'ContainerAgent',
build: 'Container',
runtimeVersion: 'PYTHON_3_12',
entrypoint: filePath('main.py'),
codeLocation: dirPath('./agents/container'),
protocol: 'HTTP',
customDockerBuildArgs: { AGENT_NAME: 'myagent' },
},
],
memories: [],
knowledgeBases: [],
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
agentCoreGateways: [],
policyEngines: [],
configBundles: [],
abTests: [],
harnesses: [],
datasets: [],
payments: [],
};

const config = getDevConfig(workingDir, project, '/test/project/agentcore');
expect(config).not.toBeNull();
expect(config?.customDockerBuildArgs).toEqual({ AGENT_NAME: 'myagent' });
});
});

describe('getAgentPort', () => {
Expand Down
Loading
Loading