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
86 changes: 86 additions & 0 deletions docs/core-concepts/operations/pod-disruptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
title: Pod Disruptions
---

A Kubernetes cluster moves pods around for reasons that have nothing to do with your workload:
draining a node for an upgrade, rebalancing, scaling down the node pool. These are *voluntary*
disruptions, and a
[PodDisruptionBudget](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/) is how a
workload tells the cluster how many of its pods may be taken down at once.

This matters most for quorum-based roles. Evicting two of three ZooKeeper servers at the same time
does not slow the ensemble down — it loses quorum and stops.

Kubedoop writes a PodDisruptionBudget **per role**, using a default the operator picks from what it
knows about the product. You do not have to configure anything to be protected.

## Configuration

Each role accepts a `roleConfig.podDisruptionBudget` block:

```yaml
apiVersion: zookeeper.kubedoop.dev/v1alpha1
kind: ZookeeperCluster
metadata:
name: zookeeper
spec:
server:
roleConfig:
podDisruptionBudget:
enabled: true
maxUnavailable: 1
roleGroups:
default:
replicas: 3
```

| Field | Type | Default | Meaning |
|-------|------|---------|---------|
| `enabled` | boolean | `true` | Whether the operator writes a PDB for this role |
| `maxUnavailable` | integer | product-specific | How many pods of the role may be down at once |

Both fields are optional, and the block itself is optional.

### `enabled` defaults to true, including when you omit it

Leaving `podDisruptionBudget` out entirely, and writing a block that only sets `maxUnavailable`,
both leave the PDB enabled. A role that mentions the budget just to raise `maxUnavailable` still
gets its PDB — you only lose it by writing `enabled: false` explicitly.

### `maxUnavailable` defaults per product

When you do not set it, the operator supplies a value based on what it knows about the product
rather than a blanket number, because the safe count differs by role: a three-node quorum tolerates
one loss, a pool of stateless workers tolerates far more.

Set it yourself only when you know the product's tolerance better than the operator does for your
topology.

## Supplying your own PDB

`enabled: false` stops the operator from writing a PDB for the role, which is what you want when
you intend to manage one yourself:

```yaml
spec:
server:
roleConfig:
podDisruptionBudget:
enabled: false
```

The operator then leaves the role alone, and any PodDisruptionBudget you create with your own
selector applies normally. Note that turning it off and *not* replacing it leaves the role with no
protection against voluntary eviction at all.

## What a PDB does not cover

A PodDisruptionBudget constrains voluntary disruptions only. A node that hard-crashes, a kernel
panic, or a pod killed for exceeding its memory limit are all involuntary — the budget has no say.
Spreading a role across failure domains is the tool for that; see
[Pod placement](./pod-placement.md).

## Related

- [Pod placement](./pod-placement.md)
- [Roles and role groups](../common-configuration-mechanisms/roles-and-role-groups.md)
138 changes: 138 additions & 0 deletions docs/core-concepts/operations/pod-placement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
title: Pod Placement
---

Where a role's pods land decides how much a single failure costs. Three ZooKeeper servers on one
node are three pods that disappear together. Kubedoop products therefore ship a default
[affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) that spreads a
role across nodes, and let you replace it when your topology calls for something else.

## The default is already there

Every product declares affinity defaults for its own roles — typically a `podAntiAffinity` that
keeps replicas of a role off the same node. You do not need to write anything to get it.

Read the section below before overriding it, because overriding does not mean adding.

## Configuring affinity

Affinity is set under `config`, which exists at two levels:

```yaml
apiVersion: zookeeper.kubedoop.dev/v1alpha1
kind: ZookeeperCluster
metadata:
name: zookeeper
spec:
server:
config:
affinity: # role level: applies to every group of this role
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/worker
operator: Exists
roleGroups:
default:
replicas: 3
gpu:
replicas: 2
config:
affinity: # role group level: applies to this group only
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: accelerator
operator: In
values: ["nvidia"]
```

The value is a standard Kubernetes
[`Affinity`](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling)
object and is passed through to the pod spec unchanged, so `nodeAffinity`, `podAffinity` and
`podAntiAffinity` all behave exactly as they do in a plain Deployment.

## Affinity replaces, it does not merge

There are three layers, from the bottom up:

1. the product's role defaults
2. the role's `config.affinity`
3. the role group's `config.affinity`

**Any layer that sets `affinity` replaces the layer beneath it entirely.** This follows the
Kubernetes rule and is what someone editing the field expects — but it has a consequence that is
easy to miss.

Setting only `nodeAffinity` at the role group level, to pin a group to an instance type, discards
the product's `podAntiAffinity` along with it. The group is now pinned to the right nodes and no
longer spread across them, which is usually the opposite of what was intended.

Nothing fails when this happens. The resource is valid, the pod spec is valid, and every status
condition stays green while the quorum quietly stops being spread.

### How to tell it happened

The operator emits a Warning event on the cluster resource:

```text
Type Reason Message
Warning AffinityOverridden role "server" group "gpu": the role group's config replaces
config.affinity wholesale, discarding the podAntiAffinity declared
beneath it. config.affinity follows the Kubernetes rule and is not
merged per member; restate the discarded member alongside your own
to keep it
```

Check for it after changing affinity:

```bash
kubectl get events --field-selector reason=AffinityOverridden
```

### Keeping what you replaced

Restate the discarded member alongside your own:

```yaml
config:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: accelerator
operator: In
values: ["nvidia"]
podAntiAffinity: # restated, or it is lost
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 70
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app.kubernetes.io/name: zookeeper
app.kubernetes.io/instance: zookeeper
app.kubernetes.io/component: server
```

## Choosing a topology key

`topologyKey` decides what "spread apart" means:

| Key | Spreads across |
|-----|----------------|
| `kubernetes.io/hostname` | Nodes — protects against a single node failing |
| `topology.kubernetes.io/zone` | Availability zones — protects against a zone failing |

Zone-level spreading needs enough nodes per zone to satisfy it. With
`requiredDuringSchedulingIgnoredDuringExecution` and fewer zones than replicas, the surplus pods
stay `Pending` forever; `preferredDuringSchedulingIgnoredDuringExecution` degrades instead of
blocking, which is why product defaults favour it.

## Related

- [Pod disruptions](./pod-disruptions.md)
- [Roles and role groups](../common-configuration-mechanisms/roles-and-role-groups.md)
171 changes: 169 additions & 2 deletions docs/core-concepts/resources/s3.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,171 @@
---
title: S3
---

# S3
Many Kubedoop products read and write object storage: Hive keeps table data there, Trino queries it,
Spark checkpoints to it. Rather than repeating endpoint and credential settings in every product
resource, Kubedoop models S3 as two cluster resources that products point at.

TODO
## Two objects

| Object | Describes |
|--------|-----------|
| `S3Connection` | Where the object store is and how to reach it: host, port, TLS, region, addressing style, credentials |
| `S3Bucket` | A bucket name, plus the connection that bucket lives on |

Both belong to the `s3.kubedoop.dev/v1alpha1` API group.

Which one a product asks for depends on how it addresses storage. A product handed a whole bucket
takes an `S3Bucket`; a product that decides bucket names itself — Hive Metastore, for instance —
takes an `S3Connection` directly.

## Inline or reference, never both

Wherever Kubedoop accepts S3 configuration it accepts the same pair of fields: `inline` to define
the object in place, or `reference` to name an existing object in the same namespace. They are
mutually exclusive, and resolution fails if you set both or neither:

```text
invalid S3 connection: inline and reference are mutually exclusive
invalid S3 connection: neither inline nor reference is set
```

Use `reference` as soon as a second product talks to the same object store, so the endpoint is
defined once.

## S3Connection

```yaml
apiVersion: s3.kubedoop.dev/v1alpha1
kind: S3Connection
metadata:
name: minio
spec:
host: minio.default.svc.cluster.local
port: 9000
pathStyle: true
region: us-east-1
credentials:
secretClass: minio-credentials
```

| Field | Required | Default | Notes |
|-------|----------|---------|-------|
| `host` | yes | — | Hostname of the object store. No scheme; the scheme follows `tls` |
| `port` | no | — | Omitted entirely when unset, so the endpoint uses the scheme's default port |
| `credentials` | yes | — | SecretClass supplying the access keys, see below |
| `pathStyle` | no | `false` | Addressing style, see below |
| `tls` | no | — | Presence switches the endpoint to `https` |
| `region` | no | `us-east-1` | Signing region |

The endpoint is assembled from these fields: the scheme is `https` when `tls` is set and `http`
otherwise, so there is no separate "use TLS" switch to keep in sync.

## Credentials

`credentials.secretClass` names a SecretClass that must publish exactly two keys:

| Key | Read by products as |
|-----|---------------------|
| `ACCESS_KEY` | `AWS_ACCESS_KEY_ID` |
| `SECRET_KEY` | `AWS_SECRET_ACCESS_KEY` |

The Secret Operator mounts them as files under `/kubedoop/secret/<volume>/`, and products source
them into the environment:

```bash
export AWS_ACCESS_KEY_ID="$(cat /kubedoop/secret/s3-credentials/ACCESS_KEY)"
export AWS_SECRET_ACCESS_KEY="$(cat /kubedoop/secret/s3-credentials/SECRET_KEY)"
```

Credentials may additionally carry a `scope`, which narrows what the issued credential covers —
see [Authentication](../security/authentication.md) for how SecretClass scopes work.

## pathStyle: the field that breaks MinIO

`pathStyle` selects how the bucket is addressed:

| Value | Resulting URL | Correct for |
|-------|---------------|-------------|
| `false` (default) | `https://<bucket>.<host>` — virtual host | AWS S3 |
| `true` | `https://<host>/<bucket>` — path | MinIO, Ceph RGW, most self-hosted backends |

The default is right for AWS and wrong for most in-cluster deployments. MinIO in particular serves
path-style only. With the default, the client resolves `<bucket>.<host>` — `warehouse.minio` for an
in-cluster MinIO — which does not exist in DNS.

Nothing rejects this at admission. The resource applies cleanly, the pods start, and the failure
only surfaces on the first object access. **Set `pathStyle: true` for MinIO, Ceph RGW and similar
backends.**

## TLS

Setting `tls` switches the endpoint to `https`. The nested `verification` decides how the server
certificate is checked:

```yaml
spec:
host: s3.example.com
tls:
verification:
server:
caCert:
secretClass: tls # a SecretClass that issues the CA certificate
```

| `verification` | Behaviour |
|----------------|-----------|
| `server.caCert.secretClass` | Verify against the CA published by that SecretClass |
| `server.caCert.webPki: {}` | Verify against the system's public CA bundle |
| `none: {}` | Do not verify the certificate |

`none` disables verification entirely and should stay out of production.

## S3Bucket

An `S3Bucket` is a bucket name plus a connection, and the connection is itself an
inline-or-reference pair:

```yaml
apiVersion: s3.kubedoop.dev/v1alpha1
kind: S3Bucket
metadata:
name: warehouse
spec:
bucketName: warehouse
connection:
reference: minio
```

`bucketName` is the name on the object store; the resource's own `metadata.name` is only what other
Kubedoop resources refer to. They do not have to match.

A bucket with no `connection` fails resolution:

```text
invalid S3 bucket "warehouse": no connection is set
```

## Using it from a product

Products expose the same inline-or-reference pair. Hive Metastore takes a connection under
`clusterConfig.s3`:

```yaml
apiVersion: hive.kubedoop.dev/v1alpha1
kind: HiveMetastore
metadata:
name: hive
spec:
clusterConfig:
s3:
reference: minio
```

Swapping `reference: minio` for an `inline:` block with the same fields as an `S3Connection` spec
is equivalent, and appropriate when only one product uses that endpoint.

## Related

- [Roles and role groups](../common-configuration-mechanisms/roles-and-role-groups.md)
- [Resource management](./resource-manage.md)
Loading
Loading