Skip to content
Draft
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
26 changes: 15 additions & 11 deletions .claude/skills/sync-settings-tfvars/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@ That asymmetry drives every rule below: the example is the authority on
*structure*, the private file is the authority on *values*, and a mistake in the
private file is unrecoverable.

## The three pairs
## The four pairs

| Env | Example (checked in) | Private (gitignored) |
| --- | --- | --- |
| `aws/` | `settings.tfvars.example` | `settings.tfvars` |
| `azure/` | `settings.tfvars.example` | `settings.tfvars` |
| `stackit/` | `settings.tfvars.example` | `settings.tfvars` |
| `local/` | `settings.tfvars.example` | `settings.tfvars` |

Sync all three unless the user names specific ones. A private file that doesn't
Sync all four unless the user names specific ones. A private file that doesn't
exist yet isn't a sync — say so and offer to seed it from the example instead of
inventing values.

Expand All @@ -33,7 +34,7 @@ inventing values.

```bash
BACKUP=$(mktemp -d) || { echo "could not create backup dir — stop"; exit 1; }
for e in aws azure local; do
for e in aws azure stackit local; do
[ -f "$e/settings.tfvars" ] || continue
cp "$e/settings.tfvars" "$BACKUP/$e-settings.tfvars" \
&& cmp -s "$e/settings.tfvars" "$BACKUP/$e-settings.tfvars" \
Expand All @@ -54,7 +55,7 @@ committed with live secrets in it. Report the backup path to the user.
### 2. Survey the drift

```bash
python3 .claude/skills/sync-settings-tfvars/scripts/structure_diff.py aws azure local
python3 .claude/skills/sync-settings-tfvars/scripts/structure_diff.py aws azure stackit local
```

This redacts every value, so its output is safe to read and quote. Per pair it
Expand All @@ -76,12 +77,12 @@ developer still recognizes, not a regenerated one.
### 4. Verify

```bash
# fmt only the files that exist — a developer using one cloud has no azure/local tfvars
# fmt only the files that exist — a developer using one cloud has no azure/stackit/local tfvars
# no -diff: on a misformatted file it prints the offending lines, secrets included
for e in aws azure local; do
for e in aws azure stackit local; do
[ -f "$e/settings.tfvars" ] && terraform fmt -check "$e/settings.tfvars"
done
python3 .claude/skills/sync-settings-tfvars/scripts/structure_diff.py aws azure local
python3 .claude/skills/sync-settings-tfvars/scripts/structure_diff.py aws azure stackit local

# definitive: exits non-zero if any value fails a validation block
cd <env> && terraform plan -var-file=settings.tfvars
Expand Down Expand Up @@ -201,14 +202,17 @@ mistake later.
example is an intentional override. Leave it, keep its rationale comment, and
list it in the report so the user can confirm it's still wanted.

**Respect per-cloud asymmetry — check before adding.** The three environments
**Respect per-cloud asymmetry — check before adding.** The four environments
have genuinely different variable sets, so never copy a section across clouds by
analogy. Confirm against that environment's `variables.tf` that a variable exists
and a value is legal before introducing it. Two real examples: AI Lake
and a value is legal before introducing it. Three real examples: AI Lake
(`enable_ai_lake`, `starrocks_size_profile`) exists for AWS and has no Azure
counterpart, and `size_profile` accepts `prod-xl` on AWS while Azure's validation
block rejects it. Comments listing valid values are part of this — an
AWS-accurate list pasted into Azure documents a value that fails validation.
block rejects it; and STACKIT declares `enable_image_cache` but validates that
it stays `false`, with no `dockerhub_*` variables at all, because STACKIT's
container registry has no Terraform resources. Comments listing valid values
are part of this — an AWS-accurate list pasted into Azure documents a value
that fails validation.

**Fix comments that are wrong, and say that you did.** Occasionally a private
file carries a comment that no longer describes the code under it — a leftover
Expand Down
19 changes: 18 additions & 1 deletion .devcontainer/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ TINKEY_VERSION="$(curl -fsSL https://api.github.com/repos/tink-crypto/tink-tinke
K9S_VERSION="$(curl -fsSL https://api.github.com/repos/derailed/k9s/releases/latest | jq -r '.tag_name')"
KUBELOGIN_VERSION="$(curl -fsSL https://api.github.com/repos/Azure/kubelogin/releases/latest | jq -r '.tag_name' | sed 's/^v//')"
K3D_VERSION="$(curl -fsSL https://api.github.com/repos/k3d-io/k3d/releases/latest | jq -r '.tag_name')"
STACKIT_CLI_VERSION="$(curl -fsSL https://api.github.com/repos/stackitcloud/stackit-cli/releases/latest | jq -r '.tag_name' | sed 's/^v//')"

for var_name in TINKEY_VERSION K9S_VERSION KUBELOGIN_VERSION K3D_VERSION; do
for var_name in TINKEY_VERSION K9S_VERSION KUBELOGIN_VERSION K3D_VERSION STACKIT_CLI_VERSION; do
val="${!var_name}"
if [ -z "${val}" ] || [ "${val}" = "null" ]; then
echo "ERROR: Failed to fetch ${var_name} from GitHub API" >&2
Expand Down Expand Up @@ -49,6 +50,22 @@ sudo unzip -q /tmp/kubelogin.zip -d /tmp
sudo install -m 0755 "/tmp/bin/linux_${PKG_ARCH}/kubelogin" /usr/local/bin/kubelogin
sudo rm -rf /tmp/kubelogin.zip /tmp/bin

# Install the STACKIT CLI. Terraform authenticates from a service account key, so
# this is for listing machine types and PostgreSQL Flex flavors.
ARCH=$(dpkg --print-architecture)
case "$ARCH" in
amd64) PKG_ARCH="amd64" ;;
arm64) PKG_ARCH="arm64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
# mktemp, not a fixed /tmp path: dpkg runs under sudo, so a pre-created file or
# symlink there would hand a local process control of what gets installed.
STACKIT_DEB="$(mktemp --suffix=.deb)"
trap 'rm -f -- "${STACKIT_DEB}"' EXIT
curl -fsSL -o "${STACKIT_DEB}" "https://github.com/stackitcloud/stackit-cli/releases/download/v${STACKIT_CLI_VERSION}/stackit_${STACKIT_CLI_VERSION}_linux_${PKG_ARCH}.deb"
sudo dpkg -i "${STACKIT_DEB}" || sudo apt-get -y -f install
command -v stackit >/dev/null || { echo "ERROR: stackit CLI install failed" >&2; exit 1; }

# Install k3d (for local deployments)
ARCH=$(uname -m)
case "$ARCH" in
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AGENTS.md

Terraform for deploying GoodData CN across AWS, Azure, and local environments.
Terraform for deploying GoodData CN across AWS, Azure, STACKIT, and local environments.

This repo is primarily authored against Claude Code, but any capable coding agent can drive it.

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ Always let the user review and approve applies themselves. You may run `terrafor

### Validating across environments

When a change affects shared modules, run `terraform plan -var-file=settings.tfvars` in all three environment directories (aws, azure, local) to catch environment-specific issues.
When a change affects shared modules, run `terraform plan -var-file=settings.tfvars` in all four environment directories (aws, azure, local, stackit) to catch environment-specific issues.

AWS and Azure may not have active clusters, so plan errors from missing state or credentials are expected. Focus on catching Terraform configuration errors (missing variables, type mismatches, invalid references).
AWS, Azure and STACKIT may not have active clusters, so plan errors from missing state or credentials are expected. Focus on catching Terraform configuration errors (missing variables, type mismatches, invalid references).

## Commit messages

Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Terraform provisions:
1. Install the following CLI utilities:
- [Terraform](https://developer.hashicorp.com/terraform/install)
- Cloud provider CLI ([AWS](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html), [Azure](https://learn.microsoft.com/cli/azure/install-azure-cli))
- For STACKIT deployments: the [STACKIT CLI](https://github.com/stackitcloud/stackit-cli) is optional — Terraform authenticates from a service account key. It is useful for listing machine types and PostgreSQL Flex flavors.
- For Azure deployments: [kubelogin](https://azure.github.io/kubelogin/install.html)
- [kubectl](https://kubernetes.io/docs/tasks/tools/)
- [helm](https://helm.sh/docs/intro/install/)
Expand Down Expand Up @@ -65,13 +66,15 @@ Requirements:
cp aws/settings.tfvars.example aws/settings.tfvars
# or (for azure)
cp azure/settings.tfvars.example azure/settings.tfvars
# or (for stackit)
cp stackit/settings.tfvars.example stackit/settings.tfvars
# or (for local)
cp local/settings.tfvars.example local/settings.tfvars
```

The example file has good defaults but you may want to modify it based on your needs.

1. Choose your provider and `cd` into its directory: `cd aws`, `cd azure`, or `cd local`
1. Choose your provider and `cd` into its directory: `cd aws`, `cd azure`, `cd stackit`, or `cd local`

1. Authenticate to your cloud provider's CLI:
- For AWS: authenticate the profile named by `aws_profile_name` in
Expand All @@ -81,6 +84,9 @@ Requirements:
`aws configure --profile <aws_profile_name>`.
- For Azure: `az login`
- Azure note: Terraform's Kubernetes authentication uses `kubelogin` with your Azure CLI session.
- For STACKIT: create a service account key and export `STACKIT_SERVICE_ACCOUNT_KEY_PATH` (or save it to `~/.stackit/credentials.json`). See the [provider authentication docs](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs#authentication).
- STACKIT note: SKE issues a short-lived kubeconfig, so rerun `../scripts/configure-kubectl.sh` when it expires.
- STACKIT note: STACKIT Object Storage refuses to delete a bucket that still holds objects, so empty the buckets before `terraform destroy`.

1. Initialize Terraform: `terraform init`

Expand All @@ -101,7 +107,7 @@ Requirements:

1. Configure authentication according to your needs:
- To use an external OIDC provider (recommended for anything beyond local testing), follow the [Set Up Authentication guide](https://www.gooddata.com/docs/cloud-native/latest/manage-organization/set-up-authentication/).
- For quick testing with the default IdP (Dex), create one or more users by staying in the provider directory (`aws`, `azure`, or `local`) and running `../scripts/create-user.sh`. If Terraform created the organization, the script will automatically read the admin credentials from the Secret `gooddata-cn/gdcn-org-admin-<org_id>`.
- For quick testing with the default IdP (Dex), create one or more users by staying in the provider directory (`aws`, `azure`, `stackit`, or `local`) and running `../scripts/create-user.sh`. If Terraform created the organization, the script will automatically read the admin credentials from the Secret `gooddata-cn/gdcn-org-admin-<org_id>`.

1. **(Optional)** If you enabled the observability stack (`enable_observability = true`), create Grafana users by running `../scripts/create-grafana-user.sh` from your provider directory. The script creates a Grafana user and optionally promotes them to admin. It automatically reads the Grafana admin credentials from the Kubernetes secret.

Expand Down
33 changes: 24 additions & 9 deletions modules/k8s-common/gooddata-cn.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,28 @@ locals {
dex_ingress_annotations = merge(local.dex_annotation_defaults, var.dex_ingress_annotations_override)
dex_tls_enabled = local.use_cert_manager
fast_storage_class = var.fast_storage_class != "" ? var.fast_storage_class : var.gdcn_storage_class

# Generic S3: endpoint override plus static keys. Covers local SeaweedFS and
# STACKIT Object Storage; null on clouds with native object storage.
generic_s3 = var.cloud == "local" ? {
scheme = "http"
s3_endpoint_override = var.local_s3_endpoint_override
s3_region = var.local_s3_region
s3_access_key = var.local_s3_access_key
s3_secret_key = var.local_s3_secret_key
s3_exports_bucket = var.local_s3_exports_bucket
s3_datasource_fs_bucket = var.local_s3_datasource_fs_bucket
s3_quiver_cache_bucket = var.local_s3_quiver_cache_bucket
} : var.cloud == "stackit" ? {
scheme = "https"
s3_endpoint_override = var.stackit_s3_endpoint_override
s3_region = var.stackit_s3_region
s3_access_key = var.stackit_s3_access_key
s3_secret_key = var.stackit_s3_secret_key
s3_exports_bucket = var.stackit_s3_exports_bucket
s3_datasource_fs_bucket = var.stackit_s3_datasource_fs_bucket
s3_quiver_cache_bucket = var.stackit_s3_quiver_cache_bucket
} : null
}

# Enforce STRICT mTLS for all inbound traffic to workloads in the GoodData.CN namespace.
Expand Down Expand Up @@ -174,15 +196,8 @@ resource "helm_release" "gooddata_cn" {
var.enable_observability ? templatefile("${path.module}/templates/gdcn-observability.yaml.tftpl", {
observability_namespace = kubernetes_namespace_v1.observability[0].metadata[0].name
}) : null,
var.cloud == "local" ? templatefile("${path.module}/templates/gdcn-local.yaml.tftpl", {
s3_endpoint_override = var.local_s3_endpoint_override
s3_region = var.local_s3_region
s3_access_key = var.local_s3_access_key
s3_secret_key = var.local_s3_secret_key
s3_exports_bucket = var.local_s3_exports_bucket
s3_datasource_fs_bucket = var.local_s3_datasource_fs_bucket
s3_quiver_cache_bucket = var.local_s3_quiver_cache_bucket
}) : null,
local.generic_s3 != null ? templatefile("${path.module}/templates/gdcn-s3-generic.yaml.tftpl", local.generic_s3) : null,
var.cloud == "local" ? templatefile("${path.module}/templates/gdcn-local-insecure.yaml.tftpl", {}) : null,
templatefile("${path.module}/templates/gdcn-size-${var.gdcn_size}.yaml.tftpl", { cloud = var.cloud }),
var.gdcn_helm_extra_values != "" ? var.gdcn_helm_extra_values : null,
])
Expand Down
10 changes: 10 additions & 0 deletions modules/k8s-common/templates/gdcn-local-insecure.yaml.tftpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Exports reach the ingress over loopback via the socat sidecar, and Chrome has
# to accept the self-signed local certificate.

visualExporterProxy:
permittedDestinations: "127.0.0.1/32 ::1/128"

exportBuilder:
extraEnvVars:
- name: CHROME_FLAGS_EXTRA
value: "--ignore-certificate-errors --allow-insecure-localhost"
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,18 @@ exportController:
s3Region: "${s3_region}"
s3Bucket: "${s3_exports_bucket}"
endpointOverride: "${s3_endpoint_override}"
scheme: "http"
scheme: "${scheme}"
s3:
accessKey: "${s3_access_key}"
secretKey: "${s3_secret_key}"

visualExporterProxy:
permittedDestinations: "127.0.0.1/32 ::1/128"

exportBuilder:
extraEnvVars:
- name: CHROME_FLAGS_EXTRA
value: "--ignore-certificate-errors --allow-insecure-localhost"

quiver:
durableStorageType: "S3"
s3DurableStorage:
s3Region: "${s3_region}"
s3Bucket: "${s3_quiver_cache_bucket}"
endpointOverride: "${s3_endpoint_override}"
scheme: "http"
scheme: "${scheme}"
authType: "aws_tokens"
s3AccessKey: "${s3_access_key}"
s3SecretKey: "${s3_secret_key}"
Expand All @@ -36,7 +28,7 @@ quiver:
s3Region: "${s3_region}"
s3Bucket: "${s3_datasource_fs_bucket}"
endpointOverride: "${s3_endpoint_override}"
scheme: "http"
scheme: "${scheme}"
authType: "aws_tokens"
s3AccessKey: "${s3_access_key}"
s3SecretKey: "${s3_secret_key}"
55 changes: 54 additions & 1 deletion modules/k8s-common/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,16 @@ variable "azure_uami_client_id" {
default = ""
}

variable "cloud" { type = string }
# An unlisted value would silently drop the cloud's object-storage values file
# from the GoodData.CN release, so keep this in sync with the env directories.
variable "cloud" {
type = string

validation {
condition = contains(["aws", "azure", "local", "stackit"], var.cloud)
error_message = "cloud must be one of: aws, azure, local, stackit."
}
}

variable "db_hostname" { type = string }

Expand Down Expand Up @@ -263,6 +272,50 @@ variable "s3_quiver_cache_bucket_id" {
default = ""
}

variable "stackit_s3_access_key" {
description = "S3 access key for STACKIT Object Storage."
type = string
default = ""
sensitive = true
}

variable "stackit_s3_datasource_fs_bucket" {
description = "Bucket name used for Quiver datasource FS (CSV uploads) in STACKIT Object Storage."
type = string
default = ""
}

variable "stackit_s3_endpoint_override" {
description = "S3 endpoint override URL (with scheme) for STACKIT Object Storage."
type = string
default = ""
}

variable "stackit_s3_exports_bucket" {
description = "Bucket name used for exports in STACKIT Object Storage."
type = string
default = ""
}

variable "stackit_s3_quiver_cache_bucket" {
description = "Bucket name used for Quiver durable cache in STACKIT Object Storage."
type = string
default = ""
}

variable "stackit_s3_region" {
description = "S3 region value for STACKIT Object Storage."
type = string
default = ""
}

variable "stackit_s3_secret_key" {
description = "S3 secret key for STACKIT Object Storage."
type = string
default = ""
sensitive = true
}

variable "starrocks_s3_bucket_id" {
type = string
default = ""
Expand Down
Loading