Skip to content

grafana-cloud-vending-machine

A reference architecture for vending Grafana Cloud stacks through a small, declarative API. It supports Grafana Cloud only. Argo CD owns what is in Git, Crossplane continuously reconciles Grafana Cloud, and External Secrets Operator (ESO) moves credentials between Kubernetes and an external secret store.

It is deliberately more than a minimal stack example. The baseline includes rotating administrator and telemetry credentials, deletion protection, stack-local provider configuration, starter content, configurable drift behaviour, OAuth and SAML SSO, reports, plugins, incident relay resources, teams, basic-role ACLs, fixed-role assignments, custom roles, and role assignments.

One stack belongs to one Grafana Cloud organization. One vending-machine installation can serve several registered organizations, each with its own permitted regions and usages. Every namespace that accepts requests carries a same-named organization ProviderConfig and credential Secret for each registry entry because the v2 provider resolves ProviderConfig references in the managed-resource namespace. The request selects an immutable spec.organization; there is no implicit single-organization fallback.

The source and issue tracker live on GitHub.

platform.example.org is a documentation placeholder

Every XRD, Composition, example, and page in this documentation uses the API group platform.example.org. It is not a production API group — it is not resolvable and it is not meant to be used as-is. Replace every occurrence with a domain your organization controls before adopting this API. See Getting started and Configuration for where this matters most.

Who this is for

Platform teams that already run Argo CD and Crossplane and want to offer Grafana Cloud stacks as a self-service, GitOps-native product — with rotating credentials, safe defaults, and an explicit Retain-by-default lifecycle with a reviewable Delete path — rather than hand-running Terraform or the Grafana Cloud API per request.

Request-to-stack flow

A request is a GrafanaCloudStackRequest custom resource committed to a directory under the repository's top-level enabled/. From there:

  1. Argo CD watches enabled/* through an ApplicationSet and creates one Argo CD Application per directory, applying the request object to the cluster.
  2. Crossplane, through a Composition pipeline backed by a Go composition function, renders the request into a full set of managed resources: a Stack, a rotating administrator service account and token, an optional rotating telemetry access policy, baseline folders and dashboards, and any SSO, plugin, report, incident, team, or access-control resources the request enables.
  3. Crossplane's Grafana provider continuously reconciles those managed resources against Grafana Cloud — creating what is missing and repairing drift on anything the request's reconciliation mode marks as enforced.
  4. External Secrets Operator moves each registered organization's credential from an external secret store into the cluster so the provider can authenticate, and moves the stack's generated administrator and telemetry tokens back out to the external store as structured per-stack documents.

Nothing under examples/catalog is applied by the supplied ApplicationSet, and the top-level enabled/ directory starts empty. A user must copy a catalog example, edit every placeholder, review the rendered output, and commit it before a stack is created. This is a deliberate safety property: cloning or installing this platform cannot, by itself, create a Grafana Cloud stack.

Repository layout

The core product is platform/. The other directories are integration examples, inert catalog entries, validation tooling, and the GitHub workflows that publish the composition function.

.
├── platform/
│   ├── apis/                 XRDs and pipeline Compositions
│   ├── function/             Go composition function, tests, package metadata
│   ├── provider/             Grafana provider, activation policy, signature gate
│   ├── rbac/                 minimum extra composition RBAC for ESO resources
│   └── kustomization.yaml
├── examples/
│   ├── README.md             catalog index and safe enablement workflow
│   └── catalog/              inert stack, SSO, Teams, RBAC, and content-ACL examples
├── enabled/                  only path watched by the example ApplicationSet; empty by default
├── deploy/
│   ├── argocd/               controller, platform, and per-request GitOps examples
│   ├── aws/                  SecretStore, ExternalSecrets, ProviderConfig, IAM policy
│   ├── crossplane/           production-oriented Helm values
│   └── external-secrets/     production-oriented Helm values
├── scripts/                  validation and public-release safety scan
└── .github/workflows/        validation and signed multi-platform function publishing

What the baseline includes

Every enabled request gets, by default or by opt-in field:

CapabilityWhat it is
Rotating administrator credentialA StackServiceAccount with the Admin role and a StackServiceAccountRotatingToken, rotated automatically rather than issued once as a static token
Rotating telemetry credentialA stack-realm AccessPolicy scoped to stacks:read, metrics:write, logs:write, traces:write, with its own rotating token, when spec.telemetryAccess.enabled is true
External-resource lifecyclespec.lifecycle.externalResources defaults to Retain; a Delete intent requires an exact platform authorization for the request namespace, name, UID, and immutable profile, a readiness wait, and three reviewed stages: arm, clear access claims, then remove the request
Deletion protectionRetain mode keeps the Stack protected; armed Delete waits for observed deleteProtection=false and targets only external state that can outlive the Stack
Stack-local provider configurationA namespaced Grafana ProviderConfig scoped to the one stack, built from the generated administrator credential
Starter contentThree baseline folder/dashboard pairs occupying the billing/usage, telemetry-endpoints, and stack-home slots, when spec.baselineDashboards.enabled is true
Configurable drift behaviourPer-resource enforced/createOnly/observeOnly/disabled reconciliation modes that decide whether Crossplane repairs an administrator's UI edit or leaves it alone
OAuth and SAML SSOPlatform-defined SSO profiles a request selects by name — github, gitlab, google, azuread, okta, generic_oauth, or saml
ReportsAn optional scheduled monthly usage report (PDF/CSV)
PluginsAn optional list of Grafana Cloud plugin installations
Incident relay resourcesOptional OnCall outgoing webhooks and Alerting contact points that call a platform-owned relay
TeamsDirectory-synchronized or directly-managed Grafana teams
Basic-role ACLsFolder/dashboard permissions granted to the built-in Viewer/Editor/Admin basic roles
Fixed-role assignmentsExisting Grafana-managed fixed roles assigned to a team by UID
Custom rolesStack-local roles built from explicit action/scope permission pairs
Role assignmentsWhole-set or item-level bindings of a role to a team
Explicit modulesInventory, Fleet pipelines, alerting, Agent Observability, Assistant governance, datasource access, Git provisioning repositories, and product toggles have their own ownership boundaries and prerequisites

What this reference proves, and what it does not

This repository is a portable public reference. It never contacts a live Grafana Cloud stack, a live cluster, or any source environment, and it carries no credentials and no live requests. That is a deliberate boundary and not a gap waiting to be filled: the validation gate is designed so it cannot make that contact, and no future change to this repository will produce live behavioural evidence.

What the gate does prove, and the limit of each:

Evidence classWhat it establishesWhat it does not
Admission against a real API serverEvery XRD installs, every catalog example is admitted, and every fail-closed CEL rule actually rejects, with create/update and weaken/admit/restore controlsThe server is ephemeral and empty. Nothing reconciles, so no provider behaviour is observed
Renderer testsWhat the composition function emits for a given request and observed state, including the refusals and the waitsThat the provider then accepts those documents, or that Grafana Cloud accepts what the provider sends
Provider CRD readbackThe emitted shapes round-trip through the pinned provider's own CRDsThat a live provider assigns the identities the renderer waits for
Pinned upstream sourceImport identities, external-name conventions and management-policy semantics, read from the exact pinned provider and runtime revisionsRuntime behaviour. A source reading is a strong argument, not a reconciliation
Signed package provenanceThe published function image matches the source commit and workflow identity that built itAnything about what that function does once installed

So credential minting, stack adoption, cross-cluster migration and drift correction are described and unit-proven here, and they are not exercised end to end. Rehearse them in an environment you own, against a disposable stack, before running them against one carrying traffic. Treat every procedure in Migration and adoption as requiring that rehearsal.

Reading further

Getting startedPrerequisites and the copy-edit-review-commit path to your first stack
InstallationBootstrapping Crossplane, ESO, and the platform components
ConfigurationPlatform policy, profiles, and the organization registry
ArchitectureThe three-controller split, reconciliation, and where state lives
SecretsESO wiring and the credential rotation model
SSOOAuth and SAML configuration
SecuritySupply-chain controls, secret handling, and the Retain-by-default lifecycle
GovernanceToken ceilings, network restrictions, product modules, retention, and tenancy
Migration and adoptionBreaking schema changes per release boundary, inventory-first adoption, and the non-destructive handover path
TroubleshootingCommon failure modes
FAQShort answers to recurring questions

Project

grafana-cloud-vending-machine is open source under the Apache 2.0 licence. Apache License 2.0. See LICENSE. Issues and pull requests are welcome on GitHub.