Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
71 changes: 67 additions & 4 deletions infra/scripts/deploy/java-on-amazon-eks/app-deploy.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,68 @@
#!/bin/bash
# java-on-amazon-eks runs the java-performance implementation of this step: the two workshops
# share the app, the sensor and the profiler, and one copy keeps a fix in one place. Replace
# this wrapper with an own script when the workshops diverge.
exec bash "$(dirname "${BASH_SOURCE[0]}")/../java-performance/$(basename "${BASH_SOURCE[0]}")" "$@"
# =============================================================================
# app-deploy.sh — deploy the participant's starting point to the cluster.
#
# Applies what ~/environment/unicorn-store-spring/k8s holds (written by app-prepare.sh):
# namespace + ServiceAccount, Pod Identity association to unicornstore-eks-pod-role,
# SecretProviderClass, Deployment, Service, Ingress. Waits for the Deployment to be
# Available. The ALB takes ~2 more minutes to provision; verify.sh checks it, so the
# bootstrap can install the platform pieces meanwhile.
#
# Needs kubectl against the cluster and the :latest image (app-prepare.sh). Idempotent.
# =============================================================================
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../lib/common.sh"
[ -f /etc/profile.d/workshop.sh ] && source /etc/profile.d/workshop.sh

APP_NAME="unicorn-store-spring"
NS="${APP_NAME}"
K8S="${HOME}/environment/${APP_NAME}/k8s"
CLUSTER_NAME="${PREFIX:-workshop}-eks"
POD_ROLE="unicornstore-eks-pod-role"

[ -f "${K8S}/deployment.yaml" ] || { log_error "${K8S}/deployment.yaml missing — run app-prepare.sh first"; exit 1; }

log_info "Namespace + ServiceAccount ${NS}..."
kubectl create namespace "${NS}" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
kubectl -n "${NS}" create serviceaccount "${APP_NAME}" --dry-run=client -o yaml | kubectl apply -f - >/dev/null

log_info "Pod Identity: ${NS}/${APP_NAME} -> ${POD_ROLE}..."
if ! aws eks list-pod-identity-associations --cluster-name "${CLUSTER_NAME}" \
--query "associations[?serviceAccount=='${APP_NAME}' && namespace=='${NS}']" --output text --no-cli-pager | grep -q .; then
aws eks create-pod-identity-association --cluster-name "${CLUSTER_NAME}" \
--namespace "${NS}" --service-account "${APP_NAME}" \
--role-arn "arn:aws:iam::${ACCOUNT_ID}:role/${POD_ROLE}" --no-cli-pager >/dev/null
log_success "Pod Identity association created"
# The Pod Identity webhook only mutates pods created after the association has propagated;
# a pod created earlier gets no AWS_* env and crash-loops ("Unable to load region").
sleep 15
else
log_info "Pod Identity association already exists"
fi

log_info "Applying manifests from ${K8S}..."
kubectl apply -f "${K8S}/secret-provider-class.yaml" >/dev/null
kubectl apply -f "${K8S}/deployment.yaml" -f "${K8S}/service.yaml" -f "${K8S}/ingress.yaml" >/dev/null

# Self-heal the same race if propagation took longer than the sleep: once a pod exists,
# check that the webhook injected the Pod Identity env; if not, recreate the pod once.
pod_env=""
for _ in $(seq 1 30); do
pod_env=$(kubectl -n "${NS}" get pods -l app="${APP_NAME}" \
-o jsonpath="{.items[*].spec.containers[?(@.name=='${APP_NAME}')].env[*].name}" 2>/dev/null || true)
kubectl -n "${NS}" get pods -l app="${APP_NAME}" -o name 2>/dev/null | grep -q . && break
sleep 2
done
if ! grep -q AWS_CONTAINER_CREDENTIALS_FULL_URI <<<"${pod_env}"; then
log_warning "Pod has no Pod Identity env (association not yet propagated) — restarting the Deployment once"
sleep 15
kubectl -n "${NS}" rollout restart deployment "${APP_NAME}" >/dev/null
fi

kubectl -n "${NS}" wait deployment "${APP_NAME}" --for condition=Available=True --timeout=300s >/dev/null \
|| { log_error "Deployment not Available:"; kubectl -n "${NS}" get pods -o wide; kubectl -n "${NS}" describe pod -l app="${APP_NAME}" | tail -30; exit 1; }
log_success "Deployment ${NS}/${APP_NAME} Available (ALB provisioning continues in the background)"

echo "✅ Success: unicorn-store-spring deployed to EKS"
78 changes: 74 additions & 4 deletions infra/scripts/deploy/java-on-amazon-eks/app-prepare.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,75 @@
#!/bin/bash
# java-on-amazon-eks runs the java-performance implementation of this step: the two workshops
# share the app, the sensor and the profiler, and one copy keeps a fix in one place. Replace
# this wrapper with an own script when the workshops diverge.
exec bash "$(dirname "${BASH_SOURCE[0]}")/../java-performance/$(basename "${BASH_SOURCE[0]}")" "$@"
# =============================================================================
# app-prepare.sh — the participant's starting point, without a cluster.
#
# Produces ~/environment/unicorn-store-spring at the commit
# "Starting point: containerized + deployed to EKS" and the :latest image in ECR:
# 1. copy apps/unicorn-store-spring, drop src/test, de-spoil (remove the org.crac
# dependency and the finished CRaC hook UnicornPublisher.crac — the CRaC step is
# discovered in the session, not handed over)
# 2. lay the end state of the immersion-day containerize + deploy pages over it
# (app/Dockerfile, app/k8s/*.yaml with the account's ECR URI filled in)
# 3. build and push the image (:latest)
# 4. git init, one "Initial commit" (sources), one "Starting point" commit
# (Dockerfile + manifests) — the same two commits the replayed labs produced
#
# Needs Docker, Maven, ECR and the IDE role; no kubectl. Runs in the bootstrap while
# the EKS cluster is still being created. Idempotent: an existing copy is replaced.
# =============================================================================
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../lib/common.sh"
[ -f /etc/profile.d/workshop.sh ] && source /etc/profile.d/workshop.sh
LOG="${WORKSHOP_LOG_DIR}/app-prepare.log"

REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../.." && pwd)"
APP_NAME="unicorn-store-spring"
SRC="${REPO_ROOT}/apps/${APP_NAME}"
OVERLAY="${SCRIPT_DIR}/app"
DST="${HOME}/environment/${APP_NAME}"
ECR_REGISTRY="${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
ECR_URI="${ECR_REGISTRY}/${APP_NAME}"
START_MSG="Starting point: containerized + deployed to EKS"

git config --global user.name >/dev/null 2>&1 || git config --global user.name "Workshop User"
git config --global user.email >/dev/null 2>&1 || git config --global user.email "user@sample.com"

# 1. Sources
log_info "Preparing ${DST} from ${SRC}..."
rm -rf "${DST}"
mkdir -p "${HOME}/environment"
cp -r "${SRC}" "${DST}"
rm -rf "${DST}/src/test" "${DST}/target"
# De-spoil: the whole <dependencies> block that carries org.crac (only crac lives there).
perl -0777 -i -pe 's{\s*<dependencies>\s*<dependency>\s*<groupId>org\.crac</groupId>.*?</dependencies>}{}s' "${DST}/pom.xml"
find "${DST}/src" -name '*.crac' -delete
( cd "${DST}" && git init -q -b main && git add -A && git commit -q -m "Initial commit" )
log_success "Sources copied, de-spoiled, initial commit made"

# 2. Containerize + deploy end state
cp "${OVERLAY}/Dockerfile" "${DST}/Dockerfile"
mkdir -p "${DST}/k8s"
for f in "${OVERLAY}"/k8s/*.yaml; do
sed "s|__ECR_URI__|${ECR_URI}|g" "$f" > "${DST}/k8s/$(basename "$f")"
done
log_success "Dockerfile + k8s manifests in place (image ${ECR_URI}:latest)"

# 3. Image
log_info "Ensuring ECR repository ${APP_NAME} exists..."
aws ecr describe-repositories --repository-names "${APP_NAME}" --region "${AWS_REGION}" >/dev/null 2>&1 \
|| aws ecr create-repository --repository-name "${APP_NAME}" --region "${AWS_REGION}" >/dev/null
aws ecr get-login-password --region "${AWS_REGION}" \
| docker login --username AWS --password-stdin "${ECR_REGISTRY}" >/dev/null 2>&1
log_info "Building ${APP_NAME}:latest (full log: ${LOG})..."
( cd "${DST}" && docker build -t "${ECR_URI}:latest" . ) >>"${LOG}" 2>&1 \
|| { log_error "docker build failed — last 40 lines of ${LOG}:"; tail -n 40 "${LOG}"; exit 1; }
docker push "${ECR_URI}:latest" >>"${LOG}" 2>&1 \
|| { log_error "docker push failed — last 40 lines of ${LOG}:"; tail -n 40 "${LOG}"; exit 1; }
log_success "Pushed ${ECR_URI}:latest"

# 4. Starting-point commit
( cd "${DST}" && git add -A && git commit -q -m "${START_MSG}" )
log_success "Committed '${START_MSG}'"

echo "✅ Success: unicorn-store-spring starting point (sources, image, manifests)"
124 changes: 120 additions & 4 deletions infra/scripts/deploy/java-on-amazon-eks/claude-code.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,121 @@
#!/bin/bash
# java-on-amazon-eks runs the java-performance implementation of this step: the two workshops
# share the app, the sensor and the profiler, and one copy keeps a fix in one place. Replace
# this wrapper with an own script when the workshops diverge.
exec bash "$(dirname "${BASH_SOURCE[0]}")/../java-performance/$(basename "${BASH_SOURCE[0]}")" "$@"
# =============================================================================
# claude-code.sh — configure Claude Code on the IDE for this workshop: the skill pack,
# the workspace MCP config (perf-sensor + EKS MCP Server) and the project permissions,
# so Claude Code started from ~/environment finds all three. No cluster needed.
#
# Copies the two skills to ~/environment/.claude/skills/, writes ~/environment/.mcp.json
# (perf-sensor over streamable-http on localhost:8090, the IDE's port-forward service;
# eks-mcp read-only) and the project permissions. Idempotent.
#
# EKS MCP Server: the awslabs eks-mcp-server over local stdio, pinned to a version,
# WITHOUT --allow-write (the server refuses mutations). Its tool list still shows the
# write tools (apply_yaml, manage_k8s_resource, ...), so the permissions below allow
# only the read tools by name; anything else prompts.
# =============================================================================
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../.." && pwd)"
SKILLS_SRC="${REPO_ROOT}/apps/perf-sensor/skills"
ENV_DIR="${ENVIRONMENT_DIR:-${HOME}/environment}"
SKILLS_DST="${ENV_DIR}/.claude/skills"
REGION="${AWS_REGION:-us-east-1}"
NS="monitoring"
EKS_MCP_VERSION="${EKS_MCP_VERSION:-0.2.1}" # pinned; bump deliberately (ide/tools.sh prewarms the same version)

# 1. Skills -> ~/environment/.claude/skills/
mkdir -p "${SKILLS_DST}"
for skill in java-on-eks-optimization java-on-eks-checklist; do
rm -rf "${SKILLS_DST:?}/${skill}"
cp -R "${SKILLS_SRC}/${skill}" "${SKILLS_DST}/${skill}"
echo "installed skill: ${SKILLS_DST}/${skill}"
done

# 2. ~/environment/.mcp.json
cat > "${ENV_DIR}/.mcp.json" <<EOF
{
"mcpServers": {
"perf-sensor": {
"type": "streamable-http",
"url": "http://localhost:8090/mcp"
},
"eks-mcp": {
"command": "uvx",
"args": [
"awslabs.eks-mcp-server@${EKS_MCP_VERSION}",
"--allow-sensitive-data-access"
],
"env": {
"AWS_REGION": "${REGION}",
"FASTMCP_LOG_LEVEL": "ERROR"
}
}
}
}
EOF
echo "wrote ${ENV_DIR}/.mcp.json"

# Auto-approve THIS workshop's two .mcp.json servers so the first `claude` in ~/environment
# doesn't show the "new MCP servers found — Enable" prompt. Project-scoped (not global): only
# perf-sensor + eks-mcp, only for this workspace. Relies on folder trust pre-accepted by
# ide/tools.sh. Merge so we don't clobber any existing project settings.
#
# permissions.allow: the read-only sensor (every perf-sensor tool), the eks-mcp READ tools by
# name, file reads, and edits — so the investigate -> explain -> apply loop runs without
# prompts. permissions.deny closes the ways the agent could widen its own scope: Bash
# (the participant rolls out; the load runs in its own terminal), edits to the Claude
# configuration itself (.claude/**, .mcp.json) or to .git/** — Edit(path) rules cover the
# Write tool too — and Agent: a subagent answers out of the skill's sight, and its
# hand-off text ("I'll be notified when the agent completes") ended up in an answer.
# Everything not listed prompts.
python3 - "${ENV_DIR}/.claude/settings.json" <<'PY'
import json, os, sys
path = sys.argv[1]
data = {}
if os.path.exists(path):
try:
with open(path) as f:
data = json.load(f)
except Exception:
data = {}
servers = set(data.get("enabledMcpjsonServers", []))
servers.update(["perf-sensor", "eks-mcp"])
data["enabledMcpjsonServers"] = sorted(servers)
perms = data.setdefault("permissions", {})
allow = [r for r in perms.get("allow", []) if r != "mcp__eks-mcp"] # replace the blanket eks-mcp grant
for rule in ["mcp__perf-sensor",
"mcp__eks-mcp__list_k8s_resources", "mcp__eks-mcp__get_pod_logs", "mcp__eks-mcp__get_k8s_events",
"mcp__eks-mcp__get_eks_insights", "mcp__eks-mcp__get_eks_metrics_guidance",
"mcp__eks-mcp__get_cloudwatch_logs", "mcp__eks-mcp__get_cloudwatch_metrics",
"mcp__eks-mcp__search_eks_troubleshoot_guide", "mcp__eks-mcp__list_api_versions",
"Read", "Grep", "Glob", "Edit", "Write"]:
if rule not in allow:
allow.append(rule)
perms["allow"] = allow
# Path deny rules are Edit(path) only: they cover every file-editing tool, and Claude Code
# rejects Write(path) rules at startup. Drop any Write(...) entries left by an earlier run.
deny = [r for r in perms.get("deny", []) if not r.startswith("Write(")]
for rule in ["Bash", "Agent",
"Edit(.claude/**)", "Edit(.mcp.json)", "Edit(**/.git/**)",
"mcp__eks-mcp__apply_yaml", "mcp__eks-mcp__manage_k8s_resource", "mcp__eks-mcp__manage_eks_stacks",
"mcp__eks-mcp__add_inline_policy", "mcp__eks-mcp__generate_app_manifest"]:
if rule not in deny:
deny.append(rule)
perms["deny"] = deny
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
json.dump(data, f, indent=2)
PY
echo "wrote ${ENV_DIR}/.claude/settings.json (MCP servers enabled; allow: sensor, eks-mcp read tools, read, edit; deny: Bash, Agent, config edits, eks-mcp write tools)"

# No workspace CLAUDE.md: the app repo documents itself (unicorn-store-spring/CLAUDE.md).
# --allow-sensitive-data-access is required for get_pod_logs / get_k8s_events (read-only;
# --allow-write is NOT passed, so the server refuses mutations even if a write tool is
# called). Auth uses the IDE role (default iam mode). uv/uvx and the eks-mcp package are
# installed and prewarmed by ide/tools.sh (install_uv) during base bootstrap.

echo
echo "Next:"
echo " # the sensor is on localhost:8090 (systemd perf-sensor-port-forward, from perf-sensor.sh install)"
echo " cd ${ENV_DIR} && claude # skills + .mcp.json are picked up from here"
47 changes: 43 additions & 4 deletions infra/scripts/deploy/java-on-amazon-eks/ide-layout.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
#!/bin/bash
# java-on-amazon-eks runs the java-performance implementation of this step: the two workshops
# share the app, the sensor and the profiler, and one copy keeps a fix in one place. Replace
# this wrapper with an own script when the workshops diverge.
exec bash "$(dirname "${BASH_SOURCE[0]}")/../java-performance/$(basename "${BASH_SOURCE[0]}")" "$@"
# =============================================================================
# ide-layout.sh — the two terminals of the session, set at bootstrap, nothing in
# ~/environment: the terminal in the editor area (Code Editor's startup editor, a zsh in
# ~/environment) is where Claude Code runs; a user-level task opens the Shell terminal in
# the bottom panel each time the IDE opens.
#
# Writes ~/.code-editor-server/data/User/tasks.json (task "Shell", runOn folderOpen) and
# sets "task.allowAutomaticTasks": "on" in the user settings (without it Code Editor asks
# before running the task) and "terminal.integrated.environmentChangesIndicator": "off" (the
# Claude Code extension asks to relaunch every terminal Claude runs in, which otherwise marks
# the tab with a warning badge for the rest of the session). Runs as ec2-user after
# ide/code-editor.sh. Idempotent.
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../lib/common.sh"

USER_DIR="${HOME}/.code-editor-server/data/User"
SETTINGS="${USER_DIR}/settings.json"
[ -f "${SETTINGS}" ] || { log_error "no Code Editor user settings at ${SETTINGS} (ide/code-editor.sh)"; exit 1; }

tmp="$(mktemp)"
jq '. + {"workbench.startupEditor": "terminal", "task.allowAutomaticTasks": "on",
"terminal.integrated.environmentChangesIndicator": "off"}' "${SETTINGS}" > "${tmp}"
mv "${tmp}" "${SETTINGS}"

cat > "${USER_DIR}/tasks.json" <<'JSON'
{
"version": "2.0.0",
"tasks": [
{
"label": "Shell",
"type": "shell",
"command": "exec zsh -l",
"isBackground": true,
"problemMatcher": [],
"presentation": {"reveal": "always", "panel": "dedicated", "focus": false, "showReuseMessage": false, "echo": false},
"runOptions": {"runOn": "folderOpen"}
}
]
}
JSON
log_success "IDE layout: Claude Code terminal in the editor area, Shell terminal in the panel"
10 changes: 8 additions & 2 deletions infra/scripts/deploy/java-on-amazon-eks/nodepool-pin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
# (e.g. m8azn at 5 GHz) and startup "improves" from hardware, not from the change.
# Family, not type, keeps capacity flexible across sizes.
#
# Consolidation: WhenEmpty only. The pool carries just the app (platform-placement.sh), and
# one m6a.xlarge fits every state of the session, rollout surge and boosted start included.
# WhenEmptyOrUnderutilized (the eks.sh default) would replace the node when the app shrinks
# and move the pod mid-measurement; WhenEmpty only removes a node nothing runs on.
#
# Replaces the three scheduling requirements (cpu-manufacturer, instance-category,
# instance-generation) with one (instance-family); capacity-type, arch, os, cpu and
# memory requirements stay. Idempotent. Runs after setup/eks.sh.
Expand Down Expand Up @@ -38,7 +43,8 @@ PATCH="$(kubectl get nodepool "${NODEPOOL}" -o json | jq -c --arg fam "${INSTANC
"eks.amazonaws.com/instance-generation",
"eks.amazonaws.com/instance-family") | not))
+ [{"key":"eks.amazonaws.com/instance-family","operator":"In","values":[$fam]}]
| {spec:{template:{spec:{requirements:.}}}}')"
| {spec:{template:{spec:{requirements:.}},
disruption:{consolidationPolicy:"WhenEmpty",consolidateAfter:"5m"}}}')"
kubectl patch nodepool "${NODEPOOL}" --type merge -p "${PATCH}" >/dev/null \
&& log_success "NodePool ${NODEPOOL} pinned to ${INSTANCE_FAMILY} (existing nodes drain on consolidation; new nodes are ${INSTANCE_FAMILY})" \
&& log_success "NodePool ${NODEPOOL} pinned to ${INSTANCE_FAMILY}, consolidation WhenEmpty (existing nodes drain on consolidation; new nodes are ${INSTANCE_FAMILY})" \
|| { log_error "NodePool patch failed"; exit 1; }
Loading
Loading