Skip to content

Create a placement group.

POST
/v1/projects/{projectId}/compute/placement-groups
Code sample: Shell / cURL
curl --request POST \
--url https://stratum.example.com/v1/projects/example/compute/placement-groups \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "name": "example", "displayName": "example", "strategy": "spread", "level": "host", "satisfaction": "strict", "adoptInstanceIds": [ "example" ], "tags": { "additionalProperty": "example" } }'

Creates a group holding a spread rule. strategy defaults to spread, level to host and satisfaction to strict, so the minimal request is a name.

This increment creates the resource only — a group does not yet influence placement. The scheduler filter that makes a strict host-level spread actually place members on distinct hosts is a separate increment. Until it lands, a group records intent and nothing more.

Requires compute:CreatePlacementGroup, which is deliberately distinct from the permission to launch an instance into a group: a platform team defining the groups an application team may use only works if the two are separable.

Adopting existing instances additionally requires compute:AdoptInstance on each named instance. Adoption mutates those instances, and ’s separation would be worth nothing if creating a group silently carried the right to reach into any instance in the project.

projectId
required
string
/^p-[a-z0-9-]+$/
Media typeapplication/json
object
name
required

Unique within the project. Instances declare membership by this name at launch.

string
/^[a-z][a-z0-9-]{1,38}[a-z0-9]$/
displayName
string
strategy

What the group asks of its members’ placement.

  • spread — keep members apart. The case with real demand, and the one that solves the two-node-database-pair problem.
  • cluster — keep members together. Specified so the resource shape accommodates affinity without needing a second resource later, but not implemented yet: creating a group with this strategy is refused with PLACEMENT_STRATEGY_NOT_AVAILABLE. A group that promised affinity and delivered nothing would leave a tenant believing they were protected, which is precisely what this feature exists to prevent.
string
default: spread
Allowed values: spread cluster
level

The topology level the strategy applies at, ordered by blast radius host < rack < az.

A host is one hypervisor node, a rack is a shared PDU and top-of-rack switch, an AZ is a physically separate datacenter.

az is in this enum but is REFUSED at create with PLACEMENT_RULE_NOT_AVAILABLE. A launch is pinned to a single availability zone, so a spread across zones could never place a second member anywhere the first is not — accepting it would promise the strongest separation in the vocabulary and enforce nothing. It is kept in the schema because the value is legal storage-side and unlocking it is a code change rather than a migration. It becomes available when a launch can span zones.

When the cluster cannot differentiate the level it degrades to host every node in one rack, or no node declaring one. A rack spread on a single-rack cluster therefore behaves exactly as a host spread, with no error, rather than refusing every launch after the first. That is a correct description of a one-rack cluster, not a limitation, and the group’s guarantee says when it happened.

Where the distinction does exist, the level is enforced and the ceiling is the number of racks rather than hosts: a member can be refused while a host is still free.

string
default: host
Allowed values: host rack az
satisfaction

What happens when the placement cannot satisfy the rule.

  • strict — the launch FAILS, naming the group, the level and the limit. The default.
  • best-effort — place anyway, and report the group as violated in its own violation state. The violation is state on the group rather than only a launch-time warning, because a warning that scrolls past leaves best-effort indistinguishable from no group at all.

strict is the default because a silently-degraded HA pair is the exact failure this feature exists to prevent. A tenant who asked for two instances on separate hosts, got two on one host, and has a warning they did not read is worse off than before they asked: they now believe they are protected.

string
default: strict
Allowed values: strict best-effort
adoptInstanceIds

Existing instances to make members of the group at creation.

This is the only way a running instance joins a group, and it exists because refusing the tenant who already runs an HA pair and now wants to express the constraint would refuse the case the feature was built for.

Membership stays immutable afterwards. Adoption is a single join at the group’s creation, not an ongoing join/leave: an instance already in another group is refused, because moving between groups is not a thing that happens.

Nothing is ever moved. A group adopted around instances that already share a host is accepted and reports itself violated. Migrating to satisfy a placement rule is a tenant-visible action nobody asked for, at the worst possible moment for exactly these instances, and impossible for some of them. The violation clears on its own as members are replaced, because a replacement is placed by the rule.

All-or-nothing: if any instance cannot be adopted, the group is not created.

Array<string>
tags

AWS-shape resource tags. Freeform UTF-8 keys (1-128 bytes, no leading/trailing whitespace) mapped to freeform UTF-8 values (0-256 bytes). Max 50 tags per resource. Reserved prefixes (stratum.dev/, stratum.platform_health., stratum:, aws:) return 400 RESERVED_NAMESPACE on customer input. PATCH replaces the whole map..

object
<= 50 properties
key
additional properties
string
<= 256 characters

Created.

Media typeapplication/json

A tenant placement group: a RULE, not a set of nodes.

It carries a strategy, a level and a satisfaction mode, and nothing about specific hosts. A tenant never names a host — they express intent and the scheduler resolves it. Exposing node identity would leak the operator’s physical layout across tenants and would invalidate every group the moment a node was rebuilt.

object
id
required
string
/^pg-[a-z0-9-]+$/
projectId
required
string
/^p-[a-z0-9-]+$/
name
required

Customer-chosen name, unique within the project. Instances declare membership by this name at launch.

string
displayName
string
strategy
required

What the group asks of its members’ placement.

  • spread — keep members apart. The case with real demand, and the one that solves the two-node-database-pair problem.
  • cluster — keep members together. Specified so the resource shape accommodates affinity without needing a second resource later, but not implemented yet: creating a group with this strategy is refused with PLACEMENT_STRATEGY_NOT_AVAILABLE. A group that promised affinity and delivered nothing would leave a tenant believing they were protected, which is precisely what this feature exists to prevent.
string
default: spread
Allowed values: spread cluster
level
required

The topology level the strategy applies at, ordered by blast radius host < rack < az.

A host is one hypervisor node, a rack is a shared PDU and top-of-rack switch, an AZ is a physically separate datacenter.

az is in this enum but is REFUSED at create with PLACEMENT_RULE_NOT_AVAILABLE. A launch is pinned to a single availability zone, so a spread across zones could never place a second member anywhere the first is not — accepting it would promise the strongest separation in the vocabulary and enforce nothing. It is kept in the schema because the value is legal storage-side and unlocking it is a code change rather than a migration. It becomes available when a launch can span zones.

When the cluster cannot differentiate the level it degrades to host every node in one rack, or no node declaring one. A rack spread on a single-rack cluster therefore behaves exactly as a host spread, with no error, rather than refusing every launch after the first. That is a correct description of a one-rack cluster, not a limitation, and the group’s guarantee says when it happened.

Where the distinction does exist, the level is enforced and the ceiling is the number of racks rather than hosts: a member can be refused while a host is still free.

string
default: host
Allowed values: host rack az
satisfaction
required

What happens when the placement cannot satisfy the rule.

  • strict — the launch FAILS, naming the group, the level and the limit. The default.
  • best-effort — place anyway, and report the group as violated in its own violation state. The violation is state on the group rather than only a launch-time warning, because a warning that scrolls past leaves best-effort indistinguishable from no group at all.

strict is the default because a silently-degraded HA pair is the exact failure this feature exists to prevent. A tenant who asked for two instances on separate hosts, got two on one host, and has a warning they did not read is worse off than before they asked: they now believe they are protected.

string
default: strict
Allowed values: strict best-effort
tags

AWS-shape resource tags. Freeform UTF-8 keys (1-128 bytes, no leading/trailing whitespace) mapped to freeform UTF-8 values (0-256 bytes). Max 50 tags per resource. Reserved prefixes (stratum.dev/, stratum.platform_health., stratum:, aws:) return 400 RESERVED_NAMESPACE on customer input. PATCH replaces the whole map..

object
<= 50 properties
key
additional properties
string
<= 256 characters
createdAt
required
string format: date-time
createdBy
string
guarantee

What this group’s spread level protects against, and what it leaves exposed, for the cluster as currently declared.

Computed on read, never stored. It is a function of the level and of what the operator has declared, and the second half moves: declaring topology.power tomorrow changes what a group created today does not promise. A stored answer would be a guarantee frozen at create time.

It exists because silence reads as a guarantee. Two racks on one UPS are not independent, so rack-level spread does not by itself mean power-independent — and an operator or tenant who assumes otherwise has bought a guarantee nobody sold them.

Absent when the control plane could not determine what the cluster declares. An absent statement is honest about not knowing; a guessed one would be the unearned guarantee this field exists to prevent.

object
level
required

The topology level the strategy applies at, ordered by blast radius host < rack < az.

A host is one hypervisor node, a rack is a shared PDU and top-of-rack switch, an AZ is a physically separate datacenter.

az is in this enum but is REFUSED at create with PLACEMENT_RULE_NOT_AVAILABLE. A launch is pinned to a single availability zone, so a spread across zones could never place a second member anywhere the first is not — accepting it would promise the strongest separation in the vocabulary and enforce nothing. It is kept in the schema because the value is legal storage-side and unlocking it is a code change rather than a migration. It becomes available when a launch can span zones.

When the cluster cannot differentiate the level it degrades to host every node in one rack, or no node declaring one. A rack spread on a single-rack cluster therefore behaves exactly as a host spread, with no error, rather than refusing every launch after the first. That is a correct description of a one-rack cluster, not a limitation, and the group’s guarantee says when it happened.

Where the distinction does exist, the level is enforced and the ceiling is the number of racks rather than hosts: a member can be refused while a host is still free.

string
default: host
Allowed values: host rack az
protects
required

What a spread at this level does defend against.

Array<string>
doesNotProtect
required

What it leaves exposed. Never empty for a level this install can express — an empty list would read as “protects against everything”.

Each entry says whether the distinction is expressible on this cluster at all, so a tenant knows whether they could ask for it. It never reports how many racks or power feeds exist, or what they are called: a tenant does not see the operator’s physical layout.

Array<string>
violation

Whether the group’s rule currently holds.

Computed on read from the members’ actual placements, never stored. A stored flag would need every path that moves or removes an instance terminate, migrate, evacuate, the reaper, a group delete — to remember to update it, and the first one that forgot would leave a group claiming a violation that had cleared, or claiming compliance it had lost.

It is also why a violation clears with no operator action: the replacement instance is placed by the rule, and the next read simply recomputes.

Reported for strict groups too, not only best-effort. A strict group is violated when it is created around instances that already break it, or when the cluster shrinks below its member count — strict is a placement-time guarantee, not a maintained invariant, and nothing is ever moved to correct a violation.

Absent when the control plane could not read the members. An unknown answer must not render as violated: false.

object
violated
required
boolean
detail
required

A sentence stating the position either way. Populated even when the group is compliant: a field that appears only on failure trains a reader to treat its absence as proof, and absence is also what a bug looks like.

Names at most eight instances per collocation and then says how many more, so the prose does not grow with the project’s instance quota. collocations is complete and is what a client should act on.

string
collocations

Present only when violated. Each entry is a set of the group’s members sharing one placement value.

It names the instances, never the shared value. At host level the value is a node id the tenant can already read off their own instances; at rack or AZ level it would be the operator’s physical layout, which a tenant never sees.

Array<object>
object
instanceIds
required
Array<string>

Example

{
"strategy": "spread",
"level": "host",
"satisfaction": "strict",
"guarantee": {
"level": "host"
}
}

Bad request.

Media typeapplication/problem+json
object
type
required
string format: uri
title
required
string
status
required
integer
>= 400 <= 599
detail
string
instance
string format: uri-reference
requestId
required
string
code
required
string
issues

The findings of a rejected IAM policy document (custom and resource policies).

Array<object>

One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.

object
code
required

The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.

string
statement
required

Index of the statement at fault; -1 for the document as a whole.

integer
>= -1
detail
required
string
action

The offending action, where one is at fault (ACTION_NOT_APPLICABLE).

string
errors

Field errors, for codes that carry them: each problem at its field (see FieldError). Documented on the responses that set it, e.g. approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED. Absent otherwise.

Array<object>

One field’s problem, the stage-2 installer API’s shape. field is the field’s path, message what is wrong with it.

object
field
required

The field’s path, e.g. links[0].interface, disks[1].confirm, mgmt.address or role; empty for the whole request.

string
message
required
string
details

Structured facts about the refusal, for codes that carry them. The shape is fixed per code and documented on the response that answers it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and IMAGE_IN_USE. Absent for codes that carry none.

object
key
additional properties
any

Example

{
"issues": [
{
"code": "ACTION_NOT_APPLICABLE",
"action": "compute:CreateVpc"
}
]
}

Conflicting state.

Media typeapplication/problem+json
object
type
required
string format: uri
title
required
string
status
required
integer
>= 400 <= 599
detail
string
instance
string format: uri-reference
requestId
required
string
code
required
string
issues

The findings of a rejected IAM policy document (custom and resource policies).

Array<object>

One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.

object
code
required

The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.

string
statement
required

Index of the statement at fault; -1 for the document as a whole.

integer
>= -1
detail
required
string
action

The offending action, where one is at fault (ACTION_NOT_APPLICABLE).

string
errors

Field errors, for codes that carry them: each problem at its field (see FieldError). Documented on the responses that set it, e.g. approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED. Absent otherwise.

Array<object>

One field’s problem, the stage-2 installer API’s shape. field is the field’s path, message what is wrong with it.

object
field
required

The field’s path, e.g. links[0].interface, disks[1].confirm, mgmt.address or role; empty for the whole request.

string
message
required
string
details

Structured facts about the refusal, for codes that carry them. The shape is fixed per code and documented on the response that answers it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and IMAGE_IN_USE. Absent for codes that carry none.

object
key
additional properties
any

Example

{
"issues": [
{
"code": "ACTION_NOT_APPLICABLE",
"action": "compute:CreateVpc"
}
]
}

Request rate limit exceeded. The API-boundary throttle caps request throughput per principal and per project; code is RATE_LIMITED (distinct from the quota engine’s QUOTA_EXCEEDED). The Retry-After header carries the seconds to wait. Applies to any authenticated endpoint — it is enforced by middleware, not per-path.

Media typeapplication/problem+json
object
type
required
string format: uri
title
required
string
status
required
integer
>= 400 <= 599
detail
string
instance
string format: uri-reference
requestId
required
string
code
required
string
issues

The findings of a rejected IAM policy document (custom and resource policies).

Array<object>

One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.

object
code
required

The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.

string
statement
required

Index of the statement at fault; -1 for the document as a whole.

integer
>= -1
detail
required
string
action

The offending action, where one is at fault (ACTION_NOT_APPLICABLE).

string
errors

Field errors, for codes that carry them: each problem at its field (see FieldError). Documented on the responses that set it, e.g. approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED. Absent otherwise.

Array<object>

One field’s problem, the stage-2 installer API’s shape. field is the field’s path, message what is wrong with it.

object
field
required

The field’s path, e.g. links[0].interface, disks[1].confirm, mgmt.address or role; empty for the whole request.

string
message
required
string
details

Structured facts about the refusal, for codes that carry them. The shape is fixed per code and documented on the response that answers it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and IMAGE_IN_USE. Absent for codes that carry none.

object
key
additional properties
any

Example

{
"issues": [
{
"code": "ACTION_NOT_APPLICABLE",
"action": "compute:CreateVpc"
}
]
}
Retry-After
integer

Seconds to wait before retrying.