Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
104 changes: 104 additions & 0 deletions web/scripts/bootstrap-preview-infra.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
#
# Bootstrap shared CloudFront resources for preview environments.
#
# Each `sst.aws.Nextjs` preview stage normally creates its own CachePolicy and
# KeyValueStore — those carry low per-account quotas (20 cache policies / 5 KV
# stores). This script creates ONE of each and writes their IDs to SSM so that
# every preview stage can reference them via `web/sst.config.ts`.
#
# Idempotent: re-running checks SSM first and only creates resources if missing.
#
# Usage:
# web/scripts/bootstrap-preview-infra.sh

set -euo pipefail

if [ -z "${AWS_ACCESS_KEY_ID:-}" ]; then
export AWS_PROFILE="${AWS_PROFILE:-ar_preview}"
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
export AWS_REGION="${AWS_REGION:-us-east-1}"
export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-$AWS_REGION}"

for bin in aws jq; do
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
command -v "$bin" >/dev/null || { echo "missing dependency: $bin" >&2; exit 1; }
done

CACHE_POLICY_NAME="relay-web-preview-shared"
KV_STORE_NAME="relay-web-preview-shared"
SSM_CACHE_POLICY_PARAM="/relay-web/preview/cache-policy-id"
SSM_KV_STORE_PARAM="/relay-web/preview/kv-store-arn"

echo "==> AWS_PROFILE=${AWS_PROFILE:-<unset>} AWS_REGION=$AWS_REGION"
aws sts get-caller-identity --query Account --output text

read_ssm() {
aws ssm get-parameter --name "$1" --query 'Parameter.Value' --output text 2>/dev/null || true
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

write_ssm() {
aws ssm put-parameter --name "$1" --value "$2" --type String --overwrite >/dev/null
}

# 1. Cache policy. Config matches SST's internal default for sst.aws.Nextjs
# (cookies=none, query strings=all, headers=x-open-next-cache-key + x-forwarded-host,
# brotli/gzip enabled, default ttl 0, max 1 year).
existing_policy_id="$(read_ssm "$SSM_CACHE_POLICY_PARAM")"
if [ -n "$existing_policy_id" ] && aws cloudfront get-cache-policy --id "$existing_policy_id" >/dev/null 2>&1; then
Comment on lines +62 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse existing cache policy when SSM parameter is missing

This idempotency check only trusts the SSM value, so if the first run creates the policy but fails before put-parameter (or if the SSM param is later deleted), a rerun will execute create-cache-policy with the same fixed name and fail. AWS CloudFront requires cache policy names to be unique (CachePolicyAlreadyExists), so the script cannot recover automatically from partial failures even though it is documented as safe to rerun.

Useful? React with 👍 / 👎.

echo "==> Cache policy already exists: $existing_policy_id"
cache_policy_id="$existing_policy_id"
else
echo "==> Creating shared cache policy $CACHE_POLICY_NAME"
cache_policy_config=$(cat <<'JSON'
{
"Name": "relay-web-preview-shared",
"Comment": "Shared SST server response cache policy for relay-web preview stages",
"DefaultTTL": 0,
"MinTTL": 0,
"MaxTTL": 31536000,
"ParametersInCacheKeyAndForwardedToOrigin": {
"EnableAcceptEncodingGzip": true,
"EnableAcceptEncodingBrotli": true,
"HeadersConfig": {
"HeaderBehavior": "whitelist",
"Headers": {
"Quantity": 2,
"Items": ["x-open-next-cache-key", "x-forwarded-host"]
}
},
"CookiesConfig": { "CookieBehavior": "none" },
"QueryStringsConfig": { "QueryStringBehavior": "all" }
}
}
JSON
)
cache_policy_id="$(aws cloudfront create-cache-policy \
--cache-policy-config "$cache_policy_config" \
--query 'CachePolicy.Id' --output text)"
echo " created cache policy: $cache_policy_id"
write_ssm "$SSM_CACHE_POLICY_PARAM" "$cache_policy_id"
fi

# 2. KV store. SST namespaces keys by md5(app + stage + componentName) so sharing
# the store across stages is safe — entries don't collide.
existing_kv_arn="$(read_ssm "$SSM_KV_STORE_PARAM")"
if [ -n "$existing_kv_arn" ] && aws cloudfront describe-key-value-store --kvs-arn "$existing_kv_arn" >/dev/null 2>&1; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Wrong CLI flag --kvs-arn for describe-key-value-store breaks script idempotency

The AWS CLI command aws cloudfront describe-key-value-store accepts --name (which can be a name or an ARN), not --kvs-arn. The --kvs-arn flag is only valid for data-plane KVS operations like list-keys-in-key-value-store, get-key-in-key-value-store, etc. Because --kvs-arn is unrecognized, the command on line 85 always fails (silently, due to 2>&1), causing the if to evaluate false and the script to fall into the else branch. On a re-run where the KV store already exists, create-key-value-store will fail with an AlreadyExists conflict, and set -euo pipefail will abort the script. This directly breaks the script's stated idempotency guarantee.

Suggested change
if [ -n "$existing_kv_arn" ] && aws cloudfront describe-key-value-store --kvs-arn "$existing_kv_arn" >/dev/null 2>&1; then
if [ -n "$existing_kv_arn" ] && aws cloudfront describe-key-value-store --name "$existing_kv_arn" >/dev/null 2>&1; then
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

echo "==> KV store already exists: $existing_kv_arn"
kv_store_arn="$existing_kv_arn"
else
echo "==> Creating shared KV store $KV_STORE_NAME"
kv_store_arn="$(aws cloudfront create-key-value-store \
--name "$KV_STORE_NAME" \
--comment "Shared KV store for relay-web preview stages" \
--query 'KeyValueStore.ARN' --output text)"
echo " created KV store: $kv_store_arn"
write_ssm "$SSM_KV_STORE_PARAM" "$kv_store_arn"
fi

echo
echo "==> SSM parameters:"
echo " $SSM_CACHE_POLICY_PARAM = $cache_policy_id"
echo " $SSM_KV_STORE_PARAM = $kv_store_arn"
echo
echo "Preview deploys will now reuse these resources. Re-run after disaster"
echo "recovery or if AWS resources are manually deleted."
16 changes: 16 additions & 0 deletions web/sst.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ export default $config({
const NEXT_PUBLIC_POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST ?? 'https://i.agentrelay.com';
const NEXT_PUBLIC_POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY ?? '';

// Non-prod stages reuse a shared CloudFront cache policy + KV store so the
// per-account quotas (20 cache policies / 5 KV stores) don't cap how many
// preview deploys can exist concurrently. The IDs are written to SSM by
// web/scripts/bootstrap-preview-infra.sh; SST namespaces KV keys by stage
// so a shared store is safe.
const previewCachePolicyId = isProd
? undefined
: aws.ssm.getParameterOutput({ name: '/relay-web/preview/cache-policy-id' }).value;
const previewKvStoreArn = isProd
? undefined
: aws.ssm.getParameterOutput({ name: '/relay-web/preview/kv-store-arn' }).value;

new sst.aws.Nextjs('Web', {
path: '.',
openNextVersion: '3.9.16',
Expand All @@ -21,6 +33,10 @@ export default $config({
},
// Production deploys land on orgin.agentrelay.net; SEO canonicals are set in Next metadata.
domain: { name: domain, dns: sst.cloudflare.dns({ proxy: true }) },
...(previewCachePolicyId ? { cachePolicy: previewCachePolicyId } : {}),
...(previewKvStoreArn
? { edge: { viewerRequest: { kvStore: previewKvStoreArn, injection: '' } } }
: {}),
});
},
});
Loading