Skip to content
Draft
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
35 changes: 29 additions & 6 deletions Dockerfile.optimized
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@
# checkov:skip=CKV_DOCKER_3: "The Dockerfile uses the official AWS Lambda Python base image (public.ecr.aws/lambda/python:3.12-arm64), which already configures the appropriate non-root user for Lambda execution"
# checkov:skip=CKV_DOCKER_2: "The Dockerfile.optimized is specifically designed for AWS Lambda container images, which don't use Docker HEALTHCHECK instructions."

# ── Air-gapped / internal registry support ────────────────────────────────────
# Override these ARGs to use images from an internal registry instead of public ones.
# Example (air-gapped):
# docker build \
# --build-arg UV_IMAGE=123456789.dkr.ecr.us-east-1.amazonaws.com/idp-base:uv-0.9.6 \
# --build-arg LAMBDA_BASE_IMAGE=123456789.dkr.ecr.us-east-1.amazonaws.com/idp-base:lambda-python-3.12-arm64 \
# -f Dockerfile.optimized .
# ─────────────────────────────────────────────────────────────────────────────
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.9.6
ARG LAMBDA_BASE_IMAGE=public.ecr.aws/lambda/python:3.12-arm64

# Use specific version to avoid network issues
FROM ghcr.io/astral-sh/uv:0.9.6 AS uv
FROM ${UV_IMAGE} AS uv

# Builder stage - bundle dependencies into Lambda task root
FROM public.ecr.aws/lambda/python:3.12-arm64 AS builder
FROM ${LAMBDA_BASE_IMAGE} AS builder

# Enable bytecode compilation to improve cold-start performance
ENV UV_COMPILE_BYTECODE=1
Expand All @@ -19,6 +30,11 @@ ENV UV_NO_INSTALLER_METADATA=1
# Enable copy mode to support bind mount caching
ENV UV_LINK_MODE=copy

# Air-gapped PyPI mirror support: pass --build-arg UV_INDEX_URL=https://your-artifactory/pypi/simple/
# When set, uv will use this instead of pypi.org to install Lambda requirements.txt packages.
ARG UV_INDEX_URL=""
ENV UV_INDEX_URL=${UV_INDEX_URL}

# Build argument for function path
ARG FUNCTION_PATH
ARG INSTALL_IDP_COMMON=true
Expand All @@ -33,17 +49,24 @@ COPY ${FUNCTION_PATH}/requirements.txt* /build/

# Install all dependencies including idp_common_pkg in one step
# Using mount from uv stage instead of COPY to avoid layer bloat
# When UV_INDEX_URL is set (air-gapped), pass --index-url explicitly rather than relying
# on the env var alone — this ensures the flag is always evaluated even when the uv
# cache layer was previously built against pypi.org.
RUN --mount=from=uv,source=/uv,target=/bin/uv \
--mount=type=cache,target=/root/.cache/uv \
if [ -f /build/requirements.txt ]; then \
sed 's|^\.\./\.\.\(/\.\.\)\?/lib/idp_common_pkg|/tmp/idp_common_pkg|' /build/requirements.txt > /tmp/requirements.txt && \
uv pip install --python python3.12 --target "${LAMBDA_TASK_ROOT}" -r /tmp/requirements.txt && \
rm /tmp/requirements.txt; \
sed 's|^\.\./\.\.\(/\.\.\)\?/lib/idp_common_pkg|/tmp/idp_common_pkg|' /build/requirements.txt > /tmp/requirements.txt && \
INDEX_ARG="" && \
if [ -n "$UV_INDEX_URL" ]; then INDEX_ARG="--index-url $UV_INDEX_URL"; fi && \
uv pip install --python python3.12 --target "${LAMBDA_TASK_ROOT}" $INDEX_ARG -r /tmp/requirements.txt && \
rm /tmp/requirements.txt; \
fi && \
rm -rf /tmp/idp_common_pkg

# Final stage - minimal runtime
FROM public.ecr.aws/lambda/python:3.12-arm64
# LAMBDA_BASE_IMAGE ARG must be re-declared after each FROM (Docker scoping rule)
ARG LAMBDA_BASE_IMAGE=public.ecr.aws/lambda/python:3.12-arm64
FROM ${LAMBDA_BASE_IMAGE}

# Conditionally install git (required for mlflow/gitpython)
ARG INSTALL_GIT=false
Expand Down
97 changes: 96 additions & 1 deletion docs/deployment-private-network.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,92 @@ idp-cli publish --source-dir . --bucket-basename my-idp-artifacts --prefix idp -

---

### Air-gapped CodeBuild — Internal Container Registries (optional)

During stack deployment, CodeBuild builds the Lambda container images inside your account. In a **fully air-gapped VPC** where CodeBuild has no internet access, it cannot reach the two public registries used by the build:

| Image | Public source | Used for |
|-------|--------------|----------|
| `ghcr.io/astral-sh/uv:0.9.6` | GitHub Container Registry | Python dependency installer inside Dockerfile |
| `public.ecr.aws/lambda/python:3.12-arm64` | Amazon Public ECR | Lambda base image |

**Solution**: mirror both images to your internal ECR (or Artifactory), then pass their URIs as CloudFormation parameters.

#### Step A: Mirror images to internal ECR

Run the provided helper script — it pulls both images, re-tags them into the deployment account's ECR, and prints the ready-to-paste parameter string:

```bash
./scripts/setup-airgapped-codebuild.sh \
--region <region> \
--account <account-id>
```

Example output:

```
✅ UV image pushed: 123456789012.dkr.ecr.us-east-1.amazonaws.com/idp-build-tools:uv-0.9.6
✅ Lambda base image pushed: 123456789012.dkr.ecr.us-east-1.amazonaws.com/idp-build-tools:lambda-python-3.12-arm64

Add these parameters to your idp-cli deploy command:
UvImage=123456789012.dkr.ecr.us-east-1.amazonaws.com/idp-build-tools:uv-0.9.6,LambdaBaseImage=123456789012.dkr.ecr.us-east-1.amazonaws.com/idp-build-tools:lambda-python-3.12-arm64
```

#### Step B: Add image parameters to the deploy command

Append the `UvImage` and `LambdaBaseImage` parameters (from the script output) to your `idp-cli deploy --parameters` string (see Step 2 below).

#### Internal PyPI mirror (optional)

If CodeBuild also cannot reach PyPI (`pypi.org`) to install Lambda Python dependencies, pass `UvIndexUrl` pointing to your internal PyPI mirror (e.g. Artifactory):

```
UvIndexUrl=https://artifactory.company.com/artifactory/api/pypi/pypi-virtual/simple/
```

#### Artifactory Docker registry (optional)

If the images are stored in Artifactory rather than ECR, you need to provide credentials so CodeBuild can log in before pulling:

1. Create a Secrets Manager secret with your Artifactory credentials (only needed once):
```bash
aws secretsmanager create-secret \
--name idp/artifactory-docker-creds \
--secret-string '{"username":"svc-build","password":"<api-key>"}' \
--region <region>
```

2. Add these parameters to your deploy command:
```
ArtifactoryDockerUrl=artifactory.company.com,ArtifactoryCredentialsSecretArn=arn:aws:secretsmanager:<region>:<account-id>:secret:idp/artifactory-docker-creds-<suffix>
```

> **Security note**: credentials are fetched at build time via the IAM role — they are never stored in CloudFormation parameters or environment variable logs.

#### CodeBuild VPC Placement (optional, for fully air-gapped subnets)

If CodeBuild's subnets have no internet access at all (no NAT Gateway), you can place CodeBuild inside your VPC using the `CodeBuildVpcId`, `CodeBuildSubnetIds`, and `CodeBuildSecurityGroupId` parameters. When these are set:

- CodeBuild runs entirely within your VPC with no public internet access
- The subnets need VPC Interface Endpoints for: `ecr.api`, `ecr.dkr`, `codebuild`, `logs`, `secretsmanager` — plus a free S3 **Gateway** endpoint
- Run the VPC endpoint deployment script with the `--codebuild-endpoints` flag to create them:

```bash
python scripts/deploy-vpc-endpoints.py \
--vpc-id <vpc-id> \
--stack-name IDP-PRIVATE \
--security-group-id <codebuild-sg-id> \
--subnet-ids <subnet-1>,<subnet-2> \
--codebuild-endpoints \
--region <region>
```

> **Note:** `CodeBuildVpcId` can be the same VPC as `ALBVpcId`, but `CodeBuildSubnetIds` should be private subnets that have the required VPC endpoints. These can be different from `LambdaSubnetIds`.

> **See also**: [Artifactory Dependency Workaround](./artifactory-dependency-workaround.md) for a comprehensive guide covering all dependency resolution options.

---

## Step 2: Deploy the IDP Stack

Replace the placeholder values and run:
Expand All @@ -155,7 +241,15 @@ idp-cli deploy \
| `LambdaSubnetIds` | subnet IDs | Subnets where Lambda functions run (can match ALBSubnetIds) |
| `EnableMCP` | `false` | Disable Bedrock AgentCore Gateway (requires public endpoint) |
| `DocumentKnowledgeBase` | `DISABLED` | Disable Knowledge Base (avoids extra VPC endpoints) |

| `UvImage` | ECR URI | *(Air-gapped only)* Internal ECR URI for the `uv` build tool image. Replaces `ghcr.io/astral-sh/uv:0.9.6`. |
| `LambdaBaseImage` | ECR URI | *(Air-gapped only)* Internal ECR URI for the Lambda Python base image. Replaces `public.ecr.aws/lambda/python:3.12-arm64`. |
| `UvIndexUrl` | HTTPS URL | *(Air-gapped only)* Internal PyPI index URL (e.g. Artifactory). Replaces `pypi.org` for Lambda dependency installs. May contain auth credentials — stored with `NoEcho`. |
| `NpmRegistryUrl` | HTTPS URL | *(Air-gapped only)* Internal npm registry URL (e.g. Artifactory). Replaces `registry.npmjs.org` for the Web UI build. |
| `ArtifactoryDockerUrl` | hostname | *(Air-gapped only)* Artifactory Docker registry hostname. Required when images are stored in Artifactory instead of ECR. |
| `ArtifactoryCredentialsSecretArn` | secret ARN | *(Air-gapped only)* Secrets Manager secret ARN with Artifactory credentials. Required with `ArtifactoryDockerUrl`. |
| `CodeBuildVpcId` | VPC ID | *(Air-gapped only)* VPC ID to place CodeBuild in for network isolation. When set, both `DockerBuildProject` and `UICodeBuildProject` run inside this VPC. Requires `CodeBuildSubnetIds` and `CodeBuildSecurityGroupId`. |
| `CodeBuildSubnetIds` | subnet IDs | *(Air-gapped only)* Comma-separated private subnet IDs for CodeBuild VPC placement. Required with `CodeBuildVpcId`. |
| `CodeBuildSecurityGroupId` | SG ID | *(Air-gapped only)* Security group ID for CodeBuild VPC placement. Must allow outbound HTTPS (443) to VPC endpoints. Required with `CodeBuildVpcId`. |
> **`AppSyncVisibility` is immutable** — it cannot be changed after the stack is created. To switch between GLOBAL and PRIVATE, delete and recreate the stack.

### Enterprise: deploying with a KMS-encrypted artifact bucket
Expand Down Expand Up @@ -379,6 +473,7 @@ When `WebUIHosting=ALB` and `AppSyncVisibility=PRIVATE`, the following are handl
| **`npm error engine Unsupported engine`** | Node.js 22.12+ required. `brew install node@22 && export PATH="/opt/homebrew/opt/node@22/bin:$PATH"` |
| **Stack fails with `conflicting DNS domain`** | A VPC endpoint already exists for that service. Re-run `check-vpc-endpoints.sh` — it will detect this and set the right `Create*=false` flags. |
| **UI loads but shows "network error"** | AppSync API is PRIVATE. From outside the VPC you need an SSM tunnel + `/etc/hosts` entry. From inside VPN/VPC it works automatically. |
| **CodeBuild fails: `pull access denied` / `name unknown` on image pull** | CodeBuild cannot reach public container registries (`ghcr.io`, `public.ecr.aws`). Run `scripts/setup-airgapped-codebuild.sh --region <region> --account <account-id>` to mirror images to ECR, then pass `UvImage=<ecr-uri>` and `LambdaBaseImage=<ecr-uri>` to the deploy command. See [Artifactory Dependency Workaround](./artifactory-dependency-workaround.md). |
| **CodeBuild fails: `AccessDenied: kms:Decrypt`** | The artifact bucket is KMS-encrypted but `ArtifactsBucketKmsKeyArn` was not passed to the deploy command. Redeploy with `--parameters "...ArtifactsBucketKmsKeyArn=<key-arn>"`. |
| **`UpdateDefaultConfig` custom resource fails with `NoSuchKey`** | Same root cause as above — `ConfigurationCopyFunction` silently skipped copying config files due to missing `kms:Decrypt`. Pass `ArtifactsBucketKmsKeyArn` and redeploy. |
| **OCR Lambda times out / Textract calls hang** | Missing `textract` VPC endpoint. Lambda can't reach `com.amazonaws.<region>.textract` — security group only allows port 443 outbound to VPC endpoints. Run `deploy-vpc-endpoints.py` to add the missing endpoint. |
Expand Down
58 changes: 58 additions & 0 deletions patterns/unified/buildspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,61 @@ phases:
- echo "Setting up Docker buildx for cross-platform builds..."
- docker buildx create --use --name multiarch-builder --driver docker-container || docker buildx use multiarch-builder
- docker buildx inspect --bootstrap
# ── Air-gapped: log into internal base image registry if provided ──────
# UV_IMAGE and LAMBDA_BASE_IMAGE env vars may point to an internal ECR
# repo instead of ghcr.io / public.ecr.aws. If they are in an ECR
# registry in the same account, the ECR login above already covers them.
# If they are in a different account, add an extra login here.
- |
if [ -n "$UV_IMAGE" ]; then
echo "Air-gapped mode: using custom UV image: $UV_IMAGE"
else
UV_IMAGE="ghcr.io/astral-sh/uv:0.9.6"
echo "Standard mode: using default UV image: $UV_IMAGE"
fi
- |
if [ -n "$LAMBDA_BASE_IMAGE" ]; then
echo "Air-gapped mode: using custom Lambda base image: $LAMBDA_BASE_IMAGE"
else
LAMBDA_BASE_IMAGE="public.ecr.aws/lambda/python:3.12-arm64"
echo "Standard mode: using default Lambda base image: $LAMBDA_BASE_IMAGE"
fi
- |
if [ -n "$UV_INDEX_URL" ]; then
echo "Air-gapped mode: uv will use custom PyPI index: $UV_INDEX_URL"
else
echo "Standard mode: uv will use default PyPI index (pypi.org)"
fi
# ── Air-gapped: Artifactory Docker registry login ──────────────────────
# When ARTIFACTORY_DOCKER_URL + ARTIFACTORY_CREDENTIALS_SECRET_ARN are
# set, CodeBuild fetches credentials from AWS Secrets Manager at runtime
# (credentials are NEVER hardcoded) and logs in so that docker buildx
# build can pull UV_IMAGE and LAMBDA_BASE_IMAGE from Artifactory.
# Customer admin creates the secret once:
# aws secretsmanager create-secret --name "idp/artifactory-docker-creds"
# --secret-string '{"username":"svc-build","password":"<api-key>"}'
- |
if [ -n "$ARTIFACTORY_DOCKER_URL" ] && [ -n "$ARTIFACTORY_CREDENTIALS_SECRET_ARN" ]; then
echo "Artifactory mode: logging into $ARTIFACTORY_DOCKER_URL via Secrets Manager"
SECRET=$(aws secretsmanager get-secret-value \
--secret-id "$ARTIFACTORY_CREDENTIALS_SECRET_ARN" \
--query SecretString --output text)
AF_USER=$(echo "$SECRET" | python3 -c "import sys,json; print(json.load(sys.stdin)['username'])")
AF_PASS=$(echo "$SECRET" | python3 -c "import sys,json; print(json.load(sys.stdin)['password'])")
echo "$AF_PASS" | docker login "$ARTIFACTORY_DOCKER_URL" \
--username "$AF_USER" --password-stdin
echo "Artifactory Docker login successful"
unset AF_USER AF_PASS SECRET
else
echo "Standard mode: no Artifactory Docker registry configured"
fi
build:
commands:
- echo "Building Unified Pattern Docker images (BDA + Pipeline)..."
- echo "Using IMAGE_VERSION from environment (content-based hash)"
- echo "Image tag will be $IMAGE_VERSION"
- echo "UV_IMAGE=${UV_IMAGE:-ghcr.io/astral-sh/uv:0.9.6}"
- echo "LAMBDA_BASE_IMAGE=${LAMBDA_BASE_IMAGE:-public.ecr.aws/lambda/python:3.12-arm64}"
# BDA functions
- export FUNCTION_bda_invoke="patterns/unified/src/bda_invoke_function"
- export FUNCTION_bda_completion="patterns/unified/src/bda_completion_function"
Expand All @@ -39,10 +89,18 @@ phases:
if [ "$func_var" = "FUNCTION_mlflow_logger" ]; then
EXTRA_ARGS="--build-arg INSTALL_GIT=true"
fi
# Build UV_INDEX_URL arg string only when set (air-gapped PyPI mirror)
UV_INDEX_ARG=""
if [ -n "$UV_INDEX_URL" ]; then
UV_INDEX_ARG="--build-arg UV_INDEX_URL=${UV_INDEX_URL}"
fi
docker buildx build \
--platform linux/arm64 \
-f Dockerfile.optimized \
--build-arg FUNCTION_PATH="${func_path}" \
--build-arg UV_IMAGE="${UV_IMAGE}" \
--build-arg LAMBDA_BASE_IMAGE="${LAMBDA_BASE_IMAGE}" \
${UV_INDEX_ARG} \
${EXTRA_ARGS} \
--tag "${ECR_URI}:${func_name}-${IMAGE_VERSION}" \
--provenance=false \
Expand Down
Loading