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 |
No GitHub secrets are required for the current deployment. OIDC replaces
long-lived AWS access keys, and the current 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.
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.
The job also sets TF_RECREATE_MISSING_LAMBDA_PACKAGE=false. GitHub-hosted
runners start without the Lambda archives referenced by remote Terraform state;
the Lambda module otherwise treats every missing local archive as a
timestamp-driven rebuild.
The workflow uses a draft plan to generate the Lambda module's package plans,
builds every content-addressed ZIP, removes the generated lambda/*/.build
directories, and then creates the final saved Terraform plan. The final plan
therefore hashes the clean release source while the referenced ZIPs already
exist. Apply reuses those exact packages, so neither missing files nor changed
source hashes can invalidate the saved plan. Source changes still produce and
deploy new content-addressed archives.
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 |
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.
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. Bedrock API keys are regional; enable the OpenAI models in that 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.