Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
main.tf file
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
(#1 of 6) |
Lecture notes |
Pdf slides |
|
(#2 of 6) |
Lecture notes |
Pdf slides |
|
(#3 of 6) |
Lecture notes |
Pdf slides |
|
(#4 of 6) |
Lecture notes |
Pdf slides |
|
(#5 of 6) |
Lecture notes |
Pdf slides |
|
(#6 of 6) |
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
*.id.
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
env.template secrets file
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Conf/config.tf.template configuration
file
Lecture notes |
Pdf slides |
|
.gitignore file
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
user_data
attribute
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
known_hosts entry on
re-creating server
Lecture notes |
Pdf slides |
|
known_hosts ...
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Tpl/hostdata.json and result
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
(#1 of 4) |
Lecture notes |
Pdf slides |
|
(#2 of 4) |
Lecture notes |
Pdf slides |
|
(#3 of 4) |
Lecture notes |
Pdf slides |
|
(#4 of 4) |
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
ns1.hdm-stuttgart.cloudLecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
dns_challenge provider
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
(#1 of 5) |
Lecture notes |
Pdf slides |
|
(#2 of 5) |
Lecture notes |
Pdf slides |
|
(#3 of 5) |
Lecture notes |
Pdf slides |
|
(#4 of 5) |
Lecture notes |
Pdf slides |
|
(#5 of 5) |
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
(#1 of 2) |
Lecture notes |
Pdf slides |
|
(#2 of 2) |
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
Lecture notes |
Pdf slides |
|
“Terraform is an infrastructure as code tool that lets you build, change, and version cloud and on-prem resources safely and efficiently.”
| Declarative schema | Physical result |
|---|---|
|
|
| Declarative schema | Physical result |
|---|---|
|
Creating IPs, networks, servers, message queues, ... |
Access you cloud project by the Hetzner GUI interface.
Go to Security --> API
Tokens --> Generate API token
Provide a name and hit “Generate API token”.
Copy the generated token's value and store it in a secure location e.g. a password manager.
The Hetzner GUI blocks subsequent read access after creation.
In case of loss: Delete the existing and generate a new one.
# Define Hetzner cloud provider
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
}
}
required_version = ">= 0.13"
}
# Configure the Hetzner Cloud API token
provider "hcloud" {
token = "your_api_token_goes_here"
}
# Create a server
resource "hcloud_server" "mySrv" {
name = "hello"
image = "debian-13"
server_type = "cx23"
}$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding latest version of hetznercloud/hcloud...
- Installing hetznercloud/hcloud v1.46.1...
- Installed hetznercloud/hcloud v1.46.1 (signed by a HashiCorp partner, key ID 5219EACB3A77198B)
...
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. ...$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions ...
+ create
Terraform will perform the following actions:
# hcloud_server.mySrv will be created
+ resource "hcloud_server" "mySrv" {
+ allow_deprecated_images = false
+ backup_window = (known after apply)
...
}
Plan: 1 to add, 0 to change, 0 to destroy.terraform apply ... Plan: 1 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes hcloud_server.mySrv: Creating... hcloud_server.mySrv: Still creating... [10s elapsed] hcloud_server.mySrv: Creation complete after 14s [id=45822789] Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
| Resource list | hcloud_server.mySrv's resource properties |
|---|---|
$ terraform state list hcloud_firewall.allowSsh hcloud_server.mySrv hcloud_ssh_key.authorizedUsers["joe"] hcloud_ssh_key.authorizedUsers["eve"] |
$ terraform state show hcloud_server.mySrv resource "hcloud_server" "mySrv" { firewall_ids = [11889577,] id = "166290307" image = "debian-13" ipv4_address = "2.29.10.236" ipv6_address = "2a01:4f9:c015:5ae7::1" ipv6_network = "2a01:4f9:c015:5ae7::/64" location = "hel1" name = "hello" primary_disk_size = 40 server_type = "cx23" ... } |
Your server "hello" was created! You can access your server with the following credentials: IPv4 128.140.108.60 IPv6 2a01:4f8:1c1c:8e3a::/64 User root Password rJ3pNvJXbqMp3XNTvFdq You will be prompted to change your password on your first login. To improve security, we recommend that you add an SSH key when creating a server.
No protection by any firewall
IP and (initial) credentials by email 😱
Solution:
# main.tf resource "hcloud_firewall" "sshFw" { name = "ssh-firewall" rule { description = "Allow incoming ssh connections" direction = "in" protocol = "tcp" port = "22" source_ips = ["0.0.0.0/0", "::/0"] } } ... resource "hcloud_server" "mySrv" { ... firewall_ids = [hcloud_firewall.sshFw.id] }
# main.tf resource "hcloud_ssh_key" "loginUserKey" { name = "goik@hdm-stuttgart.de" public_key = file("~/.ssh/id_ed25519.pub") } ... resource "hcloud_server" "mySrv" { ... ssh_keys = [hcloud_ssh_key.loginUserKey.id] }
Note: Use the Hetzner Web GUI for removing any conflicting manually installed ssh keys beforehand.
$ terraform apply # hcloud_firewall.sshFw will be created + resource "hcloud_firewall" "sshFw" { ... # hcloud_server.mySrv will be created + resource "hcloud_server" "mySrv" { ... # hcloud_ssh_key.goik will be created + resource "hcloud_ssh_key" "loginUserKey" { ... Plan: 3 to add, 0 to change, 0 to destroy. ... Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
resource "hcloud_firewall" "sshFw" {...} ... resource "hcloud_server" "mySrv" { ... firewall_ids = [hcloud_firewall.sshFw.id] }
Terraform cannot pass resource
instances: Only
string, number, bool,
list, set and map are
allowed.
Passing hcloud_firewall instance reference by its resource identity id value:
terraform apply ... hcloud_firewall.sshFw: Creation complete after 0s [id=11941059] ...
Resource id is persistent over life cycle
e.g. invoking terraform
apply.
State persistence by terraform.tfstate
file:
"resources": [
{ "type": "hcloud_firewall",
"name": "sshFw",
"provider": "provider[\"registry.terraform.io/hetznercloud/hcloud\"]",
"instances": [
{"schema_version": 0,
"attributes": {
"id": "11941059",
...See terraform output documentation:
# outputs.tf output "helloIpAddress" { value = hcloud_server.mySrv.ipv4_address description = "The server's IPv4 address" } output "helloLocation" { value = hcloud_server.mySrv.location description = "The server's datacenter location" } |
$ terraform output helloLocation = "nbg1" helloIpAddress = "159.69.152.37" |
$ terraform output helloIpAddress "159.69.152.37" |
# outputs.tf output "helloIpAddress" { value = hcloud_server.mySrv.ipv4_address description = "The server's IPv4 address" } output "helloLocation" { value = hcloud_server.mySrv.location description = "The server's datacenter location" } |
terraform
{
"helloLocation": {
"sensitive": false,
"type": "string",
"value": "nbg1"
},
"helloIpAddress": {
"sensitive": false,
"type": "string",
"value": "159.69.152.37"
}
} |
# config.auto.tfvars # Overriding default value "hello" serverName = "testServer" # variables.tf variable "serverName" { description = "Project unique server name" type = string nullable = false default = "hello" } |
# main.tf resource "hcloud_server" "mySrv" { name = var.serverName ... } terraform apply ... serverProps = { "ip" = "46.62.254.219" "name" = "testServer" } |
For security reasons, secrets should not be under version control:
# provider.tf
provider "hcloud" { token = "xdaGfz9LmwO8SWkg ... "}
...# variables.tf variable "hcloud_token" { description = "Hetzner API token" type = string nullable = false sensitive = true } # .env file # Setting environment variable export TF_VAR_hcloud_token="utRw..." |
# main.tf resource "hcloud_server" "mySrv" { name = var.serverName ... } . .env # Source environment file echo $TF_VAR_hcloud_token utRw... terraform apply ... |
# main.tf
resource "hcloud_server" "mySrv" {
count = 2
name = "www-${count.index}"
...
}# outputs.tf
output "servers" {
value = join(", ",
[
for srv in hcloud_server.mySrv :
"(${srv.name}|${srv.ipv4_address})"
])
} |
terraform apply ... Plan: 5 to add, 0 to change, 0 to destroy. Outputs: servers = "(www-0|46.62.254.219), (www-1|204.168.245.188)" |
# config.auto.tfvars
ipByName = { # key/value pairs
mail = "192.178.1.3"
database = "192.168.85.9"
} |
#main.tf
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "2.7.0"
}
}
}
resource "local_file" "serverConfigs" {
for_each = var.ipByName
filename = "Generate/${each.key}.cfg"
content = "ipv4=${each.value}"
file_permission = 0744
} |
> terraform apply ... local_file.serverConfigs["mail"]: Creation complete ... local_file.serverConfigs["database"]: Creation complete ... Apply complete! Resources: 2 added, 0 changed, 0 destroyed. > ls Generate database.cfg mail.cfg cat Generate/database.cfg ipv4=192.168.85.9
...
├── .gitignore
├── env.template
├── Conf
│ └── config.tf.template
├── 030HelloSshPublicKey
│ ├── main.tf
│ ├── providers.tf
│ ├── Readme.md
│ └── variables.tf
├── 040...
...
└── Modules
├── HostMetaDat
...
└── SshKnownHosts
...export TF_VAR_hcloud_token=add_your_provider_token
export TF_VAR_dns_secret=add_your_ds_server_secretenv.template is to be copied to
e.g. an env file.
Required credentials to be filled in.
Prior to executing e.g. terraform
init the
env file has to be sourced:
. ../env # Sourcing environment echo $TF_VAR_hcloud_token # For testing purpose only z0Tgf... terraform init ...
In contrast to env.template the
env file is being listed in
.gitignore due to sensible content.
# variables.tf
variable "hcloud_token" { # See env file
nullable = false
sensitive = true
}# env file
TF_VAR_hcloud_token="z0Tgf ... "# providers.tf
...
provider "hcloud" { token = var.hcloud_token }output "authorizedKeys" {
description = "List of authorized users' public ssh keys"
type = map(string)
value = {
goik = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKY24AeEibQGSMPtstaN4REByLCM3kzjM//apEZ9WyUB goik@hdm-stuttgart.de"
xy001 = "KEY_OF_USER_xy001 xy001@hdm-stuttgart.de"
xy002 = ...
}
}
output "loginUserName" {
description = "Login user's name with admin privileges"
type = string
value = "devops"
}env Conf/config.tf
Problems:
Multiple users and local Terraform state.
Disjoint working environments, i.e. company and home office.
Solution: Move Terraform state to shared backend.
Cloud Stack Conference talk.
Initial system configuration
Standardized installation related tasks:
packages, users, ssh keys, networks, hostnames, custom scripts
|
|
# main.tf
resource "hcloud_server" "nginxServer" {
name = "hello"
image = "debian-13"
server_type = "cx23"
ssh_keys = ...
# Post server creation init script
user_data = file("myinit.sh")
}#!/bin/bash
# myinit.sh
aptitude update && aptitude -y upgrade |
├── main.tf
├── ...
└── myinit.sh |
|
|
# cloud-init schema --system --annotate
...
apt: # E1
- debconf_selections: openssh-server openssh-server/password-authentication boolean
false openssh-server openssh-server/permit-root-login boolean false
...
# Errors: -------------
# E1: [{'debconf_selections': 'openssh-server ....boolean false'}] is not of type 'object'# cloud-init schema --config-file /var/lib/cloud/instance/user-data.txt
Valid cloud-config: /var/lib/cloud/instance/user-data.txt# main.tf resource "hcloud_server" "web" { name = var.server_name ... user_data = file("userData.yml") } #cloud-config
# userData.yml
packages:
- nginx
runcmd:
- systemctl enable nginx
- rm /var/www/html/*
- >
echo "I'm Nginx @ $(curl -4 https://ifconfig.me)
created $(date -u)" > /var/www/html/index.html |
├── main.tf
├── ...
└── userData.yml |
# main.tf resource "local_file" "user_data" { content = templatefile("tpl/userData.yml", { loginUser = "devops" }) filename = "gen/userData.yml" } resource "hcloud_server" "mySrv" { ... # Using gen/userData.yml user_data = local_file.user_data.content } |
# tpl/userData.yml ... users: - name: ${loginUser} ... |
# gen/userData.yml ... users: - name: devops ... |
resource "tls_private_key" "host" { algorithm = "ED25519" } ... resource "local_file" "user_data" { content = templatefile("tpl/userData.yml", { host_ed25519_private = tls_private_key.host.private_key_openssh host_ed25519_public = ... }) filename = "gen/user_data.yml" } |
...
ssh_keys:
ed25519_private: |
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUA...
-----END OPENSSH PRIVATE KEY----- |
# E1: File gen/user_data.yml is not valid YAML.
in "<unicode string>", line 19, column 1:
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUA ... |
# main.tf resource "local_file" "user_data" { content = templatefile("tpl/userData.yml", { host_ed25519_private = indent(4, tls....host.private_key_openssh) ...}) filename = "gen/user_data.yml" } |
# gen/usr_data.yml ssh_keys: ed25519_private: | ␣␣␣␣-----BEGIN OPENSSH PRIVATE KEY----- ␣␣␣␣b3BlbnNzaC1 ... ␣␣␣␣... ␣␣␣␣... ␣␣␣␣-----END OPENSSH PRIVATE KEY----- |
# gen/usr_data.yml ssh_keys: ed25519_private: | ␣␣␣␣-----BEGIN OPENSSH PRIVATE KEY----- ␣␣␣␣␣␣b3BlbnNzaC1 ... ␣␣␣␣␣␣... ␣␣␣␣␣␣... ␣␣␣␣␣␣-----END OPENSSH PRIVATE KEY----- |
# /etc/ssh/ssh_host_ed25519_key ... ssh_keys: ed25519_private: | -----BEGIN OPENSSH PRIVATE KEY----- ␣␣b3BlbnNzaC1 ... ␣␣... ␣␣... ␣␣-----END OPENSSH PRIVATE KEY----- |
resource "local_file" "user_data" {
content = templatefile("tpl/userData.yml", {})
filename = "gen/user_data.yml"
}#> cloud-init schema --config-file gen/user_data.ymlroot@hello:~# journalctl -f May 06 04:41:20 hello cloud-init[898]: Cloud-init v. 22.4.2 finished at Mon, 06 May 2024 04:41:20 +0000. Datasource DataSourceHetzner. Up 11.78 seconds ... May 06 04:46:16 hello sshd[927]: Invalid user abc from 43.163.218.130 port 33408 May 06 04:46:17 hello sshd[927]: Received disconnect from 43.163.218.130 port 33408:11: Bye Bye [preauth] May 06 04:46:17 hello sshd[927]: Disconnected from invalid user abc 43.163.218.130 port 33408 [preauth] ... May 06 04:50:54 hello sshd[930]: fatal: Timeout before authentication for 27.128.243.225 port 59866 ... May 06 04:52:45 hello sshd[933]: Invalid user cos from 43.163.218.130 port 59776 ... May 06 04:53:04 hello sshd[935]: Invalid user admin from 194.169.175.35 port 51128 May 06 04:53:49 hello sshd[937]: User root from 43.163.218.130 not allowed because not listed in AllowUsers May 06 04:53:49 hello sshd[937]: Disconnected from invalid user root 43.163.218.130 port 50592 [preauth]
Problem of repeated terraform apply:
$ ssh root@128.140.108.60
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!resource "local_file" "known_hosts" { content = join(" " ,[ hcloud_server.mySrv.ipv4_address , tls_private_key.host.public_key_openssh ] ) filename = "gen/known_hosts" file_permission = "644" }
157.180.68.60 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...XXgAC3eaueHMQ8ePtbnw
resource "local_file" "user_data" {
content = templatefile("tpl/userData.yml", {
host_ed25519_private = tls_private_key.host.private_key_openssh
host_ed25519_public = tls_private_key.host.public_key_openssh
devopsUsername = hcloud_ssh_key.loginUser.name
...
filename = "gen/user_data.yml" ...#cloud-config
${yamlencode(
{ ...
ssh_keys = {
ed25519_private = host_ed25519_private
ed25519_public = host_ed25519_public
} ...
}
)}
users:
- name: ${devopsUsername} ...main.tf |
tpl/ssh.sh |
|---|---|
resource "local_file" "ssh_script" { content = templatefile("tpl/ssh.sh", { ip=hcloud_server.hello.ipv4_address }) filename = "bin/ssh" file_permission = "755" depends_on = [local_file.known_hosts] } |
#!/usr/bin/env bash
GEN_DIR=$(dirname "$0")/../gen
ssh -o UserKnownHostsFile= \
"$GEN_DIR/known_hosts" devops@${ip} "$@" |
#cloud-config
chpasswd:
list: |
root:bingo9wingo
expire: False#cloud-config
write_files:
- content:
${base64encode(private_key_pem)}
encoding: base64
path: /etc/nginx/snippets/cert/private.pemCongrats to paultyng
# main.tf
resource "hcloud_server" "myServer" { ... }
resource "local_file" "hostdata" {
content = templatefile("Tpl/hostdata.json", {
ip4 = hcloud_server.mySrv.ipv4_address
ip6 = hcloud_server.mySrv.ipv6_address
location = hcloud_server.mySrv.location
})
filename = "Gen/hostdata.json"
}{ "network": {
"ipv4": "${ip4}","ipv6": "${ip6}"},
"location": "${location}"} |
{
"network": {
"ipv4": "157.180.78.16",
"ipv6": "2a01:4f9:c013:be69::1"
},
"location": "hel1"
} |
├── CreateHostByModule │ ├── main.tf │ ├── variables.tf │ └── Gen | └── hostdata.json ◀━━┓ └── Modules ┃ └── HostMetaData ┃ ├── main.tf ┃ ├── outputs.tf ┃ ├── providers.tf ┃ ├── Tpl ┃ │ └── hostdata.json ━┛ └── variables.tf
# variable.tf
variable "hcloud_token" { sensitive = true }# main.tf
module "createHostAmongMetaData" {
source = "../Modules/HostMetaData"
name = "myServer"
hcloud_token = var.hcloud_token
}# variable.tf variable "name" { type = string nullable = false } variable "hcloud_token" { type = string nullable = false } |
# main.tf
resource "hcloud_server" "newServer" {
name = var.name
image = "debian-13"
server_type = "cx23" }
resource "local_file" "hostdata" {
content = templatefile("${path.module}/Tpl/hostdata.json", {
ip4 = hcloud_server.newServer.ipv4_address
ip6 = hcloud_server.newServer.ipv6_address
location = hcloud_server.newServer.location
})
filename = "Gen/${var.name}.json" } |
{
"network": {
"ipv4": "${ip4}",
"ipv6": "${ip6}"
},
"location": "${location}"
} |
{
"network": {
"ipv4": "157.180.78.16",
"ipv6": "2a01:4f9:c013:be69::1"
},
"location": "hel1"
} |
resource "local_file" "hostdata" { content = templatefile( "${path.module}/Tpl/hostdata.json",{ ip4 = hcloud_server.newServer.ipv4_address ip6 = hcloud_server.newServer.ipv6_address location = hcloud_server.newServer.location }) filename = "Gen/${var.name}.json" } |
├── CreateHostByModule │ ├── main.tf │ ├── variables.tf │ └── Gen | └── hostdata.json ◀━━┓ └── Modules ┃ └── HostMetaData ┃ ├── main.tf ┃ ├── outputs.tf ┃ ├── providers.tf ┃ ├── Tpl ┃ │ └── hostdata.json ━┛ └── variables.tf |
resource "hcloud_server" "mySrv" { server_type = "cx23" ... } resource "hcloud_volume" "volume01" { name = "volume1" size = 10 server_id = hcloud_server.mySrv.id automount = true format = "xfs" } |
df ... /mnt/HC_Volume_100723816 |
output "volume_device" {
value = hcloud_volume.volume01.linux_device
description = "The volume's device"
} |
terraform apply ... volume_device = "/dev/disk/by-id/scsi-0HC_Volume_102626366" |
|
Desired /dev/disk/by-id/scsi-0HC_Volume_102626366 /volume01 xfs rw,defaults 0 2 |
resource "hcloud_server" "mySrv" { ... user_data = templatefile("tpl/userData.yml", { ... volume01Id = hcloud_volume.volume01.id }) } resource "hcloud_volume" "volume01" { server_id = hcloud_server.mySrv.id ... } |
Problem: Cyclic dependency mySrv <--> volume01 |
main.tf |
|---|
resource "hcloud_volume" "vol01" { name = "volume1" ... } resource "hcloud_server" "hello" { user_data = templatefile("userData.yml.tpl", { volumename = hcloud_volume.vol01.name device = hcloud_volume.vol01.linux_device }) ... } resource "hcloud_volume_attachment" "main" { volume_id=hcloud_volume.vol01.id server_id=hcloud_server.hello.id ... } |
# userData.yml.tpl runcmd: - mkdir /${volumename} - echo ${device} /${volumename} xfs rw,defaults 0 2 >> /etc/fstab |
Dedicated course related DNS server
ns1.hdm-stuttgart.cloud.
One subdomain per group e.g. g3.sdi.hdm-stuttgart.cloud corresponding to Group 3.
Zone edits require a subdomain specific hmac
secret key being provided as dnsupdate.sec
file in your personal group entry below the SDI
course:
hmac-sha512:g3.key:I5sDDS3L1BU...
The per zone secrets have been created using tsig-keygen -a hmac-sha512.
Edits become globally visible. Mind the TTL setting: A higher value means you'll have to wait longer until updates become visible.
Key file available in your working group below 113475 Software defined Infrastructure.
$ export HMAC=hmac-sha512:g3.key:YXWSeh3l... $ dig @ns1.hdm-stuttgart.cloud -y $HMAC -t AXFR g3.sdi.hdm-stuttgart.cloud ... g3.sdi.hdm-stuttgart.cloud. 600 IN SOA ns1.hdm-stuttgart.cloud. ... g3.sdi.hdm-stuttgart.cloud. 600 IN NS ns1.hdm-stuttgart.cloud. g3.sdi.hdm-stuttgart.cloud. 600 IN SOA ns1.hdm-stuttgart.cloud. ... g3.key. 0 ANY TSIG hmac-sha512. 1746433052 300 64 bak... ...
See AXFR for details.
export HMAC=hmac-sha512:g3.key:YXWSeh3l... $ nsupdate -y $HMAC > server ns1.hdm-stuttgart.cloud > update add www.g3.sdi.hdm-stuttgart.cloud 10 A 141.62.75.114 > send > quit $ dig +noall +answer @ns1.hdm-stuttgart.cloud www.g3.sdi.hdm-stuttgart.cloud www.g3.sdi.hdm-stuttgart.cloud. 9 IN A 141.62.75.114 $ dig +noall +answer @8.8.8.8 www.g3.sdi.hdm-stuttgart.cloud www.g3.sdi.hdm-stuttgart.cloud. 3 IN A 141.62.75.114
$ nsupdate -y $HMAC
> server ns1.hdm-stuttgart.cloud
> update delete www.g3.sdi.hdm-stuttgart.cloud. 10 IN A 141.62.75.114
> send
> quit
>
$ dig +noall +answer @8.8.8.8 www.g3.sdi.hdm-stuttgart.cloud
$ Examples at DNS Updates with nsupdate
goik>dig +noall +answer www.g3.sdi.hdm-stuttgart.cloud www.g3.sdi.hdm-stuttgart.cloud. 9 IN A 141.62.75.114
Providing DNS info for sdi.hdm-stuttgart.cloud and sub-zones:
g1.sdi.hdm-stuttgart.cloud
g2.sdi.hdm-stuttgart.cloud
...
Remote API for per-zone editing
terraform {
required_providers {
dns = {
source = "hashicorp/dns"
}
...
}provider "dns" {
update {
server = "ns1.hdm-stuttgart.cloud"
key_name = "g12.key." # Corresponding to your group e.g., Group 12
key_algorithm = "hmac-sha512"
key_secret = var.dns_secret
}
}This requires:
A corresponding variable "dns_secret" {...} declaration.
An e.g., export TF_VAR_dns_secret = "sVfw2a...vAUqw=="
(non - versioned!) environment variable.
resource "dns_a_record_set" "helloRecord" { zone = "${var.dnsDomainName}." # The dot matters! name = hcloud_server.mySrv.name addresses = [hcloud_server.mySrv.ipv4_address] ttl = 10 }
Likewise with dns_cname_record and other record types.
Fully automated process solely based on DNS / infrastructure challenges.
Checking organization in question.
Additional checks i.e. telephone based verification.
provider "acme" {
server_url = "https://acme-staging-v02.api.letsencrypt.org/directory"
# Production:
# server_url = "https://acme-v02.api.letsencrypt.org/directory"
}
resource "tls_private_key" "private_key" { algorithm = "RSA" }
resource "acme_registration" "reg" {
account_key_pem = tls_private_key.private_key.private_key_pem
email_address = "nobody@example.com"
}
resource "acme_certificate" "certificate" {
...
dns_challenge { ... }
} |
acme DNS provider list:
|
dns_challenge {
provider = "rfc2136"
config = {
RFC2136_NAMESERVER = "ns1.hdm-stuttgart.cloud"
RFC2136_TSIG_ALGORITHM = "hmac-sha512"
RFC2136_TSIG_KEY = "gxy.key."
RFC2136_TSIG_SECRET = var.dns_secret # Relates to environment variable TF_VAR_dns_secret
}
}... updating zone 'goik.sdi.hdm-stuttgart.cloud/IN':
deleting rrset at '_acme-challenge.goik.sdi.hdm-stuttgart.cloud' TXT
... updating zone 'goik.sdi.hdm-stuttgart.cloud/IN':
adding an RR at '_acme-challenge.goik.sdi.hdm-stuttgart.cloud' TXT
"GtJZJZjCZLWoGsQDODCFnY37TmMjRiy8Hw9M1eDGhkQ"
... deleting rrset at ... TXT
... adding an RR ... TXT "eJckWl2F43nsf27bzVOjcrTGp_VFeCj2qTVM5Uodg-4"
... deleting an RR at _acme-challenge.goik.sdi.hdm-stuttgart.cloud TXT
... updating zone ... deleting an RR ... TXTresource "hcloud_network" "privateNet" { name = "Private network" ip_range = "10.0.0.0/8" } resource "hcloud_network_subnet" "privateSubnet" { network_id = hcloud_network.privateNet.id type = "cloud" network_zone = "eu-central" ip_range = "10.0.1.0/24" } resource "hcloud_network_route" "gateway"{ network_id = hcloud_network.privateNet.id destination = "0.0.0.0/0" gateway = "10.0.1.20" }
resource "hcloud_server" "gateway" { .... public_net { ipv4 = hcloud_primary_ip.gatwewayIp.id } network { network_id = hcloud_network.privateNet.id ip = "10.0.1.20" } }
resource "hcloud_server" "intern" { .... public_net { ipv4_enabled = false ipv6_enabled = false } network { network_id = hcloud_network.privateNet.id ip = "10.0.1.30" } }
Host “intern” does not have Internet access.
Consequences:
No package updates.
No package installs
...
Allow IP forwarding on gateway host
Configure NAT enabling gateway host as router
Use an application level gateway:
Problem: apt-cacher-ng installation requires time for service to become available.
Consequence: Package installs / updates on host “intern” must be deferred.
Problem: No standard Terraform “service ready” dependency management hook.
tpl/waitForAptProxy.sh |
main.tf |
|---|---|
#!/bin/bash
echo "Waiting for apt-cacher-ng ..."
while ! nc -z ${interface} 3142;
do
sleep 8 # wait 8 second for next poll
echo apt-cacher-ng not yet ready ...
done
echo "apt-cacher-ng service ready" |
resource "local_file" "waitForAptProxy" {
content = templatefile(
"tpl/waitForAptProxy.sh", {
interface = var.frontendServer.privateIp
})
filename = "gen/waitForAptProxy"
file_permission = "755"
} |
resource "null_resource" "waitForProxy" { connection { type = "ssh" user = "devops" host_key = ...public_key_openssh agent = "true" host = ...web.ipv4_address } provisioner "remote-exec" { script = local_file.waitForAptProxy.filename } } |
resource "hcloud_server" "intern" { ... depends_on = [ hcloud_network_subnet.pSubnet ,null_resource.waitForProxy ] } |
Infrastructure as code (IAC)
Infrastructure state model like Terraform
Looks imperative but IS actually declarative.
Various language bindings: TypeScript, Python, Go, C#, Java ...
| Pulumi / Java | Terraform |
|---|---|
import com.pulumi.hcloud.Server; ... var server = new Server ( "mySrv", ServerArgs.builder() .name("hello") .image("debian-13") .serverType("cx23") .location("nbg1") .build() ); |
resource "hcloud_server" "mySrv" { name = "hello" image = "debian-13" server_type = "cx23" location = "nbg1" } |
public static void stack(Context ctx) { Server node1 = new Server ("Server1", ServerArgs.builder() .name("node1") ... .build() ); Output<String> ipv4Node1 = node1.ipv4Address(); ctx.export("Node1 ipv4", ipv4Node1); Server node2 = new Server ("Server2", ServerArgs.builder() .name("node2") ... .build() ); ctx.export("Node2 ipv4", node2.ipv4Address()); }
$ pulumi up
Previewing update (java):
Type Name Plan
+ pulumi:pulumi:Stack Hello-java create
+ ├─ hcloud:index:Server Server2 create
+ └─ hcloud:index:Server Server1 create
Outputs:
Node1 ipv4: [unknown]
Node2 ipv4: [unknown]
Resources:
+ 3 to create
Do you want to perform this update? [Use arrows to move, type to filter]
> yes
no
details
Updating (java):
Type Name Status
+ pulumi:pulumi:Stack Hello-java created (10s)
+ ├─ hcloud:index:Server Server2 created (8s)
+ └─ hcloud:index:Server Server1 created (9s)
Outputs:
Node1 ipv4: "88.99.224.53"
Node2 ipv4: "91.99.143.208"
Resources:
+ 3 created
Duration: 12sstatic private final String cloudInitCode = """
#cloud-config
packages:
- nginx
runcmd:
- systemctl enable nginx
- rm /var/www/html/*
- echo I am Nginx @ $(dig -4 TXT +short o-o.myaddr.l.google.com @ns1.google.com)
created $(date -u) >> /var/www/html/index.html""";
var server = new Server ("mySrv", ServerArgs.builder()
.name("hello")
...
.userData(cloudInitCode)
.build()
);static private final String sshPublicKeyIdentifier = "userSshPublicKey"; ... final String content = Files.readString(Path.of("/home/goik/.ssh/id_ed25519.pub")); new SshKey(sshPublicKeyIdentifier, SshKeyArgs.builder() .name(sshPublicKeyIdentifier).publicKey(content).build() ); ... var server = new Server ("mySrv", ServerArgs.builder() .name("hello") ... .sshKeys(sshPublicKeyIdentifier) .build() );
Requirements:
Firewall
Host(s) carrying web server
var firewall = new Firewall("stdFirewall", FirewallArgs.builder()
.name("my-firewall")
.rules( FirewallRuleArgs.builder()
.direction("in")
.protocol("icmp")
.sourceIps(
"0.0.0.0/0",
"::/0")
.build(),
...
FirewallRuleArgs.builder()
.direction("in")
.protocol("tcp")
.port("22")
.sourceIps(
"0.0.0.0/0",
"::/0")
.build())
.build());> pulumi package add terraform-provider hashicorp/dnssrc
└── main
├── java
│ ├── com
│ │ └── pulumi
│ │ └── dns
│ │ ├── AaaaRecordSetArgs.java
│ │ ├── AaaaRecordSet.java
│ │ ├── ARecordSetArgs.java
│ │ ├── ARecordSet.java
│ │ ├── CnameRecordArgs.java
│ │ ├── CnameRecord.java
│ │ ├── config
│ │ │ ├── inputs
│ │ │ │ └── Updates.java
│ │ │ └── outputs
│ │ │ └── UpdatesGssapi.java
│ │ ├── Config.java
│ │ ├── DnsFunctions.java
│ │ ├── inputs
│ │ │ ├── AaaaRecordSetState.java
│ │ │ ├── ARecordSetState.java
... ...var dnsProvider = new Provider("dnsProvider", ProviderArgs.builder()
.updates(ProviderUpdateArgs.builder()
.server("ns1.sdi.hdm-stuttgart.cloud")
.keyName("gxy.key.")
.keyAlgorithm("hmac-sha512")
.keySecret(DNS_SECRET)
.build() )
.build()
);var ipv4 = new ARecordSet("www_server_A_record", ARecordSetArgs.builder()
.zone("gxy.sdi.hdm-stuttgart.cloud.")
.name("www")
.addresses (server.ipv4Address() //** Ugly!
.applyValue(id -> List.of(id))
)
.ttl(100)
.build()
, CustomResourceOptions.builder()
.provider(dnsProvider)
.build()
);
var ipv6 = ... // HdM, dream on ...**: See Erroneous API Docs samples
final var server = new Server("www-server", ServerArgs.builder()
.name("www")
.image("debian-13")
...
.firewallIds(firewall.id()
.applyValue(id -> List.of(Integer.parseInt(id)) // Ugly!
)
.build());