terraform single server

using terraform for a bare metal server

SEAN K.H. LIAO

terraform single server

using terraform for a bare metal server

terraform

terraform rightly describes itself as a provisioning tool, and not a config management tool. So this idea of using terraform to manage the config for a server didn't really work out.

plan

use provisioner "file"s on null_resources to transfer the config files necessary to run nomad.

results

You have to do it right the first time, otherwise it's a manual taint of the resource and recreating it. Also it doesn't create the directories for a specified path, necessitating provisioner "remote-exec" just to create the dirs.

I should (have to?) reboot the server after everything is done, but terraform doesn't make doing that + exiting properly / validating easy. So I copped out and just did that part by hand.

files

00_certs.tf

This file creates a self signed CA and a set of client certs output to a local dir so local nomad cli commands can authenticate.

 1##################################################
 2# ca certs
 3##################################################
 4resource "tls_private_key" "ca" {
 5  algorithm   = "ECDSA"
 6  ecdsa_curve = "P384"
 7}
 8
 9resource "tls_self_signed_cert" "ca" {
10  key_algorithm     = tls_private_key.ca.algorithm
11  private_key_pem   = tls_private_key.ca.private_key_pem
12  is_ca_certificate = true
13
14  validity_period_hours = 24 * 365 * 10
15
16  allowed_uses = [
17    "cert_signing",
18  ]
19
20  subject {
21    common_name  = "medea.seankhliao.com"
22    organization = "medea / seankhliao"
23  }
24}
25resource "local_file" "ca_cert" {
26  content  = tls_self_signed_cert.ca.cert_pem
27  filename = "${path.root}/out/ca.crt"
28}
29
30
31##################################################
32# client cert
33##################################################
34resource "tls_private_key" "nomad_client" {
35  algorithm   = "ECDSA"
36  ecdsa_curve = "P384"
37}
38resource "local_file" "nomad_client_key" {
39  content  = tls_private_key.nomad_client.private_key_pem
40  filename = "${path.root}/out/client.key"
41}
42
43resource "tls_cert_request" "nomad_client" {
44  key_algorithm   = "ECDSA"
45  private_key_pem = tls_private_key.nomad_client.private_key_pem
46
47  subject {
48    common_name  = "eevee.seankhliao.com"
49    organization = "medea / seankhliao"
50  }
51}
52resource "tls_locally_signed_cert" "nomad_client" {
53  ca_key_algorithm   = tls_private_key.ca.algorithm
54  ca_private_key_pem = tls_private_key.ca.private_key_pem
55  ca_cert_pem        = tls_self_signed_cert.ca.cert_pem
56  cert_request_pem   = tls_cert_request.nomad_client.cert_request_pem
57
58  validity_period_hours = 24 * 365
59
60  allowed_uses = [
61    "digital_signature",
62    "key_encipherment",
63    "client_auth",
64  ]
65}
66resource "local_file" "nomad_client_cert" {
67  content  = tls_locally_signed_cert.nomad_client.cert_pem
68  filename = "${path.root}/out/client.crt"
69}
01_medea.tf

medea is the name of my server.

This file manages the basic setup i need from a fresh image.

 1resource "null_resource" "medea" {
 2  connection {
 3    host        = "medea.seankhliao.com"
 4    private_key = file(pathexpand("~/.ssh/id_ed25519"))
 5    agent       = false
 6  }
 7
 8  provisioner "remote-exec" {
 9    inline = [
10      "rm /etc/sysctl.d/* || true",
11      "rm /etc/ssh/ssh_host_{dsa,rsa,ecdsa}_key* || true",
12      "pacman -Rns --noconfirm btrfs-progs gptfdisk haveged xfsprogs wget vim net-tools cronie || true",
13      "pacman -Syu --noconfirm neovim zip unzip",
14      "systemctl enable --now systemd-timesyncd",
15    ]
16  }
17  provisioner "file" {
18    destination = "/root/.ssh/authorized_keys"
19    content     = <<-EOT
20      ${file(pathexpand("~/.ssh/id_ed25519.pub"))}
21      ${file(pathexpand("~/.ssh/id_ed25519_sk.pub"))}
22      ${file(pathexpand("~/.ssh/id_ecdsa_sk.pub"))}
23    EOT
24  }
25  provisioner "file" {
26    destination = "/etc/sysctl.d/30-ipforward.conf"
27    content     = <<-EOT
28      net.ipv4.ip_forward=1
29      net.ipv4.conf.lxc*.rp_filter=0
30      net.ipv6.conf.default.forwarding=1
31      net.ipv6.conf.all.forwarding=1
32    EOT
33  }
34  provisioner "file" {
35    destination = "/etc/modules-load.d/br_netfilter.conf"
36    content     = "br_netfilter"
37  }
38  provisioner "file" {
39    destination = "/etc/systemd/network/40-wg0.netdev"
40    content     = <<-EOT
41      # WireGuard
42
43      [NetDev]
44      Name = wg0
45      Kind = wireguard
46
47      [WireGuard]
48      PrivateKey = xxxx
49      # PublicKey = lombY0b15giOmoM9t0xBi+UgVkZDoOKDaEV9+ONwH1U=
50      ListenPort = 51820
51
52      # eevee
53      [WireGuardPeer]
54      PublicKey = YvSLDXl3NX1ySTX2C8D72+fCVBcqSs+fmAX3uySCDAQ=
55      AllowedIPs = 192.168.100.13/32
56
57      # pixel 3
58      [WireGuardPeer]
59      PublicKey = 3xpGlOORQb9/yg545KX+odrup3YaslxO9ie+ztJ3Y3E=
60      AllowedIPs = 192.168.100.3/32
61
62      # arch
63      [WireGuardPeer]
64      PublicKey = Lr17jGvc7uwjn9LNRR+IkCkjuP8nkHTOMHbVV+onMn0=
65      AllowedIPs = 192.168.100.1/24
66      Endpoint = yyyy
67    EOT
68  }
69  provisioner "file" {
70    destination = "/etc/systemd/network/41-wg0.network"
71    content     = <<-EOT
72      [Match]
73      Name = wg0
74
75      [Network]
76      Address = 192.168.100.2/24
77    EOT
78  }
79}
02_nomad.tf

This file provisions the server(+client) certs for nomad (it needs client to talk to itself), downloads nomad and runs it with systemd.

  1##################################################
  2# nomad server cert
  3##################################################
  4resource "tls_private_key" "nomad_server" {
  5  algorithm   = "ECDSA"
  6  ecdsa_curve = "P384"
  7}
  8
  9resource "tls_cert_request" "nomad_server" {
 10  key_algorithm   = "ECDSA"
 11  private_key_pem = tls_private_key.nomad_server.private_key_pem
 12
 13  dns_names = [
 14    "localhost",
 15    "server.global.nomad",
 16    "medea.seankhliao.com",
 17  ]
 18
 19  ip_addresses = [
 20    "127.0.0.1",
 21    "192.168.100.2",
 22    "65.21.73.144",
 23  ]
 24
 25  subject {
 26    common_name  = "medea.seankhliao.com"
 27    organization = "medea / seankhliao"
 28  }
 29}
 30resource "tls_locally_signed_cert" "nomad_server" {
 31  ca_key_algorithm   = tls_private_key.ca.algorithm
 32  ca_private_key_pem = tls_private_key.ca.private_key_pem
 33  ca_cert_pem        = tls_self_signed_cert.ca.cert_pem
 34  cert_request_pem   = tls_cert_request.nomad_server.cert_request_pem
 35
 36  validity_period_hours = 24 * 365
 37
 38  allowed_uses = [
 39    "digital_signature",
 40    "key_encipherment",
 41    "server_auth",
 42    "client_auth",
 43  ]
 44}
 45
 46##################################################
 47# nomad
 48##################################################
 49resource "null_resource" "medea_nomad" {
 50  depends_on = [
 51    null_resource.medea,
 52  ]
 53
 54  connection {
 55    host        = "medea.seankhliao.com"
 56    private_key = file(pathexpand("~/.ssh/id_ed25519"))
 57    agent       = false
 58  }
 59
 60  provisioner "file" {
 61    destination = "/etc/systemd/system/nomad.service"
 62    content     = <<-EOT
 63      [Unit]
 64      Description=nomad server
 65      Documentation=https://www.nomadproject.io/docs/agent/
 66      Requires=network-online.target
 67      After=network-online.target
 68
 69      [Service]
 70      Restart=on-failure
 71      ExecStart=/usr/local/bin/nomad agent -config /etc/nomad/server.hcl
 72      ExecReload=/usr/bin/kill -HUP $MAINPID
 73      KillSignal=SIGINT
 74
 75      [Install]
 76      WantedBy=multi-user.target
 77    EOT
 78  }
 79  provisioner "remote-exec" {
 80    inline = [
 81      "curl -Lo /tmp/nomad.zip https://releases.hashicorp.com/nomad/1.1.0/nomad_1.1.0_linux_amd64.zip",
 82      "unzip -o /tmp/nomad.zip nomad -d /usr/local/bin",
 83      "systemctl daemon-reload",
 84      "systemctl enable nomad",
 85      "rm -rf /etc/nomad || true",
 86      "mkdir -p /etc/nomad",
 87    ]
 88  }
 89  provisioner "file" {
 90    destination = "/etc/nomad/ca.crt"
 91    content     = tls_self_signed_cert.ca.cert_pem
 92  }
 93  provisioner "file" {
 94    destination = "/etc/nomad/server.key"
 95    content     = tls_private_key.nomad_server.private_key_pem
 96  }
 97  provisioner "file" {
 98    destination = "/etc/nomad/server.crt"
 99    content     = tls_locally_signed_cert.nomad_server.cert_pem
100  }
101  provisioner "file" {
102    destination = "/etc/nomad/server.hcl"
103    content     = <<-EOT
104      bind_addr = "0.0.0.0"
105      data_dir  = "/var/lib/nomad"
106
107      tls {
108        http = true
109        rpc  = true
110
111        ca_file   = "/etc/nomad/ca.crt"
112        cert_file = "/etc/nomad/server.crt"
113        key_file  = "/etc/nomad/server.key"
114
115        verify_server_hostname = true
116        verify_https_client    = true
117      }
118
119      leave_on_interrupt = true
120      leave_on_terminate = true
121
122      disable_update_check = true
123
124      server {
125        enabled = true
126        bootstrap_expect = 1
127      }
128
129      client {
130        enabled = true
131      }
132
133      log_level = "INFO"
134    EOT
135  }
136}
03_consul.tf

Similar, but for consul. The other option is to run it inside nomad as a system job, which may actually work better, since this is janky. Once nomad has talked to consul, it doesn't like being disconnected.

  1##################################################
  2# consul server cert
  3##################################################
  4resource "tls_private_key" "consul_server" {
  5  algorithm   = "ECDSA"
  6  ecdsa_curve = "P384"
  7}
  8
  9resource "tls_cert_request" "consul_server" {
 10  key_algorithm   = "ECDSA"
 11  private_key_pem = tls_private_key.consul_server.private_key_pem
 12
 13  dns_names = [
 14    "localhost",
 15    "server.dc1.consul",
 16    "medea.seankhliao.com",
 17  ]
 18
 19  ip_addresses = [
 20    "127.0.0.1",
 21    "192.168.100.2",
 22    "65.21.73.144",
 23  ]
 24
 25  subject {
 26    common_name  = "medea.seankhliao.com"
 27    organization = "medea / seankhliao"
 28  }
 29}
 30resource "tls_locally_signed_cert" "consul_server" {
 31  ca_key_algorithm   = tls_private_key.ca.algorithm
 32  ca_private_key_pem = tls_private_key.ca.private_key_pem
 33  ca_cert_pem        = tls_self_signed_cert.ca.cert_pem
 34  cert_request_pem   = tls_cert_request.consul_server.cert_request_pem
 35
 36  validity_period_hours = 24 * 365
 37
 38  allowed_uses = [
 39    "digital_signature",
 40    "key_encipherment",
 41    "server_auth",
 42    "client_auth",
 43  ]
 44}
 45
 46##################################################
 47# consul
 48##################################################
 49resource "null_resource" "medea_consul" {
 50  depends_on = [
 51    null_resource.medea,
 52  ]
 53
 54  connection {
 55    host        = "medea.seankhliao.com"
 56    private_key = file(pathexpand("~/.ssh/id_ed25519"))
 57    agent       = false
 58  }
 59
 60  provisioner "file" {
 61    destination = "/etc/systemd/system/consul.service"
 62    content     = <<-EOT
 63      [Unit]
 64      Description=Consul Agent
 65      Documentation=https://consul.io/docs/
 66      Requires=network-online.target
 67      After=network-online.target
 68
 69      [Service]
 70      Restart=on-failure
 71      ExecStart=/usr/local/bin/consul agent -config-file /etc/consul/server.hcl
 72      ExecReload=/usr/bin/kill -HUP $MAINPID
 73      KillSignal=SIGINT
 74
 75      [Install]
 76      WantedBy=multi-user.target
 77    EOT
 78  }
 79  provisioner "remote-exec" {
 80    inline = [
 81      "curl -Lo /tmp/consul.zip https://releases.hashicorp.com/consul/1.9.5/consul_1.9.5_linux_amd64.zip",
 82      "unzip -o /tmp/consul.zip consul -d /usr/local/bin",
 83      "systemctl daemon-reload",
 84      "systemctl enable consul",
 85      "rm -rf /etc/consul || true",
 86      "mkdir -p /etc/consul",
 87    ]
 88  }
 89  provisioner "file" {
 90    destination = "/etc/consul/ca.crt"
 91    content     = tls_self_signed_cert.ca.cert_pem
 92  }
 93  provisioner "file" {
 94    destination = "/etc/consul/server.key"
 95    content     = tls_private_key.consul_server.private_key_pem
 96  }
 97  provisioner "file" {
 98    destination = "/etc/consul/server.crt"
 99    content     = tls_locally_signed_cert.consul_server.cert_pem
100  }
101  provisioner "file" {
102    destination = "/etc/consul/server.hcl"
103    content     = <<-EOT
104      bind_addr = "0.0.0.0"
105      data_dir  = "/var/lib/consul"
106      ports {
107        http = -1
108        https = 8501
109        grpc = 8502
110      }
111
112      ca_file   = "/etc/consul/ca.crt"
113      cert_file = "/etc/consul/server.crt"
114      key_file  = "/etc/consul/server.key"
115
116      verify_incoming = true
117      verify_server_hostname = true
118
119      leave_on_terminate = true
120
121      disable_update_check = true
122
123      server = true
124      bootstrap_expect = 1
125
126      client_addr = "0.0.0.0"
127
128      ui_config {
129        enabled = true
130      }
131
132      log_level = "INFO"
133    EOT
134  }
135}