Skip to content

Commit 7ccc081

Browse files
authored
Terraform love (tinkerbell#126)
## Description All sorts of things mainly aimed at terraform setup. Quick one-liners for each change follow, more info/context usually found in the commit message. > direnv: use `has` from stdlib Simplification of .envrc by making use of stdlib. > git: Manage git ignores in just one .gitignore file I was originally updating .gitignore in the terraform folder, but the root file had relevant entries so I decided to just use one. > tf: Get rid of mention of ewr1 in comment This bit of comment was unhelpful and actually very wrong. > tf: Add output value for the provisioner ssh hostname Makes for easier ssh'ing to the provisioner, can do `ssh $(tf output -raw provisioner_ssh)`. > tf/setup: Configure bash to be stricter/safer Speaks for itself I think. > tf/setup: Ensure all functions use same execution mode Some functions were running in a subshell, this is not necessary and relatively unknown. > tf/setup: Use apt-get helper function Pulling common apt-get args into a function instead of copy/pasting in each call. > tf/setup: Only explicitly install the docker packages Why are we manuall installing deps when apt is better at it than humans? > tf/setup: Don't hard code the arch when adding docker apt-repository Pretty self explanatory, bit me when I tried a aarch64 machine. > tf/setup: Install docker-compose using pip Also ran into no file (binary ?) for non x86_64 machines in GitHub releases. > tf/setup: Make main function actually functional main (as a func) wasn't really doing anything useful before, now the file can be sourced vs executed. > tf/setup: Do not restart docker service No need to restart docker, just because we installed docker-compose. > tf/setup: Persist 2 separate network config This way machine stays useful after reboots. > tf/setup: Improve correctness of get_second_interface_from_bond0 Mostly a nit/theoretical fix, but nice to have imo. > tf/setup: Persist iptables gw rules This way machine stays useful after reboots. > tf: Add local variable for worker_macs So lines that need the mac read better. > tf: Add output for worker_macs This way we can get to watching boots logs more quickly. > tf: Add outputs for provisioner and worker ids For look up in EMAPI/Portal. > tf: Modify compose/.env file for repeat docker-compose runs Before this docker-compose would do the wrong thing if ran manually because the env vars were not around. > tf: Put all setup logic in setup.sh I originally did this because there was a race between userdata and tf remote-exec, but that is no longer true. This is good to have anyway so that we only need to look at one file to grok the setup/run process instead of two. > tf: Add some interactive user goodies Similar to the ones for vagrant, makes for nicer interactive use. > tf: Use format instead of formatlist for worker_sos output This way we can ssh into sos more easily. > vagrant: Move all provisioner code into just one script One shell script is easier to read than a bunch of shell blocks in a ruby file imo. > vagrant: Install docker and docker-compose via setup.sh Running vagrant w/o these plugins causes vagrant to fetch them and exit, which means we need to bring vagrant up again. This is pretty poor experience imo, by installing in setup.sh we don't need anything from the host os. > deploy: Use the same folder path on both terraform and vagrant Who cares that we're running in tf vs vagrant anyway, this way mental models/paths are valid for both. This also lets the setup.sh scripts have more things in common that are semantically common, which hopefully means they'll be easier to keep in sync. ## Why is this needed I tried running sandbox on some arm machines and ran into all sorts of hard coded assumptions and race conditions that don't seem to hit in x86 land. This got me to spend some time running lots of tf setups and hitting the a few bugs. Having to cd and run full command names also got boring. ## How Has This Been Tested? Lots of `terraform apply`. ## How are existing users impacted? What migration steps/scripts do we need? More reliable/generic tf setup. ## Checklist: I have: - [ ] updated the documentation and/or roadmap (if required) - [ ] added unit or e2e tests - [ ] provided instructions on how to upgrade
2 parents ce0305c + 9ce32c3 commit 7ccc081

File tree

11 files changed

+289
-114
lines changed

11 files changed

+289
-114
lines changed

.envrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
which nix &>/dev/null && use nix
1+
has nix && use nix
2+
dotenv_if_exists

.gitignore

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1+
# hidden files/dirs
2+
.*
3+
!deploy/compose/state/webroot/misc/osie/current/.keep
4+
!deploy/compose/state/webroot/workflow/.keep
5+
!deploy/.env
6+
!.gitignore
7+
18
# Local .terraform directories
2-
**/.terraform/*
39
.terraform*
10+
!.terraform.lock.hcl
411

512
# .tfstate files
613
*.tfstate
714
*.tfstate.*
815

9-
!.terraform.lock.hcl
10-
envrc
11-
out
12-
!deploy/.env
13-
.vagrant
16+
compose.tar.gz
17+
compose.zip
18+
deploy/compose/state/webroot/*.gz
1419
deploy/compose/state/webroot/misc/osie/current/*
1520
deploy/compose/state/webroot/workflow/*
16-
!deploy/compose/state/webroot/misc/osie/current/.keep
17-
!deploy/compose/state/webroot/workflow/.keep
18-
deploy/compose/state/webroot/*.gz
21+
envrc
22+
out
1923
workflow_id.txt
20-
compose.tar.gz
21-
compose.zip

deploy/compose/state/webroot/.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

deploy/terraform/.gitignore

Lines changed: 0 additions & 5 deletions
This file was deleted.

deploy/terraform/cloud-config.cfg

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ write_files:
66
content: ${COMPOSE_ZIP}
77
path: /root/compose.zip
88

9-
runcmd:
10-
- cd /root/sandbox/compose && unzip /root/compose.zip
11-
- cd /root/sandbox/compose && TINKERBELL_CLIENT_MAC=${WORKER_MAC} TINKERBELL_TEMPLATE_MANIFEST=/manifests/template/ubuntu-equinix-metal.yaml TINKERBELL_HARDWARE_MANIFEST=/manifests/hardware/hardware-equinix-metal.json docker-compose up -d
9+
- encoding: b64
10+
content: ${SETUPSH}
11+
path: /root/setup.sh
12+
owner: root:root
13+
permissions: "0755"
1214

15+
runcmd:
16+
- /root/setup.sh ${WORKER_MAC}

deploy/terraform/main.tf

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ provider "metal" {
2020
auth_token = var.metal_api_token
2121
}
2222

23-
# Create a new VLAN in datacenter "ewr1"
23+
# Create a new VLAN in datacenter
2424
resource "metal_vlan" "provisioning_vlan" {
2525
description = "provisioning_vlan"
2626
metro = var.metro
@@ -85,6 +85,7 @@ data "archive_file" "compose" {
8585

8686
locals {
8787
compose_zip = data.archive_file.compose.output_size > 0 ? filebase64("${path.module}/compose.zip") : ""
88+
worker_macs = flatten([for wp in metal_device.tink_worker[*].ports[*] : [for p in wp : p.mac if p.name == "eth0"]])
8889
}
8990

9091
data "cloudinit_config" "setup" {
@@ -94,15 +95,12 @@ data "cloudinit_config" "setup" {
9495
gzip = false # not supported on Equinix Metal
9596
base64_encode = false # not supported on Equinix Metal
9697

97-
part {
98-
content_type = "text/x-shellscript"
99-
content = file("${path.module}/setup.sh")
100-
}
10198
part {
10299
content_type = "text/cloud-config"
103100
content = templatefile("${path.module}/cloud-config.cfg", {
104101
COMPOSE_ZIP = local.compose_zip
105-
WORKER_MAC = metal_device.tink_worker.ports[1].mac
102+
SETUPSH = filebase64("${path.module}/setup.sh")
103+
WORKER_MAC = local.worker_macs[0]
106104
})
107105
}
108106
}

deploy/terraform/outputs.tf

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@ output "provisioner_ip" {
22
value = metal_device.tink_provisioner.network[0].address
33
}
44

5+
output "provisioner_id" {
6+
value = metal_device.tink_provisioner.id
7+
}
8+
9+
output "provisioner_ssh" {
10+
value = format("%s.packethost.net", split("-", metal_device.tink_provisioner.id)[0])
11+
}
12+
13+
output "worker_id" {
14+
value = metal_device.tink_worker.id
15+
}
16+
17+
output "worker_macs" {
18+
value = local.worker_macs
19+
}
20+
521
output "worker_sos" {
6-
value = formatlist("%s@sos.%s.platformequinix.com", metal_device.tink_worker[*].id, metal_device.tink_worker.deployed_facility)
22+
value = format("%s@sos.%s.platformequinix.com", metal_device.tink_worker.id, metal_device.tink_worker.deployed_facility)
723
}

deploy/terraform/setup.sh

Lines changed: 141 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,174 @@
11
#!/usr/bin/env bash
22

3-
set -xo pipefail
4-
53
install_docker() {
64
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
7-
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
5+
add-apt-repository "deb https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
86
update_apt
9-
DEBIAN_FRONTEND=noninteractive apt install -y apt-transport-https ca-certificates curl gnupg-agent gnupg2 software-properties-common docker-ce docker-ce-cli containerd.io
7+
apt-get install --no-install-recommends containerd.io docker-ce docker-ce-cli
108
}
119

1210
install_docker_compose() {
13-
curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
14-
chmod +x /usr/local/bin/docker-compose
11+
apt-get install --no-install-recommends python3-pip
12+
pip install docker-compose
1513
}
1614

17-
update_apt() (
18-
$APT update
19-
DEBIAN_FRONTEND=noninteractive $APT --yes --force-yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade
20-
)
15+
install_iptables_persistent() {
16+
apt-get install --no-install-recommends iptables-persistent
17+
}
2118

22-
restart_docker_service() (
23-
service docker restart
24-
)
19+
apt-get() {
20+
DEBIAN_FRONTEND=noninteractive command apt-get \
21+
--allow-change-held-packages \
22+
--allow-downgrades \
23+
--allow-remove-essential \
24+
--allow-unauthenticated \
25+
--option Dpkg::Options::=--force-confdef \
26+
--option Dpkg::Options::=--force-confold \
27+
--yes \
28+
"$@"
29+
}
30+
31+
update_apt() {
32+
apt-get update
33+
apt-get upgrade
34+
}
2535

2636
# get_second_interface_from_bond0 returns the second interface of the bond0 interface
2737
get_second_interface_from_bond0() {
28-
local return_value
29-
return_value=$(cut -d' ' -f2 /sys/class/net/bond0/bonding/slaves | xargs)
30-
echo "${return_value}"
38+
local addr=$1
39+
40+
# if the ip is in a file in interfaces.d then lets assume this is a re-run and we can just
41+
# return the basename of the file (which should be named same as the interface)
42+
f=$(grep -lr "${addr}" /etc/network/interfaces.d)
43+
[[ -n ${f:-} ]] && basename "$f" && return
44+
45+
# sometimes the interfaces aren't sorted as expected in the /slaves file
46+
#
47+
# seeing as how this function is named *second* I figured its best to be
48+
# precise (via head -n2) when choosing the iface instead of choosing the last
49+
# iface and hoping there are only 2
50+
tr ' ' '\n' </sys/class/net/bond0/bonding/slaves | sort | head -n2 | tail -n1
3151
}
3252

3353
# setup_layer2_network removes the second interface from bond0 and uses it for the layer2 network
3454
# https://metal.equinix.com/developers/docs/layer2-networking/hybrid-unbonded-mode/
3555
setup_layer2_network() {
36-
local layer2_interface="$1"
37-
#local ip_addr="$2"
38-
ifenslave -d bond0 "${layer2_interface}"
39-
#ip addr add ${ip_addr}/24 dev "${layer2_interface}"
40-
ip addr add 192.168.56.4/24 dev "${layer2_interface}"
41-
ip link set dev "${layer2_interface}" up
56+
local interface=$1
57+
local addr=$2
58+
59+
# I tried getting rid of the following "manual" commands in favor of
60+
# persisting the network config and then restarting the network but that
61+
# didn't always work and was hard to recover from without a reboot so we're
62+
# stuck doing it once imperatively and also persisting the config
63+
ifenslave -d bond0 "${interface}"
64+
ip addr add "${addr}/24" dev "${interface}"
65+
ip link set dev "${interface}" up
66+
67+
# persist the new network settings
68+
# gets rid of the auto ${interface} block
69+
# "/^auto ${interface}/,/^\s*$/ d"
70+
# gets rid of ${interface} in bond config
71+
# "s|${interface}||" \
72+
# gets rid empty lines
73+
# 's|\s*$||' \
74+
# gets rid of source lines from previous runs, having this here helps in debugging/developing
75+
# '/^source / d' \
76+
# appends a source line to the end of the file that will pick up iface-conf file we generate
77+
# '$ s|$|\n\nsource /etc/network/interfaces.d/*|' \
78+
sed -i \
79+
-e "/^auto ${interface}/,/^\s*$/ d" \
80+
-e "s|${interface}||" \
81+
-e 's|\s*$||' \
82+
-e '/^source / d' \
83+
-e '$ s|$|\n\nsource /etc/network/interfaces.d/*|' \
84+
/etc/network/interfaces
85+
86+
cat >"/etc/network/interfaces.d/${interface}" <<-EOF
87+
auto ${interface}
88+
iface ${interface} inet static
89+
address ${addr}
90+
EOF
4291
}
4392

4493
# make_host_gw_server makes the host a gateway server
4594
make_host_gw_server() {
46-
local incoming_interface="$1"
47-
local outgoing_interface="$2"
95+
local incoming_interface=$1
96+
local outgoing_interface=$2
97+
98+
# drop all rules, especially interested in droppin docker's we don't want to persist docker's rules
99+
# docker will re-create them when starting back up
100+
systemctl stop docker
101+
netfilter-persistent flush
102+
48103
iptables -t nat -A POSTROUTING -o "${outgoing_interface}" -j MASQUERADE
49104
iptables -A FORWARD -i "${outgoing_interface}" -o "${incoming_interface}" -m state --state RELATED,ESTABLISHED -j ACCEPT
50105
iptables -A FORWARD -i "${incoming_interface}" -o "${outgoing_interface}" -j ACCEPT
106+
107+
netfilter-persistent save
108+
systemctl start docker
109+
}
110+
111+
extract_compose_files() {
112+
mkdir -p /sandbox
113+
unzip /root/compose.zip -d /sandbox/compose
114+
}
115+
116+
setup_compose_env_overrides() {
117+
local worker_mac=$1
118+
readarray -t lines <<-EOF
119+
TINKERBELL_CLIENT_MAC=$worker_mac
120+
TINKERBELL_TEMPLATE_MANIFEST=/manifests/template/ubuntu-equinix-metal.yaml
121+
TINKERBELL_HARDWARE_MANIFEST=/manifests/hardware/hardware-equinix-metal.json
122+
EOF
123+
for line in "${lines[@]}"; do
124+
grep -q "$line" /sandbox/compose/.env && continue
125+
echo "$line" >>/sandbox/compose/.env
126+
done
127+
}
128+
129+
create_tink_helper_script() {
130+
cat >/usr/local/bin/tink <<-'EOF'
131+
#!/usr/bin/env bash
132+
133+
exec docker-compose -f /sandbox/compose/docker-compose.yml exec tink-cli tink "$@"
134+
EOF
135+
chmod +x /usr/local/bin/tink
136+
}
137+
138+
tweak_bash_interactive_settings() {
139+
grep -q 'cd /sandbox/compose' ~root/.bashrc || echo 'cd /sandbox/compose' >>~root/.bashrc
140+
readarray -t aliases <<-EOF
141+
dc=docker-compose
142+
EOF
143+
for alias in "${aliases[@]}"; do
144+
grep -q "$alias" ~root/.bash_aliases || echo "alias $alias" >>~root/.bash_aliases
145+
done
51146
}
52147

53-
main() (
54-
#local provisioner_ip="$1"
148+
main() {
149+
worker_mac=$1
150+
layer2_ip=192.168.56.4
55151

152+
update_apt
56153
install_docker
57154
install_docker_compose
58-
restart_docker_service
59-
mkdir -p /root/sandbox/compose
155+
install_iptables_persistent
156+
60157
local layer2_interface
61-
layer2_interface="$(get_second_interface_from_bond0)"
62-
setup_layer2_network "${layer2_interface}" #"${provisioner_ip}"
63-
make_host_gw_server "${layer2_interface}" "bond0"
64-
)
158+
layer2_interface=$(get_second_interface_from_bond0 ${layer2_ip})
159+
setup_layer2_network "${layer2_interface}" ${layer2_ip}
160+
make_host_gw_server "${layer2_interface}" bond0
161+
162+
extract_compose_files
163+
setup_compose_env_overrides "$worker_mac"
164+
docker-compose -f /sandbox/compose/docker-compose.yml up -d
165+
166+
create_tink_helper_script
167+
tweak_bash_interactive_settings
168+
}
169+
170+
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
171+
set -euxo pipefail
65172

66-
main #"$1"
173+
main "$@"
174+
fi

0 commit comments

Comments
 (0)