Terraform: Difference between revisions
m (Fixing internal link) |
m (Fixing links) |
||
Line 161: | Line 161: | ||
The values for <code>image_id</code> and <code>flavor_id</code> are one reason I like to have a terminal session open running the OpenStack CLI, connected to the cloud I’m targeting with Terraform: I switch over to it and issue <code>flavor list</code> or <code>image list</code>. These list the names and IDs. | The values for <code>image_id</code> and <code>flavor_id</code> are one reason I like to have a terminal session open running the OpenStack CLI, connected to the cloud I’m targeting with Terraform: I switch over to it and issue <code>flavor list</code> or <code>image list</code>. These list the names and IDs. | ||
If using Horizon (the OpenStack web interface), this is semi-possible–see the [[# | If using Horizon (the OpenStack web interface), this is semi-possible–see the [[#Finding_image_and_flavour_UUIDs_in_Horizon|guide in the appendix]]. | ||
Note that no volumes are supplied. A compute instance in Compute Canada will already have an associated volume but a persistent instance will probably fail unless there is sufficient empty space in the image itself. It is [ | Note that no volumes are supplied. A compute instance in Compute Canada will already have an associated volume but a persistent instance will probably fail unless there is sufficient empty space in the image itself. It is [[OpenStack#Booting_from_a_Volume|recommended that a boot volume be created]] for VMs using persistent flavours. | ||
=== Trying it out === | === Trying it out === |
Revision as of 21:01, 5 June 2019
This is not a complete article: This is a draft, a work in progress that is intended to be published into an article, which may or may not be ready for inclusion in the main wiki. It should not necessarily be considered factual or authoritative.
Originally appeared on the ACME group blog; has been edited from the original.
Getting Started with Terraform[edit]
Terraform is seeing growing use within the Compute Canada Federation. Its infrastructure-as-code allows its users to maintain OpenStack resources as a collection of definitions which can be easily updated using favourite text editors, shared among members of a group, and stored in version control.
In this series we introduce Compute Canada Federation colleagues to Terraform and demonstrate its use on our OpenStack clouds.
In this first tutorial, we set up our local workspace for Terraform and create a VM with a floating IP and attached volume.
Preparation[edit]
Before starting with Terraform, you need access to an OpenStack tenant with available resources, Terraform itself, and a few things configured on your workstation or laptop.
Access to OpenStack[edit]
For access to the cloud, see Getting a Cloud project on the Compute Canada Docs wiki. If you’ve never used OpenStack before, it would be helpful to familiarize yourself with it by creating a VM, attaching a volume, associating a floating IP, and ensuring you can login to the VM afterwards. This guide also assumes you already have an SSH keypair created and the public key stored with OpenStack.
If you don’t know how to do these things, the Cloud Quick Start guide provided by Compute Canada will get you going.
The experience of creating these resources using the web interface will lay a foundation for understanding both what Terraform is doing, and where it has value.
Terraform[edit]
See the Terraform downloads page for the latest client. This guide is based on Terraform 0.12, which as of this writing has been in the wild for less than a month. Version 0.12 contains a fair number of changes over v0.11, including a clearer syntax, and though the release is quite new, it’s been in development for a while, solves some fundamental issues users were having in using Terraform effectively, and at any rate is the direction Terraform is going.
Credentials[edit]
You will also need credentials for access to your OpenStack project available to Terraform. There are three ways to provide your credentials to Terraform, described in the next section. Horizon, the OpenStack web interface, offers a simple way to download credentials: once logged in, click on API Access in the navigation bar, and on that page is a drop-down menu entitled “Download OpenStack RC File”. From here you may download a clouds.yaml
file or an RC file which can be sourced from your shell session.
The third method is to take the credentials and represent them directly in the Terraform configuration.
The RC file is a series of shell commands sourced from your existing session. Because it exports variables which must be available in your current shell session, this file is not run directly, as a script (in which case all exported variables would disappear when the script exited and its shell context destroyed). So, once downloaded, the RC file can then be sourced like so:
$ . openrc.sh
It will then prompt your for your OpenStack password, which along with necessary information about you, your tenant and the cloud you’re connecting to will be stored in a bunch of environment variables prefixed by OS_
, such as $OS_AUTH_URL
and so on. Have a look at the RC file to see what it’s doing. In Bash, you can type echo $OS_<tab>
and auto-completion will list variables with that prefix.
My preferred method is to create a configuration in $HOME/.config/openstack/clouds.yaml
. If you don’t have such a file already, you can download one as described above and move it into place. I also recommend changing the name given to the cloud in the downloaded file to something meaningful, especially if you use more than one OpenStack cloud. Then, to use the CLI tools described below, you simply create an environment variable $OS_CLOUD
with the name of the cloud you want to use.
$ export OS_CLOUD=arbutus
Later on I’ll explain how to use those with Terraform.
OpenStack session[edit]
I find it helpful to have a terminal window open running the OpenStack CLI. This provides a handy reference for the specifications I’ll be building, such as looking up flavour and image UUIDs, and for verifying the actions performed by Terraform. Horizon can also be used for this, of course, but it’s more hand-wavey. Because of the mouse, you see.
The OpenStack CLI (referred to as “OSC”) is a Python client which can be best installed through Python Pip, and available for multiple OSes and distributions.
Terraform workspace[edit]
Finally, create a directory for your Terraform configuration and state files and consider this your home base for this guide. This is where we will start.
Defining OpenStack provider[edit]
First, we describe the OpenStack Provider we want to use. This is where we tell Terraform we want to use OpenStack, and how. On terraform init
the most recent version of the plugin will be installed in the working directory and on terraform apply
the included credentials will be used to connect to the specified cloud.
There are three ways to define the credentials Terraform will need to connect to a cloud instance and manage resources in the appropriate project (“tenant” in the older OpenStack parlance). The first is to explicity define them directly in Terraform.
provider "openstack" {
tenant_name = "some_tenant"
tenant_id = "1a2b3c45678901234d567890fa1b2cd3"
auth_url = "https://cloud.example.org:5000/v3"
user_name = "joe"
password = "sharethiswithyourfriends!"
user_domain_name = "CentralID"
}
For some OpenStack instances the above would specify the complete set of information necessary to connect to the instance and manage resources in the given project (“tenant”). However, Terraform supports partial credentials in which you could leave some values out of the Terraform configuration and supply them a different way. This would allow us, for example, to leave the password out of the configuration file.
There are two ways to specify OpenStack credentials outside of Terraform: Using environment variables or a definition in clouds.yaml
. These can be combined so that, typically, passwords are not stored in clouds.yaml
either.
To specify OpenStack credentials using clouds.yaml
, specify cloud
in the provider stanza:
provider "openstack" {
cloud = "my_cloud"
}
…or it could be defined in the environment as $OS_CLOUD
, and the provider definition could be empty:
provider "openstack" {
}
Similarly, environment variables can be used alone to specify the project ID, authentication URL, etc. or can be combined with a partial definition in the provider stanza.
The configuration reference of the OpenStack Provider describes the available options in detail.
What should you use?[edit]
It may be tempting to leave some of the details in the environment so that the Terraform configuration is more portable or reusable, but as we will see later, the Terraform configuration will and must contain details which are specific to each cloud, such as flavour and image UUIDs, network names, and tenants.
The most important consideration in what goes into your configuration in this regard is security. You probably want to avoid storing your credentials in the Terraform configuration, even if you’re not sharing it with anyone, even if it’s on your own workstation and nobody has access but you. Even if you’re not worried about hacking, it is definitely not good practice to store passwords and such in configuration files which may wind up getting copied and moved around your filesystem as you try things out. But also, always remember the “ABC” of Hacking: Always Be Concerned about Hacking!
Initializing Terraform[edit]
To ensure we have credentials and such set up correctly, we can initialize Terraform in our workspace and check the configuration we have so far. With the provider definition in a file called, for example, nodes.tf
, we first run terraform init
:
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "openstack" (terraform-providers/openstack)
1.19.0...
The following providers do not have any version constraints in configuration,
so the latest version was installed.
To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.
* provider.openstack: version = "~> 1.19"
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
This shows success in initializing Terraform and downloading the OpenStack provider plugin so the OpenStack stanzas will be handled correctly. This does not test out the credentials we’ve configured because this doesn’t actually try to connect to the defined provider.
We can’t test that out until we’ve got something in our definition which attempts to manage a resource. Until then, Terraform doesn’t have any defined resources for which to check state, and so terraform plan
succeeds, but it’s an empty victory.
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
No changes. Infrastructure is up-to-date.
This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.
The command terraform plan
compiles the Terraform definition and attempts to determine how to reconcile the resulting desired state with the actual state on the cloud, and produces a plan of what it would if the changes were applied. We’ll see that shortly.
Defining a VM[edit]
So let’s look at defining a basic VM.
Important: It is good practice to always specify flavours and images using their IDs even when Terraform supports using the name. Although the name is more readable, the ID is what actually defines the state of the resource and the ID of a given image or flavour will never change. It is possible, however, for the name to change. If a flavour or image is retired, for example, and replaced with another of the same name, the next time you run Terraform, the updated ID will be detected and Terraform will determine that you want to rebuild or resize the associated resource. This is a destructive (and reconstructive) operation.
A minimal OpenStack VM may be defined as follows in Terraform:
resource "openstack_compute_instance_v2" "myvm" {
name = "myvm"
image_id = "80ceebef-f9aa-462e-a793-d3c1cf96123b"
flavor_id = "0351ddb0-00d0-4269-80d3-913029d1a111"
key_pair = "Aluminum"
security_groups = ["default"]
}
This will create a VM with the given name, image and flavor, and associate with it a key pair and the default security group.
Note: If you’re following along (please do!), use your own values for
image_id
,flavor_id
, andkey_pair
, or this will probably fail!
The values for image_id
and flavor_id
are one reason I like to have a terminal session open running the OpenStack CLI, connected to the cloud I’m targeting with Terraform: I switch over to it and issue flavor list
or image list
. These list the names and IDs.
If using Horizon (the OpenStack web interface), this is semi-possible–see the guide in the appendix.
Note that no volumes are supplied. A compute instance in Compute Canada will already have an associated volume but a persistent instance will probably fail unless there is sufficient empty space in the image itself. It is recommended that a boot volume be created for VMs using persistent flavours.
Trying it out[edit]
To see what happens now that we have a node definition added to nodes.tf
, let’s try terraform plan
again. A reminder that this will not actually apply the changes to our OpenStack project.
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
Error: One of 'auth_url' or 'cloud' must be specified
on nodes.tf line 1, in provider "openstack":
1: provider "openstack" {
Whoops! I chose the empty provider scenario described above, but forgot to define $OS_CLOUD
in my environment. Let’s try again:
$ export OS_CLOUD=dleske.xav-105.graham
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# openstack_compute_instance_v2.myvm will be created
+ resource "openstack_compute_instance_v2" "myvm" {
+ access_ip_v4 = (known after apply)
+ access_ip_v6 = (known after apply)
+ all_metadata = (known after apply)
+ availability_zone = (known after apply)
+ flavor_id = "0351ddb0-00d0-4269-80d3-913029d1a111"
+ flavor_name = (known after apply)
+ force_delete = false
+ id = (known after apply)
+ image_id = "80ceebef-f9aa-462e-a793-d3c1cf96123b"
+ image_name = (known after apply)
+ key_pair = "Aluminum"
+ name = "myvm"
+ power_state = "active"
+ region = (known after apply)
+ security_groups = [
+ "default",
]
+ stop_before_destroy = false
+ network {
+ access_network = (known after apply)
+ fixed_ip_v4 = (known after apply)
+ fixed_ip_v6 = (known after apply)
+ floating_ip = (known after apply)
+ mac = (known after apply)
+ name = (known after apply)
+ port = (known after apply)
+ uuid = (known after apply)
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
So let’s have a look. This is a lot of information but it’s definitely required to check this before applying changes to ensure there are no surprises.
These values are to resources as they’d be defined in OpenStack. Anything marked known after apply
will be determined from the state of newly created resources queried from OpenStack. Other values are set according to what we’ve defined or determined by the Terraform and the OpenStack plugin as either calculated or default values.
If you are in a hurry and don’t mind risking destroying or rebuilding resources by mistake, at least make sure you double-check the last line of the plan:
Plan: 1 to add, 0 to change, 0 to destroy.
In this case we know we’re adding a resource so this looks right. If the other values were non-zero then we’d better have another look at our configuration, state and what’s actually defined in OpenStack and make whatever corrections are necessary.
Side note: What happens to existing OpenStack resources?[edit]
In my case, as we go through this tutorial I will be managing resources with Terraform in an OpenStack project which already has some VMs I have defined separately. If this is the case for you, you may be wondering whether what we’re doing here will affect those resources.
It will not. Terraform has no knowledge of resources already defined in the project and does not attempt to determine existing state. (There is nascent functionality in Terraform to handle this but it is incomplete and varies widely from provider to provider. Support in OpenStack is very limited.) Terraform bases its actions on the given configuration and previously determined state relevant to that configuration. Any existing resources are not represented in either and are invisible to Terraform.
It is possible to pull existing OpenStack resources into Terraform but it is not a trivial amount of work and way outside the scope of this tutorial. The important thing here is that any existing resources in your OpenStack project are safe from inadvertent mangling from Terraform–but just to be on the safe side, why don’t you make sure you read the output plans carefully? :)
Applying the configuration[edit]
So far we’ve only asked Terraform what it thinks it would do. Let’s try it out for real. This is where we use terraform apply
to actually effect the changes described in the plan.
$ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
[... repeat of the plan from above ...]
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:
[... we interrupt this output to bring you an important message! ...]
I’ll interrupt the output there to point out Terraform repeats the plan above, so it’s completely reasonable to skip terraform plan
and go straight to terraform apply
. Terraform will generate the plan and get you to sign off on it before going ahead. So I’ll reiterate that (singing) you should always check the plan before applying it.
[... We now return you to previously scheduled programming ...]
Enter a value: yes
openstack_compute_instance_v2.myvm: Creating...
Error: Error creating OpenStack server: Expected HTTP response code [] when
accessing [POST
https://graham.cloud.computecanada.ca:8774/v2.1/43b86742c5ee4eaf800a36d7d234d95c/servers],
but got 409 instead
{"conflictingRequest": {"message": "Multiple possible networks found, use a
Network ID to be more specific.", "code": 409}}
on nodes.tf line 4, in resource "openstack_compute_instance_v2" "myvm":
4: resource "openstack_compute_instance_v2" "myvm" {
Dang. But I can solve this.
In my particular case, I have two private networks defined in my project and I happen to know one of them is leftover from a Kubernetes cluster I didn’t completely tear back down. That’s not all, though. OpenStack clouds, at least in CCF, will have one default private network and also the external network, which is where floating IPs are assigned. I can’t get rid of those, nor do I want to, so we need to add something to our VM definition.
Adding a network[edit]
All we need at this stage is the name of the network we need on the VM. The name of this network differs from cloud to cloud within Compute Canada, but typically they are on a 192.168.X.Y network, and sometimes named for the OpenStack project. In this case the network name in my project is dleske-tenant-net
, so I add a network
resource sub-block to my VM definition so I have:
resource "openstack_compute_instance_v2" "myvm" {
name = "myvm"
image_id = "80ceebef-f9aa-462e-a793-d3c1cf96123b"
flavor_id = "0351ddb0-00d0-4269-80d3-913029d1a111"
key_pair = "Aluminum"
security_groups = ["default"]
network {
name = "dleske-tenant-net"
}
}
I then try again.
$ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# openstack_compute_instance_v2.myvm will be created
+ resource "openstack_compute_instance_v2" "myvm" {
+ access_ip_v4 = (known after apply)
+ access_ip_v6 = (known after apply)
+ all_metadata = (known after apply)
+ availability_zone = (known after apply)
+ flavor_id = "0351ddb0-00d0-4269-80d3-913029d1a111"
+ flavor_name = (known after apply)
+ force_delete = false
+ id = (known after apply)
+ image_id = "80ceebef-f9aa-462e-a793-d3c1cf96123b"
+ image_name = (known after apply)
+ key_pair = "Aluminum"
+ name = "myvm"
+ power_state = "active"
+ region = (known after apply)
+ security_groups = [
+ "default",
]
+ stop_before_destroy = false
+ network {
+ access_network = false
+ fixed_ip_v4 = (known after apply)
+ fixed_ip_v6 = (known after apply)
+ floating_ip = (known after apply)
+ mac = (known after apply)
+ name = "dleske-tenant-net"
+ port = (known after apply)
+ uuid = (known after apply)
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
openstack_compute_instance_v2.myvm: Creating...
openstack_compute_instance_v2.myvm: Still creating... [10s elapsed]
openstack_compute_instance_v2.myvm: Still creating... [20s elapsed]
openstack_compute_instance_v2.myvm: Still creating... [30s elapsed]
openstack_compute_instance_v2.myvm: Creation complete after 32s [id=1f7f73ff-b9b5-40ad-9ddf-d848efe13e42]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
And there we have it, a VM created by Terraform. If you’re following along (please do!) you should see your new VM on Horizon or in the output of server list
in your OpenStack terminal window:
(openstack) server list -c ID -c Name -c Status +--------------------------------------+--------+--------+ | ID | Name | Status | +--------------------------------------+--------+--------+ | 1f7f73ff-b9b5-40ad-9ddf-d848efe13e42 | myvm | ACTIVE | | c3fa7d11-4122-412a-ad19-32e52cbb8f66 | store | ACTIVE | | f778f65f-c9d5-4808-930b-9f50d82a8c9c | puppet | ACTIVE | | 9b42cbf3-3782-4472-bdd0-9028bbb73460 | lbr | ACTIVE | +--------------------------------------+--------+--------+
You can see here I have three other VMs created previously which are surviving.
Where we are now[edit]
We now have a working VM which has successfully been initialized and is on the private network. We can’t log in and check it out however because we haven’t assigned a floating IP to this host, so it’s not directly accessible from outside the tenant.
If we had another host in that tenant with a floating IP, we could use that host as a jump host (sometimes called a bastion host) to the new VM, as they will both be on the same private network. This is a good strategy to use for nodes that do not need to be directly accessible from the internet, such as a database server, or just to preserve floating IPs, which are a limited resource until ipv6 becomes a thing.
For this case, we’ll add a floating IP to our new VM. First, though, note there is now a file in your workspace called terraform.tfstate
. This was created by Terraform during the application of the new configuration and confirmation of its success. The state file contains details about the managed resources Terraform uses to determine how to arrive at a new state described by configuration updates. In general, you will not need to look at this file, but know that without it, Terraform cannot properly manage resources and if you delete it, you will need to restore it or recreate it, or manage those resources without Terraform.
Add a floating IP[edit]
Floating IPs are not created directly on a VM in OpenStack: they are allocated to the project from a pool and associated with the VM’s private network interface.
Assuming we do not already have a floating IP allocated for this use, we declare a desired floating IP resource like the following example. The only thing we need is to know the pool from which to allocate the floating IP; in Compute Canada clouds this is the external network.
resource "openstack_networking_floatingip_v2" "myvm_fip" {
pool = "provider-199-2"
}
I can add this right away and either apply it or just use terraform plan
to show what would happen. I’m going to apply it to demonstrate something about Terraform.
$ terraform apply
openstack_compute_instance_v2.myvm: Refreshing state...
[id=1f7f73ff-b9b5-40ad-9ddf-d848efe13e42]
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# openstack_networking_floatingip_v2.myvm_fip will be created
+ resource "openstack_networking_floatingip_v2" "myvm_fip" {
+ address = (known after apply)
+ all_tags = (known after apply)
+ fixed_ip = (known after apply)
+ id = (known after apply)
+ pool = "provider-199-2"
+ port_id = (known after apply)
+ region = (known after apply)
+ tenant_id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
openstack_networking_floatingip_v2.myvm_fip: Creating...
openstack_networking_floatingip_v2.myvm_fip: Creation complete after 9s
[id=20190061-c2b6-4740-bbfc-6facbb300dd4]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Yay. So now I have a floating IP allocate–but I haven’t associated it to anything, is it just sort of dangling there? No, it’s allocated to the project now, and Terraform knows about it, so when we add the next piece, even though we aren’t creating either resource, we are creating an association between the compute instance and floating IP.
That looks like the following. We’re introducing something new here:
resource "openstack_compute_floatingip_associate_v2" "myvm_fip" {
floating_ip = openstack_networking_floatingip_v2.myvm_fip.address
instance_id = openstack_compute_instance_v2.myvm.id
}
This new resource defines as its attributes references to other resources and their attributes.
If you’ve been looking at the OpenStack provider documentation, you may have noted the syntax differs from what I’m presenting here, because Terraform version 0.12 as mentioned does not require string interpolation for all use of variables.
References like this are typically <resource type>.<resource name>.<attribute>
. Others you may soon see include var.<variable name>
. But this is hopefully pretty clear: this resource forms an association between the VM we created earlier, and the floating IP we allocated in the next step.
$ terraform apply
openstack_networking_floatingip_v2.myvm_fip: Refreshing state...
[id=20190061-c2b6-4740-bbfc-6facbb300dd4]
openstack_compute_instance_v2.myvm: Refreshing state...
[id=1f7f73ff-b9b5-40ad-9ddf-d848efe13e42]
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# openstack_compute_floatingip_associate_v2.myvm_fip will be created
+ resource "openstack_compute_floatingip_associate_v2" "myvm_fip" {
+ floating_ip = "X.Y.Z.W"
+ id = (known after apply)
+ instance_id = "1f7f73ff-b9b5-40ad-9ddf-d848efe13e42"
+ region = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
openstack_compute_floatingip_associate_v2.myvm_fip: Creating...
openstack_compute_floatingip_associate_v2.myvm_fip: Creation complete after 5s
[id=X.Y.Z.W/1f7f73ff-b9b5-40ad-9ddf-d848efe13e42/]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
I replaced the actual IP with letters because I remember the ABCs of hacking. (I’ll probably inadvertently include it somewhere else.)
So, here’s what I created:
- Flavor: p1-3gb
ID: 1f7f73ff-b9b5-40ad-9ddf-d848efe13e42
Image: CentOS-7-x64-2018-05
Name: myvm
Networks: dleske-tenant-net=192.168.2.11, X.Y.Z.W
Status: ACTIVE
Note that it has an associated floating IP and I could probably SSH into that bad boy right now.
$ ssh centos@X.Y.Z.W hostname
The authenticity of host 'X.Y.Z.W (X.Y.Z.W)' can't be established.
ECDSA key fingerprint is SHA256:XmN5crnyxvE1sezdpo5tG5Z2nw0Z+2pspvkNSGpB99A.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'X.Y.Z.W' (ECDSA) to the list of known hosts.
myvm.novalocal
Boop.
As mentioned before though it’s undesirable to create a persistent instance without a root volume, so let’s do that next.
Add a volume[edit]
We are going to add a root volume to the VM. Since we’re replacing its boot disk, this is a destructive operation. This is something you need to watch out for in Terraform, and one of the chief reasons for reading your plans carefully before applying. It’s unlikely you’re going to accidentally create a bunch of resources and have that cause critical issues; if you accidentally issue terraform destroy
and follow it up with agreeing yes
to the prompt which if I recall is bright red and has more exclamation points than a junior high diary, then that’s on you. But it can be deceptively easy to accidentally create configuration changes that require rebuilding something important.
You may recall from earlier my admonition to use UUIDs for flavours and images. This is why.
At any rate, this is what we want to do this time, so let’s get to it.
Since this is a root volume, we’re creating it as part of the compute instance, not as an independent volume to be attached later. So we’ll add another subblock to our compute instance resource, along with our network block.
block_device {
uuid = "80ceebef-f9aa-462e-a793-d3c1cf96123b"
source_type = "image"
destination_type = "volume"
volume_size = 10
boot_index = 0
delete_on_termination = true
}
The uuid
attribute is actually the UUID of the image we want to use. This is just moved from the resource definition where it already resided and given a different name (instead of image_id
). The other attributes are self-explanatory, except for destination_type
, which is here set to volume
to indicate this is to be stored with an OpenStack-provided volume rather than using disk on the hypervisor. delete_on_termination
is important–for testing, you will probably want this to be true
so you don’t have to remember to constantly clean up leftover volumes, but for real use you should consider setting it to false
as a last defence against accidental deletion of resources.
Do not leave the
image_id
attribute defined in the outer compute instance definition! This will work, but Terraform will see a change from “boot from volume” to “boot directly from image” on every run, and so will always attempt to rebuild your instance. (This is probably a flaw in the OpenStack provider.)
Here’s how the plan looks:
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement
Terraform will perform the following actions:
# openstack_compute_floatingip_associate_v2.myvm_fip must be replaced
-/+ resource "openstack_compute_floatingip_associate_v2" "myvm_fip" {
floating_ip = "199.241.167.122"
~ id = "199.241.167.122/1f7f73ff-b9b5-40ad-9ddf-d848efe13e42/" -> (known after apply)
~ instance_id = "1f7f73ff-b9b5-40ad-9ddf-d848efe13e42" -> (known after apply) # forces replacement
~ region = "RegionOne" -> (known after apply)
}
# openstack_compute_instance_v2.myvm must be replaced
-/+ resource "openstack_compute_instance_v2" "myvm" {
~ access_ip_v4 = "192.168.2.11" -> (known after apply)
+ access_ip_v6 = (known after apply)
~ all_metadata = {} -> (known after apply)
~ availability_zone = "nova" -> (known after apply)
flavor_id = "0351ddb0-00d0-4269-80d3-913029d1a111"
~ flavor_name = "p1-3gb" -> (known after apply)
force_delete = false
~ id = "1f7f73ff-b9b5-40ad-9ddf-d848efe13e42" -> (known after apply)
image_id = "80ceebef-f9aa-462e-a793-d3c1cf96123b"
~ image_name = "CentOS-7-x64-2018-05" -> (known after apply)
key_pair = "Aluminum"
name = "myvm"
power_state = "active"
~ region = "RegionOne" -> (known after apply)
security_groups = [
"default",
]
stop_before_destroy = false
+ block_device {
+ boot_index = 0 # forces replacement
+ delete_on_termination = true # forces replacement
+ destination_type = "volume" # forces replacement
+ source_type = "image" # forces replacement
+ uuid = "80ceebef-f9aa-462e-a793-d3c1cf96123b" # forces replacement
+ volume_size = 10 # forces replacement
}
~ network {
access_network = false
~ fixed_ip_v4 = "192.168.2.11" -> (known after apply)
+ fixed_ip_v6 = (known after apply)
+ floating_ip = (known after apply)
~ mac = "fa:16:3e:3b:79:27" -> (known after apply)
name = "dleske-tenant-net"
+ port = (known after apply)
~ uuid = "5c96bf54-a396-47c5-ab12-574f630bcb80" -> (known
after apply)
}
}
So note there are several warnings of what’s going to be replaced and what’s going to change, not to mention this tossoff line:
Plan: 2 to add, 0 to change, 2 to destroy.
2 to add (oh good!) and 2 to destroy (oh dang!). This is why we pay attention to the Terraform plan, kids.
I go ahead and after chugging away for a bit, my VM is reborn with a spiffy 10GB root volume. To get on, I first have to remove the previous build’s SSH host keys from my known_hosts
file, and then I’m off to the races. So, the first thing I do is log on and apply all available updates.
[centos@myvm ~]$ sudo yum update -y
...
[ goes for ages ]
So we now have a working, Terraformed VM and a way to get to it and a place on it to store data once we get there. I think that’s a pretty good start! In upcoming tutorials I’ll delve a little deeper with some more advanced topics.
The full example[edit]
provider "openstack" {
}
resource "openstack_compute_instance_v2" "myvm" {
name = "myvm"
flavor_id = "0351ddb0-00d0-4269-80d3-913029d1a111"
key_pair = "Aluminum"
security_groups = ["default"]
network {
name = "dleske-tenant-net"
}
block_device {
uuid = "80ceebef-f9aa-462e-a793-d3c1cf96123b"
source_type = "image"
destination_type = "volume"
volume_size = 10
boot_index = 0
delete_on_termination = true
}
}
resource "openstack_networking_floatingip_v2" "myvm_fip" {
pool = "provider-199-2"
}
resource "openstack_compute_floatingip_associate_v2" "myvm_fip" {
floating_ip = openstack_networking_floatingip_v2.myvm_fip.address
instance_id = openstack_compute_instance_v2.myvm.id
}
Appendix[edit]
References[edit]
The following might be of interest to those exploring further and building on the work done in this tutorial. Note that as of this writing the OpenStack provider’s documentation conforms to v0.11 syntax, but this should work under v0.12 without trouble.
- Introduction to Terraform
- OpenStack provider
- OpenStack compute instance resource: many examples of different use cases for creating VMs under OpenStack with Terraform.
- Compute Canada cloud documentation and the Cloud Quick Start Guide
Examples[edit]
- The Magic Castle project
- Terraform Ceph Ansible deployment testing project
Finding image and flavour UUIDs in Horizon[edit]
For those more comfortable using the web interface to OpenStack, here is a quick cheat sheet on finding flavour and image UUIDs in Horizon. You’ll need to log into the web interface of the desired cloud for this information.
To find an image’s UUID, find the Images menu item under Compute (1).
You’ll get a list of images available to your project. Click on the one you’d like to use. (2)
…and there’s the ID.
It’s a little tougher for flavours. Actually this should spur you to get comfortable with the OpenStack CLI. Winky face and whatnot.
For this you have to fake out launching an instance, but that doesn’t even give you the ID of the flavour. But at least you’ll know the name of the flavour you want.
Once the launch dialog is open, select the Flavor pane.
Now you should have a list of flavours and it’ll also show you which ones fit within your quotas. All you’ve got here is the name, though.
To actually get the ID, you have two options:
- Use the name for the first Terraform run, and then get the ID from the output or state file, and finally, switch your configuration to use the ID instead. This should not attempt to recreate the VM, but check before you agree to
terraform apply
. - Switch to using the OpenStack CLI. (Recommended.)
Comments?[edit]
Feedback on this tutorial is welcome–please let me know in Slack (app link).