From 80943d4c31cf027edd1711e4d80b4f4ace5662d3 Mon Sep 17 00:00:00 2001 From: ayushtr-aws Date: Tue, 25 Aug 2026 09:34:07 -0400 Subject: [PATCH 1/3] feat(workshop): predeploy ABCA for Workshop Studio --- manage-workshop-stack.sh | 285 ++++++++++++++++++++++++++++++++------- 1 file changed, 237 insertions(+), 48 deletions(-) diff --git a/manage-workshop-stack.sh b/manage-workshop-stack.sh index 92915649..b4097345 100755 --- a/manage-workshop-stack.sh +++ b/manage-workshop-stack.sh @@ -1,73 +1,262 @@ -#!/bin/bash +#!/usr/bin/env bash # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 # -# Workshop Studio custom-bootstrap entry point (smoke test). -# -# Per the Workshop Studio cookbook "Bootstrapping new Workshop Studio AWS -# Accounts using a custom bash script", the reusable WorkshopStack.yaml -# CodeBuild project clones this repo at RepoUrl@RepoBranchName and runs: -# -# ./manage-workshop-stack.sh -# -# This is the SIMPLEST possible version: it just creates (and deletes) one -# S3 bucket, to prove the ABCA repo can be bootstrapped end-to-end in the -# workshop environment before we layer in the real deploy. -# -# Template parameters are passed in as environment variables: -# PARTICIPANT_ROLE_ARN, PARTICIPANT_ASSUMED_ROLE_ARN, -# ASSETS_BUCKET_NAME, ASSETS_BUCKET_PREFIX, IS_WORKSHOP_STUDIO_ENV +# Workshop Studio custom-bootstrap entry point. +# Runs in the privileged CodeBuild project provisioned by WorkshopStack.yaml. -set -uo pipefail +set -euo pipefail -STACK_OPERATION=$(echo "${1:-}" | tr '[:upper:]' '[:lower:]') - -# Region CodeBuild runs in (falls back through the usual AWS env vars). +STACK_OPERATION=$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]') REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-us-east-1}}" +STACK_NAME="${ABCA_STACK_NAME:-backgroundagent-dev}" +BLUEPRINT_REPO="${BLUEPRINT_REPO:-aws-samples/sample-abca-playground}" +PARTICIPANT_USERNAME="${PARTICIPANT_USERNAME:-participant@workshop.local}" +PARTICIPANT_SECRET_NAME="${PARTICIPANT_SECRET_NAME:-abca-workshop/participant-credentials}" + +export AWS_REGION="$REGION" +export AWS_DEFAULT_REGION="$REGION" +export BLUEPRINT_REPO +export MISE_EXPERIMENTAL=1 -# Deterministic, account-scoped bucket name so create is idempotent and delete -# can always find it again. Bucket names must be globally unique + lowercase. -ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) -BUCKET_NAME="abca-workshop-smoke-${ACCOUNT_ID}-${REGION}" +install_toolchain() { + if [[ ! -x "${HOME}/.local/bin/mise" ]]; then + curl -fsSL https://mise.run | sh + fi + + eval "$("${HOME}/.local/bin/mise" activate bash)" + mise use --global node@22 + mise install + + corepack enable + corepack prepare yarn@1.22.22 --activate + + yarn install --frozen-lockfile +} + +prepare_arm64_builder() { + docker info >/dev/null + docker run --privileged --rm tonistiigi/binfmt --install arm64 >/dev/null +} -create_stack() { - echo "Creating smoke-test bucket: ${BUCKET_NAME} (${REGION})" +stack_output() { + local output_key="$1" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --region "$REGION" \ + --query "Stacks[0].Outputs[?OutputKey=='${output_key}'].OutputValue | [0]" \ + --output text +} - # Already own it? Nothing to do -> keeps create/update idempotent + retryable. - if aws s3api head-bucket --bucket "${BUCKET_NAME}" 2>/dev/null; then - echo "Bucket already exists; nothing to do." - return 0 +read_source_github_token() { + if [[ -z "${GITHUB_TOKEN_SOURCE_SECRET_ARN:-}" ]]; then + echo "GITHUB_TOKEN_SOURCE_SECRET_ARN is required" >&2 + return 1 fi - # us-east-1 rejects a LocationConstraint; every other region requires one. - if [[ "${REGION}" == "us-east-1" ]]; then - aws s3api create-bucket --bucket "${BUCKET_NAME}" --region "${REGION}" + local secret_value + if [[ -n "${GITHUB_TOKEN_BROKER_ROLE_ARN:-}" ]]; then + local credentials access_key secret_key session_token + credentials=$(aws sts assume-role \ + --role-arn "$GITHUB_TOKEN_BROKER_ROLE_ARN" \ + --role-session-name abca-workshop-token-seed \ + --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \ + --output text) + read -r access_key secret_key session_token <<<"$credentials" + + secret_value=$( + AWS_ACCESS_KEY_ID="$access_key" \ + AWS_SECRET_ACCESS_KEY="$secret_key" \ + AWS_SESSION_TOKEN="$session_token" \ + aws secretsmanager get-secret-value \ + --secret-id "$GITHUB_TOKEN_SOURCE_SECRET_ARN" \ + --query SecretString \ + --output text + ) else - aws s3api create-bucket --bucket "${BUCKET_NAME}" --region "${REGION}" \ - --create-bucket-configuration LocationConstraint="${REGION}" + secret_value=$(aws secretsmanager get-secret-value \ + --secret-id "$GITHUB_TOKEN_SOURCE_SECRET_ARN" \ + --query SecretString \ + --output text) + fi + + if [[ "$secret_value" == \{* ]]; then + secret_value=$(jq -r '.token // .github_token // empty' <<<"$secret_value") fi - echo "Created bucket ${BUCKET_NAME}." + if [[ -z "$secret_value" || "$secret_value" == "null" ]]; then + echo "The source GitHub token secret is empty" >&2 + return 1 + fi + + printf '%s' "$secret_value" } -delete_stack() { - echo "Deleting smoke-test bucket: ${BUCKET_NAME}" +seed_github_token() { + local target_secret_arn github_token + target_secret_arn=$(stack_output GitHubTokenSecretArn) + github_token=$(read_source_github_token) - if ! aws s3api head-bucket --bucket "${BUCKET_NAME}" 2>/dev/null; then - echo "Bucket does not exist; nothing to delete." - return 0 + printf '%s' "$github_token" | + aws secretsmanager put-secret-value \ + --secret-id "$target_secret_arn" \ + --secret-string file:///dev/stdin \ + >/dev/null +} + +ensure_participant_secret() { + local password secret_json + + if aws secretsmanager describe-secret \ + --secret-id "$PARTICIPANT_SECRET_NAME" \ + --region "$REGION" \ + >/dev/null 2>&1; then + return fi - # Empty then remove; --force handles a non-empty bucket in one shot. - aws s3 rb "s3://${BUCKET_NAME}" --force - echo "Deleted bucket ${BUCKET_NAME}." + password="Wksp!$(openssl rand -hex 16)Aa9" + secret_json=$(jq -cn \ + --arg username "$PARTICIPANT_USERNAME" \ + --arg password "$password" \ + '{username: $username, password: $password}') + + printf '%s' "$secret_json" | + aws secretsmanager create-secret \ + --name "$PARTICIPANT_SECRET_NAME" \ + --description "ABCA Workshop Studio participant login" \ + --secret-string file:///dev/stdin \ + --region "$REGION" \ + >/dev/null } -case "${STACK_OPERATION}" in - create|update) create_stack ;; - delete) delete_stack ;; +ensure_participant_user() { + local user_pool_id secret_json username password + user_pool_id=$(stack_output UserPoolId) + secret_json=$(aws secretsmanager get-secret-value \ + --secret-id "$PARTICIPANT_SECRET_NAME" \ + --region "$REGION" \ + --query SecretString \ + --output text) + username=$(jq -r '.username' <<<"$secret_json") + password=$(jq -r '.password' <<<"$secret_json") + + if ! aws cognito-idp admin-get-user \ + --user-pool-id "$user_pool_id" \ + --username "$username" \ + --region "$REGION" \ + >/dev/null 2>&1; then + aws cognito-idp admin-create-user \ + --user-pool-id "$user_pool_id" \ + --username "$username" \ + --temporary-password "$password" \ + --message-action SUPPRESS \ + --user-attributes \ + Name=email,Value="$username" \ + Name=email_verified,Value=true \ + --region "$REGION" \ + >/dev/null + fi + + aws cognito-idp admin-set-user-password \ + --user-pool-id "$user_pool_id" \ + --username "$username" \ + --password "$password" \ + --permanent \ + --region "$REGION" \ + >/dev/null +} + +deploy_stack() { + install_toolchain + prepare_arm64_builder + + pushd cdk >/dev/null + npx cdk bootstrap \ + "aws://$(aws sts get-caller-identity --query Account --output text)/${REGION}" + npx cdk deploy "$STACK_NAME" \ + --require-approval never \ + --context "blueprintRepo=${BLUEPRINT_REPO}" + popd >/dev/null + + seed_github_token + ensure_participant_secret + ensure_participant_user +} + +delete_participant_secret() { + aws secretsmanager delete-secret \ + --secret-id "$PARTICIPANT_SECRET_NAME" \ + --force-delete-without-recovery \ + --region "$REGION" \ + >/dev/null 2>&1 || true +} + +delete_bootstrap_stack() { + local bucket_name repository_name + + bucket_name=$(aws cloudformation describe-stacks \ + --stack-name CDKToolkit \ + --region "$REGION" \ + --query "Stacks[0].Outputs[?OutputKey=='BucketName'].OutputValue | [0]" \ + --output text 2>/dev/null || true) + repository_name=$(aws cloudformation describe-stacks \ + --stack-name CDKToolkit \ + --region "$REGION" \ + --query "Stacks[0].Outputs[?OutputKey=='ContainerAssetsRepositoryName'].OutputValue | [0]" \ + --output text 2>/dev/null || true) + + if [[ -n "$bucket_name" && "$bucket_name" != "None" ]]; then + aws s3 rm "s3://${bucket_name}" --recursive --region "$REGION" || true + fi + if [[ -n "$repository_name" && "$repository_name" != "None" ]]; then + aws ecr delete-repository \ + --repository-name "$repository_name" \ + --force \ + --region "$REGION" \ + >/dev/null 2>&1 || true + fi + + if aws cloudformation describe-stacks \ + --stack-name CDKToolkit \ + --region "$REGION" \ + >/dev/null 2>&1; then + aws cloudformation delete-stack --stack-name CDKToolkit --region "$REGION" + aws cloudformation wait stack-delete-complete \ + --stack-name CDKToolkit \ + --region "$REGION" + fi + + if [[ -n "$bucket_name" && "$bucket_name" != "None" ]]; then + aws s3api delete-bucket --bucket "$bucket_name" --region "$REGION" \ + >/dev/null 2>&1 || true + fi +} + +destroy_stack() { + install_toolchain + + if aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --region "$REGION" \ + >/dev/null 2>&1; then + pushd cdk >/dev/null + npx cdk destroy "$STACK_NAME" --force + popd >/dev/null + fi + + delete_participant_secret + delete_bootstrap_stack +} + +case "$STACK_OPERATION" in + create|update) + deploy_stack + ;; + delete) + destroy_stack + ;; *) - echo "Invalid stack operation: '${STACK_OPERATION}' (expected create|update|delete)" + echo "Invalid stack operation: '$STACK_OPERATION' (expected create|update|delete)" >&2 exit 1 ;; esac From 2b25a3a5fdfef7039f41528c2823f8ac9270e26f Mon Sep 17 00:00:00 2001 From: ayushtr-aws Date: Tue, 25 Aug 2026 09:37:34 -0400 Subject: [PATCH 2/3] fix(workshop): prefer native arm CodeBuild --- manage-workshop-stack.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manage-workshop-stack.sh b/manage-workshop-stack.sh index b4097345..b5f545c6 100755 --- a/manage-workshop-stack.sh +++ b/manage-workshop-stack.sh @@ -36,7 +36,9 @@ install_toolchain() { prepare_arm64_builder() { docker info >/dev/null - docker run --privileged --rm tonistiigi/binfmt --install arm64 >/dev/null + if [[ "$(uname -m)" != "aarch64" ]]; then + docker run --privileged --rm tonistiigi/binfmt --install arm64 >/dev/null + fi } stack_output() { From 27807b798174935e3ece0534929ebf24621767af Mon Sep 17 00:00:00 2001 From: ayushtr-aws Date: Tue, 25 Aug 2026 09:39:21 -0400 Subject: [PATCH 3/3] perf(workshop): install only deploy dependencies --- manage-workshop-stack.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/manage-workshop-stack.sh b/manage-workshop-stack.sh index b5f545c6..26512342 100755 --- a/manage-workshop-stack.sh +++ b/manage-workshop-stack.sh @@ -26,7 +26,6 @@ install_toolchain() { eval "$("${HOME}/.local/bin/mise" activate bash)" mise use --global node@22 - mise install corepack enable corepack prepare yarn@1.22.22 --activate