Jan 26 2022

Terraform Shared Resources: SSH Keys Case Study

Published by at 10:08 am under DevOps,Terraform

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.

Reuse Terraform Shared Resources


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.


No responses yet

Trackback URI | Comments RSS

Leave a Reply