Skip to content

Tutorials

Launch your first instances

Launch a Linux instance and a Windows instance, sign in to each, and delete them. Then do the same with the OpenStack CLI and with OpenTofu, and see that each tool builds the same thing.

Well-Architected

Pillar: Operational excellence. The same result from the portal, the CLI and code, and only the code version can be repeated exactly.

Also touches: Security and sovereignty (sign-in limited to your own IP address) and Spend efficiency (cleaning up what you no longer need).

What you'll build

Resource Name Details
Key pair first-instance Your SSH public key, for signing in to Linux
Security group first-instance SSH (22) and RDP (3389) from your IP address only
Linux instance web-01 Ubuntu, flavour v1.c1r2, 20 GB root volume
Windows instance win-01 Windows Server, flavour v1.c2r8, 80 GB root volume
Floating IPs two One for each instance

Both instances boot from a volume that's deleted with the instance, on your project's default network.

Before you start

Commands use <cloud-name>, the name of the entry in your clouds.yaml. They're the same in PowerShell and in a macOS or Linux shell unless a tab says otherwise.

Step 1: Create an SSH key pair

Linux instances sign you in with an SSH key. Create one on your computer; the private key stays there, and you give Breqwatr Cloud the public key.

mkdir -Force $HOME\.ssh
ssh-keygen -t ed25519 -f $HOME\.ssh\first-instance
mkdir -p ~/.ssh
ssh-keygen -t ed25519 -f ~/.ssh/first-instance
mkdir -p ~/.ssh
ssh-keygen -t ed25519 -f ~/.ssh/first-instance

This creates first-instance (the private key) and first-instance.pub (the public key) in the .ssh folder in your home folder.

Step 2: Build it

Pick a tab and follow it to the end of this step. Then sign in (step 3), clean up (step 4), and come back for the next tab.

Add your public key. Open your account menu at the top right and choose My Account. In SSH Keys, choose Add key, enter the name first-instance, paste the contents of first-instance.pub into Public key, and choose Add key.

The Add SSH key dialog on the profile page, with first-instance as the name and an ssh-ed25519 public key pasted into Public key

Create the security group.

  1. In your project, open Security Groups and choose New security group. Name it first-instance and create it.
  2. Open first-instance and choose Add rule: Protocol TCP, Open Port Port, Port 22, Source CIDR <your-ip>/32. Choose Add rule.
  3. Add a second rule the same way, with Port 3389.

Launch the Linux instance. Open Instances and choose New instance:

  1. Image: an Ubuntu image from Public Images.
  2. Flavor: v1.c1r2.
  3. Disk size (GB): 20. Leave Delete volume when instance is deleted ticked and Storage backend on Use default.
  4. Network: default.
  5. SSH Key: first-instance.
  6. Security Groups: leave default ticked and tick first-instance.
  7. Enter the name web-01 and choose Create instance.

Image and Flavor each open a picker from Browse.... Ubuntu is on the picker's Linux tab:

The Choose image picker open on its Linux tab, listing Almalinux 9.6, Cirros, Debian 12, Debian 13 and Ubuntu 24.04 LTS

The New instance form filled in for web-01: Ubuntu 24.04 LTS, flavour v1.c1r2, a 20 GB disk with Delete volume when instance is deleted ticked, Storage backend on Use default, the default network, the first-instance SSH key, and the default and first-instance security groups ticked

Launch the Windows instance. Choose New instance again: a Windows Server image, Flavor v1.c2r8, Disk size (GB) 80, Network default, and the default and first-instance security groups. Windows images don't ask for an SSH key. Name it win-01 and choose Create instance.

Give each instance a floating IP. Open IP Addresses and choose Allocate IP. On the new address, open the menu at the end of its row, choose Associate and pick web-01. Do the same for win-01.

The Associate floating IP dialog on IP Addresses, for an unassociated address, with web-01 chosen as the instance and an Associate button

Add your public key, then create the security group and its two rules:

openstack --os-cloud <cloud-name> keypair create --public-key $HOME/.ssh/first-instance.pub first-instance
openstack --os-cloud <cloud-name> security group create first-instance
openstack --os-cloud <cloud-name> security group rule create --ingress --protocol tcp --dst-port 22 --remote-ip <your-ip>/32 first-instance
openstack --os-cloud <cloud-name> security group rule create --ingress --protocol tcp --dst-port 3389 --remote-ip <your-ip>/32 first-instance

Find the image names and the external network that floating IPs come from:

openstack --os-cloud <cloud-name> image list
openstack --os-cloud <cloud-name> network list --external

An instance boots from a volume created from an image, so get each image's ID:

openstack --os-cloud <cloud-name> image show "<linux-image>" -f value -c id
openstack --os-cloud <cloud-name> image show "<windows-image>" -f value -c id

Launch both instances. delete_on_termination=true deletes each root volume with its instance:

openstack --os-cloud <cloud-name> server create --flavor v1.c1r2 --network default --security-group default --security-group first-instance --key-name first-instance --block-device source_type=image,uuid=<linux-image-id>,destination_type=volume,volume_size=20,boot_index=0,delete_on_termination=true --wait web-01
openstack --os-cloud <cloud-name> server create --flavor v1.c2r8 --network default --security-group default --security-group first-instance --block-device source_type=image,uuid=<windows-image-id>,destination_type=volume,volume_size=80,boot_index=0,delete_on_termination=true --wait win-01

Create a floating IP for each and attach it. The first command prints the address:

openstack --os-cloud <cloud-name> floating ip create <external-network> -f value -c floating_ip_address
openstack --os-cloud <cloud-name> server add floating ip web-01 <web-01-address>
openstack --os-cloud <cloud-name> floating ip create <external-network> -f value -c floating_ip_address
openstack --os-cloud <cloud-name> server add floating ip win-01 <win-01-address>

Create a folder called first-instance with this file in it, main.tf. Replace the placeholders with your IP address, the image names and the external network from openstack image list and openstack network list --external.

terraform {
  required_version = ">= 1.13"
  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "~> 3.4"
    }
  }
}

# Reads the cloud named in the OS_CLOUD environment variable from clouds.yaml
provider "openstack" {}

locals {
  my_ip            = "<your-ip>/32"
  external_network = "<external-network>"
}

data "openstack_networking_network_v2" "default" {
  name = "default"
}

data "openstack_networking_secgroup_v2" "default" {
  name = "default"
}

data "openstack_images_image_v2" "linux" {
  name        = "<linux-image>"
  most_recent = true
}

data "openstack_images_image_v2" "windows" {
  name        = "<windows-image>"
  most_recent = true
}

resource "openstack_compute_keypair_v2" "first_instance" {
  name       = "first-instance"
  public_key = file(pathexpand("~/.ssh/first-instance.pub"))
}

resource "openstack_networking_secgroup_v2" "first_instance" {
  name = "first-instance"
}

resource "openstack_networking_secgroup_rule_v2" "ssh" {
  security_group_id = openstack_networking_secgroup_v2.first_instance.id
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = local.my_ip
}

resource "openstack_networking_secgroup_rule_v2" "rdp" {
  security_group_id = openstack_networking_secgroup_v2.first_instance.id
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 3389
  port_range_max    = 3389
  remote_ip_prefix  = local.my_ip
}

# A port for each instance, so its floating IP can be attached to it
resource "openstack_networking_port_v2" "web_01" {
  name               = "web-01"
  network_id         = data.openstack_networking_network_v2.default.id
  security_group_ids = [data.openstack_networking_secgroup_v2.default.id, openstack_networking_secgroup_v2.first_instance.id]
}

resource "openstack_networking_port_v2" "win_01" {
  name               = "win-01"
  network_id         = data.openstack_networking_network_v2.default.id
  security_group_ids = [data.openstack_networking_secgroup_v2.default.id, openstack_networking_secgroup_v2.first_instance.id]
}

resource "openstack_compute_instance_v2" "web_01" {
  name        = "web-01"
  flavor_name = "v1.c1r2"
  key_pair    = openstack_compute_keypair_v2.first_instance.name

  block_device {
    uuid                  = data.openstack_images_image_v2.linux.id
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 20
    boot_index            = 0
    delete_on_termination = true
  }

  network {
    port = openstack_networking_port_v2.web_01.id
  }
}

resource "openstack_compute_instance_v2" "win_01" {
  name        = "win-01"
  flavor_name = "v1.c2r8"

  block_device {
    uuid                  = data.openstack_images_image_v2.windows.id
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 80
    boot_index            = 0
    delete_on_termination = true
  }

  network {
    port = openstack_networking_port_v2.win_01.id
  }
}

resource "openstack_networking_floatingip_v2" "web_01" {
  pool       = local.external_network
  port_id    = openstack_networking_port_v2.web_01.id
  depends_on = [openstack_compute_instance_v2.web_01]
}

resource "openstack_networking_floatingip_v2" "win_01" {
  pool       = local.external_network
  port_id    = openstack_networking_port_v2.win_01.id
  depends_on = [openstack_compute_instance_v2.win_01]
}

output "web_01_address" {
  value = openstack_networking_floatingip_v2.web_01.address
}

output "win_01_address" {
  value = openstack_networking_floatingip_v2.win_01.address
}

Tell OpenTofu which clouds.yaml entry to use, then initialize, review and apply:

$env:OS_CLOUD = "<cloud-name>"
tofu init
tofu plan
tofu apply
export OS_CLOUD=<cloud-name>
tofu init
tofu plan
tofu apply
export OS_CLOUD=<cloud-name>
tofu init
tofu plan
tofu apply

tofu plan lists 10 resources to add. Type yes when apply asks. The two addresses print at the end; tofu output shows them again.

Step 3: Sign in

Connect as the image's default user, ubuntu on Ubuntu, with your private key:

ssh -i $HOME/.ssh/first-instance ubuntu@<web-01-address>

The same command works in PowerShell, macOS and Linux. Type yes the first time to accept the instance's host key.

Breqwatr's Windows images ask you to set the Administrator password on the instance's console at first boot. Allow a few minutes after launch for the first boot to finish.

  1. Open the console. In the portal, open Instances, choose win-01, then Console. From the CLI, or after OpenTofu, get the console's address and open it in your browser:

    openstack --os-cloud <cloud-name> console url show win-01
    
  2. At Press Ctrl+Alt+Delete to unlock, choose Send CtrlAltDel at the top right of the console.

  3. Choose Sign in under Administrator. Windows says "The user's password must be changed before signing in." Choose OK.
  4. Enter a new password in New password and Confirm password, and press Enter. Windows confirms "Your password has been changed."
  5. Connect over RDP to <win-01-address>, as Administrator, with that password:
    • Windows: Remote Desktop Connection (mstsc).
    • macOS: Microsoft's Windows App, from the App Store.
    • Linux: an RDP client such as Remmina.

Step 4: Clean up

Delete everything before you start the next way, so each way starts from an empty project.

  1. In Instances, open web-01 and choose Delete. Tick Also delete 1 attached volume, type CONFIRM, and choose Delete instance; the dialog doesn't continue without the tick. Do the same for win-01. Their root volumes go with them.

    The Delete instance dialog for web-01, with Also delete 1 attached volume ticked, CONFIRM typed and the Delete instance button

  2. In IP Addresses, release both addresses.

  3. In Security Groups, delete first-instance.
  4. In My Account, under SSH Keys, delete first-instance.
openstack --os-cloud <cloud-name> server delete --wait web-01 win-01
openstack --os-cloud <cloud-name> floating ip delete <web-01-address> <win-01-address>
openstack --os-cloud <cloud-name> security group delete first-instance
openstack --os-cloud <cloud-name> keypair delete first-instance
tofu destroy

Type yes when asked. OpenTofu deletes the 10 resources it created, and nothing else.

Check it worked

  • After each way, openstack --os-cloud <cloud-name> server list shows web-01 and win-01 as ACTIVE, each with a private and a floating IP address.
  • SSH lands you at a shell prompt on web-01, and RDP at the Windows desktop on win-01.
  • After cleaning up, server list is empty again, and openstack --os-cloud <cloud-name> volume list shows no volumes left behind.

What you learned

  • The portal, the CLI and OpenTofu drive the same APIs and build the same instances; see GUI, API and infrastructure as code.
  • An instance is an image, a flavour and a root volume, with access set up at first boot; see Images, flavours and instances.
  • Security groups open only the ports a workload needs, to only the addresses that need them; see Networking model.
  • Code can be applied and destroyed again and again with the same result, which is what makes instances disposable; see Pets vs cattle.

Next