Dev to Deploy; Cloud Native Stack Part-2

This is a follow up to the earlier post Dev to Deploy; Cloud Native Stack Part-1 . In this post I will walk through on how to provision on Hetzner a server in cloud using Terraform and have a preconfigured snapshot built using Packer to have it ready with your configuration.

Sign up for an account on Hetzner if you don’t have one and then setup a project on it. I have my project UltronEx on it. Once you have the project created add your public SSH in the access section. After that from with in the access section go to API TOKEN and generate a token, make sure you copy it and save it as it won’t be retrievable and you will need to generate new one in case of losing it.

After you get a token from you Hetzner account store it in a environment variable under HCLOUD_TOKEN .

Packer

{
"variables": {
"hcloud_token": "{{env `HCLOUD_TOKEN`}}"
},
"builders": [
{
"token": "{{ user `hcloud_token` }}",
"server_name": "base-packer",
"snapshot_name": "debian-base-snapshot-{{timestamp}}",
"snapshot_labels": { "name": "debian-base-snapshot-{{timestamp}}" },
"type": "hcloud",
"image": "debian-10",
"location": "nbg1",
"server_type": "cx11",
"ssh_username": "root"
}
],
"provisioners": [
{
"type": "shell",
"inline": [
"sleep 30",
"apt-get update",
"apt-get -y upgrade",
"apt-get update && apt-get install -y wget curl gcc make python python-dev python-setuptools python-pip libffi-dev libssl-dev libyaml-dev"
]
},
{
"type": "ansible",
"extra_arguments": ["--vault-password-file=~/.helsing_ansible_vault_pass"],
"playbook_file": "../../../../ansible/base.yml"
}
]

Packer script has 3 sections variables , builders , provisioners . The variables section is where you would define which variable holds the API TOKEN you had generated earlier.

"variables": {
"hcloud_token": "{{env `HCLOUD_TOKEN`}}"
}

In the builders section you define the base OS you would use and the server instance this image is going to be for. You can check the Hetzner docs to find the list of different OS and server types. Over here I am using the CX11 server type and building a Debian 10 base snapshot.

"builders": [
{
"token": "{{ user `hcloud_token` }}",
"server_name": "base-packer",
"snapshot_name": "debian-base-snapshot-{{timestamp}}",
"snapshot_labels": { "name": "debian-base-snapshot-{{timestamp}}" },
"type": "hcloud",
"image": "debian-10",
"location": "nbg1",
"server_type": "cx11",
"ssh_username": "root"
}
]

The final phase is the provisioners and you can see its an array which means it can take multiple provisioner types. The most common is shell which is mostly used to run some base commands before you use another provisioner to install required software and configuration. Its not required to use multiple proviosiner you can use only the shell provisioner and run all your commands and be done with it.

"provisioners": [
{
"type": "shell",
"inline": [
"sleep 30",
"apt-get update",
"apt-get -y upgrade",
"apt-get update && apt-get install -y wget curl gcc make python python-dev python-setuptools python-pip libffi-dev libssl-dev libyaml-dev"
]
},
{
"type": "ansible",
"extra_arguments": ["--vault-password-file=~/.helsing_ansible_vault_pass"],
"playbook_file": "../../../../ansible/base.yml"
}
]

Once you have your Packer script in a file called packer.json you can validate it using

packer validate packjer.json

and then build your snapshot using

packer build packer.json

Now that you have a base snapshot for yourself you can build custom snapshots that can be used to launch immutable servers. For UltronEx I have a packer.json file as such

{
"variables": {
"hcloud_token": "{{env `HCLOUD_TOKEN`}}"
},
"builders": [
{
"token": "{{ user `hcloud_token` }}",
"server_name": "ultronex-packer",
"snapshot_name": "debian-ultronex-snapshot-{{timestamp}}",
"type": "hcloud",
"image_filter": {
"with_selector": [
"name==debian-base-snapshot-1581565454"
],
"most_recent": true
},
"snapshot_labels": { "name": "debian-ultronex-snapshot-{{timestamp}}" },
"location": "nbg1",
"server_type": "cx11",
"ssh_username": "root"
}
],
"provisioners": [
{
"type": "shell",
"inline": [
"sleep 30",
"apt-get update",
"apt-get -y upgrade",
"apt-get update && apt-get install -y wget curl gcc make python python-dev python-setuptools python-pip libffi-dev libssl-dev libyaml-dev"
]
},
{
"type": "ansible",
"extra_arguments": ["--vault-password-file=~/.helsing_ansible_vault_pass"],
"playbook_file": "../../../../ansible/ultronex.yml"
}
]
}

If you notice both the scripts are almost identical with only one difference, this time I am not using the image but rather have image_filter to select the base snapshot I created earlier

"image_filter": {
"with_selector": [
"name==debian-base-snapshot-1581565454"
],
"most_recent": true
}

Take note that the name label should match the name of the snapshot name which would be now visible in your Hetzner console under snapshots in your project.

After this follow the validate and build steps to generate your immutable snapshot to be used with Terraform to launch a server.

Terraform

variable "HCLOUD_TOKEN" {}

provider "hcloud" {
token = var.HCLOUD_TOKEN
}

data "hcloud_image" "ultronex_image" {
with_selector = "name=debian-ultronex-snapshot-1581566034"
}

data "hcloud_ssh_keys" "all_keys" {
}

resource "hcloud_server" "ultronex_server" {
name = "ultronex-hetzner"
image = data.hcloud_image.ultronex_image.id
server_type = "cx11"
labels = { "name" = "ultronex-hetzner" }
location = "nbg1"
ssh_keys = data.hcloud_ssh_keys.all_keys.ssh_keys.*.name
}

There are some important sections to know the variable , provider , data and resource .

The variable is how you indicate where the API TOKEN will be used from in the code. provider is quite self explanatory indicating which service provider you are trying to provision. The data is where you define the values of what is used so they are more like place holders. The final and the most important is the resource this is where you define the server you are trying to create.

The data section is where you will define the name of your snapshot you want to use to build this server from, it can be the base or the specific snapshot you have in your library

data "hcloud_image" "ultronex_image" {
with_selector = "name=debian-ultronex-snapshot-1581566034"
}

In the resource block few things to be careful about is to make sure your server_type is the same as the one used to build the snapshot and the location is the one you want. Hetzner has 3 data centers, 2 in Germany and 1 in Finland.

resource "hcloud_server" "ultronex_server" {
name = "ultronex-hetzner"
image = data.hcloud_image.ultronex_image.id
server_type = "cx11"
labels = { "name" = "ultronex-hetzner" }
location = "nbg1"
ssh_keys = data.hcloud_ssh_keys.all_keys.ssh_keys.*.name
}

This is a very important piece of line

ssh_keys  = data.hcloud_ssh_keys.all_keys.ssh_keys.*.name

This is what will allow you to be able to access your servers from your localhost or the host of your choosing. This copies the public keys from your project that you had added earlier in the access section of your project.

Once you have it, run the following commands in the directory and you should have a server in the cloud

terraform initterraform plan -out plan.outterraform apply plan.out

I hope you find this post helpful. I have not delved into details of how to install Packer , Terraform as they are beyond the focus of this post and there are plenty of resources around them and they are well documented tools.

Image for post
Image for post

Coder during the day, squash player in the evening and cricketer over the weekends. Doubts are the ants in the pants, that keep faith moving

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store