Jeff Dickman - Blog There's something to this...

Terraform OpenVPN Marketplace AMI and Multiple VPCs

Recently, I was working on configuring an OpenVPN server that existing in a services VPC. The services that VPN clients needed to access were located in a peered VPC. For what it’s worth, the OpenVPN AMI in the AWS Marketplace only configures its internal routing permissions for the VPC CIDR block that the instance comes online for. You can manually configure additonal CIDR blocks via the web console, but ugh… every time a new OpenVPN instance is set up… tedius and prone to issues. Automation is the way to go here…

First of all, OpenVPN has a pretty robust method for configuration via CLI commands. So we’ll leveral that along with user-data to do the configuration.

The binary, sacli, you can use to configure OpenVPN is located in the /usr/local/openvpn_as/scripts/ directory. Depending on your OS flavor, it may need to be run via sudo. In the case of the marketplace AMI, it is running on Ubuntu, so I’ll be using sudo.

The command we need to run is:

./sacli --key "vpn.server.routing.private_network.0" --value "X.X.X.X/20" ConfigPut

Where X.X.X.X is the CIDR block we’re trying to add. It’s worth noting that “vpn.server.routing.private_network.0” is actually an array. So, the following would hold true here:

vpn.server.routing.private_network.0 = VPC CIDR - we don't want to change this.
vpn.server.routing.private_network.1 = Open, we can use this.
vpn.server.routing.private_network.2 = Open, we can use this.
vpn.server.routing.private_network.3 = Open, we can use this.

We can keep adding the numbers as we need to.

Using Terraform, how can we use this command to reconfigure OpenVPN via the user-data? For the purposes of other automation I’m running, I have a variable called env_cidr_blocks already created. This variable is a flat list, that has each of the CIDR blocks in quotes. This should be what I need to build the command.

env_cidr_blocks=["10.20.11.0/20","10.20.12.0/20","10.20.13.0/20\","10.20.14.0/20"]

Putting together the CIDR list into the user-data is pretty straight forward. We can use string formatlist interpolation within terraform to create the necessary values. However, the challenge here is that the formatlist interpolation syntax does not give us the ability to get an index count that we can use to increment the array as well.

I spent quite a bit of time trying to figure out how to tease an index out of the formatlist command. String interpolation should have a way to do this…truly the command itself should have an option to give an index count, but alas Google searching says I’m not the only person to have hit on this issue.

The solution is suprisingly simple. Instead of using the formatlist command to get an index count, we’re going to use a couple of template_files working together.

By using an inline template, we can pass the command line we need once for each element in the list. We use the count variable to generate multiple templates - one for each item in the list.

data "template_file" "env_cidr_blocks" {
  count    = "${length(var.env_cidr_blocks)}"
  template = "sudo ./sacli --key 'vpn.server.routing.private_network.$${cidr_count}' --value '$${cidr_block}' ConfigPut"
  vars {
    cidr_block   = "${element(var.env_cidr_blocks, count.index)}"
    cidr_count   = "${count.index + 1}"
  }
}

Adding the +1 to the cidr_count variable allows us to retain the default VPC_CIDR block value, which is which is in the position vpn.server.routing.private_network.0.

Once we have those templates created, we can then put them together into our user-data template

data "template_file" "user_data" {
  template = "${file("${path.module}/templates/user-data.sh")}"
  vars {
    env_cidr_blocks   = "${join("\n", data.template_file.env_cidr_blocks.*.rendered)}"
  }
}

Pretty straight forward from here. You would then have a variable that contains each line of the commands you need run. You then just insert that variable into your user-data template as a **${env_cidr_blocks}.

It’s a fairly cool alternative that allows you to get an index count or range of a flat list without using the Terraform formatlist command.

References:

OpenVPN Command Line Documentation, Terraform String Interpolation - formatlist, Terraform template_file documentation