diff --git a/ovhcloud/README.md b/ovhcloud/README.md new file mode 100644 index 0000000..11d3433 --- /dev/null +++ b/ovhcloud/README.md @@ -0,0 +1,79 @@ +# Flatcar Provisioning Automation for OVHcloud + +This repository provides tools to automate Flatcar provisioning on [OVHcloud][ovhcloud] using [Terraform][terraform]. + +## Features + +- Minimal configuration required (demo deployment works with default settings w/o any customisation, just run `terraform apply`!). +- Deploy one or multiple servers. +- Per-server custom configuration via separate [container linux config][container-linux-config] files. + +## Prerequisites + +1. OVHcloud credentials: username, tenant name, password, authentication URL, region. + +## Disclaimer + +This Terraform code is tested against a [DevStack][devstack] instance, for actual OVHcloud deployment the "network" attribute might need to be modified. + +## HowTo + +This will create a server in 'RegionOne' using a medium instance size in the 'public' network with 'default' security group. +See "Customisation" below for advanced settings. + +1. Clone the repo. +2. Add credentials in a `terraform.tfvars` file, expected credentials name can be found in `provider.tf` +3. Run + ```shell + terraform init + ``` +4. Edit [`server1.yaml`][server-1] and provide your own custom provisioning configuration in [container linux config][container-linux-config] syntax. +5. Plan and apply. + This will also auto-generate an SSH key pair to log in to the server after provisioning. + The public key of that key pair will be registered with OpenStack.
+ Invoke Terraform: + ```shell + terraform plan + terraform apply + ``` + +Terraform will print server information (name, ipv4 and v6) after deployment concluded. +The deployment will create an SSH key pair in `.ssh/`. + +After provisioning concluded the private key of the key pair generated during provisioning can be used to connect to node. As mentioned in the disclaimer, the tested setup relies on DevStack so we need to proxy jump on the devstack instance before reaching the deployed instance: +```shell +ssh -J user@[DEVSTACK-IP] -i ./.ssh/provisioning_private_key.pem -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null core@[SERVER-IP] +``` + +_NOTE_: +* Server IP address can be found at any moment after deployment by running `terraform output` +* If you update server configuration(s) in `server-configs` and re-run `terraform apply`, the instance will be **replaced**. +Consider adding [`create_before_destroy`](https://www.terraform.io/docs/configuration/meta-arguments/lifecycle.html#syntax-and-arguments) to the `openstack_compute_instance_v2` resource in [`compute.tf`](compute.tf) to avoid services becoming unavailable during reprovisioning. +* SSH key is passed twice: in the Butane configuration and in the `key_pair` argument of the instance - while the SSH keys in the Butane configuration are added by Ignition during the first boot of the instance, `key_pair` are processed by `coreos-metadata-sshkeys@.service` (a.k.a [`afterburn`][afterburn]) from the OpenStack [metadata service][metadata-service]. It means that you can install SSH keys without Ignition and change them as wanted. + +### Customisation + +The provisioning automation can be customised via settings in [`terraform.tfvars`][terraform.tfvars]: + - `cluster_name`: Descriptive name of your cluster, will be used to generate server names. + `flatcar-terraform` by default. + - `machines`: Add more machines to your deployment. + Each machine name must be unique and requires a respective `[NAME].yaml` server configuration in [`server-configs`](server-configs). + An example / default configuration for machine `server1` is provided with [`server1.yaml`](server-configs/server1.yaml). + During provisioning, server names are generated by concatenating the cluster name and the machine name - the defaults above will create a single server named `flatcar-terraform-server1`. + - `ssh_keys`: Additional SSH public keys to add to core user's `authorized_keys`. + Note that during provisioning, a provisioning RSA key pair will be generated and stored in the local directory `.ssh/provisioning_private_key.pem` and `.ssh/provisioning_key.pub`, respectively. + The private key can be used for ssh connections (`core` user) to all provisioned servers. + - `release_channel`: Select one of "lts", "stable", "beta", or "alpha". + Read more about channels [here](https://www.flatcar.org/releases). + - `flatcar_version`: Select the desired Flatcar version for the given channel (default to "current", which is the latest). + - `flavor_name`: The spec of the machine, it default to ds1G (1vCPU, 10GB of disk and 1GB of memory) + - `ssh`: A boolean to create and attach a security group to the instances to allow SSH connections. (_NOTE_: At this moment, when creating an instance with this variable enabled then turning it off does not work as the security group is not firstly detached from the instance so the security group can't be deleted) + +[afterburn]: https://coreos.github.io/afterburn/ +[container-linux-config]: https://www.flatcar.org/docs/latest/provisioning/config-transpiler/configuration/ +[devstack]: https://opendev.org/openstack/devstack +[metadata-service]: https://docs.openstack.org/nova/zed/user/metadata.html +[ovhcloud]: https://www.ovhcloud.com/ +[server-1]: server-configs/server1.yaml +[terraform]: https://www.terraform.io/ +[terraform-tfvars]: terraform.tfvars diff --git a/ovhcloud/compute.tf b/ovhcloud/compute.tf new file mode 100644 index 0000000..c3a74ac --- /dev/null +++ b/ovhcloud/compute.tf @@ -0,0 +1,103 @@ +# we let 'Nova' generate a new keypair. +resource "openstack_compute_keypair_v2" "provisioning_keypair" { + name = "Provisioning key for Flatcar cluster ${var.cluster_name}" +} + +# keypair is saved locally for later SSH connections. +resource "local_file" "provisioning_key" { + filename = "${path.module}/.ssh/provisioning_private_key.pem" + content = openstack_compute_keypair_v2.provisioning_keypair.private_key + directory_permission = "0700" + file_permission = "0400" +} + +resource "local_file" "provisioning_key_pub" { + filename = "${path.module}/.ssh/provisioning_key.pub" + content = openstack_compute_keypair_v2.provisioning_keypair.public_key + directory_permission = "0700" + file_permission = "0440" +} + +# Get the flavor ID +data "openstack_compute_flavor_v2" "flatcar" { + name = var.flavor_name +} + +# We create the OVHcloud image +# +# XXX not supporting web_download + specify properties (notably +# for block-storage to avoid using virtio-blk instead of SCSI) +resource "openstack_images_image_v2" "flatcar" { + name = "${var.cluster_name}-${var.release_channel}.${var.flatcar_version}" + image_source_url = "https://${var.release_channel}.release.flatcar-linux.net/amd64-usr/${var.flatcar_version}/flatcar_production_openstack_image.img.gz" + # XXX do not use it, OVH openstack seems to not handle this well :( + # web_download = false + verify_checksum = true + decompress = true + container_format = "bare" + disk_format = "qcow2" + protected = false + hidden = false + visibility = "private" + + # See: https://docs.openstack.org/glance/stein/admin/useful-image-properties.html + # See: https://wiki.openstack.org/wiki/VirtDriverImageProperties + properties = { + architecture = "x86_64" + image_original_user = "core" + distro_family = "gentoo" + os_distro = "gentoo" + os_version = var.flatcar_version + os_release_channel = var.release_channel + os_arch = "amd64" + os_type = "linux" + # XXX OVHcloud supports 256 volumes by VM, use SCSI to be able to use this + # feature. If you do not specify this it will fallback to virtio-blk and + # you'll only be able to use 26 volumes (including the root one). + hw_disk_bus = "scsi" + hw_scsi_model = "virtio-scsi" + hypervisor_type = "qemu" + hw_qemu_guest_agent = true + hw_vif_model = "virtio" + hw_vif_multiqueue_enabled = true + hw_time_hpet = true + } + + timeouts { + create = "5m" + } +} + +# 'instance' are the OpenStack instances created from the 'flatcar' image +# using user data. +resource "openstack_compute_instance_v2" "instance" { + for_each = toset(var.machines) + name = "${var.cluster_name}-${each.key}" + image_id = openstack_images_image_v2.flatcar.id + flavor_id = data.openstack_compute_flavor_v2.flatcar.id + key_pair = openstack_compute_keypair_v2.provisioning_keypair.name + + network { + name = "public" + } + + user_data = data.ct_config.machine-ignitions[each.key].rendered + + security_groups = flatten([["default"], var.ssh ? [openstack_networking_secgroup_v2.ssh[0].name] : []]) +} + +data "ct_config" "machine-ignitions" { + for_each = toset(var.machines) + strict = true + content = file("${path.module}/server-configs/${each.key}.yaml") + snippets = [ + data.template_file.core_user.rendered + ] +} + +data "template_file" "core_user" { + template = file("${path.module}/core-user.yaml.tmpl") + vars = { + ssh_keys = jsonencode(concat(var.ssh_keys, [openstack_compute_keypair_v2.provisioning_keypair.public_key])) + } +} diff --git a/ovhcloud/core-user.yaml.tmpl b/ovhcloud/core-user.yaml.tmpl new file mode 100644 index 0000000..b21c5ef --- /dev/null +++ b/ovhcloud/core-user.yaml.tmpl @@ -0,0 +1,7 @@ +variant: flatcar +version: 1.0.0 + +passwd: + users: + - name: core + ssh_authorized_keys: ${ssh_keys} diff --git a/ovhcloud/network.tf b/ovhcloud/network.tf new file mode 100644 index 0000000..d891507 --- /dev/null +++ b/ovhcloud/network.tf @@ -0,0 +1,27 @@ +resource "openstack_networking_secgroup_v2" "ssh" { + name = "ssh-terraform" + description = "Allow SSH from the outside - Managed by Terraform" + count = var.ssh ? 1 : 0 +} + +resource "openstack_networking_secgroup_rule_v2" "ssh-ipv4" { + direction = "ingress" + ethertype = "IPv4" + protocol = "tcp" + port_range_min = 22 + port_range_max = 22 + remote_ip_prefix = "0.0.0.0/0" + security_group_id = openstack_networking_secgroup_v2.ssh[0].id + count = var.ssh ? 1 : 0 +} + +resource "openstack_networking_secgroup_rule_v2" "ssh-ipv6" { + direction = "ingress" + ethertype = "IPv6" + protocol = "tcp" + port_range_min = 22 + port_range_max = 22 + remote_ip_prefix = "::/0" + security_group_id = openstack_networking_secgroup_v2.ssh[0].id + count = var.ssh ? 1 : 0 +} diff --git a/ovhcloud/outputs.tf b/ovhcloud/outputs.tf new file mode 100644 index 0000000..c5e6423 --- /dev/null +++ b/ovhcloud/outputs.tf @@ -0,0 +1,28 @@ +output "provisioning_public_key_file" { + value = local_file.provisioning_key_pub.filename +} + +output "provisioning_private_key_file" { + value = local_file.provisioning_key.filename +} + +output "ipv4" { + value = { + for key in var.machines : + "${var.cluster_name}-${key}" => openstack_compute_instance_v2.instance[key].access_ip_v4 + } +} + +output "ipv6" { + value = { + for key in var.machines : + "${var.cluster_name}-${key}" => openstack_compute_instance_v2.instance[key].access_ip_v6 + } +} + +output "name" { + value = { + for key in var.machines : + "${var.cluster_name}-${key}" => openstack_compute_instance_v2.instance[key].name + } +} diff --git a/ovhcloud/provider.tf b/ovhcloud/provider.tf new file mode 100644 index 0000000..0d057fa --- /dev/null +++ b/ovhcloud/provider.tf @@ -0,0 +1,28 @@ +terraform { + required_version = ">= 0.14.0" + required_providers { + openstack = { + source = "terraform-provider-openstack/openstack" + version = "~> 1.48.0" + } + ct = { + source = "poseidon/ct" + version = "0.11.0" + } + template = { + source = "hashicorp/template" + version = "~> 2.2.0" + } + } +} + +# Configure the OpenStack Provider +# See: https://registry.terraform.io/providers/terraform-provider-openstack/openstack/latest/docs +# See: https://help.ovhcloud.com/csm/en-public-cloud-compute-terraform?id=kb_article_view&sysparm_article=KB0050797 +provider "openstack" { + user_name = var.user_name + tenant_name = var.tenant_name + password = var.password + auth_url = var.auth_url + region = var.region +} diff --git a/ovhcloud/server-configs/server1.yaml b/ovhcloud/server-configs/server1.yaml new file mode 100644 index 0000000..c3cce4a --- /dev/null +++ b/ovhcloud/server-configs/server1.yaml @@ -0,0 +1,25 @@ +variant: flatcar +version: 1.0.0 + +# This is a simple NGINX example. +# Replace the below with your own config. +# Refer to https://www.flatcar.org/docs/latest/provisioning/config-transpiler/configuration/ for more information. + +systemd: + units: + - name: nginx.service + enabled: true + contents: | + [Unit] + Description=NGINX example + After=docker.service + Requires=docker.service + [Service] + TimeoutStartSec=0 + ExecStartPre=-/usr/bin/docker rm --force nginx1 + ExecStart=/usr/bin/docker run --name nginx1 --pull always --net host docker.io/nginx:1 + ExecStop=/usr/bin/docker stop nginx1 + Restart=always + RestartSec=5s + [Install] + WantedBy=multi-user.target diff --git a/ovhcloud/variables.tf b/ovhcloud/variables.tf new file mode 100644 index 0000000..08c86f2 --- /dev/null +++ b/ovhcloud/variables.tf @@ -0,0 +1,72 @@ +variable "machines" { + type = list(string) + description = "Machine names, corresponding to machine-NAME.yaml.tmpl files" + default = [] +} + +variable "cluster_name" { + type = string + description = "Cluster name used as prefix for the machine names" + default = "terraform-flatcar" +} + +variable "ssh_keys" { + type = list(string) + default = [] + description = "Additional SSH public keys for user 'core'." +} + +variable "release_channel" { + type = string + description = "Release channel" + default = "stable" + + validation { + condition = contains(["lts", "stable", "beta", "alpha"], var.release_channel) + error_message = "release_channel must be lts, stable, beta, or alpha." + } +} + +variable "flavor_name" { + type = string + description = "The OpenStack flavor to use (a.k.a the spec of the instance)" + default = "b2-7" +} + +variable "flatcar_version" { + type = string + description = "The Flatcar version associated to the release channel" + default = "current" +} + +variable "user_name" { + type = string + description = "OpenStack username" +} + +variable "tenant_name" { + type = string + description = "OpenStack tenant name" +} + +variable "password" { + type = string + description = "OpenStack password" +} + +variable "auth_url" { + type = string + description = "OpenStack authentication URL" +} + +variable "region" { + type = string + description = "OpenStack region" + default = "RegionOne" +} + +variable "ssh" { + type = bool + description = "Allow SSH connection from the outside" + default = false +}