Skip to content

Demo deployments

The release workflow deploys every published release to the AWS demo account. It calls the reusable Deploy Demo workflow after creating the immutable release tag and GitHub Release. The reusable workflow can also deploy an existing release tag manually.

The Deploy Main Demo workflow deploys every commit pushed to main through the same reusable workflow. It uses an immutable commit SHA, a separate GitHub Environment, Terraform state key, AWS region, and project namespace so it can coexist with the release demo in the same AWS account.

The release GitHub deployment environment is named demo-release. Its existing Terraform logical environment remains prod; changing it would rename or replace resources already recorded in the remote state. The main demo also uses the prod logical environment to retain production capacity, retention, and deletion behavior, but changes project_name to collaborative-ai-dlc-main to isolate account-global resource names.

Configure the GitHub environment

In the repository, open Settings → Environments and create an environment named demo-release. Configure all of these protection settings before granting the deployment role access to the AWS account:

  1. Under Deployment protection rules, add the collaborative-ai-dlc team (or at least two maintainers) as required reviewers.
  2. Enable Prevent self-review so the person who starts a release cannot approve its deployment.
  3. Disable administrator bypass for the protection rules.
  4. Under Deployment branches and tags, choose Selected branches and tags, add main, and save the rule.

Both release-triggered and manually triggered deployments wait for this approval. The workflow then verifies that the requested annotated release tag resolves to a commit reachable from main before requesting AWS credentials.

Add these environment variables:

Variable Value
AWS_ACCOUNT_ID The 12-digit demo AWS account ID
AWS_REGION eu-west-1
AWS_ROLE_ARN ARN of the OIDC deployment role created below
BEDROCK_MODEL eu.anthropic.claude-sonnet-4-6
TF_STATE_BUCKET Name of the existing demo Terraform state bucket
TF_STATE_KEY terraform.tfstate
TF_STATE_REGION eu-west-1

Optionally set AIDLC_RELEASE_PINNING to on to let new intents pin an AI-DLC release (see AI-DLC release compatibility). When it is unset, the workflow writes nothing and Terraform's default off applies; any value other than on or off fails the plan. The same variable applies to the main demo environment.

No GitHub secrets are required for a local-authentication deployment without a custom domain. OIDC replaces long-lived AWS access keys, and the Terraform variables contain no credentials. Do not create AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY secrets. Application credentials continue to live in AWS Secrets Manager or Systems Manager Parameter Store.

Optional enterprise SSO

The workflow reads the application domain (APP_DOMAIN, APP_DOMAIN_ALIASES, ACM_CERTIFICATE_ARN) from environment secrets. Enterprise SSO uses these additional, optional settings:

Name Kind Value
AUTH_MODE Variable local (default), hybrid, or sso-only
SSO_CONFIG Secret Provider file JSON as described in Enterprise SSO
AUTH_DOMAIN Secret Custom Cognito login hostname, for example auth.<APP_DOMAIN>
AUTH_CERTIFICATE_ARN Secret us-east-1 certificate covering AUTH_DOMAIN; empty reuses ACM_CERTIFICATE_ARN
AUTH_ROUTE53_ZONE_ID Secret Optional hosted zone for auth DNS; independent of application DNS
AUTH_DOMAIN_ACTIVE Variable false (default); set true only after custom DNS and IdP callbacks are ready

SSO_CONFIG stores the provider configuration with Secrets Manager ARNs, never OIDC client-secret values. Store each OIDC client secret in Secrets Manager in the deployment account and Region. hybrid and sso-only require SSO_CONFIG, and local rejects it. The workflow validates the provider file with scripts/sso-config.mjs and writes it as prod.sso.tfvars.json, so the deployment plan uses it. Terraform keeps the full provider map sensitive in plan output.

For SAML, demo secrets must use metadata.url or inline metadata.xml. The workflow rejects metadata.file because a GitHub secret contains no companion XML files. Local installer and standalone deployment provider files continue to support metadata.file.

AUTH_DOMAIN is not resolved before deployment, because its DNS record can only be created after the first apply. Its parent must have a public DNS A record; Terraform waits for that record before creating the Cognito domain. To enable SSO with a custom login domain:

  1. Set AUTH_DOMAIN (and AUTH_CERTIFICATE_ARN if needed), leave AUTH_DOMAIN_ACTIVE unset or false, and deploy. Preserve AUTH_MODE and SSO_CONFIG on an existing SSO deployment; for a new local deployment, leave them unset.
  2. If AUTH_ROUTE53_ZONE_ID is unset, create the AUTH_DOMAIN DNS record pointing at the auth_dns_target Terraform output. Verify DNS resolution and HTTPS.
  3. Register auth_custom_oidc_idp_callback_url or auth_custom_saml_acs_url with the identity provider while retaining its old callback.
  4. Set AUTH_DOMAIN_ACTIVE=true and deploy again to switch the frontend login origin. For a new SSO deployment, also set AUTH_MODE=hybrid and SSO_CONFIG.
  5. Verify sign-in before removing old callbacks or moving to sso-only. Set AUTH_DOMAIN_ACTIVE=false and redeploy to roll back the login origin.

The workflow generates prod.tfvars and prod.s3.tfbackend in the runner's temporary directory. It never runs bootstrap.sh and therefore never creates or selects a new state bucket.

After preparing the environment, the job runs scripts/deploy-terraform.sh followed by scripts/deploy-frontend.sh. The Terraform script installs the locked root dependencies, initializes the selected backend, saves one plan, checks it for protected-resource destruction, and applies that plan without an interactive prompt. The frontend script uses the resulting Terraform outputs to build and publish the frontend.

The Terraform Lambda module owns bundling and ZIP creation through its normal resource graph. Missing local archives are recreated on clean runners using the module defaults; do not set TF_RECREATE_MISSING_LAMBDA_PACKAGE=false. Package filenames may remain unknown until apply when an upstream resource is changing. The workflow does not prebuild archives or require these filenames to be known before apply. This can rebuild local packages on each clean runner.

Current deployment scripts also own Public ECR authentication. For older release scripts, the workflow loads only the authentication helper from the immutable executing workflow commit (github.workflow_sha) and wraps the Terraform script. The selected release sources stay intact. The existing script's destruction check remains in effect; there is no separate workflow package validator.

The same immutable workflow commit supplies with-public-ecr-auth.mjs. After the package checks and before invoking the selected release's apply script, it authenticates to Public ECR in us-east-1, checks the two pinned base-image manifests from the deployment checkout, and provides an isolated DOCKER_CONFIG through apply. This also protects older releases whose deploy script predates Public ECR authentication. It leaves release sources and private ECR push credentials unchanged. Both deployment roles need the Public ECR token permissions; the workflow does not modify their policies.

The manifest check is an early access check, not a full layer download or evidence that the deployed build will succeed. See base-image authentication for token cleanup, direct Terraform use, limits and the private-cache option.

Deployment concurrency

The reusable workflow first resolves TF_STATE_BUCKET, TF_STATE_KEY, and TF_STATE_REGION from the selected GitHub Environment in a small configuration job. The deployment job uses that captured backend for both Terraform and its concurrency group, so configuration changes while it waits cannot make the group refer to a different state than Terraform uses.

The concurrency group is a hash of the state bucket and key. Callers using the same state share a group even when their target labels or project names differ; different state objects can deploy in parallel. Hashing preserves distinctions between case-sensitive S3 keys despite GitHub's case-insensitive group names. cancel-in-progress: false protects an active deployment. GitHub's default queue behavior retains the most recent pending deployment for each group.

The configuration job has no GitHub token permissions, does not request AWS credentials, and uses deployment: false to avoid recording a deployment. Environment protection rules still apply to both jobs: when required reviewers are configured, approve backend resolution first and deployment afterward. Wait timers also apply to both jobs. Environments using custom deployment protection GitHub Apps must set resolve-backend.environment.deployment to true, because those rules require a deployment object.

Configure the continuous main demo

The main deployment is intentionally a separate target rather than another view of the release state. Before enabling .github/workflows/deploy-main.yml, create a GitHub Environment named demo-main and restrict its deployment branches to main. Required reviewers are optional: adding them makes every push wait for approval; omitting them makes deployment continuous after a push lands on protected main.

Provision the dedicated Terraform state bucket and demo-main deployment role through the account's normal administrative process before configuring the GitHub Environment. The state bucket and application deployment both use eu-central-1; the manual IAM and state requirements are documented below.

Add these environment variables to demo-main:

Variable Value
AWS_ACCOUNT_ID The same 12-digit demo AWS account ID
AWS_REGION eu-central-1
AWS_ROLE_ARN ARN of the main-demo OIDC deployment role described below
BEDROCK_MODEL A Bedrock inference profile available in the application region
TF_STATE_BUCKET The existing demo state bucket, or a dedicated state bucket
TF_STATE_KEY main/terraform.tfstate
TF_STATE_REGION The state bucket's region; this may differ from AWS_REGION

Optionally set BEDROCK_REGION to send agent inference to a different region than the application, for example when the Bedrock API key belongs to us-east-1. BEDROCK_MODEL and the models selected in the application must then be inference profiles of that region's geo (us., eu., apac.) or global.. When it is unset, inference uses AWS_REGION.

The workflow supplies the isolation values itself:

  • Terraform environment = "prod", preserving production behavior.
  • Terraform project_name = "collaborative-ai-dlc-main", isolating IAM roles, CloudFront functions and origin controls, SSM paths, Lambda functions, and other fixed names that are account-global or otherwise shared across regions.
  • ref_type = "commit", requiring the full immutable commit SHA and verifying that it is reachable from main before requesting AWS credentials.

Do not reuse terraform.tfstate: changing the provider region in the release state would plan replacement of the release deployment rather than a second stack. Sharing the bucket is safe when the key and lock-file permissions are separate.

Optional post-deploy release tests

After a successful main deployment, the workflow can ask a release-testing setup in the deployment account to test the deployed revision, for example an AWS DevOps Agent release test. The request is sent only when both environment secrets below exist in demo-main. With neither secret the step exits without doing anything; with only one it fails with a warning. It runs after Verify deployment and uses continue-on-error, so a failed request never fails the deployment. The step assumes the role through a web-identity profile built from the job's OIDC token, separate from the deployment credentials.

Secret Value
POST_DEPLOY_TEST_ROLE_ARN An OIDC role trusted like the deployment role, allowed only to invoke the function
POST_DEPLOY_TEST_FUNCTION Name of the Lambda function in us-east-1 that starts the tests

The workflow invokes the function asynchronously in us-east-1. On failure it prints only the stage, the AWS error code and the failing operation, never AWS messages, so public logs contain no account-specific values. The payload is:

{ "trigger": { "runId": 123456789, "headSha": "<40-character commit SHA>" } }

runId is the workflow run ID and headSha is the deployed commit. The role's trust policy must require audience sts.amazonaws.com and subject repo:aws-samples/sample-collaborative-ai-dlc:environment:demo-main, like the main-demo deployment role below, and the role needs lambda:InvokeFunction on the function and nothing more.

The run is still in_progress when the function is called, so the function must not wait for the run's conclusion. It should read the run through the GitHub API and proceed only if the workflow, main branch and head_sha match and the Verify deployment step succeeded. A re-run reuses runId and the payload has no attempt number, so treat repeated requests for the same run as duplicates.

Create or update the main-demo deployment role

Reuse the account's existing GitHub Actions OIDC provider, but prefer a separate role for the continuous deployment. Its trust policy must use the protected GitHub Environment subject:

{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::<account-id>:oidc-provider/token.actions.githubusercontent.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
      "token.actions.githubusercontent.com:sub": "repo:aws-samples/sample-collaborative-ai-dlc:environment:demo-main"
    }
  }
}

Use the same deployment policy as the release demo, described in Grant deployment permissions, on the separate main-demo role. If a customer-managed policy restricts aws:RequestedRegion or contains region-qualified ARNs, add the main demo's application region. IAM and CloudFront permissions remain account-global.

Grant the role access to the chosen backend bucket and only the new state objects (plus s3:GetBucketLocation and s3:ListBucket on the bucket):

arn:aws:s3:::<state-bucket>/main/terraform.tfstate
arn:aws:s3:::<state-bucket>/main/terraform.tfstate.tflock

The state object needs s3:GetObject and s3:PutObject; the lock object also needs s3:DeleteObject. Add KMS permissions when the backend bucket uses a customer-managed key.

Check regional and application prerequisites

Before the first deployment:

  1. Choose a region present in the AgentCore VPC AZ map in terraform/modules/compute/agentcore/main.tf, or explicitly configure its supported AZ IDs.
  2. Confirm the selected Bedrock inference profile is available there, or in BEDROCK_REGION when it is set. Bedrock API keys are regional; enable the OpenAI models in the inference region when using Codex.
  3. Check regional quotas for VPCs, elastic IPs/NAT gateways, Neptune, Fargate/ECS, Lambda, ECR, and Bedrock AgentCore, plus account quotas for IAM roles and CloudFront resources.
  4. Use a distinct custom hostname, or leave the main demo on its generated CloudFront hostname. A CloudFront alias cannot belong to both deployments.
  5. After deployment, configure the new region's users, agent credentials, source-control credentials, and tracker credentials. OAuth providers must accept the main demo's distinct callback URL; some providers require a separate OAuth application.

Protect release tags

Open Settings → Rules → Rulesets, create a tag ruleset for v*, and set its enforcement status to Active. Enable rules that restrict updates and deletions and block force pushes. Leave tag creation unrestricted so the release workflow's GITHUB_TOKEN can create each new version tag.

This makes existing release tags immutable. The separate reachability check in the deployment workflow ensures that even a newly created release tag can only deploy code that has passed through main.

Create the AWS OIDC provider

An AWS account has at most one IAM OIDC provider for GitHub Actions. Reuse it if token.actions.githubusercontent.com is already configured.

export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
export OIDC_PROVIDER_ARN="arn:aws:iam::$AWS_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"

aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn "$OIDC_PROVIDER_ARN" >/dev/null 2>&1 ||
aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com

For an existing provider, verify that its client ID list includes sts.amazonaws.com.

Create the deployment role

Create a trust policy scoped to this repository and the demo-release GitHub Environment. The environment condition is important: pull requests and jobs that do not pass the environment's protection rules cannot assume the role.

export ROLE_NAME="CollaborativeDemoGitHubDeploy"

cat > /tmp/collaborative-demo-github-trust.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "$OIDC_PROVIDER_ARN"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
        "token.actions.githubusercontent.com:sub": "repo:aws-samples/sample-collaborative-ai-dlc:environment:demo-release"
      }
    }
  }]
}
EOF

if aws iam get-role --role-name "$ROLE_NAME" >/dev/null 2>&1; then
  aws iam update-assume-role-policy \
    --role-name "$ROLE_NAME" \
    --policy-document file:///tmp/collaborative-demo-github-trust.json
  aws iam update-role \
    --role-name "$ROLE_NAME" \
    --max-session-duration 10800
else
  aws iam create-role \
    --role-name "$ROLE_NAME" \
    --max-session-duration 10800 \
    --assume-role-policy-document file:///tmp/collaborative-demo-github-trust.json
fi

aws iam get-role \
  --role-name "$ROLE_NAME" \
  --query 'Role.Arn' \
  --output text

Use the returned ARN as the GitHub AWS_ROLE_ARN environment variable.

Grant access to the existing state

The role must be able to read and update the existing state and its native S3 lock file. Substitute the same bucket and key configured in GitHub:

export TF_STATE_BUCKET="<existing-state-bucket>"
export TF_STATE_KEY="terraform.tfstate"

cat > /tmp/collaborative-demo-state-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListTerraformState",
      "Effect": "Allow",
      "Action": ["s3:GetBucketLocation", "s3:ListBucket"],
      "Resource": "arn:aws:s3:::$TF_STATE_BUCKET"
    },
    {
      "Sid": "ReadWriteTerraformState",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::$TF_STATE_BUCKET/$TF_STATE_KEY"
    },
    {
      "Sid": "LockTerraformState",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::$TF_STATE_BUCKET/$TF_STATE_KEY.tflock"
    }
  ]
}
EOF

aws iam put-role-policy \
  --role-name "$ROLE_NAME" \
  --policy-name CollaborativeDemoTerraformState \
  --policy-document file:///tmp/collaborative-demo-state-policy.json

If the bucket uses a customer-managed KMS key, also grant the role kms:Decrypt, kms:Encrypt, and kms:GenerateDataKey for that key.

Grant deployment permissions

The maintained demo account currently attaches AWS-managed AdministratorAccess (arn:aws:iam::aws:policy/AdministratorAccess) to both deployment roles:

GitHub Environment IAM role
demo-release CollaborativeDemoGitHubDeploy
demo-main CollaborativeMainDemoGitHubDeploy

Each role trusts only its corresponding GitHub Environment. The main demo mirrors the existing release deployment permissions; it does not require a broader policy than the release demo.

This documents the current demo account configuration. AdministratorAccess is not required simply because Terraform creates IAM roles or CloudFront resources. A reviewed customer-managed policy can grant the necessary service permissions while restricting role management and iam:PassRole to application roles. Permissions boundaries on those roles can limit the permissions the deployment is allowed to delegate. Developing and validating that policy is separate hardening work.

For other accounts, prefer a reviewed customer-managed deployment policy. Attach the policy deliberately selected for the account:

aws iam attach-role-policy \
  --role-name "$ROLE_NAME" \
  --policy-arn "<terraform-deployment-policy-arn>"

The policy must cover every service managed by the stack, including IAM role and policy management, iam:PassRole, Lambda, API Gateway, Cognito, EC2, Elastic Load Balancing, ECS, ECR, S3, DynamoDB, Neptune, CloudFront, CloudWatch, EventBridge, SQS, Secrets Manager, Systems Manager, and Bedrock AgentCore.

AdministratorAccess grants permissions for all AWS actions on all resources, including resources unrelated to the deployment. Retaining it in the maintained demo account is an explicit permissions choice. Scoped state policies and separate project names do not reduce the permissions granted by that policy.

Verify before enabling automatic deployment

Before publishing the first release, confirm that the demo-release environment has the required reviewers, self-review prevention, administrator bypass disabled, and the main deployment branch rule. Also confirm that the v* tag ruleset is active and the deployment role has the selected deployment policy, with its scope reviewed for the account.

Every published release then waits for an independent approval, creates a Terraform plan, applies that exact saved plan, deploys the frontend, and verifies the application URL. Plans stay on the runner and are not uploaded as artifacts because Terraform plans can contain sensitive values.