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
Now you are ready to follow Packer guide for Hetzner to create a base
snapshot that you can use to build different kind of snapshots to launch servers. Below is Packer code that allows you to build a snapshot. We will go through each section.
{
"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
The Terraform for Hetzner is pretty simple. Hetzner doesn’t have overly complicated requirements to setup a single server and can be done with very minimal code. Below is the code for it
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.