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:
- Under Deployment protection rules, add the
collaborative-ai-dlcteam (or at least two maintainers) as required reviewers. - Enable Prevent self-review so the person who starts a release cannot approve its deployment.
- Disable administrator bypass for the protection rules.
- 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:
- Set
AUTH_DOMAIN(andAUTH_CERTIFICATE_ARNif needed), leaveAUTH_DOMAIN_ACTIVEunset orfalse, and deploy. PreserveAUTH_MODEandSSO_CONFIGon an existing SSO deployment; for a new local deployment, leave them unset. - If
AUTH_ROUTE53_ZONE_IDis unset, create theAUTH_DOMAINDNS record pointing at theauth_dns_targetTerraform output. Verify DNS resolution and HTTPS. - Register
auth_custom_oidc_idp_callback_urlorauth_custom_saml_acs_urlwith the identity provider while retaining its old callback. - Set
AUTH_DOMAIN_ACTIVE=trueand deploy again to switch the frontend login origin. For a new SSO deployment, also setAUTH_MODE=hybridandSSO_CONFIG. - Verify sign-in before removing old callbacks or moving to
sso-only. SetAUTH_DOMAIN_ACTIVE=falseand 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 frommainbefore 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:
- Choose a region present in the AgentCore VPC AZ map in
terraform/modules/compute/agentcore/main.tf, or explicitly configure its supported AZ IDs. - Confirm the selected Bedrock inference profile is available there, or in
BEDROCK_REGIONwhen it is set. Bedrock API keys are regional; enable the OpenAI models in the inference region when using Codex. - 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.
- Use a distinct custom hostname, or leave the main demo on its generated CloudFront hostname. A CloudFront alias cannot belong to both deployments.
- 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.