OpenStack¶
The OpenStack modules provision a cluster on a network and subnet you bring. The cluster role creates the node and control-plane security groups, an Octavia load balancer for the Kubernetes API (internal by default) and a Nova server group that spreads the control plane; the machine role creates one Nova server per Machine on a Neutron port of that subnet and adds control-plane servers to the API pools before they boot. The code lives in captf-io/openstack-modules. The modules are pre-release; see the status note in Cloud Modules.
OpenStack has no machinepool role: it has no native scaling group, so use a MachineDeployment of individual machines.
Images¶
| Role | Image | Page |
|---|---|---|
| cluster | ghcr.io/captf-io/openstack-cluster | Cluster |
| machine | ghcr.io/captf-io/openstack-machine | Machine |
The machine image carries no io.captf.capacity or io.captf.node-info label: flavor_name has no default, so there is no default node size to describe. For Cluster Autoscaler scale-from-zero, set the capacity.cluster-autoscaler.kubernetes.io/* annotations on the MachineDeployment instead.
Prerequisites¶
- Network. An existing Neutron network and subnet, IPv4 or IPv6, with DHCP and a router, routed to wherever the nodes pull images from. The management cluster must reach the subnet: the API endpoint is an internal VIP on it unless you make it public. A public endpoint also needs an external network to take the floating IP from, and a gateway on the subnet’s router.
- Services. Nova, Neutron with security groups and resource tags, Glance, and Octavia with the amphora provider. Octavia API 2.12 or later for
api_allowed_cidrs. - Permissions. An application credential of a user with the
memberrole in the project. Clouds still on Octavia’s legacy policy also requireload-balancer_member. The credential needs no unrestricted flag: the modules create no credentials. - Node images. A Glance image with cloud-init (or Ignition) and the kubelet, kubeadm or RKE2 and container runtime your bootstrap provider expects, for example one built with image-builder. The image must take its hostname from the metadata service or config drive, so that the Node name equals the server name.
- Cloud controller manager. Install the OpenStack cloud controller manager in the workload cluster, with a
cloud.confand a second, dedicated application credential you supply: the modules create no node identity. The repository’sexamples/cloud-controller-manager.yamldelivers both with a ClusterResourceSet. Keep itsmanage-security-groupsoff: it would edit the node ports’ security groups behind the machine role. For volumes, install Cinder CSI with the samecloud.conf.
Identity Secret¶
The provider reads its credentials from a clouds.yaml file in the identity Secret; the provider block sets nothing but the region. See Identities and Credentials for how the keys reach a Job.
| Key | Example | Purpose |
|---|---|---|
OS_CLOUD | openstack | The clouds.yaml entry to use |
OS_CLIENT_CONFIG_FILE | /var/run/captf/credentials/clouds.yaml | Where the provider finds clouds.yaml |
clouds.yaml | an application credential entry | Mounted as a file at that path |
cacert.pem | PEM bundle | Optional: a private CA, named by cacert in clouds.yaml |
The plain OS_AUTH_URL, OS_APPLICATION_CREDENTIAL_ID, OS_APPLICATION_CREDENTIAL_SECRET, OS_REGION_NAME and OS_CACERT keys work too.
apiVersion: v1
kind: Secret
metadata:
name: openstack
namespace: captf-system
type: Opaque
stringData:
OS_CLOUD: openstack
OS_CLIENT_CONFIG_FILE: /var/run/captf/credentials/clouds.yaml
clouds.yaml: |
clouds:
openstack:
auth_type: v3applicationcredential
auth:
auth_url: https://keystone.example.com:5000/v3
application_credential_id: <application-credential-id>
application_credential_secret: <application-credential-secret>
region_name: RegionOne
interface: public
identity_api_version: 3
---
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
kind: TerraformClusterIdentity
metadata:
name: openstack
spec:
secretRef:
name: openstack
namespace: captf-system
allowedNamespaces:
list:
- team-a
Quick start¶
The repository’s examples/ holds clusterctl templates for each step.
-
Create the identity from
examples/identity.yaml: -
Put the cloud controller manager’s manifests in a ConfigMap and apply
examples/cloud-controller-manager.yaml, with a second application credential; the file’s header has the commands. -
Generate the cluster from
examples/cluster-kubeadm.yaml: aTerraformCluster, aKubeadmControlPlaneof three machines and aMachineDeploymentof two, with health checks for both.export CLUSTER_NAME=demo KUBERNETES_VERSION=v1.33.1 \ OPENSTACK_SUBNET_ID=<subnet uuid> OPENSTACK_CONTROL_PLANE_FLAVOR=m1.large \ OPENSTACK_FLAVOR=m1.large OPENSTACK_IMAGE_NAME=ubuntu-2404-kube-v1.33.1 clusterctl generate yaml --from examples/cluster-kubeadm.yaml | kubectl apply -n "$NAMESPACE" -f - -
Install a CNI once the API server answers. Nodes keep the
node.cloudprovider.kubernetes.io/uninitializedtaint until the cloud controller manager runs.
API endpoint¶
The shared behavior applies; on OpenStack:
- Internal. The endpoint is the Octavia load balancer’s VIP on
subnet_id, one TCP listener per port: kube-apiserver onCluster.spec.clusterNetwork.apiServerPort, and with RKE2 the supervisor on 9345. Listener idle timeouts are an hour, so watches,kubectl execandlogs -fsurvive. - Public.
api_load_balancer_public = trueputs a floating IP fromfloating_ip_poolon the VIP port and makes it the endpoint host. It needsapi_allowed_cidrs, which restricts the listeners; the node subnet is always added to them. - Hairpin. The amphora provider source-NATs every client to its own address on the node subnet, so a control-plane node reaches itself through the VIP, and the control-plane security group admits the API port from the subnet. Through a public endpoint, nodes reach the floating IP via their router, which source-NATs them to its gateway address: put that address in
api_allowed_cidrs, or the first control-plane node never reaches itself. The ovn provider rejectsapi_allowed_cidrs(a precondition stops that combination) and its hairpin is not verified. - Endpoint guard.
terraform_data.api_endpoint_guardrecordssubnet_id,api_load_balancer_public,floating_ip_pool,cluster_network.api_server_portand the load balancer’s actual provider, and fails a later plan that would change one.
Exports¶
Schema captf.io/openstack-cluster/v1. Machines receive it as captf_cluster_outputs.
| Key | Value |
|---|---|
schema | captf.io/openstack-cluster/v1 |
region | The cluster’s region, the machines’ provider region |
network_id | Network of subnet_id |
subnet_id | subnet_id |
failure_domains | One key per availability zone, each an empty object |
distribution | kubeadm or rke2 |
provider_id_format | default or regional |
security_group_ids | control_plane (control-plane and node group) and worker (node group) lists |
control_plane_server_group_id | The server group control-plane machines join, or null |
node_allowed_address_cidrs | The allowed address pairs of every node port |
api | The endpoint host and port, and pools keyed kube_apiserver and rke2_supervisor, each {id, port} with the backend port; null with a supplied endpoint |
Tags¶
The captf_tags keys and additional_tags map / to : in the key (captf.io:cluster). Neutron and Octavia resources carry them as "<key>=<value>" strings in tags; the Nova server carries them as metadata, because a Nova tag holds at most 60 characters and no /. A pair longer than 255 characters fails a precondition instead of being cut.
Not taggable in provider 3.4.0: security group rules, health monitors and server groups. The boot-from-volume root volume is untagged too: Nova creates it from the server’s block device mapping, which takes no metadata.
Design notes
- Bring your own network. The modules create no network, subnet or router, so a cluster fits whatever network design the cloud already has.
- No node identity. An application credential belongs to the user Terraform runs as, Keystone limits what one credential can create, and the secret would sit in state; so you supply the cloud controller manager’s
cloud.conf. - Amphora by default. Its source NAT gives the hairpin path the contract requires;
loadbalancer_provider = nulltakes the cloud’s default instead. - Health from Octavia’s provisioning status. Cluster health reads the load balancer’s
provisioning_status, neveroperating_status, which follows the members. - No image lookup. The server takes
image_idorimage_namedirectly, and later image changes are ignored: a Glance data source would fail every refresh and destroy once the image is rotated out, and the provider rebuilds a server in place when its image changes. - Register before boot. The server depends on its pool members, so a control-plane machine is in the API pools before it boots.
- Bootstrap in user data. OpenStack has no instance identity to fetch a staged payload with, so the control-plane payload, with the cluster CA keys, is readable from the metadata service; see Machine.
The evidence for each is in the repository’s DESIGN.md.
Not yet verified
- Octavia ovn provider: hairpin from a member to its own VIP, and the
SOURCE_IP_PORTpool method. - Octavia tag limits, assumed equal to Neutron’s.
- How often a server refresh, and so a drift Job or destroy, fails while a server is in a transient Nova state (
REBOOT, a resize,RESCUE). - The cloud controller manager’s
manage-security-groupsediting node ports’ groups. - Cinder and Nova availability zone names that differ, for boot volumes.
- Octavia returning a listener’s allowed CIDRs in sorted order.
- Nodes reaching a public endpoint source-NATed to the router’s gateway address.
- The kubeadm templates’ Node name equaling the server name; Nova cuts hostnames to 63 characters.
- The cloud controller manager v1.34.1 manifests with the control-plane node selector changed to
"". - Neutron’s ML2/OVN driver counting allowed address pairs as security group members, which native pod routing relies on.