Over the past couple of months, I’ve been falling in love with Terraform. For those of you who haven’t used it, it allows you to keep a description of your infrastructure in a source repository and build out environments in a rapid, simple manner using various cloud computing platforms.
Up until recently, I was running Terraform exclusively against AWS and the results were astounding – 34 objects (Security Groups, Instances, Load Balancers, Virtual Private Clouds etc) build in under two minutes. It’s now got to the point where I can stand up an multi-node ELK stack in AWS in under 30 minutes.
When I was recently set the target of replicating that stack in Microsoft Azure, I thought “awesome, this will be easy!”. The fact that I am even writing this blog post should tell you that it wasn’t quite as straight forward as I thought it would be however here is my experience for you all to laugh at – after all, if you learn from other people’s mistakes, it’s cheaper…
The differences between AWS and Azure
The first and major difference is that what AWS calls a “Virtual Private Cloud” is known as a “Virtual Network” inside Azure as far as I can tell. You create a Virtual Network, divide that into Subnets and then assign Hosted Services to those Subnets. Instances are then assigned to Hosted Services – one Instance per Service. This strikes me as a little strange (why not just assign the instances to a subnet?) however this is probably a misunderstanding on my part.
The second difference that I found between the two environments is that you appear to only be able to upload SSH Keys via the web interface at the point that you create an instance. Not such an issue with Windows instances, however with Linux instances this is a real pain as it means SSH Keys are effectively tied to each machine unlike AWS where SSH Keys can be used across multiple instances. There is also an issue in that Terraform at present does not appear to be able to upload keys to Azure in the same way that it does for AWS, leaving us with password-based access for all our nodes stored in plain text in terraform.tfstate.
The third and final difference that I’ll concentrate on in this article is how you access your instances in Azure vs AWS. My standard approach is to build a Bastion Host and this approach still works, you just need to be careful how you configure it.
Solving the problems
As far as the terminology differences are concerned, Mani Chandrasekaran has written a fantastic post on the subject which helped me work out where I was going wrong. The Terraform for setting up the Azure equivalent of a VPC is as follows:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
resource "azure_hosted_service" "azure_test_nat" { | |
name = "azure_test_nat" | |
location = "North Europe" | |
ephemeral_contents = false | |
description = "Nat Gateway Hosted service created by Terraform." | |
label = "azure_test_nat" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
resource "azure_instance" "nat" { | |
name = "${azure_virtual_network.azure_test.id}-nat" | |
hosted_service_name = "${azure_hosted_service.azure_test_nat.name}" | |
image = "Ubuntu Server 14.04 LTS" | |
size = "Basic_A1" | |
storage_service_name = "${azure_storage_service.azure_test_storage.name}" | |
location = "North Europe" | |
virtual_network = "${azure_virtual_network.azure_test.id}" | |
subnet = "public" | |
username = "terraform" | |
password = "${var.ssh_user_password}" | |
security_group = "${azure_security_group.public_ssh.name}" | |
endpoint { | |
name = "SSH" | |
protocol = "tcp" | |
public_port = 22 | |
private_port = 22 | |
} | |
connection { | |
user = "terraform" | |
password = "${var.ssh_user_password}" | |
} | |
provisioner "remote-exec" { | |
inline = [ | |
"sudo iptables -t nat -A POSTROUTING -j MASQUERADE", | |
"echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward > /dev/null", | |
] | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Configure the Azure Provider | |
provider "azure" { | |
settings_file = "${var.azure_settings_file}" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
resource "azure_security_group" "public_ssh" { | |
name = "public_ssh" | |
location = "North Europe" | |
} | |
resource "azure_security_group" "private_ssh" { | |
name = "private_ssh" | |
location = "North Europe" | |
} | |
resource "azure_security_group_rule" "public_ssh_access" { | |
name = "ssh-access-rule" | |
security_group_names = ["${azure_security_group.public_ssh.name}"] | |
type = "Inbound" | |
action = "Allow" | |
priority = 200 | |
source_address_prefix = "*" | |
source_port_range = "*" | |
destination_address_prefix = "10.128.2.0/24" | |
destination_port_range = "22" | |
protocol = "TCP" | |
} | |
resource "azure_security_group_rule" "private_ssh_access" { | |
name = "private_ssh-access-rule" | |
security_group_names = ["${azure_security_group.private_ssh.name}"] | |
type = "Inbound" | |
action = "Allow" | |
priority = 200 | |
source_address_prefix = "10.128.2.0/24" | |
source_port_range = "*" | |
destination_address_prefix = "10.128.1.0/24" | |
destination_port_range = "22" | |
protocol = "TCP" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
resource "azure_storage_service" "azure_test_storage" { | |
name = "azure_test_storage" | |
location = "North Europe" | |
description = "Made by Terraform." | |
account_type = "Standard_LRS" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
variable "azure_settings_file" { | |
description = "The settings file available from https://manage.windowsazure.com/publishsettings" | |
} | |
variable "ssh_user_password" { | |
description = "The password for the SSH User" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
resource "azure_virtual_network" "azure_test" { | |
name = "azure_test" | |
address_space = ["10.128.0.0/16"] | |
location = "North Europe" | |
subnet { | |
name = "private" | |
address_prefix = "10.128.1.0/24" | |
} | |
subnet { | |
name = "public" | |
address_prefix = "10.128.2.0/24" | |
} | |
} |
The above provisions a single instance into a security group inside a subnet nested in a Virtual Network.
It uses a variable (ssh_user_password) to set the password of the instance so you can log in (solving the issue of SSH Key Distribution as best we can at the current time), giving you the option to either specify that at run time or export it to the environment variable “TF_VAR_ssh_user_password” before running terraform apply.
If you don’t already know, Terraform has the ability to create a directed dependency graph of your infrastructure so you can visualise the system before you apply it. The output of the above (with TF_VAR_azure_settings_file and TF_VAR_ssh_user_password set to an appropriate value) is as follows:
Now that we know that all the components do what we want and we can see how they all link together, we can look at adding an additional instance that we can SSH to via the Bastion Host:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
resource "azure_hosted_service" "azure_test_web" { | |
name = "azure_test_web" | |
location = "North Europe" | |
ephemeral_contents = false | |
description = "Consul Hosted service created by Terraform." | |
label = "azure_test_web" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
resource "azure_instance" "azure_test_web" { | |
name = "${azure_virtual_network.azure_test.id}-web" | |
hosted_service_name = "${azure_hosted_service.azure_test_web.name}" | |
image = "Ubuntu Server 14.04 LTS" | |
size = "Standard_D3" | |
storage_service_name = "${azure_storage_service.azure_test_storage.name}" | |
location = "North Europe" | |
virtual_network = "${azure_virtual_network.azure_test.id}" | |
subnet = "private" | |
username = "terraform" | |
password = "${var.ssh_user_password}" | |
endpoint { | |
name = "HTTP" | |
protocol = "tcp" | |
public_port = 80 | |
private_port = 80 | |
} | |
} |
Again, we’re using the variables to set the instance password and we’re passing around “internal” variables to associate things correctly, however our graph now looks like this:
And we can only SSH to the web instance via the jump box. Port 80 for the web instance is still available via the Hosted Service however running nmap against the system shows that the only port exposed is port 80.
Things currently missing from Terraform for Microsoft Azure
There are a few things missing from Terraform, however this is to be expected given that the Azure provider is very new. The ones that stand out the most are as follows:
- Ability to upload and assign SSH keys as part of an instance definition – the API allows this and the SDK appears to however Terraform does not at this time
- Management of traffic-master load balancers etc. – This is in progress and relies on the upstream SDK merging changes
Other than that, at the moment I’ve not found Terraform lacking when it comes to Azure and being able to build out my infrastructure in Azure using the same tools I use for AWS is awesome 🙂
If you’ve solved any of the problems that I’ve found above, please let me know in the comments below!
Reblogged this on Automate thinkable.
LikeLike
It looks like you are provisioning (older) ‘Classic’ resources via the Service Management APIs rather than the newer (v2) resources. Azure Resource Manager (ARM) deployment templates are typically the entry point to these, which offers a source-controllable (JSON) payload and a reliable and incremental way of building out environments whilst dealing with sorting out dependencies. This will rely on a completely new SDK rather than just a merging of changes (it’s a completely different technology, authentication). There’s a published REST API though, this is what I’m using for my current work with Chef Provisioning. Would be happy to chat about them sometime if it helps.
Stuart
LikeLike
Thanks Stuart, that’s good to know.
Terraform uses the Azure SDK for Go under the hood ( https://github.com/Azure/azure-sdk-for-go/ ) so at the moment I’m limited to whatever mechanism that provides.
The primary reason for using Terraform to do this is because we are already using Terraform to launch instances in AWS meaning that we don’t need to cross-train our engineers in a number of tools.
The Azure provider for Terraform is still in its infancy so hopefully there will be a refactor at some point to use the ARM.
Cheers,
Matt
LikeLike
We solved the issue of the SSH key by using the azure cli with the local-exec provisioner as here: https://github.com/alphagov/cf-terraform/commit/71b18939ee0d49ac78f06e56b7d5552b5d94a806
Actually we are solving a lot of missing features in terraform provider using the azure client, and we will write some feature requests soon.
LikeLike
I have put together some example files for using Terraform with Azure RM.
https://superautomation.blogspot.co.uk/2016/11/terraform-with-azure-resource-manager.html
LikeLike
I created some example files for using Terraform with Azure Resource Manager here:
https://superautomation.blogspot.co.uk/2016/11/terraform-with-azure-resource-manager.html
LikeLike
Hi Dave, do you have something similar in Linux to copy SSH keys after Azure VM’s are provisioned using terraform?
LikeLike