mirror of
https://github.com/kubernetes-sigs/kubespray.git
synced 2026-10-29 22:03:22 -02:30
Add contrib/terraform/opennebula: provision Kubespray VMs on OpenNebula (#13390)
This commit is contained in:
@@ -36,6 +36,7 @@ terraform_validate:
|
||||
- exoscale
|
||||
- hetzner
|
||||
- vsphere
|
||||
- opennebula
|
||||
- upcloud
|
||||
|
||||
.terraform_apply:
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
# Kubernetes on OpenNebula with Terraform
|
||||
|
||||
Provision a Kubespray-ready cluster on OpenNebula 7.2+: Terraform creates the
|
||||
VMs from an existing contextualized template and generates the Ansible
|
||||
inventory, then the standard Kubespray playbook deploys Kubernetes.
|
||||
|
||||
## Requirements
|
||||
|
||||
- OpenNebula >= 7.2 with the XML-RPC endpoint reachable from this machine
|
||||
- An existing VM template with the [one-context](https://github.com/OpenNebula/one-apps)
|
||||
package installed (all OpenNebula marketplace OS appliances qualify) (for
|
||||
the contextualization requirement — see the disk-options note below for a
|
||||
separate template constraint). The template's network context must be
|
||||
enabled (`NETWORK = "YES"` is set by this module).
|
||||
- An existing Virtual Network whose addresses are reachable from the machine
|
||||
running Terraform and Ansible
|
||||
- Terraform >= 1.3 or OpenTofu (provider `OpenNebula/opennebula ~> 1.5` — the
|
||||
release line adapted to the OpenNebula 7.x API)
|
||||
- The VM template should NOT define its own NIC — the module attaches the
|
||||
network explicitly, and a template NIC would leave nodes dual-homed with
|
||||
the default route on the wrong interface.
|
||||
- To use any disk option (`master_disk_size`/`worker_disk_size`/`additional_disk_size`)
|
||||
the template's first disk must reference its image by numeric `IMAGE_ID` —
|
||||
templates created via Sunstone wizards or the CLI often reference the image
|
||||
by name (`IMAGE=`), which the provider cannot read; the module fails at
|
||||
plan time with a clear error in that case.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```ShellSession
|
||||
$ cd kubespray
|
||||
$ cp -LRp contrib/terraform/opennebula/sample-inventory inventory/mycluster
|
||||
$ cd inventory/mycluster
|
||||
$ $EDITOR cluster.tfvars # template_name, network_name, master/worker_count, ssh_public_keys
|
||||
$ export OPENNEBULA_ENDPOINT="https://one.example.com:2634/RPC2"
|
||||
$ export OPENNEBULA_USERNAME="oneadmin"
|
||||
$ export OPENNEBULA_PASSWORD="opennebula"
|
||||
$ terraform -chdir=../../contrib/terraform/opennebula init
|
||||
$ terraform -chdir=../../contrib/terraform/opennebula apply \
|
||||
-var-file=$PWD/cluster.tfvars \
|
||||
-var inventory_file=$PWD/inventory.ini
|
||||
```
|
||||
|
||||
**Pass `-var inventory_file=$PWD/inventory.ini` on every subsequent `apply`
|
||||
as well** — without it the variable falls back to a path relative to the
|
||||
module directory and the previously rendered inventory file is removed.
|
||||
|
||||
After `apply`, the VMs are RUNNING in OpenNebula and `inventory.ini` contains
|
||||
all Kubespray groups. Kubespray pins its Ansible version — install it in a
|
||||
virtualenv from the repo root before deploying:
|
||||
|
||||
```ShellSession
|
||||
$ python3 -m venv ~/.venvs/kubespray && source ~/.venvs/kubespray/bin/activate && \
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Deploy Kubernetes:
|
||||
|
||||
```ShellSession
|
||||
$ cd ../.. && \
|
||||
ansible-playbook -i inventory/mycluster/inventory.ini cluster.yml -b
|
||||
```
|
||||
|
||||
Smoke-test checklist: VMs visible and RUNNING in Sunstone; `inventory.ini`
|
||||
written next to your `cluster.tfvars`; `ansible -i inventory/mycluster/inventory.ini -m ping all`
|
||||
succeeds (one-context may need a few seconds after RUNNING before SSH is up).
|
||||
|
||||
To tear the cluster down: `terraform -chdir=contrib/terraform/opennebula destroy
|
||||
-var-file=$PWD/inventory/mycluster/cluster.tfvars` (run from the repo root;
|
||||
this also deletes the rendered `inventory.ini`).
|
||||
|
||||
The Terraform state lives in `contrib/terraform/opennebula/` — manage one
|
||||
cluster per checkout, or use `terraform workspace` for more.
|
||||
|
||||
## Variables
|
||||
|
||||
### Required
|
||||
|
||||
- `template_name`: name of the existing contextualized OpenNebula VM template
|
||||
- `network_name`: name of the existing Virtual Network for the nodes
|
||||
- `master_count` / `worker_count`: how many control-plane and worker nodes to
|
||||
create (named `master-0..`, `worker-0..`; at least one master required) —
|
||||
the simple way to define the cluster
|
||||
- `machines`: advanced alternative to the counts (takes precedence when set);
|
||||
map of machines, key = machine name (VM/hostname becomes `<prefix>-<key>`)
|
||||
- `node_type`: `master` or `worker` (at least one master required)
|
||||
- `ip`: optional static IP inside an address range of the network; empty
|
||||
string lets OpenNebula assign a lease. When `network_reservation_size > 0`
|
||||
the static IP must fall inside the **reservation's** address range, not
|
||||
just the parent network's
|
||||
- `ssh_public_keys`: list of SSH public keys injected into the VMs via
|
||||
contextualization
|
||||
|
||||
### Connection
|
||||
|
||||
Set `OPENNEBULA_ENDPOINT`, `OPENNEBULA_USERNAME`, `OPENNEBULA_PASSWORD`
|
||||
(and optionally `OPENNEBULA_INSECURE=true`) in the environment, or the
|
||||
`one_endpoint` / `one_username` / `one_password` / `one_insecure` variables
|
||||
(e.g. `one_endpoint = "https://one.example.com:2634/RPC2"`).
|
||||
OpenNebula XML-RPC sends the username:password with every request — use a
|
||||
TLS endpoint in production (`http://…:2633` is the plaintext out-of-the-box
|
||||
default).
|
||||
|
||||
### Optional
|
||||
|
||||
- `prefix` (default `k8s`): VM name prefix — must be unique per OpenNebula
|
||||
user when the extras are enabled (the extra-disk image, VM group and
|
||||
reservation are named `<prefix>-...` and OpenNebula rejects duplicate
|
||||
names)
|
||||
- `ansible_user` (default `root`): SSH user written into the inventory
|
||||
- `inventory_file` (default `inventory.ini`): where the inventory is
|
||||
rendered — relative paths resolve against `contrib/terraform/opennebula/`
|
||||
because of `-chdir`; prefer an absolute path (e.g. set it in your
|
||||
`cluster.tfvars`)
|
||||
- `master_cpu`/`master_vcpu`/`master_memory` (1 / 2 / 4096 MB)
|
||||
- `worker_cpu`/`worker_vcpu`/`worker_memory` (1 / 4 / 8192 MB)
|
||||
- `master_disk_size`/`worker_disk_size` (MB, default 0 = keep the template's
|
||||
disk; grow-only and create-time — raising it later does NOT grow an
|
||||
existing cluster's root disks). Any disk option drops the template's
|
||||
secondary disks (only the first disk is re-declared).
|
||||
- `vm_create_timeout` (default `20m`)
|
||||
- `additional_disk_size` (MB, default 0): extra non-persistent DATABLOCK disk
|
||||
cloned per node (requires `image_datastore_name`) — strictly create-time in
|
||||
BOTH directions: enabling it on an existing cluster silently hot-attaches
|
||||
the re-declared OS disk as a duplicate OS-image clone on every node (apply
|
||||
succeeds, wrong result), resizing it later fails against the in-use image,
|
||||
and disabling it can attempt to detach the boot disk. Decide disk options
|
||||
at cluster creation; changing them means recreating the VMs. The module
|
||||
automatically re-declares the template's first disk alongside the extra
|
||||
disk (preserving the template's `SIZE=` override; `TARGET`/`DRIVER` fall
|
||||
back to image defaults); templates with more than one disk lose their
|
||||
secondary disks whenever any disk option is used.
|
||||
- `masters_anti_affinity` (default `false`): spread masters across hosts with
|
||||
an `ANTI_AFFINED` VM group; requires at least as many hosts as masters — on
|
||||
fewer hosts the VM stays pending until `vm_create_timeout` — changing this
|
||||
on an existing cluster replaces every master VM.
|
||||
- `network_reservation_size` (default 0): carve an address reservation of
|
||||
this size out of `network_name` and attach the nodes to it — create-time
|
||||
only (the provider cannot grow a reservation in place); use
|
||||
`network_reservation_first_ip`/`network_reservation_ar_id` to pin the
|
||||
reserved range so static `machines` IPs can target it. Toggling the
|
||||
reservation on/off (0↔N) on an existing cluster re-attaches every node's
|
||||
NIC on the other network and re-leases all IPs — it will break a deployed
|
||||
cluster; treat it as a create-time choice. The reservation must be at
|
||||
least as large as the number of nodes (enforced at plan time). Leaving
|
||||
`network_reservation_first_ip`/`network_reservation_ar_id` unpinned causes
|
||||
a harmless perpetual in-place diff on `plan` (the provider reads back the
|
||||
allocated values) — pin both to avoid it.
|
||||
- `network_reservation_first_ip` (default `""`): first IP of the reservation
|
||||
carved from the parent network (empty = let OpenNebula choose)
|
||||
- `network_reservation_ar_id` (default `null`): address-range ID of the
|
||||
parent network to reserve from (null = provider default)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **CoreDNS / nodelocaldns in CrashLoopBackOff after deploy** (validated on
|
||||
Ubuntu 24.04 marketplace images): the nodes' `/etc/resolv.conf` points at
|
||||
the systemd-resolved stub (`127.0.0.53`), which CoreDNS detects as a
|
||||
forwarding loop — or the OpenNebula VNet defines no `DNS` attribute at all.
|
||||
Fix: set `upstream_dns_servers` (e.g. `[8.8.8.8, 1.1.1.1]` or your internal
|
||||
resolver) in `inventory/$CLUSTER/group_vars/all/all.yml` and re-run
|
||||
`cluster.yml`. Infrastructure-level alternative: set the `DNS` attribute on
|
||||
the OpenNebula Virtual Network so contextualized guests get a real resolver.
|
||||
- **A failed first run can leave the cluster half-installed**: if
|
||||
`cluster.yml` dies before the network plugin is deployed, a re-run may wait
|
||||
forever on "control plane nodes to be Ready" (the nodes stay NotReady
|
||||
without a CNI). Recreate the VMs (`terraform destroy` + `apply`) and deploy
|
||||
again on clean machines.
|
||||
- **Transient `Service is in unknown state` handler failures** during heavy
|
||||
image-pull phases are usually a one-off systemd/dbus hiccup — re-running
|
||||
`cluster.yml` is safe and idempotent.
|
||||
|
||||
## Known issues
|
||||
|
||||
- Deleting VMs/images/networks manually in OpenNebula breaks `terraform
|
||||
refresh`/`plan` with `NO_EXISTS` ([provider #623](https://github.com/OpenNebula/terraform-provider-opennebula/issues/623));
|
||||
workaround: `terraform state rm <resource>`.
|
||||
- Overriding the template disk (any disk option: `*_disk_size > 0` or
|
||||
`additional_disk_size > 0`) can produce perpetual
|
||||
in-place updates in `plan` with some template layouts
|
||||
([provider #598](https://github.com/OpenNebula/terraform-provider-opennebula/issues/598)).
|
||||
- A network reservation supports a single address range
|
||||
([provider #625](https://github.com/OpenNebula/terraform-provider-opennebula/issues/625)).
|
||||
- IPv4 only: IPv6 read-back from templates is broken in provider 1.5.0
|
||||
([provider #615](https://github.com/OpenNebula/terraform-provider-opennebula/issues/615)).
|
||||
- Provider CI upstream covers OpenNebula 6.10/7.0; 7.2 keeps XML-RPC
|
||||
compatibility and has no known provider issues.
|
||||
@@ -0,0 +1,50 @@
|
||||
prefix = "k8s"
|
||||
|
||||
# Prefer an absolute path (relative paths resolve against the module directory):
|
||||
# inventory_file = "/path/to/inventory/mycluster/inventory.ini"
|
||||
|
||||
# Existing OpenNebula objects
|
||||
template_name = "i-did-not-read-the-docs" # e.g. ubuntu2404-context
|
||||
network_name = "i-did-not-read-the-docs" # e.g. private
|
||||
|
||||
# Node counts (nodes are named master-0.., worker-0..)
|
||||
master_count = 1
|
||||
worker_count = 2
|
||||
|
||||
# Advanced alternative: named nodes and/or static IPs. Takes precedence over
|
||||
# master_count/worker_count when set.
|
||||
# machines = {
|
||||
# "master-0" : {
|
||||
# "node_type" : "master",
|
||||
# "ip" : "" # optional static IP from an address range of the network, e.g. 192.168.0.10
|
||||
# },
|
||||
# "worker-0" : {
|
||||
# "node_type" : "worker",
|
||||
# "ip" : ""
|
||||
# }
|
||||
# }
|
||||
|
||||
ssh_public_keys = [
|
||||
# Put your public SSH key here
|
||||
"ssh-rsa I-did-not-read-the-docs",
|
||||
]
|
||||
|
||||
# SSH user for the generated inventory (one-context installs keys for root
|
||||
# on standard OpenNebula marketplace images)
|
||||
# ansible_user = "root"
|
||||
|
||||
## Sizing
|
||||
# master_vcpu = 2
|
||||
# master_memory = 4096
|
||||
# worker_vcpu = 4
|
||||
# worker_memory = 8192
|
||||
# master_disk_size = 0 # MB; 0 keeps the template disk size
|
||||
# worker_disk_size = 0 # MB; 0 keeps the template disk size
|
||||
|
||||
## OpenNebula extras
|
||||
# additional_disk_size = 0 # MB; extra DATABLOCK disk on every node
|
||||
# image_datastore_name = "" # required when additional_disk_size > 0
|
||||
# masters_anti_affinity = false
|
||||
# network_reservation_size = 0 # carve an address reservation out of network_name
|
||||
# network_reservation_first_ip = "" # pin the reserved range's first IP
|
||||
# network_reservation_ar_id = null # parent address-range ID to reserve from (null = provider default)
|
||||
@@ -0,0 +1,96 @@
|
||||
provider "opennebula" {
|
||||
# Credentials/endpoint fall back to the OPENNEBULA_* environment variables
|
||||
# when the corresponding variable is left empty.
|
||||
endpoint = var.one_endpoint != "" ? var.one_endpoint : null
|
||||
username = var.one_username != "" ? var.one_username : null
|
||||
password = var.one_password != "" ? var.one_password : null
|
||||
insecure = var.one_insecure
|
||||
}
|
||||
|
||||
data "opennebula_template" "node" {
|
||||
name = var.template_name
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = length([
|
||||
for machine in local.machines : machine if machine.node_type == "master"
|
||||
]) > 0
|
||||
error_message = "Define at least one control-plane node: set master_count >= 1 or add a machines entry with node_type \"master\"."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "opennebula_virtual_network" "network" {
|
||||
name = var.network_name
|
||||
}
|
||||
|
||||
locals {
|
||||
# Effective node set: the explicit machines map wins; otherwise it is
|
||||
# generated from master_count/worker_count (master-0.., worker-0..).
|
||||
machines = length(var.machines) > 0 ? var.machines : merge(
|
||||
{ for i in range(var.master_count) : "master-${i}" => { node_type = "master", ip = "" } },
|
||||
{ for i in range(var.worker_count) : "worker-${i}" => { node_type = "worker", ip = "" } },
|
||||
)
|
||||
# The provider's data source flattens template disks that reference their
|
||||
# image by name (IMAGE=) as image_id = -1; treat that sentinel as unusable.
|
||||
template_disk_image_id = try(data.opennebula_template.node.disk[0].image_id, -1) >= 0 ? data.opennebula_template.node.disk[0].image_id : null
|
||||
# Preserve the template's DISK-level SIZE override when the OS disk has to
|
||||
# be re-declared (0 = no override, use the image's size)
|
||||
template_disk_size = try(data.opennebula_template.node.disk[0].size, 0)
|
||||
}
|
||||
|
||||
module "kubernetes" {
|
||||
source = "./modules/kubernetes-cluster"
|
||||
|
||||
prefix = var.prefix
|
||||
machines = local.machines
|
||||
|
||||
template_id = data.opennebula_template.node.id
|
||||
template_disk_image_id = local.template_disk_image_id
|
||||
template_disk_size = local.template_disk_size
|
||||
network_id = data.opennebula_virtual_network.network.id
|
||||
|
||||
## Master ##
|
||||
master_cpu = var.master_cpu
|
||||
master_vcpu = var.master_vcpu
|
||||
master_memory = var.master_memory
|
||||
master_disk_size = var.master_disk_size
|
||||
|
||||
## Worker ##
|
||||
worker_cpu = var.worker_cpu
|
||||
worker_vcpu = var.worker_vcpu
|
||||
worker_memory = var.worker_memory
|
||||
worker_disk_size = var.worker_disk_size
|
||||
|
||||
## OpenNebula extras ##
|
||||
additional_disk_size = var.additional_disk_size
|
||||
image_datastore_name = var.image_datastore_name
|
||||
masters_anti_affinity = var.masters_anti_affinity
|
||||
network_reservation_size = var.network_reservation_size
|
||||
network_reservation_first_ip = var.network_reservation_first_ip
|
||||
network_reservation_ar_id = var.network_reservation_ar_id
|
||||
|
||||
ssh_public_keys = var.ssh_public_keys
|
||||
vm_create_timeout = var.vm_create_timeout
|
||||
}
|
||||
|
||||
#
|
||||
# Generate ansible inventory
|
||||
#
|
||||
|
||||
resource "local_file" "inventory" {
|
||||
content = templatefile("${path.module}/templates/inventory.tpl", {
|
||||
connection_strings_master = join("\n", formatlist("%s ansible_user=${var.ansible_user} ansible_host=%s ip=%s etcd_member_name=etcd%d",
|
||||
keys(module.kubernetes.master_ip),
|
||||
values(module.kubernetes.master_ip),
|
||||
values(module.kubernetes.master_ip),
|
||||
range(1, length(module.kubernetes.master_ip) + 1))),
|
||||
connection_strings_worker = join("\n", formatlist("%s ansible_user=${var.ansible_user} ansible_host=%s ip=%s",
|
||||
keys(module.kubernetes.worker_ip),
|
||||
values(module.kubernetes.worker_ip),
|
||||
values(module.kubernetes.worker_ip))),
|
||||
list_master = join("\n", keys(module.kubernetes.master_ip)),
|
||||
list_worker = join("\n", keys(module.kubernetes.worker_ip))
|
||||
})
|
||||
filename = var.inventory_file
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
locals {
|
||||
masters = {
|
||||
for name, machine in var.machines :
|
||||
name => machine
|
||||
if machine.node_type == "master"
|
||||
}
|
||||
workers = {
|
||||
for name, machine in var.machines :
|
||||
name => machine
|
||||
if machine.node_type == "worker"
|
||||
}
|
||||
context = {
|
||||
NETWORK = "YES"
|
||||
SET_HOSTNAME = "$NAME"
|
||||
SSH_PUBLIC_KEY = join("\n", var.ssh_public_keys)
|
||||
}
|
||||
node_network_id = var.network_reservation_size > 0 ? opennebula_virtual_network.reservation[0].id : var.network_id
|
||||
}
|
||||
|
||||
# Optional reservation carved out of the parent network (one AR only —
|
||||
# provider issue #625)
|
||||
resource "opennebula_virtual_network" "reservation" {
|
||||
count = var.network_reservation_size > 0 ? 1 : 0
|
||||
name = "${var.prefix}-reservation"
|
||||
reservation_vnet = var.network_id
|
||||
reservation_size = var.network_reservation_size
|
||||
reservation_first_ip = var.network_reservation_first_ip != "" ? var.network_reservation_first_ip : null
|
||||
reservation_ar_id = var.network_reservation_ar_id
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = var.network_reservation_size >= length(var.machines)
|
||||
error_message = "network_reservation_size must be at least the number of machines (each node leases one address from the reservation)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Optional shared non-persistent DATABLOCK image; every VM attaching it gets
|
||||
# its own clone, deleted together with the VM
|
||||
data "opennebula_datastore" "image" {
|
||||
count = var.additional_disk_size > 0 ? 1 : 0
|
||||
name = var.image_datastore_name
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = var.image_datastore_name != ""
|
||||
error_message = "image_datastore_name must be set when additional_disk_size > 0."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "opennebula_image" "extra_disk" {
|
||||
count = var.additional_disk_size > 0 ? 1 : 0
|
||||
name = "${var.prefix}-extra-disk"
|
||||
datastore_id = data.opennebula_datastore.image[0].id
|
||||
type = "DATABLOCK"
|
||||
size = var.additional_disk_size
|
||||
persistent = false
|
||||
permissions = "600"
|
||||
}
|
||||
|
||||
# Anti-affinity: spread control-plane VMs across hosts
|
||||
resource "opennebula_virtual_machine_group" "cluster" {
|
||||
count = var.masters_anti_affinity ? 1 : 0
|
||||
name = "${var.prefix}-vm-group"
|
||||
|
||||
role {
|
||||
name = "master"
|
||||
policy = "ANTI_AFFINED"
|
||||
}
|
||||
}
|
||||
|
||||
resource "opennebula_virtual_machine" "master" {
|
||||
for_each = local.masters
|
||||
|
||||
name = "${var.prefix}-${each.key}"
|
||||
template_id = var.template_id
|
||||
cpu = var.master_cpu
|
||||
vcpu = var.master_vcpu
|
||||
memory = var.master_memory
|
||||
|
||||
context = local.context
|
||||
|
||||
nic {
|
||||
model = "virtio"
|
||||
network_id = local.node_network_id
|
||||
ip = each.value.ip != "" ? each.value.ip : null
|
||||
}
|
||||
|
||||
# Any disk block replaces ALL of the template's disks (provider merge
|
||||
# semantics), so the OS disk must be re-declared whenever another disk
|
||||
# block is emitted.
|
||||
dynamic "disk" {
|
||||
for_each = var.master_disk_size > 0 || var.additional_disk_size > 0 ? [1] : []
|
||||
content {
|
||||
image_id = var.template_disk_image_id
|
||||
size = var.master_disk_size > 0 ? var.master_disk_size : (var.template_disk_size > 0 ? var.template_disk_size : null)
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "disk" {
|
||||
for_each = var.additional_disk_size > 0 ? [1] : []
|
||||
content {
|
||||
image_id = opennebula_image.extra_disk[0].id
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "vmgroup" {
|
||||
for_each = var.masters_anti_affinity ? [1] : []
|
||||
content {
|
||||
vmgroup_id = opennebula_virtual_machine_group.cluster[0].id
|
||||
role = "master"
|
||||
}
|
||||
}
|
||||
|
||||
timeouts {
|
||||
create = var.vm_create_timeout
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = (var.master_disk_size <= 0 && var.additional_disk_size <= 0) || var.template_disk_image_id != null
|
||||
error_message = "master_disk_size/additional_disk_size require the template's first disk to reference its image by numeric IMAGE_ID (templates referencing the image by name are not readable through the provider data source)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "opennebula_virtual_machine" "worker" {
|
||||
for_each = local.workers
|
||||
|
||||
name = "${var.prefix}-${each.key}"
|
||||
template_id = var.template_id
|
||||
cpu = var.worker_cpu
|
||||
vcpu = var.worker_vcpu
|
||||
memory = var.worker_memory
|
||||
|
||||
context = local.context
|
||||
|
||||
nic {
|
||||
model = "virtio"
|
||||
network_id = local.node_network_id
|
||||
ip = each.value.ip != "" ? each.value.ip : null
|
||||
}
|
||||
|
||||
# Any disk block replaces ALL of the template's disks (provider merge
|
||||
# semantics), so the OS disk must be re-declared whenever another disk
|
||||
# block is emitted.
|
||||
dynamic "disk" {
|
||||
for_each = var.worker_disk_size > 0 || var.additional_disk_size > 0 ? [1] : []
|
||||
content {
|
||||
image_id = var.template_disk_image_id
|
||||
size = var.worker_disk_size > 0 ? var.worker_disk_size : (var.template_disk_size > 0 ? var.template_disk_size : null)
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "disk" {
|
||||
for_each = var.additional_disk_size > 0 ? [1] : []
|
||||
content {
|
||||
image_id = opennebula_image.extra_disk[0].id
|
||||
}
|
||||
}
|
||||
|
||||
timeouts {
|
||||
create = var.vm_create_timeout
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = (var.worker_disk_size <= 0 && var.additional_disk_size <= 0) || var.template_disk_image_id != null
|
||||
error_message = "worker_disk_size/additional_disk_size require the template's first disk to reference its image by numeric IMAGE_ID (templates referencing the image by name are not readable through the provider data source)."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
output "master_ip" {
|
||||
value = {
|
||||
for name, vm in opennebula_virtual_machine.master :
|
||||
vm.name => vm.nic[0].computed_ip
|
||||
}
|
||||
}
|
||||
|
||||
output "worker_ip" {
|
||||
value = {
|
||||
for name, vm in opennebula_virtual_machine.worker :
|
||||
vm.name => vm.nic[0].computed_ip
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
variable "prefix" {}
|
||||
|
||||
variable "machines" {
|
||||
description = "Cluster machines"
|
||||
type = map(object({
|
||||
node_type = string
|
||||
ip = optional(string, "")
|
||||
}))
|
||||
}
|
||||
|
||||
variable "template_id" {
|
||||
description = "ID of the existing contextualized OpenNebula VM template"
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "template_disk_image_id" {
|
||||
description = "Image ID of the template's first disk (null when the template defines no image disk); required only for disk resizing"
|
||||
type = number
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "template_disk_size" {
|
||||
description = "SIZE (MB) declared on the template's first disk (0 = image default); used when re-declaring the OS disk"
|
||||
type = number
|
||||
default = 0
|
||||
}
|
||||
|
||||
variable "network_id" {
|
||||
description = "ID of the OpenNebula Virtual Network the nodes attach to"
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "master_cpu" {}
|
||||
variable "master_vcpu" {}
|
||||
variable "master_memory" {}
|
||||
variable "master_disk_size" {}
|
||||
|
||||
variable "worker_cpu" {}
|
||||
variable "worker_vcpu" {}
|
||||
variable "worker_memory" {}
|
||||
variable "worker_disk_size" {}
|
||||
|
||||
variable "ssh_public_keys" {
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "vm_create_timeout" {}
|
||||
|
||||
variable "additional_disk_size" {
|
||||
default = 0
|
||||
}
|
||||
|
||||
variable "image_datastore_name" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "masters_anti_affinity" {
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "network_reservation_size" {
|
||||
default = 0
|
||||
}
|
||||
|
||||
variable "network_reservation_first_ip" {
|
||||
description = "First IP of the reservation carved from the parent network (empty = let OpenNebula choose)"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "network_reservation_ar_id" {
|
||||
description = "Address-range ID of the parent network to reserve from (null = provider default)"
|
||||
type = number
|
||||
default = null
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
opennebula = {
|
||||
source = "OpenNebula/opennebula"
|
||||
version = "~> 1.5"
|
||||
}
|
||||
}
|
||||
required_version = ">= 1.3.0"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
output "master_ip_addresses" {
|
||||
value = module.kubernetes.master_ip
|
||||
}
|
||||
|
||||
output "worker_ip_addresses" {
|
||||
value = module.kubernetes.worker_ip
|
||||
}
|
||||
|
||||
output "one_template" {
|
||||
value = var.template_name
|
||||
}
|
||||
|
||||
output "one_network" {
|
||||
value = var.network_name
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
prefix = "k8s"
|
||||
|
||||
# Prefer an absolute path (relative paths resolve against the module directory):
|
||||
# inventory_file = "/path/to/inventory/mycluster/inventory.ini"
|
||||
|
||||
# Existing OpenNebula objects
|
||||
template_name = "i-did-not-read-the-docs" # e.g. ubuntu2404-context
|
||||
network_name = "i-did-not-read-the-docs" # e.g. private
|
||||
|
||||
# Node counts (nodes are named master-0.., worker-0..)
|
||||
master_count = 1
|
||||
worker_count = 2
|
||||
|
||||
# Advanced alternative: named nodes and/or static IPs. Takes precedence over
|
||||
# master_count/worker_count when set.
|
||||
# machines = {
|
||||
# "master-0" : {
|
||||
# "node_type" : "master",
|
||||
# "ip" : "" # optional static IP from an address range of the network, e.g. 192.168.0.10
|
||||
# },
|
||||
# "worker-0" : {
|
||||
# "node_type" : "worker",
|
||||
# "ip" : ""
|
||||
# }
|
||||
# }
|
||||
|
||||
ssh_public_keys = [
|
||||
# Put your public SSH key here
|
||||
"ssh-rsa I-did-not-read-the-docs",
|
||||
]
|
||||
|
||||
# SSH user for the generated inventory (one-context installs keys for root
|
||||
# on standard OpenNebula marketplace images)
|
||||
# ansible_user = "root"
|
||||
|
||||
## Sizing
|
||||
# master_vcpu = 2
|
||||
# master_memory = 4096
|
||||
# worker_vcpu = 4
|
||||
# worker_memory = 8192
|
||||
# master_disk_size = 0 # MB; 0 keeps the template disk size
|
||||
# worker_disk_size = 0 # MB; 0 keeps the template disk size
|
||||
|
||||
## OpenNebula extras
|
||||
# additional_disk_size = 0 # MB; extra DATABLOCK disk on every node
|
||||
# image_datastore_name = "" # required when additional_disk_size > 0
|
||||
# masters_anti_affinity = false
|
||||
# network_reservation_size = 0 # carve an address reservation out of network_name
|
||||
# network_reservation_first_ip = "" # pin the reserved range's first IP
|
||||
# network_reservation_ar_id = null # parent address-range ID to reserve from (null = provider default)
|
||||
@@ -0,0 +1 @@
|
||||
../../../../inventory/sample/group_vars
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
[all]
|
||||
${connection_strings_master}
|
||||
${connection_strings_worker}
|
||||
|
||||
[kube_control_plane]
|
||||
${list_master}
|
||||
|
||||
[etcd]
|
||||
${list_master}
|
||||
|
||||
[kube_node]
|
||||
${list_worker}
|
||||
|
||||
[k8s_cluster:children]
|
||||
kube_control_plane
|
||||
kube_node
|
||||
@@ -0,0 +1,187 @@
|
||||
## OpenNebula connection. Leave empty to use the OPENNEBULA_ENDPOINT,
|
||||
## OPENNEBULA_USERNAME, OPENNEBULA_PASSWORD and OPENNEBULA_INSECURE
|
||||
## environment variables.
|
||||
|
||||
variable "one_endpoint" {
|
||||
description = "OpenNebula XML-RPC API endpoint, e.g. https://one.example.com:2634/RPC2"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "one_username" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "one_password" {
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "one_insecure" {
|
||||
description = "Skip TLS certificate verification of the OpenNebula endpoint (null = use OPENNEBULA_INSECURE if set)"
|
||||
type = bool
|
||||
default = null
|
||||
}
|
||||
|
||||
## Cluster
|
||||
|
||||
variable "prefix" {
|
||||
default = "k8s"
|
||||
}
|
||||
|
||||
variable "inventory_file" {
|
||||
default = "inventory.ini"
|
||||
}
|
||||
|
||||
variable "template_name" {
|
||||
description = "Name of the existing contextualized OpenNebula VM template"
|
||||
}
|
||||
|
||||
variable "network_name" {
|
||||
description = "Name of the existing OpenNebula Virtual Network the nodes attach to"
|
||||
}
|
||||
|
||||
variable "master_count" {
|
||||
description = "Number of control-plane nodes to create (named master-0..N-1); ignored when machines is set"
|
||||
type = number
|
||||
default = 0
|
||||
|
||||
validation {
|
||||
condition = var.master_count >= 0
|
||||
error_message = "master_count must be >= 0."
|
||||
}
|
||||
}
|
||||
|
||||
variable "worker_count" {
|
||||
description = "Number of worker nodes to create (named worker-0..N-1); ignored when machines is set"
|
||||
type = number
|
||||
default = 0
|
||||
|
||||
validation {
|
||||
condition = var.worker_count >= 0
|
||||
error_message = "worker_count must be >= 0."
|
||||
}
|
||||
}
|
||||
|
||||
variable "machines" {
|
||||
description = "Advanced node definition (named nodes, optional static IPs); takes precedence over master_count/worker_count when non-empty"
|
||||
type = map(object({
|
||||
node_type = string
|
||||
ip = optional(string, "")
|
||||
}))
|
||||
default = {}
|
||||
|
||||
validation {
|
||||
condition = alltrue([
|
||||
for machine in var.machines : contains(["master", "worker"], machine.node_type)
|
||||
])
|
||||
error_message = "machines[*].node_type must be \"master\" or \"worker\"."
|
||||
}
|
||||
}
|
||||
|
||||
variable "ssh_public_keys" {
|
||||
description = "List of public SSH keys which are injected into the VMs."
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "ansible_user" {
|
||||
description = "SSH user written into the generated inventory (the user one-context injects keys for; root on standard OpenNebula images)"
|
||||
default = "root"
|
||||
}
|
||||
|
||||
## Master
|
||||
|
||||
variable "master_cpu" {
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "master_vcpu" {
|
||||
default = 2
|
||||
}
|
||||
|
||||
variable "master_memory" {
|
||||
description = "Memory in MB"
|
||||
default = 4096
|
||||
}
|
||||
|
||||
variable "master_disk_size" {
|
||||
description = "OS disk size in MB; 0 keeps the template's disk unchanged"
|
||||
default = 0
|
||||
|
||||
validation {
|
||||
condition = var.master_disk_size >= 0
|
||||
error_message = "master_disk_size must be >= 0 (MB)."
|
||||
}
|
||||
}
|
||||
|
||||
## Worker
|
||||
|
||||
variable "worker_cpu" {
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "worker_vcpu" {
|
||||
default = 4
|
||||
}
|
||||
|
||||
variable "worker_memory" {
|
||||
description = "Memory in MB"
|
||||
default = 8192
|
||||
}
|
||||
|
||||
variable "worker_disk_size" {
|
||||
description = "OS disk size in MB; 0 keeps the template's disk unchanged"
|
||||
default = 0
|
||||
|
||||
validation {
|
||||
condition = var.worker_disk_size >= 0
|
||||
error_message = "worker_disk_size must be >= 0 (MB)."
|
||||
}
|
||||
}
|
||||
|
||||
variable "vm_create_timeout" {
|
||||
default = "20m"
|
||||
}
|
||||
|
||||
## OpenNebula extras
|
||||
|
||||
variable "additional_disk_size" {
|
||||
description = "Size (MB) of an extra DATABLOCK disk attached to every node; 0 disables it"
|
||||
default = 0
|
||||
|
||||
validation {
|
||||
condition = var.additional_disk_size >= 0
|
||||
error_message = "additional_disk_size must be >= 0 (MB)."
|
||||
}
|
||||
}
|
||||
|
||||
variable "image_datastore_name" {
|
||||
description = "IMAGE datastore for the additional DATABLOCK disk (required when additional_disk_size > 0)"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "masters_anti_affinity" {
|
||||
description = "Spread master VMs across hosts using an ANTI_AFFINED VM group"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "network_reservation_size" {
|
||||
description = "If > 0, create a reservation of this many addresses from network_name and attach the nodes to it"
|
||||
default = 0
|
||||
|
||||
validation {
|
||||
condition = var.network_reservation_size >= 0
|
||||
error_message = "network_reservation_size must be >= 0."
|
||||
}
|
||||
}
|
||||
|
||||
variable "network_reservation_first_ip" {
|
||||
description = "First IP of the reservation carved from the parent network (empty = let OpenNebula choose)"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "network_reservation_ar_id" {
|
||||
description = "Address-range ID of the parent network to reserve from (null = provider default)"
|
||||
type = number
|
||||
default = null
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
opennebula = {
|
||||
source = "OpenNebula/opennebula"
|
||||
version = "~> 1.5"
|
||||
}
|
||||
local = {
|
||||
source = "hashicorp/local"
|
||||
version = ">= 2.0"
|
||||
}
|
||||
}
|
||||
required_version = ">= 1.3.0"
|
||||
}
|
||||
Reference in New Issue
Block a user