Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions compute/eks/addons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,17 +494,17 @@ The chart generates an admin password into a Secret; `grafana_helm_values` is th
- `method = "load_balancer"` (the default): the chosen shared ALB reaches Grafana's pods through an IP target group, a host-header rule on the HTTPS listener, and a TargetGroupBinding the load balancer controller keeps registered. `load_balancer = "private"` (the default) uses the internal ALB, reachable only from the VPC and networks connected to it, such as a VPN or a Tailscale subnet router; its security group admits `private_alb_ingress_cidr_blocks`, which by default covers the private ranges a subnet router's traffic arrives from. `"public"` uses the internet-facing ALB, so anyone can reach the sign-in page. Plan refuses it without the chosen ALB's HTTPS listener. Point the hostname at `private_alb_dns_name` or `public_alb_dns_name`; a public record works for the private ALB too, since it resolves to private addresses only reachable from inside.
- `method = "ingress"`: the Grafana chart's own Ingress routes `hostname` to the Service and asks for TLS for it, served by the controller of `ingress_class_name` (the cluster's default IngressClass when null), with `ingress_annotations` and `ingress_tls_secret_name` passed to it as they are. With the [Tailscale Kubernetes operator](https://tailscale.com/kb/1439/kubernetes-operator-cluster-ingress)'s `tailscale` class and a `<name>.<tailnet>.ts.net` hostname, Grafana becomes one device on the tailnet named `<name>`, with a Let's Encrypt certificate, and nothing else in the VPC is reachable through it. The tailnet needs MagicDNS and HTTPS certificates turned on. Install the class's controller before turning this on: with any shared load balancer, the AWS Load Balancer Controller runs too, and its admission webhook refuses an Ingress whose IngressClass does not exist yet (`invalid ingress class: IngressClass.networking.k8s.io "tailscale" not found`).

**Sign-in** (`grafana_auth`, `grafana_auth_providers`) is Grafana's own, under Grafana's own setting names:
**Sign-in** (`grafana_auth_providers`, `grafana_default_role`) is Grafana's own, under Grafana's own setting names. `grafana_auth_providers` lists every way to sign in, one entry each, and plan refuses an empty list:

- `grafana_auth.login_form_enabled` (default `true`) keeps username and password for Grafana's local users, including the generated admin. `false` turns off the login form and HTTP basic auth both, so a password works nowhere.
- `grafana_auth_providers` lists any of Grafana's OAuth providers side by side, one entry each: `google`, `github`, `gitlab`, `azuread` (Microsoft Entra ID), `okta` and `generic_oauth` (Keycloak, Auth0, Authentik or any OpenID Connect provider). Each entry becomes Grafana's `[auth.<provider>]` section. In the form, each one is a card edited in its own dialog.
- `basic` (the default, `[{ provider = "basic" }]`) is username and password for Grafana's local users, including the generated admin, and takes no other field. Without it, the login form and HTTP basic auth are both off, so a password works nowhere.
- Any of Grafana's OAuth providers side by side: `google`, `github`, `gitlab`, `azuread` (Microsoft Entra ID), `okta` and `generic_oauth` (Keycloak, Auth0, Authentik or any OpenID Connect provider). Each entry becomes Grafana's `[auth.<provider>]` section. In the form, each one is a card edited in its own dialog.
- Typed fields cover what each provider needs: allowed domains, groups, organizations or team IDs, role mapping (`role_attribute_path`), and the tenant, org URL, self-managed URL or endpoints.
- `settings` passes any other key of the section through verbatim.
- `grafana_default_role` (`Viewer`, `Editor` or `Admin`; default `Viewer`) is the role an account gets the first time it signs in through one, unless its role mapping says otherwise.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
- Every client secret is a Secrets Manager ARN. The External Secrets Operator writes it into a Secret, and Grafana reads it as `GF_AUTH_<PROVIDER>_CLIENT_SECRET`, never as a Helm value.
- Grafana takes environment variables as an EKS workload does. `grafana_environment_variables` is plain `{ name, value }` objects, and `grafana_secrets` is `{ name, value_from }` references, each a full Secrets Manager or SSM Parameter Store ARN. A Secrets Manager ARN may end in ECS's `:<json key>:<version stage>:<version id>` to read one key of a JSON secret, as in `arn:aws:secretsmanager:us-east-2:111122223333:secret:prod/app-AbCdEf:GRAFANA_CLIENT_SECRET::`. The External Secrets Operator reads every reference inside the cluster, so none is a Helm value.
- Grafana reads any setting from `GF_<SECTION>_<KEY>`, so `GF_AUTH_<PROVIDER>_CLIENT_ID` and `GF_AUTH_<PROVIDER>_CLIENT_SECRET` in `grafana_secrets` give a provider its client, in place of `client_id` and `client_secret_arn`.
- Grafana expands `$__env{NAME}` in every setting, so a role mapping can name a group from an environment variable: `contains(groups[*], '$__env{ADMINS_GROUP}') && 'Admin' || 'Viewer'`.
- `grafana_auth.default_role` (default `Viewer`) is the role an account gets on first sign-in, unless the provider's role mapping says otherwise.
- Roles can follow groups at every sign-in through `role_attribute_path`. For Google Workspace groups, Grafana reads a user's direct group memberships, as group emails, only when `scopes` includes `https://www.googleapis.com/auth/cloud-identity.groups.readonly`, and the Cloud Identity API is enabled in the OAuth client's Google Cloud project. For example, `scopes = "openid email profile https://www.googleapis.com/auth/cloud-identity.groups.readonly"` with `role_attribute_path = "contains(groups[*], 'grafana-admins@example.com') && 'Admin' || 'Viewer'"`.
- Plan refuses these configurations:
- No way to sign in at all.
Expand Down Expand Up @@ -853,8 +853,8 @@ failed during initialization have no provider resources to migrate.
| grafana_role_creation_enabled | Create the IAM role Amazon Managed Grafana assumes to read the workspace and log groups. | `bool` | `false` | no |
| grafana_source_account_id | Account whose Grafana workspaces may assume that role (`aws:SourceAccount`). Null uses this account. | `string` | `null` | no |
| grafana_access | `{ enabled, method, hostname, load_balancer, listener_rule_priority, ingress_class_name, ingress_annotations, ingress_tls_secret_name }` — serve the in-cluster Grafana over HTTPS at `hostname`. `method = "load_balancer"` (default) routes it on the `private` (default) or `public` ALB's HTTPS listener, a null priority letting AWS assign one; `"ingress"` gives it an Ingress of `ingress_class_name` (the cluster default when null), such as `tailscale`. | `object` | `{}` | no |
| grafana_auth | `{ login_form_enabled, default_role }` — username and password sign-in (default `true`) and the role for new accounts (`Viewer`, `Editor` or `Admin`; default `Viewer`). | `object` | `{}` | no |
| grafana_auth_providers | One entry per Grafana OAuth provider: `provider` (`google`, `github`, `gitlab`, `azuread`, `okta`, `generic_oauth`), `client_id` and `client_secret_arn` (or `GF_AUTH_<PROVIDER>_CLIENT_ID` and `GF_AUTH_<PROVIDER>_CLIENT_SECRET` in `grafana_secrets`), and Grafana's own settings for it: `tenant_id` (Entra ID), `url` (Okta org, self-managed GitLab), `name`, `auth_url`, `token_url`, `api_url`, `scopes` (generic), `allowed_domains`, `allowed_groups`, `allowed_organizations`, `team_ids`, `role_attribute_path`, and a verbatim `settings` map. | `list(object)` | `[]` | no |
| grafana_auth_providers | One entry per way to sign in: `provider` (`basic` for username and password, which takes no other field, or an OAuth provider: `google`, `github`, `gitlab`, `azuread`, `okta`, `generic_oauth`), `client_id` and `client_secret_arn` (or `GF_AUTH_<PROVIDER>_CLIENT_ID` and `GF_AUTH_<PROVIDER>_CLIENT_SECRET` in `grafana_secrets`), and Grafana's own settings for it: `tenant_id` (Entra ID), `url` (Okta org, self-managed GitLab), `name`, `auth_url`, `token_url`, `api_url`, `scopes` (generic), `allowed_domains`, `allowed_groups`, `allowed_organizations`, `team_ids`, `role_attribute_path`, and a verbatim `settings` map. | `list(object)` | `[{ provider = "basic" }]` | no |
| grafana_default_role | The role an account gets the first time it signs in through an OAuth provider: `Viewer`, `Editor` or `Admin`. | `string` | `"Viewer"` | no |
| grafana_environment_variables | Plain environment variables for Grafana, `{ name, value }` objects as an EKS workload's `environment_variables`. | `list(object)` | `[]` | no |
| grafana_secrets | Secret environment variables for Grafana, `{ name, value_from }` objects as an EKS workload's `secrets`: a full Secrets Manager or SSM Parameter Store ARN, read through External Secrets. A Secrets Manager ARN may end in `:<json key>:<version stage>:<version id>`. | `list(object)` | `[]` | no |
| logs_enabled | Collect container logs with Alloy into an in-cluster Loki storing to S3 in this account. | `bool` | `false` | no |
Expand Down
4 changes: 2 additions & 2 deletions compute/eks/addons/grafana.tf
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ resource "helm_release" "grafana" {
}

precondition {
condition = var.grafana_auth.login_form_enabled || length(local.grafana_auth_entries) > 0
error_message = "Grafana would have no way to sign in: grafana_auth.login_form_enabled is false and grafana_auth_providers is empty."
condition = length(local.grafana_auth_entries) > 0
error_message = "Grafana would have no way to sign in: grafana_auth_providers is empty. Add basic for username and password, or an OAuth provider."
}

precondition {
Expand Down
25 changes: 13 additions & 12 deletions compute/eks/addons/grafana_auth.tf
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
################################################################################
# In-cluster Grafana sign-in
#
# Grafana's own authentication, as Grafana's own configuration names it:
# username and password for local users, and the OAuth providers Grafana
# supports out of the box. Each grafana_auth_providers entry becomes one
# [auth.<provider>] section in grafana.ini.
# Grafana's own authentication, as Grafana's own configuration names it. The
# basic entry of grafana_auth_providers is username and password for local
# users; each other entry is one of the OAuth providers Grafana supports out of
# the box and becomes one [auth.<provider>] section in grafana.ini.
#
# Every client secret is a Secrets Manager ARN. The External Secrets Operator
# materializes it into a Kubernetes Secret, and Grafana reads it from
Expand All @@ -16,8 +16,10 @@
locals {
grafana_auth_entries = var.grafana_enabled ? var.grafana_auth_providers : []

# The validation guarantees each provider appears once.
grafana_auth_by_provider = { for entry in local.grafana_auth_entries : entry.provider => entry }
grafana_password_sign_in_enabled = contains([for entry in var.grafana_auth_providers : entry.provider], "basic")

# The OAuth providers. The validation guarantees each provider appears once.
grafana_auth_by_provider = { for entry in local.grafana_auth_entries : entry.provider => entry if entry.provider != "basic" }

grafana_auth_values = {
for provider, entry in local.grafana_auth_by_provider : provider => {
Expand Down Expand Up @@ -128,20 +130,19 @@ locals {

# grafana.ini: SigV4 for the AMP data source always; the role new accounts
# get; the external URL OAuth providers redirect back to while grafana_access
# serves Grafana; and the sign-in methods. Turning username and password
# off removes the login form and HTTP basic auth both, so a password is
# accepted nowhere.
# serves Grafana; and the sign-in methods. Without the basic provider there
# is no login form and no HTTP basic auth, so a password is accepted nowhere.
grafana_ini = merge(
{
auth = merge(
{ sigv4_auth_enabled = true },
var.grafana_auth.login_form_enabled ? {} : { disable_login_form = true },
local.grafana_password_sign_in_enabled ? {} : { disable_login_form = true },
)
users = {
auto_assign_org_role = var.grafana_auth.default_role
auto_assign_org_role = var.grafana_default_role
}
},
{ for section, settings in { "auth.basic" = { enabled = false } } : section => settings if !var.grafana_auth.login_form_enabled },
{ for section, settings in { "auth.basic" = { enabled = false } } : section => settings if !local.grafana_password_sign_in_enabled },
local.grafana_access_enabled ? {
server = {
domain = local.grafana_hostname
Expand Down
Loading
Loading