Skip to content

feat: add multi-cluster setup for ecs casting - #141

Draft
Nageshbansal wants to merge 41 commits into
mainfrom
fix/casting/ecscasting
Draft

feat: add multi-cluster setup for ecs casting#141
Nageshbansal wants to merge 41 commits into
mainfrom
fix/casting/ecscasting

Conversation

@Nageshbansal

@Nageshbansal Nageshbansal commented Jun 29, 2026

Copy link
Copy Markdown
Member

Features

  • Emits one ECS service per ClickHouse Keeper and per shard replica, each pinned to the node holding its data, so both run as clusters rather than single tasks.
  • Finds the cluster, subnets, VPC, security group and roles by the tags stamped on them. The region is the only thing stated.
  • Adds the ECS annotations. Stating one replaces its lookup with the value itself and emits no data source, for a cluster foundry did not provision.

Fixes

  • Corrects the ECS annotation table in docs/reference/casting-file.md. Drops ecs-config-bucket, which does not exist, and maps the task execution role to execution_role_arn.

Refactors

  • Flattens the templates out of module/ so the deployment forges as one stack.

Example

apiVersion: v1alpha1
kind: Installation
metadata:
  name: foundry
  annotations:
    foundry.signoz.io/ecs-region: us-east-1
spec:
  deployment:
    platform: ecs
    mode: ec2
    flavor: terraform
  infrastructure:
    name: foundry

Related: https://github.com/SigNoz/platform-pod/issues/2743

Nageshbansal and others added 23 commits July 14, 2026 18:55
- resource ref: no fabricated default, apiVersion enum, name pattern
- reject absent resource kind before the merge can default it to Installation
- reject unsupported consumer kinds (self-reference) at load
- drop the no-op resource molding: the kind has zero moldings
- rework tests to the Subject_Outcome table idiom; add loader coverage
@Nageshbansal
Nageshbansal marked this pull request as ready for review July 22, 2026 11:07
…e molding

Persistence moves onto the node groups: each group declares whether its
nodes persist data, and the standalone storage block goes away. The molding
now merges contributions (enricher deltas, operator overrides) at the
document level, so casting-specific keys survive, and validates the shared
shape of the resolved document before storing it. A contribution owns the
node groups it states.
Castings deposit their outputs into a pourer instead of returning
materials, matching the CollectionAgent contract: the planner builds the
pourer for the kind's output directory, the casting adds content under
relative paths, and the planner pours the staged entries into materials.
Enrichers follow the established shape: a stateless named type with a
constructor.
ListTypeSet and ListTypeOrdered both degrade to atomic on a list of maps,
so nothing could merge a list of objects: an override stating one element
replaced the whole list and silently dropped the rest.

ListTypeMap(key) mirrors Kubernetes' listType: map with a listMapKey.
Elements match on the key, a matched pair merges as a document so the
override states only what it changes, and override-only elements append.
It degrades to atomic when an element is not a map or lacks the key.
The requirement document invented its own words for a machine pool, and
one of them meant two different things: `disk` was the data volume on a
persistent group and the root volume on an ephemeral one, with different
minimums. There was also no way to name a machine type in the casting,
only in a terraform variable.

Adopt the vocabulary every node-pool abstraction already uses. kOps is
the closest precedent -- the one machine-pool shape that spans AWS, GCE,
Azure and DigitalOcean -- and where it had to choose it picked
machineType, minSize and maxSize, which overlap with GKE's and eksctl's
terms too. Narrowed to what foundry has to understand:

  nodeGroups:
    - name: persistent
      storage: persistent
      minSize: 3
      maxSize: 3
      machineType: ""      # empty resolves from cpu/memory
      cpu: 2
      memory: 8
      rootVolume: {size: 30}
      dataVolume: {size: 50}

cpu and memory stay as the portable criteria so the document does not
name provider machine types, with machineType as the escape hatch. Volumes
are nested objects rather than flat scalars so volume type or encryption
can arrive without new top-level keys, and splitting them lets the AMI
snapshot floor apply to every root disk unconditionally. dataVolume is
singular, not a list as kOps has it: the claim controller assumes one
volume per node, and a list would advertise what foundry cannot honour.

StorageClass is a value object rather than a string with an enum tag, so
an unknown class fails at unmarshal instead of deep in the molding, and
each class carries what it implies -- whether it needs a data volume,
whether the group is pinned. Adding a class is one var entry rather than
another branch wherever groups are checked.

Node groups now merge by name, so a contribution or an operator override
states only the group and the fields it changes. Before this, stating one
group deleted the others. `spec.resource.spec.config.data` merges last,
so an operator's own document beats the casting's contribution.
The scaffolding provides the Kind, the casting contract, the resource
molding and the registry; a concrete casting is a separate change with its
own review. Carrying one here made the scaffolding PR argue for an EKS
substrate at the same time as the machinery that hosts it.

The registry is left empty. The first casting to register lands with the
platform it serves.
Foundry generates rather than reconciles, so it can never ask a platform
what it created. A consuming casting finds a producing casting's resources
only by deriving the same names and filtering the same tags, with nothing
between them to reconcile a mismatch.

Derive both from one description of a resource, so a fact stated once
cannot render two ways: a subnet's visibility reaches its name and its tag
from the same field, and the tags a consumer filters on are a subset of the
tags the producer stamps rather than a parallel list.

Each resource type declares its own ordered qualifiers, so adding a
resource is one var entry.

Add domain.MetadataPrefix for the foundry key namespace, shared by labels,
annotations and tags.
A node group's only identity is its storage class. A consuming casting
selects nodes by class and has no way to name a group, so a second group of
the same class is unreachable: nothing can be steered onto it, and the claim
controller spreads stateful identities across every volume of that class by
id, which reads as replication while leaving copies on one disk.

Key nodeGroups by class and drop the name and storage fields. One group per
class becomes structurally impossible to violate rather than a rule to
check, and the class already supplies every identifier the generated
Terraform needs.

This also removes the reason the keyed list merge was added: a map merges
under plain document semantics, so revert internal/domain to its three list
types.
Doc comments carried design discussion rather than documentation: why a
decision was reached, what a reviewer should notice, how the code is
structured. Cut to what a reader cannot infer -- formats, invariants, and
the reason a check exists.

Field comments in the api package restated their own description tag, which
is the published documentation and strictly more informative. Drop them and
keep the tag, matching the tag-only style in meta.go.

Fix Selection's doc, which still explained itself in terms of node group
names after the storage class became a group's only identity.
Covers what an Infrastructure casting declares, the requirement document it
produces, the order the two castings apply in, and the naming and tagging
convention that joins them.

The requirement document and every derived name in it are the molding's and
the convention's real output rather than hand-written examples.

States plainly that no platform is registered yet, so forging reports an
unsupported deployment until the castings land, and records the limits that
would otherwise be found the hard way: one node group per storage class, and
a substrate that does not grow when SigNoz is scaled.
… deployment

The requirement document now has two halves. The top is a declaration in kOps'
vocabulary: networking with keyed subnets, instanceGroups, cloudLabels. The
bottom is derived from it once the declaration settles, and holds every name
and tag the substrate stamps. A casting interpolates the derived half rather
than assembling names of its own, which is what keeps a producing and a
consuming casting from spelling the same string two ways.

Derivation walks a provider's topology, so the molding takes a Deriver the
registry supplies per deployment instead of reaching for one cloud's. The
convention package splits to match: Substrate, Key, NodeGroup, Selection,
Identity and the tag facts stay neutral, and the AWS noun table and topology
walk land beside the casting that uses them.

TagKey names a fact and no longer carries a prefix. A GCP label key rejects
the dot and the slash and an Azure tag name rejects the slash, so the spelling
is rendered where the resource is created.

StorageClass and SubnetType move to api/v1alpha1. An installation casting
selects on both and should not import the Infrastructure kind's API to do it.

Drops resource.kind. Once workload identity moved to the workload castings it
selected only a node-group baseline, and one of its two values had no consumer:
a collection agent lands on capacity that already exists and asks a substrate
for nothing. One baseline now covers every substrate, and a substrate that
keeps nothing drops the persistent group with a null.

Drops the edge addresses, which the molding recorded and no template read.
The neutral core states what a substrate is; the aws package states how AWS
spells it. Both the infrastructure casting's deriver and the installation
casting's tag filters read from here, so it sits in the base rather than in
either consumer.
Provisions the substrate an ECS/EC2 installation lands on: a VPC with public
and private subnets, an ECS cluster, node groups per storage class, and the
IAM the nodes need. The enricher contributes the declaration; the molding
derives every name and tag through the aws convention, so the templates
interpolate the resources map rather than assembling identifiers inline.

Registers the casting with terraform as its tooler and awsconvention.Resources
as its deriver.
Emits one ECS service per ClickHouse Keeper and per shard replica, each pinned
to the substrate node holding its data, so ClickHouse and Keeper can run as
clusters rather than single tasks.

Every identifier the stack needs arrives as a variable defaulted to what the
casting resolved. Left alone, the stack finds the cluster, subnets, VPC,
security group and roles by their foundry.signoz.io tags, which is what the
infrastructure casting stamps. Stating the matching annotation replaces that
lookup with the value itself and emits no data source, so an existing cluster
still works.

Flattens the templates out of module/ so the deployment forges as one stack.
Drops ecs-config-bucket, which no longer exists, fixes the task execution role
to map to execution_role_arn, and records that every annotation but the region
is an optional override of a tag lookup.
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.

1 participant