Skip to content

feat(aws-pcs): cluster-scoped Name tag + PCS-API login lookup in docs - #1183

Merged
DaisukeMiyamoto merged 6 commits into
awslabs:mainfrom
DaisukeMiyamoto:feat/cluster-scoped-name-and-lookup
Jul 14, 2026
Merged

DaisukeMiyamoto merged 6 commits into
awslabs:mainfrom
DaisukeMiyamoto:feat/cluster-scoped-name-and-lookup

Conversation

@DaisukeMiyamoto

Copy link
Copy Markdown
Collaborator

What

Two coupled changes so multi-cluster deployments in one account/VPC stop
colliding at the tag layer.

1. Cluster-scoped Name tag on every CNG instance

PCS-${CngName} → ${ClusterName}-${CngName} in all four hand-maintained
CNG UserData templates (add-cng, add-cng-p5, add-cng-p6-b200,
add-cng-p6-b300). Two deploy-all stacks in the same VPC no longer both
show PCS-login / PCS-cpu1 in the EC2 console; each is prefixed by
the stack name.

2. Docs derive LOGIN_INSTANCE_ID from the stack — by purpose

  • §6 Accessing the Cluster + docs/IAM.md (open a shell on the login
    CNG): resolve via the PCS API —
    CFN Outputs.ClusterId → pcs list-compute-node-groups (name==login)
    → ec2 describe-instances by aws:pcs:compute-node-group-id.
    Tag-naming conventions no longer matter.
  • §8.2 Monitoring (reach the monitoring stack): filter EC2 by
    tag:monitoring-role=login + tag:aws:pcs:cluster-id=<the cluster>.
    Same instance today but a different intent — the tag is what the
    monitoring stack itself uses to identify its host.

Every code block is self-contained so it can be copy-pasted as one unit
from GitHub — no cross-block variable threading.

Ancillary changes (in support of the above)

  • cluster-user-iam.yaml SSM scope updated in lock-step with (1) —
    ssm:resourceTag/Name: PCS-login* → *-login, so cluster-user's
    login-only SSM scope still resolves under the new Name tag. Same
    intent, adjusted for the new naming.
  • Two new tests/lint-docs.sh banned patterns to prevent regression
    of (2)
    — aws:pcs:compute-node-group-name (a tag PCS never emits;
    a doc filter using it returns zero rows silently) and
    Name=tag:Name,Values=PCS- (the old flat naming, superseded).
  • tests/readme-walkthrough-test.md (Test 15) — a pre-merge test
    that follows README §3 → §6 → §8.2 as written; every command must
    return a real value, Grafana must load and accept admin +
    SSM-retrieved password. Runs on every PR touching those sections or
    the tags/policies they depend on. Formalises the walkthrough
    performed for this PR's Testing section so the same class of
    copy-paste-first-step regression can't slip through again.

Testing (Test 15 executed on this PR's tree)

Deployed a fresh pcs-walkthrough cluster in us-east-2 from the modified
templates. Every README code block executed verbatim as one paste:

  • §6 login lookup returned i-06f3486a5eefaf3b0
  • New Name tag confirmed: pcs-walkthrough-login (was PCS-login)
  • §8.2 admin password retrieval returned a 32-char value from SSM
  • §8.2 Option A monitoring-role=login + aws:pcs:cluster-id lookup
    returned the same instance
  • §8.2 port-forward opened localhost:8443 → /grafana/ = 302,
    /grafana/api/health = 200 with {"database":"ok","version":"13.0.2"}
  • Basic auth with the SSM-retrieved password returned the admin user JSON
  • All 12 provisioned dashboards listed by the Grafana API (Cluster
    Summary, Slurm Detail, GPU Node List, GPU Health, Cluster Costs,
    Storage, …)

Also passes locally:

  • bash tests/lint-docs.sh
  • aws cloudformation validate-template on all four CNG templates plus
    cluster-user-iam.yaml

…E walkthrough test

Two coupled changes so multi-cluster deployments in one account/VPC stop
colliding, plus a walkthrough test so this class of docs regression fails
CI instead of the customer.

1) Cluster-scoped Name tag on every CNG instance
   PCS-${CngName} -> ${ClusterName}-${CngName}
   Applied lock-step in all four hand-maintained CNG templates (add-cng,
   add-cng-p5, add-cng-p6-b200, add-cng-p6-b300). Two deploy-all stacks
   in the same VPC no longer both show PCS-login / PCS-cpu1 in the EC2
   console.

2) cluster-user-iam.yaml SSM resourceTag scope
   "PCS-login*" -> "*-login"
   Same intent (login CNGs only, not compute) under the new Name tag.

3) Docs derive LOGIN_INSTANCE_ID from the stack via the PCS API, not tags
   README §6 (Accessing the Cluster), §8.2 (Grafana port-forward, admin
   password retrieval, public IP lookup) and docs/IAM.md now resolve the
   login node in one line:
     CFN Outputs.ClusterId -> pcs list-compute-node-groups (name=login)
       -> ec2 describe-instances by aws:pcs:compute-node-group-id
   Tag-naming conventions no longer matter to the docs; only the PCS API
   contract (which is stable) does. Every code block is self-contained so
   it can be copy-pasted as one unit from GitHub.

4) lint-docs.sh banned patterns
   - aws:pcs:compute-node-group-name (tag key that PCS never emits — a
     doc filter using it returns zero rows silently)
   - Name=tag:Name,Values=PCS- (the old flat naming, superseded)

5) tests/README-walkthrough-test.md (new, Test 15)
   Pre-merge test: follow README §3 -> §6 -> §8.2 as written, every
   command must return a real value, Grafana must load and accept
   admin+password. Catches the tag/CLI/permission drift that stalls a
   first-time user at the connect step.

Verified live on a fresh us-east-2 cluster (pcs-walkthrough): new Name
tag emitted (pcs-walkthrough-login), one-line login lookup returned the
correct instance id, SSM start-session opened, Grafana port-forward
opened, admin+SSM-retrieved password authenticated, and all twelve
provisioned dashboards (Cluster Summary, Slurm Detail, GPU Node List, …)
are listed by the Grafana API.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review Batch 1/3 — IAM & Access Scoping

"Condition": {
"StringLike": {
"ssm:resourceTag/Name": "PCS-login*"
"ssm:resourceTag/Name": "*-login"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ssm:resourceTag/Name: *-login is a leading-wildcard grant — it doesn't scope to the cluster, and it can match compute nodes — 🔴 (security) [confirmed]

Observation: The cluster-user SSM condition goes from PCS-login* (anchored prefix) to *-login (leading wildcard), while Resource stays arn:aws:ec2:*:*:instance/*. A leading * in StringLike matches any instance Name ending in -login, and cluster-user-iam.yaml has no ClusterName/ClusterId parameter to narrow it. So a cluster-user can ssm:StartSession (shell / port-forward / SSH-over-SSM) on:

  1. Every other cluster's login node — this PR names them <ClusterName>-login, so all of them match *-login. The change makes the Name cluster-unique but the policy discards that uniqueness.
  2. Any unrelated account instance ending in -login (bastion-login, vpn-login, …) — strictly broader than the old PCS-login*, which at least required the PCS- prefix.
  3. A compute CNG whose name ends in login (e.g. CNG data-login → Name=<cluster>-data-login) — this breaks the template's own stated guarantee ("cannot open shells on compute nodes"); the old PCS-login* prefix would not have matched it.

The IAM.md prose ("so any login CNG matches; compute/GPU CNGs do not", "the most stable signal available") reassures where it should warn — it omits the account-wide breadth.
Impact: In the very multi-cluster/one-VPC scenario this PR targets, the IAM layer provides no per-cluster isolation and can reach unrelated instances. I confirmed this with iam simulate-custom-policy on the exact statement — under *-login, ssm:StartSession is allowed on myclusterB-login (another cluster), bastion-login / vpn-login (unrelated), and myclusterA-data-login (a compute CNG ending in login), and correctly denied only on myclusterA-cpu1 / -gpu-p5. Every one of those four unintended allows was implicitDeny under the old PCS-login* — so this is a demonstrated widening, not a theoretical one. (Today's real blast radius is small — 1 *-login instance live in the test region — but the glob is unbounded by design.)
Suggestion: Scope by the AWS-managed, operator-immutable aws:pcs:cluster-id tag — the same lever the §8.2 monitoring path in this PR already uses. Add a ClusterId parameter to this template and AND both keys in one StringLike (keys AND together), so it's PCS-instances-of-this-cluster-whose-role-is-login:

"StringLike": {
  "ssm:resourceTag/aws:pcs:cluster-id": "<ClusterId param>",
  "ssm:resourceTag/Name": "*login*"
}

(cluster-id alone would expose compute nodes; Name alone is account-wide — you need both.) If *-login stays for now, please make the IAM.md / description prose state plainly that it's an account-wide leading-wildcard match, so an operator tightening for production isn't misled. — verified live 2026-07-11: cluster-user-iam.yaml params are only GroupName/AttachUsers; instances carry an immutable aws:pcs:cluster-id tag.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review Batch 2/3 — Docs, Tags & Maintainability

The 4-way byte-identical UserData duplication keeps forcing lock-step edits — 🟢 (observation)

Observation: This is the third change in recent memory that had to be applied identically to the four hand-maintained CNG templates (the needrestart guard, the cross-region-S3 --region fix, and now the Name/CngName retag) — I diffed the four added blocks here and they're byte-identical (good, no drift this time).
Impact: None today; it's a standing fragility — the next edit that lands in three of four is where a real divergence appears.
Suggestion: The new tests/lint-docs.sh is a natural home for a cross-file assertion that the four Tags: blocks stay byte-identical, so drift fails CI. Or factor the shared UserData into one source. (Not a blocker for this PR.)

Comment thread architectures/aws-pcs/README.md Outdated
STACK_NAME=pcs-ml-cluster # your CloudFormation stack name
AWS_REGION=us-east-1 # your region

LOGIN_INSTANCE_ID=$(aws ec2 describe-instances --region "$AWS_REGION" --filters "Name=tag:aws:pcs:compute-node-group-id,Values=$(aws pcs list-compute-node-groups --cluster-identifier $(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$AWS_REGION" --query 'Stacks[0].Outputs[?OutputKey==`ClusterId`].OutputValue' --output text) --region "$AWS_REGION" --query 'computeNodeGroups[?name==`login`].id' --output text)" "Name=instance-state-name,Values=running" --query 'Reservations[0].Instances[0].InstanceId' --output text)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The nested LOGIN_INSTANCE_ID=$(…) one-liner fails silently to None, and --cluster-identifier is unquoted — 🟢 [confirmed]

Observation: The triple-nested substitution (this line in §6, and the §8.2 copy) is functionally correct — the JMESPath backticks are protected by single quotes, and nested $(…) parses fine. But there's no error path: a wrong STACK_NAME/AWS_REGION or a missing ClusterId output makes the inner describe-stacks return empty → list-compute-node-groups runs with an empty identifier → the Values= filter matches nothing → --output text prints literal None → aws ssm start-session --target None fails opaquely. Separately, --cluster-identifier $(…) is unquoted, so on an empty ClusterId the token vanishes and the following --region is consumed as the identifier value — a different, more confusing error than a clean "empty identifier."
Impact: A first-time copy-paste into the wrong stack/region gets None with no hint why. (Test 15's "no None/empty" gate is exactly the right guard — but the snippet itself gives the user no diagnostic.)
Suggestion: Split into named steps with a guard, and quote the identifier — IAM.md already splits out CLUSTER_ID for the Grafana step, so the pattern exists:

CLUSTER_ID=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$AWS_REGION" \
  --query 'Stacks[0].Outputs[?OutputKey==`ClusterId`].OutputValue' --output text)
[ -n "$CLUSTER_ID" ] && [ "$CLUSTER_ID" != "None" ] || { echo "No ClusterId — check STACK_NAME/AWS_REGION"; return 1; }
CNG_ID=$(aws pcs list-compute-node-groups --cluster-identifier "$CLUSTER_ID" --region "$AWS_REGION" \
  --query 'computeNodeGroups[?name==`login`].id' --output text)
LOGIN_INSTANCE_ID=$(aws ec2 describe-instances --region "$AWS_REGION" \
  --filters "Name=tag:aws:pcs:compute-node-group-id,Values=$CNG_ID" "Name=instance-state-name,Values=running" \
  --query 'Reservations[0].Instances[0].InstanceId' --output text)

Value: !Sub '${ClusterName}-${CngName}'
- Key: CngName
Value: !Sub 'PCS-${CngName}'
Value: !Sub '${ClusterName}-${CngName}'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The CngName tag no longer holds the CNG name — 🟢 [confirmed]

Observation: The custom tag Key: CngName now gets Value: !Sub '${ClusterName}-${CngName}' — identical to Name. A tag keyed CngName whose value is <cluster>-<cng> is misleading; the bare CNG name is !Ref CngName. (It was already mildly off as PCS-${CngName}, but this PR edits the line and could set it right.)
Impact: Redundant with Name; anything grouping/filtering by tag:CngName (dashboards, cost allocation) now sees a cluster-prefixed value, not the CNG name.
Suggestion:

Suggested change
Value: !Sub '${ClusterName}-${CngName}'
Value: !Ref CngName

(or drop the duplicate tag, since Name already carries the console-readable value). Applies to the same line in all four add-cng*.yaml.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review Batch 3/3 — Positives & Sources

Things That Look Great

  • The PCS-API login lookup is the right approach, and I verified it works live. On a real cluster (pcs_3duasoz99i), list-compute-node-groups → computeNodeGroups[?name=='login'].id → describe-instances by aws:pcs:compute-node-group-id resolves the login node correctly. Moving off the human-facing Name tag to the PCS API (and the immutable aws:pcs:* tags) is exactly the robust, naming-independent path.
  • The new walkthrough test (Test 15) is excellent. A pre-merge test that follows README §3 → §6 → §8.2 as written, gating on "every command returns a real value (no None/Not found)", is precisely the guard for the copy-paste-first-step regressions this doc set has hit before. The failure-classification section (None → stale snippet; AccessDenied → IAM scope drift; Grafana 403 → monitoring) is genuinely useful.
  • The lint bans are well-formed — aws:pcs:compute-node-group-name (bare, no tag: prefix, so it catches both the CLI-filter and console-hint forms) plus Name=tag:Name,Values=PCS- to retire the old flat naming. This is the correct, complete guard.
  • The monitoring path (§8.2) is properly per-cluster scoped (aws:pcs:cluster-id + monitoring-role=login) — that same immutable-tag pattern is the fix the SSM IAM policy above wants.
  • Applied consistently — the Name/CngName change is byte-identical across all four CNG templates, and the README/IAM.md/lint changes move in lock-step.
  • Self-contained copy-paste blocks with STACK_NAME/AWS_REGION set once per block — a real usability improvement over cross-block variable threading.

Sources

  • Live verification (2026-07-11, acct 159553542841): PCS cluster pcs_3duasoz99i (us-east-1) — aws pcs list-compute-node-groups returns a login CNG pcs_y20b00q2o6, and describe-instances by tag:aws:pcs:compute-node-group-id resolves login node i-0ca17ac97d8d60571. cluster-user-iam.yaml parameters confirmed to be only GroupName/AttachUsers (no cluster identifier).
  • IAM StringLike leading-wildcard semantics: a *-prefixed pattern matches any suffix — IAM policy element: condition operators. Demonstrated live (aws iam simulate-custom-policy, 2026-07-11) on the PR's exact ssm:StartSession statement: *-login allows myclusterB-login, bastion-login, vpn-login, myclusterA-data-login (all implicitDeny under PCS-login*); both correctly deny myclusterA-cpu1 / -gpu-p5.
  • PCS instance tags (aws:pcs:cluster-id, aws:pcs:compute-node-group-id) are AWS-applied/immutable — AWS PCS docs.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Few comments

The custom tag `CngName` had value `!Sub '${ClusterName}-${CngName}'`
— identical to the new `Name` tag introduced by this PR. A tag keyed
`CngName` whose value carries the cluster prefix is misleading and
duplicates `Name`; anything filtering / grouping by `tag:CngName`
(dashboards, cost allocation) would see a cluster-prefixed value, not
the CNG name.

Same value change in all four `add-cng*.yaml` — this custom tag
survives on every CNG (login/compute/GPU families).

Reported by KeitaW during PR review.
…op *login glob

Reported by KeitaW on this PR. The triple-nested substitution used
in README §6, IAM.md, and (from awslabs#1172) JUPYTER.md was functionally
correct but had two footguns:

1. **Fail-silent to `None`.** A wrong STACK_NAME/AWS_REGION or a
   missing ClusterId stack output made the inner describe-stacks
   return empty → the outer AWS calls printed the literal string
   "None" → `aws ssm start-session --target None` failed opaquely.

2. **Unquoted `--cluster-identifier $(…)`.** On an empty ClusterId
   the token disappeared and `--region` was consumed as the
   identifier value — a more confusing error than a clean "empty
   identifier".

Split the pipeline into three named steps (CLUSTER_ID, LOGIN_CNG_ID,
LOGIN_INSTANCE_ID), quote every substitution, and add an early guard
that stops with a clear message on missing/None CLUSTER_ID. Same
change in all three snippets (README §6, README §8.2, IAM.md
"What the cluster user can do", JUPYTER.md Step 3).

JUPYTER.md previously used `Name=tag:Name,Values=*login` — a
leading-wildcard match that this PR's Item 1 review calls out for
matching any `*-login` in the account (other clusters, `bastion-login`,
compute CNGs ending in `login`, …). Replaced with the same PCS-API
+ `aws:pcs:compute-node-group-id` resolver used elsewhere in the
patch, which is scoped to the login CNG of the named stack.
Reported by KeitaW on this PR. The SSMSessionToLoginNodeInstance
condition used `ssm:resourceTag/Name: "*-login"` — a leading
wildcard that matches ANY instance in the account whose Name tag
ends with `-login`:

  * every other cluster's login node (this PR names them
    <ClusterName>-login, so all of them satisfy `*-login`);
  * unrelated `-login` instances (`bastion-login`, `vpn-login`, …)
    that were denied under the previous `PCS-login*` prefix;
  * compute CNGs whose own name ends in `login` (e.g. CNG named
    `data-login` → Name=<cluster>-data-login), breaking the
    template's own "cannot open shells on compute nodes" guarantee.

Add a required `ClusterStackName` parameter and switch the Condition
to `StringEquals` with `!Sub "${ClusterStackName}-login"` — the exact
Name tag the login CNG carries (add-cng*.yaml Name value =
<ClusterName>-<CngName>, deploy-all passes StackName as ClusterName).
Also rewrite the PolicyDocument from a JSON-string literal to
native YAML so the Sub reference is legible.

Verified via `aws iam simulate-custom-policy` on the deployed
policy — the four unintended-allow cases in the original report all
flip to `implicitDeny`, and the legitimate target-cluster login
stays `allowed`:

    Name=<target>-login       → allowed
    Name=<other>-login        → implicitDeny
    Name=bastion-login        → implicitDeny
    Name=<target>-cpu1        → implicitDeny
    Name=<target>-data-login  → implicitDeny

Trade-off: one deploy of `cluster-user-iam.yaml` now grants access to
exactly one cluster. Deploy it once per target cluster. Docs (IAM.md
scope wording + deploy example, iam-test.md Test B expectations and
`simulate-custom-policy` recipe) updated to reflect this.
@DaisukeMiyamoto

Copy link
Copy Markdown
Collaborator Author

Thanks for the security catch — Item 1 is exactly the kind of thing this PR should have gotten right the first time. All three should-fixes are addressed in 4b52e3e / 9ecd426 / b9ee204 (three separate commits, one per Item).

Item 1 — cluster-user-iam.yaml scope (4b52e3e)

Adopted your recommendation to key on an operator-immutable cluster identifier, via a required ClusterStackName parameter — the stack name is what the operator typed at deploy time and is already the ClusterName the templates pass through (add-cng*.yaml tags Name=<ClusterName>-<CngName>; deploy-all passes ${AWS::StackName} as ClusterName). So the StringLike becomes an exact-match StringEquals against !Sub "${ClusterStackName}-login" — which is the actual Name tag the login CNG carries.

Verified with aws iam simulate-custom-policy on the deployed policy — all four unintended-allow cases in your report flip to implicitDeny, and the legitimate target-cluster login stays allowed:

ssm:resourceTag/Name context Result
pcs-um-acct-login (target) allowed
other-cluster-login implicitDeny
bastion-login implicitDeny
pcs-um-acct-cpu1 (compute) implicitDeny
pcs-um-acct-data-login (compute w/ login suffix) implicitDeny

Trade-off: one deploy of cluster-user-iam.yaml now grants access to exactly one cluster — deploy it once per target cluster. The template Description, IAM.md's scope wording, the deploy example, and iam-test.md Test B expectations + a simulate-custom-policy recipe are all updated to reflect this. I also rewrote the PolicyDocument from a JSON-string literal to native YAML so the !Sub reference is legible.

Item 2 — split login-lookup snippet (9ecd426)

Adopted your suggestion. All three copies (README §6, README §8.2, IAM.md "What the cluster user can do") now split into three named steps, quote every substitution, and add an early guard:

```bash
CLUSTER_ID=$(aws cloudformation describe-stacks ... --query 'Stacks[0].Outputs[?OutputKey==`ClusterId`].OutputValue' --output text)
[ -n "$CLUSTER_ID" ] && [ "$CLUSTER_ID" != "None" ] || { echo "No ClusterId — check STACK_NAME/AWS_REGION"; return 1; }
LOGIN_CNG_ID=$(aws pcs list-compute-node-groups --cluster-identifier "$CLUSTER_ID" ...)
LOGIN_INSTANCE_ID=$(aws ec2 describe-instances ... "Name=tag:aws:pcs:compute-node-group-id,Values=$LOGIN_CNG_ID" ...)
```

While in there I also fixed the same fail-silent + Name=*login glob in the already-merged docs/JUPYTER.md from #1172 — it was the same pattern, and Item 1's exact-tag argument applies to it too. So JUPYTER.md Step 3 now uses the same PCS-API resolver instead of "Name=tag:Name,Values=*login".

Item 3 — bare CngName tag (b9ee204)

Taken as-is: the CngName custom tag now holds !Ref CngName in all four add-cng*.yaml. As you noted, prefixed values were already off-spec pre-PR (commit 10613e3, #1120, meant the tag to "preserve the per-node-group name" but coded PCS-${CngName}); this PR was widening the drift, and the fix restores the original intent.

9ecd426 bundled a JUPYTER.md change into what should have been a
README/IAM.md-only fix. Two reasons to back it out:

1. JUPYTER.md's `--filters "Name=tag:Name,Values=*login"` is a client-
   side discovery filter for an end user's own SSM session, not an IAM
   Condition. Item 1's exact-tag argument is about who a policy grants
   session-start rights to; a shell filter that returns the wrong login
   just leads to `ssm start-session` failing under the IAM scope the
   principal actually has. Not a security issue for JUPYTER.md.
2. The replacement pipeline was not run verbatim on a live cluster —
   only the equivalent shape in README/IAM.md was. Landing an
   unverified pipeline in Jupyter's connect step (the load-bearing
   step of that guide) is worse than the original three-line filter,
   which shipped in awslabs#1172 with KeitaW's live-cluster verification.

Keep 9ecd426's README §6 / §8.2 / IAM.md changes — those are the ones
KeitaW asked for.
@DaisukeMiyamoto

Copy link
Copy Markdown
Collaborator Author

Correction on my previous comment. The JUPYTER.md change I bundled into 9ecd426 was scope-creep on my part — I saw the same Name=*login glob and reflexively rewrote it to match the README/IAM.md fix, but:

  1. JUPYTER.md's filter is a client-side describe-instances search for the end user's own SSM session, not an IAM Condition. Item 1's exact-tag argument is about who a policy grants session-start rights to; a shell filter that resolves the wrong instance just fails against the IAM scope the principal actually has. It isn't a security issue for JUPYTER.md.
  2. The replacement pipeline in JUPYTER.md was not verbatim-run on a live cluster; only the README / IAM.md shape was. The Jupyter connect step is the load-bearing step of that guide, and feat(aws-pcs): add Jupyter-on-compute-node guide with verification procedure #1172 shipped its current form with your live-cluster verification.

Reverted in 43a54fc. The README §6, README §8.2, and IAM.md parts of 9ecd426 — the ones you actually asked for in Item 2 — stay.

@KeitaW KeitaW left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving with nit. Kindly resolve conflict before merge!

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review (round 2) — resolution scoreboard

Thorough response round — all three round-1 findings are addressed with dedicated commits, and I verified the fixes rather than trusting the messages. Headline: the login-node IAM scope is now correct and I re-verified it live — the old *-login leading-wildcard is gone, replaced by StringEquals ${ClusterStackName}-login, and running the PR's own iam-test.md matrix against the resolved policy gives exactly allowed for <stack>-login and implicitDeny for other-cluster-login / bastion-login / <stack>-cpu1 / <stack>-data-login (round 1 those were allowed).

# Round-1 finding Status
1 ssm:resourceTag/Name: *-login leading-wildcard SSM grant ✅ Resolved (4b52e3e) — ClusterStackName param + StringEquals ${ClusterStackName}-login; verified live via simulate-custom-policy (allowed + implicitDeny×4). deploy-all wiring (ClusterName=${AWS::StackName}, login CngName: login) confirms the real login node matches.
2 nested LOGIN_INSTANCE_ID=$(…) fails silently to None ✅ Resolved (mostly) (9ecd426) — split, quoted, guarded on CLUSTER_ID. One residual last-hop, inline below.
3 CngName tag held PCS-<name> not the CNG name ✅ Resolved (b9ee204) — !Ref CngName, byte-identical across all four templates.
— (obs) 4-way byte-identical UserData duplication ➖ Unchanged / informational — still 4 copies; the new lint doesn't assert Tags:-block identity (non-blocking).

Three round-2 comments inline: one 🟡 (the new lint gives a false all-clear) and two 🟢 doc-accuracy notes. Details on each line.

$'S3 (public )?hosting is not allowed\tNEVERMATCH\tPostInstallScriptUrl now accepts s3:// URLs'
$'architectures/aws-pcs/iam/\tNEVERMATCH\tthe iam/ directory was removed; use docs/IAM.md + assets/cluster-*-iam.yaml'
$'aws:pcs:compute-node-group-name\tNEVERMATCH\ttag key does not exist — use `aws pcs list-compute-node-groups` + `tag:aws:pcs:compute-node-group-id`'
$'Name=tag:Name,Values=PCS-\tNEVERMATCH\tthe Name tag is <ClusterName>-<CngName>; resolve the login node via the PCS API instead'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The new lint gives a false all-clear — two Jupyter docs still carry the patterns it's meant to eradicate

This PR replaces the mutable-Name-tag login discovery and the PCS-login* scope across README / IAM.md / iam-test.md, and adds these two BANNED patterns to stop them creeping back. The gap: two sibling docs describing the same login-node flow still carry the pre-PR forms, and both sit inside the lint's own DOC_GLOBS (docs/*.md tests/*.md):

  • docs/JUPYTER.md:120 and tests/jupyter-notebook-test.md:119 — login discovery via --filters "Name=tag:Name,Values=*login" + Reservations[0].Instances[0] (un-cluster-scoped, arbitrary first match — the exact shared-VPC "wrong cluster" hazard §6/§8/IAM.md now avoid).
  • docs/JUPYTER.md:144 — describes the SSM condition as ssm:resourceTag/Name = PCS-login* (the old StringLike). The claim it supports is still true, but the named mechanism is stale and misleads anyone auditing a restricted role from that doc.

I checked live that these two new patterns (aws:pcs:compute-node-group-name, Name=tag:Name,Values=PCS-) match none of those forms, so the lint passes green while the drift survives in-tree — it reads as "no stale lookups" when three remain. I see you deliberately reverted the JUPYTER.md scope change (43a54fc, "out of scope, untested") — deferring the Jupyter rewrite is a reasonable call for a PR already carrying a security fix. The narrower ask is to keep the lint honest about it: either widen the two patterns so they actually catch Values=*login / PCS-login* (which forces the small Jupyter fix now), or add an explicit "Jupyter docs deferred to #NNNN" note so a green lint isn't read as full coverage. I'd lean toward widening the patterns — making this class fail CI is the whole point of adding them.

[ -n "$CLUSTER_ID" ] && [ "$CLUSTER_ID" != "None" ] || { echo "No ClusterId — check STACK_NAME/AWS_REGION"; return 1; }

LOGIN_CNG_ID=$(aws pcs list-compute-node-groups --cluster-identifier "$CLUSTER_ID" --region "$AWS_REGION" --query 'computeNodeGroups[?name==`login`].id' --output text)
LOGIN_INSTANCE_ID=$(aws ec2 describe-instances --region "$AWS_REGION" --filters "Name=tag:aws:pcs:compute-node-group-id,Values=$LOGIN_CNG_ID" "Name=instance-state-name,Values=running" --query 'Reservations[0].Instances[0].InstanceId' --output text)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The split login-lookup guards only the first of three hops — a renamed login CNG still lands on --target None

The split + CLUSTER_ID guard is a solid fix for the round-1 silent-None. The residual: the guard covers CLUSTER_ID, but LOGIN_CNG_ID and LOGIN_INSTANCE_ID are unguarded. If list-compute-node-groups finds no CNG named login (renamed, or not yet running), LOGIN_CNG_ID comes back empty → the describe-instances filter becomes Values= → LOGIN_INSTANCE_ID=None → aws ssm start-session --target None fails with an opaque SSM error — the round-1 class, one hop down. A single guard right before the session covers all three hops:

[ -n "$LOGIN_INSTANCE_ID" ] && [ "$LOGIN_INSTANCE_ID" != "None" ] || { echo "No running login node for cluster $CLUSTER_ID"; return 1; }

Two asides: the existing CLUSTER_ID guard uses return 1, which only halts inside a function or a sourced script — pasted at an interactive prompt (which the README instructs) return prints an error and execution continues anyway, so the guard's "stop here" intent isn't enforced interactively. And credit due — the new Test 15 gate (echo $LOGIN_INSTANCE_ID must print an i-…) already catches this before a customer would, so this is polish, not a hole. docs/IAM.md's equivalent block has the same shape and would take the same one-line guard.

**CLI** (AWS CloudShell has the required permissions):
**CLI** (AWS CloudShell has the required permissions). Set `STACK_NAME` /
`AWS_REGION` once, then a single command resolves the login node's
instance ID — it works no matter what your CNG is named because the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

§6 rationale is slightly self-contradictory — the resilience is against the Name tag, not the CNG name

Small wording thing: the §6 intro says the lookup "works no matter what your CNG is named because the lookup goes through the PCS API." But the lookup hardcodes computeNodeGroups[?name==\login`], so it *does* depend on the login CNG being named login(which deploy-all guarantees). What the PCS-API path actually buys you is independence from the mutable EC2Name` tag — that's the real win worth stating.

Suggested change
instance ID — it works no matter what your CNG is named because the
instance ID — it works no matter how instances are Name-tagged because the

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Walkthrough Test 15, Step 4 — "the same way" conflates two lookup mechanisms

Minor nit in the new (excellent) walkthrough test: Step 4 says §8.2 Option A "produces LOGIN_INSTANCE_ID the same way" as Step 2 (§6). They're different paths — §6 uses the PCS API + aws:pcs:compute-node-group-id, while §8.2 filters on monitoring-role=login + aws:pcs:cluster-id. Both valid, but a reader following the gates literally will notice the commands differ. Dropping "the same way" (or noting the different tag path) keeps the "as written" contract precise.


Things that look great (round 2)

  • The IAM scope fix is exactly right and I verified it live. ClusterStackName + StringEquals (not StringLike) fails closed — a wrong stack name yields AccessDenied, not an over-grant — and the inline comment enumerating what's excluded (bastion-login, other clusters, *login-suffixed compute CNGs) matches the simulated reality one-for-one.
  • The JSON→YAML policy rewrite is faithful. All seven statements preserved, and — the easy CFN foot-gun here — ${aws:username} in SSMSessionTerminateOwn is correctly left outside !Sub, so CloudFormation doesn't try to resolve it as a template parameter (a clean validate-template confirms). The only !Sub is the intended ${ClusterStackName}-login.
  • You turned my round-1 demonstration into a durable test. iam-test.md now scopes the check with simulate-custom-policy across the five representative names with the exact expected allowed / implicitDeny×4 — regression guard institutionalized, and its expectations match my live run precisely.
  • ClusterStackName is properly constrained — AllowedPattern + a clear ConstraintDescription, so a bad value fails at deploy with a readable message rather than silently mis-scoping.
  • Scope discipline — the self-revert of the out-of-scope JUPYTER.md change (43a54fc) is the right call for a PR already carrying a security fix.

Sources

  • Verified live (2026-07-13, acct 159553542841): validate-template on the rewritten cluster-user-iam.yaml → valid (GroupName/AttachUsers/ClusterStackName, CAPABILITY_NAMED_IAM); simulate-custom-policy on the resolved ssm:StartSession statement → pcs-ml-cluster-login=allowed, other-cluster-login/bastion-login/pcs-ml-cluster-cpu1/pcs-ml-cluster-data-login=implicitDeny; bash tests/lint-docs.sh → PASS.
  • deploy-all wiring: pcs-ml-cluster-deploy-all.yaml passes ClusterName: !Sub '${AWS::StackName}' to every CNG stack and sets login CngName: login, so the login node's Name = <stack>-login.
  • PCS-emitted instance tags (docs + live EC2, 3 regions): aws:pcs:cluster-id and aws:pcs:compute-node-group-id exist; aws:pcs:compute-node-group-name does not (0 instances) — confirming the lint ban and the id-based lookups. Finding CNG instances, Connect to your cluster.
  • IAM StringEquals exact-match (no wildcards): IAM condition operators.

Bring in upstream/main (up to awslabs#1179 apt-lock fix + awslabs#1172 Jupyter guide +
several 3.test_cases fixes) so this PR fast-forwards cleanly again.

Only real conflict: tests/README.md added a Test 15 on both sides —
upstream added the Jupyter test (from awslabs#1172), this PR added a README
walkthrough test. Keep both: Jupyter is Test 15, README walkthrough
becomes Test 16. readme-walkthrough-test.md's title updated to match.

The add-cng*.yaml auto-merges — awslabs#1179 (DPkg::Lock::Timeout) applies
around this PR's Name/CngName tag edits without conflict.
@DaisukeMiyamoto
DaisukeMiyamoto merged commit 13ad2c3 into awslabs:main Jul 14, 2026
@DaisukeMiyamoto
DaisukeMiyamoto deleted the feat/cluster-scoped-name-and-lookup branch July 14, 2026 22:52
KeitaW pushed a commit that referenced this pull request Aug 7, 2026
…verbatim-verified) (#1222)

* docs(aws-pcs): improve JUPYTER + USER-MANAGEMENT walkthroughs (verbatim-verified)

Two doc rewrites bundled to save review roundtrips — both fell out of
end-to-end verbatim replays on live Tokyo/us-east-2 clusters, so every
recipe below has been executed from the doc text alone.

## docs/USER-MANAGEMENT.md — full rewrite

- Frame the doc around the single-user default first; multi-user
  becomes an opt-in section, so readers who don't need OpenLDAP can
  close the tab immediately.
- New §2 "Adding your first LDAP user" walkthrough: recover admin
  password (§2.1) → add user with SSH key (§2.2) → log in (§2.3, both
  direct SSH and SSH-over-SSM via the PCS-API login lookup) → verify
  on a compute node as the user (§2.4).
- §2.1 admin-password recipe now takes CLUSTER_ID/AWS_REGION from IMDS
  on the login node (no reader-supplied "pcs_xxxxxxxx" placeholder).
- §3 "Detailed operations" restructured as a complete operator toolkit
  (add / list / delete / reset-password / create-group / add-member),
  all using `-W` / `-W -S` prompt paths so nothing sensitive lands in
  shell history.
- §4 "Slurm accounting" split into 4.1 Register / 4.2 Limits and
  enforcement / 4.3 Reporting. §4.1 reproduces the two-project scenario
  from AWS's "Introducing managed accounting for AWS PCS" blog; §4.2
  documents `associations,limits,safe` accepts+pends rather than
  rejects (contra the blog); §4.3 keeps the two `sacct`/`sreport`
  tool-behaviour notes verified in earlier live testing.
- Explicit callouts: console vs CLI deploy paths, submit-from-$HOME,
  `--time=<mm>` required when a GrpTRESRunMins quota is set.

## assets/scripts/ldap-add-user.sh — reject duplicate UID/username

Same helper the walkthrough drives. LDAP does NOT enforce
uidNumber uniqueness (it's not part of the DN), so
`ldap-add-user.sh bob 10001 …` after alice at 10001 silently produced
two users sharing UID 10001 = same POSIX principal on shared /home + /fsx.
Pre-check with an unauthenticated ldapsearch; refuse the add with a
clear stderr message. Live-verified: dup UID and dup name both trip
the guard; a distinct UID creates cleanly.

## assets/cluster.yaml — ClusterName default `pcs-ml-cluster`

Aligns with the README/quick-create link's stack name so a first-time
user gets the same identifier whichever deploy path they follow.
deploy-all continues to pass `${AWS::StackName}` through, so this
default only affects direct `cluster.yaml` stacks.

## docs/JUPYTER.md — preinstall torch, GPU verify cell, tokenless mode, safer login lookup

- Step 1 also installs `torch --index-url .../cu124`, so the first
  notebook can `import torch` without an install/restart roundtrip.
- Step 2 explicitly `cd $HOME` before `sbatch`. Slurm's job CWD is
  the submitter's, and `--output=%u-jupyter-%j.log` resolves against
  that; submitting from an SSM shell (`/var/…`) sent the log where
  Step 3 couldn't find it (reproduced live).
- Step 3 replaces the leading-wildcard `Name=*login` login filter
  and stale `ssm:resourceTag/Name = PCS-login*` IAM callout with the
  cluster-scoped PCS-API resolver (matches README §6 / #1183 pattern).
- New "Verify GPU visibility from the notebook" cell prints
  CUDA_VISIBLE_DEVICES / SLURM_JOB_GPUS, enumerates PyTorch device
  properties, runs a matmul. Verified live on g6.12xlarge:
  device_count()=1, NVIDIA L4 22 GiB SM 8.9, matmul completes.
- New "Running without a token (single-user clusters only)" section
  documents the `--ServerApp.token='' --ServerApp.password=''` form
  with a prominent DO-NOT-USE-ON-MULTI-USER callout — Jupyter binds
  to the compute node's private IP, so the token is what separates
  users on multi-user clusters.

## tests/multi-user-test.md — regression coverage for the above

- B5a: duplicate uidNumber / username rejected by ldap-add-user.sh
  (guards commit 6f3b6ad).
- B7: password reset via `ldappasswd -W -S` interactive prompt.
- B8: group create + `add: memberUid` via ldapadd/ldapmodify -W.
- B9: end-to-end SSH-over-SSM as an LDAP user with PCS-API instance
  lookup, then `srun` on a compute node.
- Part F: reproduces the blog scenario (project accounts, per-user
  cap, --account=, safe-enforcement PD, `sacct`/`sreport` recipes).

## Verified

Doc-only re-run on a fresh Tokyo (ap-northeast-1) `pcs-ml-cluster`
stack deployed from the public bucket with
`DirectoryService=OpenLDAP-LoginNode`, `ManagedAccounting=enabled`,
`MonitoringStack=Prometheus-LoginNode`. Every fenced block above
executed as written (§6 login lookup, §7 srun container, §8.2
Grafana password + Option A monitoring-role lookup;
USER-MANAGEMENT §2/§3/§4/§5; JUPYTER Step 1/2/3 auth gates,
403/200/200, token file mode 600). GPU verify cell exercised on a
separate g6.12xlarge cluster.

* docs(aws-pcs): tighten IAM.md — collapse duplicate tables, trim redundancy

Two rounds of tightening; content preserved, ~30 lines of overlap removed.

- Merge the header "roles" table and the "Deploying" stacks table: they
  keyed the same two rows to different columns (What can do / Creates,
  Template link / Launch button). Fold into one "Role · Can do · Deploy"
  table where each row carries both the YAML link and the Launch button,
  and inline "broad — do not hand out" / "safe to hand out widely"
  callouts absorb the standalone splitting-the-roles paragraph.
- §Deploying the policies: shorten the AttachUsers /
  AttachImageBuilderPolicy paragraph — the details are already in the CLI
  example above and the template's parameter descriptions.
- §What the cluster user can do: drop the second CLUSTER_ID resolve
  (already resolved once at the top of the block).
- §Considerations "Login-node access is scoped": trim implementation
  detail about the Name-tag emission chain (readers can check the
  template); keep the exact-match scope and the operator-mutable
  caveat.
- Merge "admin policy split" and "no AmazonPCSFullAccess" bullets —
  same message (why the customer-managed policies look the way they do).
- §Verifying: shorten the private-bucket callout to one sentence.

* docs(aws-pcs): JUPYTER.md — decouple torch install from GPU-work framing

The torch install in Step 1 is prerequisite for the GPU-visibility
verify cell below, not for running Jupyter on a GPU queue (the queue
picks the queue; the kernel doesn't need torch to launch). Reframe:

- Step 1: "For GPU work, also install PyTorch now" →
  "To run the GPU-visibility verify cell below, also install PyTorch".
- Step 2: drop the "For GPU work, the venv already has torch" note
  (misleading — a GPU job doesn't require torch). Keep the pointer to
  Using GPUs for allocation behaviour.

* docs(aws-pcs): tighten USER-MANAGEMENT.md — drop duplicated sections

Post-review pass. Content preserved, ~90 lines of redundancy removed.

- §5.3 "Invalid credentials from ldap*": deleted — it was a one-liner
  redirecting to §2.1. Folded the redirect into §2.1's admin-password
  fallback callout so a reader who mistypes at a -W prompt still sees
  the recovery path in situ.
- §5.4/§5.5 renumbered to §5.3/§5.4.
- §6 "How it works": collapsed the ASCII diagram plus the separate
  "tag-based discovery" sub-section into one bullet list plus a
  login-node-replacement recovery snippet. Roughly a third of the
  previous size, same operational information.
- §8 "UID / GID conventions": absorbed into §3.1 (Add a user), which
  was already pointing at it for the allocation policy; the table now
  sits next to the invocation it informs.
- §9 "Template structure": deleted — its ASCII diagram restated §1
  "modular deployment" (which already documents DirectoryRole /
  IamProfileArn per CNG type).
- §10 "Upgrading to AWS Simple AD (future)": deleted — placeholder,
  no operational content, still tracked in ROADMAP.md.

* docs(aws-pcs): tighten JUPYTER.md Using-GPUs section, drop Notes

- Using GPUs: put "How the GPU allocation behaves" before the verify
  cell (allocation is the frame, verify is the check). Collapse the
  bullets: --gres is the only knob (merges the old "Slurm enforces"
  and "Multi-GPU works" points), drop scontrol-example detail, drop
  the p5.48xlarge measurement note, keep containerized-kernel pointer
  as one line.
- Notes section removed. The Multi-user/token bullet duplicated the
  Step 2 comment on the token being the user boundary; the
  Multiple-clusters bullet was stale (the Step 3 lookup no longer uses
  Name=*login); the SSH-tunnel alternative and /fsx-execve caveat are
  out of the load-bearing flow. "Do not run Jupyter on the login node"
  moved up into Prerequisites where readers will hit it before
  submitting.

* docs(aws-pcs): tighten IAM.md — one column for launch, drop CLI mirror + verify section

Post-review pass, ~80 more lines out.

- Table: collapse "Template" and "Launch" into one Launch column; the
  Launch button already resolves to the same YAML, listing the file
  path next to it was redundant. Move "Can do" into the Role cell so
  the table stays two-column.
- §Deploying the policies (CLI example) removed. The Launch buttons in
  the table cover the deploy path; the parameter-choice notes
  (AttachUsers, AttachImageBuilderPolicy, ClusterStackName) fit in one
  paragraph under the table.
- §What the cluster user can do (~25-line CLI example) removed. The
  same session / port-forward / Grafana-password recipes live in
  README §6 / §8.2, and this doc's job is the policies, not a copy of
  the user runbook.
- §Considerations: drop the "Combined CRUD is intentional" paragraph
  (internal design detail; only matters when tweaking the admin
  policy, in which case reading the template beats reading prose).
- §Refining to least-privilege via CloudTrail removed. It's an
  advanced follow-on with its own tooling, not a policy-reference
  concern.
- §Verifying the policies removed. The one-liner pointer to
  iam-test.md was thin, and the private-bucket note gets absorbed
  into §Considerations.
- §Not covered by these policies: fold into §Considerations as a
  short bullet list.

* docs(aws-pcs): trim IAM.md Considerations to what needs reader action

- Drop "Login-node access is scoped to one stack": the exact-match
  ssm:resourceTag condition + operator-mutable Name caveat is already
  documented in cluster-user-iam.yaml's Description and inline
  comments where a reader tightening the policy will actually read it.
- Drop "The admin policy is split into core + Image Builder": internal
  reason (IAM 6,144-char limit) doesn't lead to a reader action —
  AttachImageBuilderPolicy handling is already noted under the table.
- Drop "Private template bucket": edge case for private-bucket hosting;
  those users hit the missing s3:GetObject themselves and it's
  unrelated to reference-policy design.

Keeps: sample-grade disclaimer + AWS reference link, AWS-managed
pairing hints (with the AmazonEC2FullAccess warning), and the list of
roles this template doesn't cover.

* docs(aws-pcs): add PCS-Ready DLAMI version history

Document every published x86_64 PCS-Ready DLAMI build with its PCS Agent,
Slurm, base DLAMI, driver/CUDA, DCGM, EFA and containerd versions, so
readers can pick a build (and know whether node lifecycle actions, which
need agent 1.5.0-1, are available).

* docs(aws-pcs): note enforcement-required accounting registration in README §7

Adding a short pointer so the srun example in §7 does not silently fail
on clusters deployed with AccountingPolicyEnforcement set — the
submitter (including ubuntu) has to be registered in sacctmgr first.
Details stay in USER-MANAGEMENT.md §4.

* docs(aws-pcs): address PR #1222 review feedback (blocking + should-fix + nit)

USER-MANAGEMENT.md:
- §3 note on Slurm binary path swap when SlurmVersion=25.05
- §4.1 sacctmgr uses $S alias to absolute path (secure_path bypass)
- §4.2 hold demo now creates myjob.sh/smalljob.sh, adds --partition,
  tightens the cap so 4-task job trips AssocGrpCPURunMinutesLimit
- §4.2 use --ntasks-per-node=4 to match sinfo-reported CPUs on cpu1
- §4 accounting-enforcement wording: templates offer ,safe only
- sreport end date off-by-one (2026-04-30 → 2026-05-01, ...-03-31 → -04-01)
- README anchor #quick-start → #3-quick-start

multi-user-test.md:
- Part B: reorder — new B7 group add uses testuser2 still present,
  B9 deletes testuser2 at the end
- Part B8 (was B9) restructures SSH-over-SSM into three explicit steps
  (laptop keygen → login-node authorized_keys append → laptop ssh)
- Part F1 use sudo $S with absolute path, add carol UID 10004
- Part F2 batch script gets a shebang (printf '#!/bin/bash\n...\n')
- Part F3 --ntasks-per-node=4 matches sinfo CPUs on cpu1
- Part F3 anchor #41-worked-example... → #41-register-users-to-accounts
- Part F WithAssoc on F1 sacctmgr show; sreport end=now

JUPYTER.md:
- Step 1 pin PyTorch cu130 (matches DLAMI's CUDA 13.2 stack)
- Tokenless mode drop --ServerApp.disable_check_xsrf=True; note Step 3
  sub-step 2 is skipped and the banner heredoc's token line removed
- Multi-user warning reworded to trusted-same-principal formulation
- Verify GPU cell no longer flags all-visible nvidia-smi as a bug

IAM.md:
- Cluster-user row narrows claim: no AWS-API mutations, no compute-node
  sessions; login-node SSM shell is ssm-user with passwordless sudo
- Restore Name-tag operator-mutable caveat on the scoping description

PCS-READY-DLAMI.md:
- Reframe intro as a dated snapshot (as of 2026-08-05); drop the
  "describe-images below" reference (command was never added)

assets/scripts/ldap-add-user.sh:
- Duplicate-detection extended: getent passwd for username and UID
  (catches local /etc/passwd shadowing, e.g. `ubuntu` UID 1000), plus
  hard-reject UID outside the 10001-59999 LDAP-user range

Verified end-to-end via pcs-ml-cluster-deploy-all.yaml on a fresh
Tokyo cluster (DirectoryService=OpenLDAP-LoginNode + ManagedAccounting=
enabled + AccountingPolicyEnforcement=associations,limits,safe): §2.1
password recovery, §2.2 add first user (alice), §4.1 register users,
§4.2 hold demo (limit trips), Part F1/F2/F3 all pass verbatim.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants