Skip to main contentIBM Cloud Patterns

Getting Started with Terraform

Learn the basics of Infrastructure as Code with Terraform on IBM Cloud

After you have configured and verified Terraform and the IBM Cloud provider, this section will go through a step-by-step example of creating a resource on IBM Cloud using the provider. It covers how to create a virtual server from scratch. To do this, there is example terraform code to create a vpc and subnet, add a virtual server to the vpc, configure an access list and expose the virtual server on the Internet, and then access it over ssh. If you are an experienced Terraform user, you may skim through this section and simply note the resource types and parameters used in each step.

Deploy a single instance

The first step to use Terraform with IBM Cloud is to configure the provider. Lets start creating a directory to store the “Hello World” project.

mkdir hello_world_terraform_ibm
cd hello_world_terraform_ibm

Remember to export in the terminal or console the variable IC_API_KEY with the IBM Cloud API key as shown in the

Environment Setup
.

The Terraform code can be done with JSON format or using HashiCorp Configuration Language (HCL). During this pattern guide we’ll use only HCL and we will call it HCL or Terraform Code.

The Terraform code can be done with JSON format or using HashiCorp Configuration Language (HCL). During this pattern guide we’ll use only HCL and we will call it HCL or Terraform Code.

Create the file main.tf with the provider block.

provider "ibm" {
generation = 2
region = "us-south"
}

We will be working with VPC Infrastructure Gen 2, the provider accepts the following common input parameters:

Input parameterDescription
generationthe generation of IBM Cloud VPC Infrastructure, in this case it’ll be 2
regionthe region where you want to create the VPC resources, examples: us-south or us-east
ibmcloud_api_keythe IBM Cloud API key. However we recommend to enter this parameter with the environment variable IC_API_KEY

To find all the available regions with IBM Cloud CLI you can use the is sub-command (for more information on installing this plugin refer to Setup Environment):

ibmcloud is regions

Now execute the terraform sub-commands init and plan:

terraform init
terraform plan

The init sub-command downloads all the terraform components needed to execute or apply the terraform code, in this simple example it does not need to download any, the only component we need is the IBM Cloud provider and it is already installed. You’ll see there is a new .terraform directory, this is where terraform will store all the required components for your terraform project.

The output of plan shows the resources that will be created, updated and deleted, in this case terraform won’t do anything.

To create a VM or instance we use the resource ibm_is_instance but in order to have an instance you first need the networking resources to build the instance on top of it.

On IBM Cloud there is no VPC created by default, you will need to create your VPC using the ibm_is_vpc resource. Then we’ll need a subnet on that VPC, the subnets are created with the resource ibm_is_subnet.

The Terraform code to create the VPC and the Subnet is as follows:

resource "ibm_is_vpc" "iac_test_vpc" {
name = "terraform-test-vpc"
tags = [ "iac-terraform-test" ]
}
resource "ibm_is_subnet" "iac_test_subnet" {
name = "terraform-test-subnet"
vpc = ibm_is_vpc.iac_test_vpc.id
zone = "us-south-1"

The most important input parameters to create the VPC and the Subnet are listed below.

For the ibm_is_vpc resource:

Input parameterDescription
nameto name the VPC
resource_groupthe ID of the resource group for the VPC, the default resource group is default
tagstags to associate with your VPC, they will help you to find the VPC more easily. Separate multiple tags with a comma

For the ibm_is_subnet resource:

Input parameterDescription
nameto name the Subnet
vpcthe ID of the VPC, use the resource name with .id
zonethe subnet zone name
resource_groupthe ID of the resource group for the Subnet
tagstags to associate with your Subnet, they will help you to find it more easily. Separate multiple tags with a comma

The IBM Cloud virtual server instance is created with the resource ibm_is_instance, a basic terraform code to create an instance would be like so:

resource "ibm_is_instance" "iac_test_instance" {
name = "terraform-test-instance"
image = "r006-14140f94-fcc4-11e9-96e7-a72723715315"
profile = "cx2-2x4"
primary_network_interface {
name = "eth1"
subnet = ibm_is_subnet.iac_test_subnet.id
}

Some of the most important input parameters are:

Input parameterDescription
nameto name the new instance
imagethe ID of the virtual server image to use for the instance
profilename of the profile to use for your instance
vpcthe ID of the VPC where you want to create the instance
zonename of the VPC zone to create the instance
keysa comma separated list of SSH keys that you want to add to your instance
primary_network_interface.subnetthe ID of the subnet. Only one primary network interface can be specified for an instance
tagsa list of tags to add to your instance. Tags can help you find your instance
user_datauser data to transfer to the instance

To list all the available images and profiles we will use ibmcloud is, specify the Gen 2 platform before running the command:

ibmcloud is target --gen 2 # this is a one time execution only, unless you swap to gen 1
ibmcloud is instance-profiles
ibmcloud is images

That’s a lot of images, to narrow our search let’s filter the output to get the available images for Ubuntu 18.4 (LTS Bionic) and for amd64 architecture using regular *nix commands or with jq, if you have jq installed (how to install jq):

ibmcloud is images | grep available | grep ubuntu-18 | grep amd64 | cut -f1 -d" "
ibmcloud is images --json | jq -r '.[] | select(.status=="available" and .operating_system.name=="ubuntu-18-04-amd64").id'

Repeating for the instance profiles searching just the amd64 architecture with the minimum CPU and Memory to get the lowest cost.

ibmcloud is instance-profiles | grep amd64 | sort -k4 -k5 -n | head -1 | cut -f1 -d" "
ibmcloud is instance-profiles --json | jq -r 'map(select(.vcpu_architecture.value=="amd64")) | sort_by(.memory.value)[0].name'

Notice that in the terraform code there are no keys this is because the instance will be used only to start a web service, and we are not going to ssh to the instance at this time.

You can use user_data to run common configuration tasks when your instance starts. For example, you can specify cloud-init directives or shell scripts for Linux images. You will see an example of using the user_data parameter in the next session.

Add to the main.tf file the terraform code for the vpc, subnet and instance. Now, if you execute terraform plan it shows the resources that will be created, updated and deleted. In this case terraform will create 3 resources: the VPC, the subnet and the instance.

Everything is ready now to get that instance in the cloud, just execute the terraform apply sub-command and wait about 10 to 60 seconds.

terraform apply

“Hello World” from IBM Cloud

There is an instance created but it does nothing and you can’t do anything with that instance. Make this instance start a Web server with a “Hello World” page by adding a short script in the user_data parameter. Now, the ibm_is_instance resource will look like:

resource "ibm_is_instance" "iac_test_instance" {
name = "terraform-test-instance"
image = "r006-14140f94-fcc4-11e9-96e7-a72723715315"
profile = "cx2-2x4"
primary_network_interface {
name = "eth1"
subnet = ibm_is_subnet.iac_test_subnet.id
}

When the instance is up and running we need to know the IP address. You could go to the IBM Cloud console and get it from there, however, there is a more effective way to get the IP address with terraform: use the output directive.

Add the following code to your main.tf file:

output "ip_address" {
value = ibm_is_instance.iac_test_instance.primary_network_interface[0].primary_ipv4_address
}

The beauty of Terraform is that you don’t have to destroy everything and re-create it for every change, the provider will update all the resources that can be updated and re-create those that don’t support an update. In this case, the instance has to be re-created because of the user_data, but the VPC and subnet will remain intact, as they were not modified. So, to re-apply the changes just execute apply again:

terraform apply

When terraform complete the task, it prints the IP address of the instance. If you need it again, just execute terraform output ip_address.

To view what you have created on the IBM Cloud console, go to the Navigation Menu () >> VPC Infrastructure, then select Network >> VPCs, Subnets and Compute >> Virtual server instances to view all the resources you have created.

IBM Cloud Resources

Terraform State

You may have noticed that every time you run terraform apply there is a new or updated file named terraform.tfstate. This file is the Terraform State file and it store information about the infrastructure created. All the resources and variables are in that file.

This file is in JSON format and it has a private API, it changes (or may change) every time there is a new Terraform version, so you can read it but it’s not recommended to use it to get data from it. Instead, use the output variables and the output Terraform command.

Every time you run Terraform it fetch the latest status of every resource in the code and compare it with the state from the terraform.tfstate file to determine what changes need to be applied.

For a simple and personal project is fine to have the Terraform state in a local file. Sometimes it’s stored in a version control system (i.e. GitHub) however this is not recommended because the state file contain sensitive information that should not be exposed.

For enterprise projects or if a team is working with the same Terraform code make it’s recommended to use a remote state in a shared storage. The most common options to implement remote state is to use a Terraform Backend or use a service such as Terraform Enterprise, Terraform Cloud or IBM Cloud Schematics.

The setup and use of Terraform backends is explained in the Setup Terraform Remote State using etcd as backend section. The use of IBM Cloud Schematics is explained in the next section IBM Cloud Schematics

Exposing the service to the world in a secure manner

Now the instance is running and possibly it’s serving a web page with Hello World but unfortunately you can’t see it. You can’t see it because (1) the IP address is private, internal to the IBM Cloud network and (2) there aren’t any firewall rules to allow access to the instance. Resolve that by adding a ibm_is_floating_ip, a ibm_is_security_group_rule and a few ibm_is_security_group resources:

resource "ibm_is_floating_ip" "iac_test_floating_ip" {
name = "terraform-test-ip"
target = ibm_is_instance.iac_test_instance.primary_network_interface.0.id
tags = [ "iac-terraform-test" ]
}
resource "ibm_is_security_group" "iac_test_security_group" {
name = "terraform-test-sg-public"
vpc = ibm_is_vpc.iac_test_vpc.id

Then add the new security group to the primary_network_interface of the instance, so it looks like so:

resource "ibm_is_instance" "iac_test_instance" {
...
primary_network_interface {
name = "eth1"
subnet = ibm_is_subnet.iac_test_subnet.id
security_groups = [ ibm_is_security_group.iac_test_security_group.id ]
}
...
}

The ibm_is_floating_ip resource creates a floating IP address that can be used to access the targeted instance from the public network.

The main input parameters are:

Input parameterDescription
nameto name the floating IP address
resource_groupthe ID of the resource group for the floating IP, the default resource group is default
tagstags to associate with your floating IPs, they will help you to find them more easily. Separate multiple tags with a comma

The most important output parameter is:

Output parameterDescription
addressthe floating IP address

The ibm_is_security_group resource allows you to create a virtual firewall with rules to control the inbound and outbound traffic. These rules are created with the resource ibm_is_security_group_rule.

The most important input parameters for ibm_is_security_group are:

Input parameterDescription
nameto name the Security Group
vpcthe VPC ID for the Security Group. Use the id output parameter of the selected ibm_is_vpc resource.
resource_groupthe ID of the resource group to create the Security Group there

After it’s created you can access all the rules in the security group with the following output parameters:

Output parameterDescription
idthe Security Group ID
rulesa nested block describing the rules of this security group
rules.directionthe direction of the traffic either inbound or outbound
rules.protocolthe type of the protocol all, icmp, tcp or udp
rules.port_maxthe inclusive upper bound of TCP/UDP port range
rules.port_minthe inclusive lower bound of TCP/UDP port range

The primary_network_interface of the instance and each ibm_is_security_group_rule will reference the new security group using the id output parameter.

Each ibm_is_security_group_rule defines a traffic rule either for inbound or outbound direction. The most important input parameters are:

Input parameterDescription
groupthe Security Group ID
directiondirection of the traffic either inbound or outbound
tcpa nested block describing the tcp protocol of this security group rule
tcp.port_minthe inclusive lower bound of TCP port range
tcp.port_maxthe inclusive upper bound of TCP port range
udpa nested block describing the udp protocol of this security group rule
udp.port_minthe inclusive lower bound of UDP port range
udp.port_maxthe inclusive upper bound of UDP port range

In addition to the protocols tcp and udp security group rules recognize icmp and ALL.

Having now a public IP address and the port 8080 open to inbound traffic to the instance, we can access the Web service. Change the ip_address output variable to:

output "ip_address" {
value = ibm_is_floating_ip.iac_test_floating_ip.address
}

Apply the changes again and execute the following line to view the output of the published web page:

$ curl "http://$(terraform output ip_address):8080"
Hello World

Deploy a configurable server

The web server is serving on port 8080 but this port has to be set in the security group and the user data configuration. If we decide to change the port in the user data it may be possible to forget to change it in the other security group or vice versa.

To make this code more DRY and configurable let’s define an input variable for the port, like so:

variable "port" {
default = 8080
}

We can provide the value of this variable in the following ways, where the earlier option takes precedence over the later:

  1. With the -var command line option of terraform
  2. In variable definitions files (.tfvars) such as terraform.tfvars
  3. As environment variables starting with TF_VAR_
  4. Default value in the variable definition

So, if we want a port other than 8080 just use the parameter -var:

terraform apply -var="port=8081"

To use the variable in the code, just replace the 8080 by var.port or, in case we want to interpolate the variable in a string, we use ${var.port}. So the changes in the code look like this:

resource "ibm_is_instance" "iac_test_instance" {
...
user_data = <<-EOUD
#!/bin/bash
echo "Hello World" > index.html
nohup busybox httpd -f -p ${var.port} &
EOUD
...
}

Now, execute terraform apply using the port 8081. Notice that this will re-create the instance and update the other resources.

terraform apply -var="port=8081"
curl "http://$(terraform output ip_address):8081"

Alternatively, using environment variables would look like this:

export TF_VAR_port=8082
terraform apply
curl "http://$(terraform output ip_address):${TF_VAR_port}"

SSH Access

We accomplished the initial requirements having the Web service running and printing the “Hello World” but what if we want to have SSH access to the instance?

Open the main.tf file to add the resources ibm_is_ssh_key and ibm_is_security_group_rule to open port 22 to allow SSH access to the instance.

variable "public_key_file" { default = "~/.ssh/id_rsa.pub" }
locals {
public_key = "${file(pathexpand(var.public_key_file))}"
}
resource "ibm_is_security_group_rule" "iac_test_security_group_rule_tcp_ssh" {
group = ibm_is_security_group.iac_test_security_group.id
direction = "inbound"
tcp {

This SSH Key has to be linked to the instance through the keys parameter, so replace the keys = [] parameter in the instance iac_test_instance for:

keys = [ ibm_is_ssh_key.iac_test_key.id ]

The resource ibm_is_ssh_key creates a SSH key to access a Gen 2 instance. It requires the following input parameters:

Input parameterDescription
namethe name of the key
public_keythe content of the public key
resource_groupan optional ID of the resource group for the key
tagsoptional tags to associate with your key, they will help you to find it more easily. Separate multiple tags with a comma

Instead of passing to Terraform the content of the public key, this code takes the public key filename in the variable public_key_file either as a parameter -var="public_key_file=FILENAME" or as an environment variable TF_VAR_public_key_file=FILENAME and places the file content into the local variable public_key which will be used by the IBM Cloud provider to create a SSH Key resource. If no parameter is given for the public key filename, the default value is set to the well known default filename for RSA public keys: ~/.ssh/id_rsa.pub.

After applying the changes with terraform apply you can access to the instance with SSH either to login or execute remote commands. For example, use the following command to remotely execute echo 'Hello World' using the private key ~/.ssh/id_rsa

$ ssh -i ~/.ssh/id_rsa ubuntu@$(terraform output ip_address) "echo 'Hello World'"
Hello World

Final Terraform code

All the developed code is in a main.tf file, however to have a more organized project, you can split the Terraform code in different files. This project is simple and small but as your Terraform code grows it is a common pattern to split the code into directories and Terraform Modules. This is something you’ll see in the next section: Getting started with Schematics

This code is also available in the Getting Started folder of the IaC Pattern Guides GitHub repository.

Here is an example of how to split the terraform code of this “Hello World” example into a logical structure based on the types of resources being managed.

main.tf

This file is the first file that Terraform will access, so include here the provider and the instance.

main.tf
provider "ibm" {
generation = 2
region = "us-south"
}
resource "ibm_is_ssh_key" "iac_test_key" {
name = "terraform-test-key"
public_key = local.public_key
}

variables.tf and output.tf

These 2 files will have all the input and output variables:

variables.tf
variable "public_key_file" { default = "~/.ssh/id_rsa.pub" }
locals {
public_key = "${file(pathexpand(var.public_key_file))}"
}
output.tf
output "ip_address" {
value = ibm_is_floating_ip.iac_test_floating_ip.address
}
output "entrypoint" {
value = "http://${ibm_is_floating_ip.iac_test_floating_ip.address}:${var.port}/"
}

network.tf

All the networking resources will be stored in this file:

network.tf
resource "ibm_is_vpc" "iac_test_vpc" {
name = "terraform-test-vpc"
}
resource "ibm_is_subnet" "iac_test_subnet" {
name = "terraform-test-subnet"
vpc = ibm_is_vpc.iac_test_vpc.id
zone = "us-south-1"
ipv4_cidr_block = "10.240.0.0/24"

Other files

Besides the Terraform files you will need the following files:

  • the SSH keys (default are ~/.ssh/id_rsa and ~/.ssh/id_rsa.pub).
  • the terraform_key.json created to store the IBM Cloud API Key
  • the terraform.tfstate and terraform.tfstate.backup with the Terraform State

Optionally you can also have a terraform.tfvars to set values to the input variables, like so:

terraform.tfvars
port = 8081

All these files contain or may contain sensitive information and should not be stored in the GitHub repository. Make sure there is a .gitignore file with the following content:

.gitignore
terraform_key*.json
.terraform
*.tfstate*
id_rsa*
*.tfvars*

Clean up

After spending a few minutes admiring your masterpiece and showing it to your friends, you can destroy everything to save money with the command:

terraform destroy

After the command completes, if you check the terraform.tfstate file, it’s almost empty, showing no information about the resources.

Reference