Jan 26 2022
Terraform Shared Resources: SSH Keys Case Study
Terraform lets you automate infrastructure creation and change in the cloud, what is commonly called infrastructure as code. I need to create a virtual machine, which must embed 3 SSH keys that belong to administrators. I want this Terraform shared resource to be reusable by other modules. This example on IBM Cloud is based on the IBM plugin for Terraform but this method remains valid for other cloud providers indeed.
I did not include the VPC creation, neither subnets and security groups to make it more readable.
Resources in a Single Module
We’ll start with 2 files: ssh.tf containing the code that creates administrator SSH keys, and vm.tf in the same directory, that creates the server. Keys are then given to the VM as input settings.
resource "ibm_is_ssh_key" "user1_sshkey" {
name = "user1"
public_key = "ssh-rsa AAAAB3[...]k+XR=="
}
resource "ibm_is_ssh_key" "user2_sshkey" {
name = "user2"
public_key = "ssh-rsa AAAAB3[...]Zo9R=="
}
resource "ibm_is_ssh_key" "user3_sshkey" {
name = "user3"
public_key = "ssh-rsa AAAAB3[...]67GqV="
}
resource "ibm_is_instance" "server1" {
name = "server1"
image = var.image
profile = var.profile
vpc = ibm_is_vpc.vpc.id
zone = var.zone1
primary_network_interface {
subnet = ibm_is_subnet.subnet1.id
security_groups = [ibm_is_vpc.vpc.default_security_group]
}
keys = [
ibm_is_ssh_key.user1_sshkey.id,
ibm_is_ssh_key.user2_sshkey.id,
ibm_is_ssh_key.user3_sshkey.id
]
}
The code is pretty simple but raises a major problem:
SSH keys are not reusable in another Terraform module. If we copy/paste that piece of code to create a second VM, an error will throw keys already exist. Also, adding a new key requires to modify the 2 Terraform files.
Terraform Shared Resources
As a consequence, we need to create SSH keys in a brand new independent Terraform module and make them available to other modules. We can achieve this exporting key ids with output values. Outputs make it possible to expose variables to the command line or other Terraform modules.
Let’s move the key declaration to a new Terraform directory to which we’ll add an output ssh_keys that sends back an array with their respective ids, knowing this is what VMs expect as input.
resource "ibm_is_ssh_key" "user1_sshkey" {
name = "user1"
public_key = "ssh-rsa AAAAB3[...]k+XR=="
}
resource "ibm_is_ssh_key" "user2_sshkey" {
name = "user2"
public_key = "ssh-rsa AAAAB3[...]Zo9R=="
}
resource "ibm_is_ssh_key" "user3_sshkey" {
name = "user3"
public_key = "ssh-rsa AAAAB3[...]67GqV="
}
output "ssh_keys" {
value = [
ibm_is_ssh_key.user1_sshkey.id,
ibm_is_ssh_key.user2_sshkey.id,
ibm_is_ssh_key.user3_sshkey.id
]
}
Once you launch terraform apply, you can display output values with terraform output:
$ terraform output
ssh_keys = [
"r010-3e98b94b-9518-4e11-9ac4-a014120344dc",
"r010-b271dce5-4744-48c3-9001-a620e99563d9",
"r010-9358c6ab-0eed-4de7-a4a0-4ba20b2c04c9",
]
This is exactly what we need. All is left to do is get this output through a data lookup and process it in the VM module.
data "terraform_remote_state" "ssh_keys" {
backend = "local"
config = {
path = "../ssh_keys/terraform.tfstate"
}
}
resource "ibm_is_instance" "server1" {
name = "server1"
image = var.image
profile = var.profile
primary_network_interface {
subnet = ibm_is_subnet.subnet1.id
security_groups = [ibm_is_vpc.vpc.default_security_group]
}
vpc = ibm_is_vpc.vpc.id
zone = var.zone1
keys = data.terraform_remote_state.ssh_keys.outputs.ssh_keys
}
That’s better, we are able to handle SSH keys independently of other Terraform modules and reuse them at will. The data lookup path is the relative path to the directory that contains the ssh.tf file.
Variables in Key/Value Maps
That’s better but we could make shared resources (SSH keys in this case) creation more elegant.
Indeed, adding a new key has to be done in 2 different places: create a Terraform resource, and add it to the values returned in the output. Which is tedious and error-prone.
Moreover, it is quite difficult to read, it would be better to separate code and values.
To do this, we are going to store the keys in a map (an array) in a file terraform.tfvars, that is loaded automatically. A file called terraform.tfvars, loads automatically in Terraform. Name it anything else .tfvars
ssh_keys = {
"user1" = "ssh-rsa AAAAB3[...]k+XR=="
"user2" = "ssh-rsa AAAAB3[...]Zo9R=="
"user3" = "ssh-rsa AAAAB3[...]67GqV="
}
In ssh.tf, we’ll loop on that key/value array to create resources, and export them as outputs.
# Array definition
variable "ssh_keys" {
type = map(string)
}
resource "ibm_is_ssh_key" "keys" {
for_each = var.ssh_keys
name = each.key
public_key = each.value
}
output "ssh_keys" {
value = values(ibm_is_ssh_key.keys)[*].id
}
Getting values is a bit tricky. I started to display an output values(ibm_is_ssh_key.keys) to analyse the structure and get ids I needed.
In the end, a new shared resource (an SSH key in this case) can be created with a simple insert in a map, witthin a file that only contains variables. In one single place. Anybody can take care of it without reading or understanding the code.