Repository: tosuke-homelab/infra
Pull request #2: feat: add Linode
Author: login: tosuke
Labels: name: gcloud/no-changes
name: linode/no-changes
Created: 2025-05-01T10:50:55Z | Merged: 2025-05-26T15:45:01Z



Diff:
diff --git a/.github/workflows/on-merge-group.yaml b/.github/workflows/on-merge-group.yaml
index 62eb884..5f47a07 100644
--- a/.github/workflows/on-merge-group.yaml
+++ b/.github/workflows/on-merge-group.yaml
@@ -22,6 +22,7 @@ jobs:
   terraform-lint:
     name: Terraform Lint
     needs: [changes]
+    if: ${{ needs.changes.outputs.terraform-targets != '[]' }}
     strategy:
       fail-fast: false
       matrix:
@@ -33,6 +34,7 @@ jobs:
   terraform-apply:
     name: Terraform Apply
     needs: [changes]
+    if: ${{ needs.changes.outputs.terraform-targets != '[]' }}
     strategy:
       fail-fast: false
       matrix:
diff --git a/.github/workflows/on-pull-request.yaml b/.github/workflows/on-pull-request.yaml
index 7063c6a..0b66aa8 100644
--- a/.github/workflows/on-pull-request.yaml
+++ b/.github/workflows/on-pull-request.yaml
@@ -27,6 +27,7 @@ jobs:
   terraform-lint:
     name: Terraform Lint
     needs: [changes]
+    if: ${{ needs.changes.outputs.terraform-targets != '[]' }}
     strategy:
       fail-fast: false
       matrix:
@@ -38,6 +39,7 @@ jobs:
   terraform-plan:
     name: Terraform Plan
     needs: [changes]
+    if: ${{ needs.changes.outputs.terraform-targets != '[]' }}
     strategy:
       fail-fast: false
       matrix:
@@ -45,3 +47,5 @@ jobs:
     uses: ./.github/workflows/wf-terraform-plan.yaml
     with:
       target: ${{ matrix.target }}
+    secrets:
+      LINODE_PLAN_TOKEN: ${{ secrets.LINODE_PLAN_TOKEN }}
diff --git a/.github/workflows/wf-changes.yaml b/.github/workflows/wf-changes.yaml
index 8a16c06..5742080 100644
--- a/.github/workflows/wf-changes.yaml
+++ b/.github/workflows/wf-changes.yaml
@@ -24,3 +24,7 @@ jobs:
               - aqua.yaml
               - .github/workflows/**
               - terraform/gcloud/**
+            linode:
+              - aqua.yaml
+              - .github/workflows/**
+              - terraform/linode/**
diff --git a/.github/workflows/wf-terraform-plan.yaml b/.github/workflows/wf-terraform-plan.yaml
index 3406b60..88df71b 100644
--- a/.github/workflows/wf-terraform-plan.yaml
+++ b/.github/workflows/wf-terraform-plan.yaml
@@ -6,6 +6,9 @@ on:
       target:
         required: true
         type: string
+    secrets:
+      LINODE_PLAN_TOKEN:
+        required: true
 
 jobs:
   plan:
diff --git a/.gitignore b/.gitignore
index ae035c5..9db25bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 .terraform/
-.env
\ No newline at end of file
+.terraform.lock.hcl
+.env
diff --git a/terraform/linode/.env.template b/terraform/linode/.env.template
new file mode 100644
index 0000000..a062d87
--- /dev/null
+++ b/terraform/linode/.env.template
@@ -0,0 +1 @@
+LINODE_TOKEN=your_linode_api_token_here
diff --git a/terraform/linode/.terraform.lock.hcl b/terraform/linode/.terraform.lock.hcl
new file mode 100644
index 0000000..e98d8da
--- /dev/null
+++ b/terraform/linode/.terraform.lock.hcl
@@ -0,0 +1,44 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/http" {
+  version     = "3.5.0"
+  constraints = "3.5.0"
+  hashes = [
+    "h1:dl73+8wzQR++HFGoJgDqY3mj3pm14HUuH/CekVyOj5s=",
+    "zh:047c5b4920751b13425efe0d011b3a23a3be97d02d9c0e3c60985521c9c456b7",
+    "zh:157866f700470207561f6d032d344916b82268ecd0cf8174fb11c0674c8d0736",
+    "zh:1973eb9383b0d83dd4fd5e662f0f16de837d072b64a6b7cd703410d730499476",
+    "zh:212f833a4e6d020840672f6f88273d62a564f44acb0c857b5961cdb3bbc14c90",
+    "zh:2c8034bc039fffaa1d4965ca02a8c6d57301e5fa9fff4773e684b46e3f78e76a",
+    "zh:5df353fc5b2dd31577def9cc1a4ebf0c9a9c2699d223c6b02087a3089c74a1c6",
+    "zh:672083810d4185076c81b16ad13d1224b9e6ea7f4850951d2ab8d30fa6e41f08",
+    "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+    "zh:7b4200f18abdbe39904b03537e1a78f21ebafe60f1c861a44387d314fda69da6",
+    "zh:843feacacd86baed820f81a6c9f7bd32cf302db3d7a0f39e87976ebc7a7cc2ee",
+    "zh:a9ea5096ab91aab260b22e4251c05f08dad2ed77e43e5e4fadcdfd87f2c78926",
+    "zh:d02b288922811739059e90184c7f76d45d07d3a77cc48d0b15fd3db14e928623",
+  ]
+}
+
+provider "registry.terraform.io/linode/linode" {
+  version     = "2.38.0"
+  constraints = "2.38.0"
+  hashes = [
+    "h1:yCrFQZrlHOrN+NUu+32AiuLqqHZTh5SDvyrgPF1hPAg=",
+    "zh:0fb598f8433b5fb36adea4f0307d3f488206fc63df3769ee240ee864741df151",
+    "zh:1374f38a71b6f4613bba3efc719bba10f7ddbe4d39ad9341b3e14d7d63f9873b",
+    "zh:48ad6d482c6e7cc91e317f7ea4f32a358cca5b74f925a82614c08aa676c928a6",
+    "zh:4cfe1beb2e2765c86beaf8831f2d5a87cfd87a736a0f71c6e04345f7e255b449",
+    "zh:5a295a2c0867c45fbde5f4a341f9db93a50b20ef02ea09e6ce9709880af3bd37",
+    "zh:5f69b1395fdb2ec9ab2e7ab66e12ddb58630c863511646c7a88c24eaf81e8280",
+    "zh:6514398a612662216752066c6bd8f2c90b434a29012b2ab23d9f85d014d923ec",
+    "zh:6d4b060af0ef557dd19a076f7b04294d7587127f0ebb544cfb0d98a2f54c8cb2",
+    "zh:b2d5362f6569145b6550fb32f2e4d4ec668ad93838c9962a524d24aef636ca27",
+    "zh:b3a6b98aeff95644d98b376f8d04f154e3c697f4a29f1d585c2ca7b4c7a282b0",
+    "zh:b602c8148a779ccb83b219531fa0e79eadd49a1725ae163dd8806174fecc6805",
+    "zh:ba58f52199af29eadb42079423209509950784c686c3c3540f5e8ea0121cb762",
+    "zh:be9d2de4dfe4e359ee645855ff0fff5cfd916ecbf71b6766cca873b686de35a5",
+    "zh:bf5be4b38a0c82d9dcf554e44a35cdbc1dd58e318a5a70ca69ce9ab5db39ba48",
+  ]
+}
diff --git a/terraform/linode/main.tf b/terraform/linode/main.tf
index e69de29..d4c0991 100644
--- a/terraform/linode/main.tf
+++ b/terraform/linode/main.tf
@@ -0,0 +1,137 @@
+terraform {
+  required_version = ">= 1.11.0"
+  backend "gcs" {
+    bucket  = "tosuke-homelab-tfstate"
+    prefix  = "linode"
+  }
+
+  required_providers {
+    linode = {
+      source  = "linode/linode"
+      version = "2.38.0"
+    }
+    http = {
+      source  = "hashicorp/http"
+      version = "3.5.0"
+    }
+  }
+}
+
+provider "linode" {
+  api_version = "v4beta"
+}
+
+data "linode_sshkeys" "ssh_keys" {}
+
+data "linode_region" "region" {
+  id = "jp-tyo-3"
+}
+
+locals {
+  border_primary     = "border-0"
+  border_secondaries = toset(["border-1"])
+  border_nodes = setunion(
+    [local.border_primary],
+    local.border_secondaries,
+  )
+
+  border_addresses = {
+    for key in local.border_nodes :
+    key => setunion(
+      flatten([
+        for key in setsubtract(local.border_nodes, [key]) :
+        linode_instance.border[key].ipv4
+      ]),
+      [linode_ipv6_range.border_ipv6.range]
+    )
+  }
+}
+
+resource "linode_instance" "border" {
+  for_each = local.border_nodes
+
+  label = each.key
+  tags  = ["border"]
+
+  region = data.linode_region.region.id
+  type   = "g6-nanode-1"
+
+  metadata {
+    user_data = base64encode(templatefile("nix-infect-cloud-init.yaml", {
+      fqdn         = "linode-${each.key}.nodes.tosuke.dev"
+      hostname     = "linode-${each.key}"
+      resolvers    = jsonencode(flatten([for resolver in data.linode_region.region.resolvers : split(",", resolver.ipv6)]))
+      nixos_infect = base64gzip(data.http.nixos_infect.response_body)
+    }))
+  }
+}
+
+data "http" "nixos_infect" {
+  url = "https://raw.githubusercontent.com/elitak/nixos-infect/master/nixos-infect"
+}
+
+resource "linode_instance_config" "border" {
+  for_each  = local.border_nodes
+  linode_id = linode_instance.border[each.key].id
+  label     = "border config"
+  booted    = true
+
+  kernel = "linode/latest-64bit"
+
+  helpers {
+    network = false
+  }
+
+  device {
+    device_name = "sda"
+    disk_id     = linode_instance_disk.border_boot[each.key].id
+  }
+  device {
+    device_name = "sdb"
+    disk_id     = linode_instance_disk.border_swap[each.key].id
+  }
+
+  interface {
+    purpose = "public"
+  }
+  interface {
+    purpose = "vlan"
+    label   = "private"
+  }
+}
+
+resource "linode_instance_disk" "border_boot" {
+  for_each        = local.border_nodes
+  linode_id       = linode_instance.border[each.key].id
+  label           = "boot"
+  size            = linode_instance.border[each.key].specs[0].disk - 512
+  image           = "linode/ubuntu24.04"
+  authorized_keys = [for key in data.linode_sshkeys.ssh_keys.sshkeys : key.ssh_key]
+}
+
+resource "linode_instance_disk" "border_swap" {
+  for_each   = local.border_nodes
+  linode_id  = linode_instance.border[each.key].id
+  label      = "swap"
+  size       = 512
+  filesystem = "swap"
+}
+
+resource "linode_ipv6_range" "border_ipv6" {
+  linode_id     = linode_instance.border[local.border_primary].id
+  prefix_length = 56
+}
+
+resource "linode_instance_shared_ips" "border_shared_primary" {
+  linode_id = linode_instance.border[local.border_primary].id
+  addresses = local.border_addresses[local.border_primary]
+}
+
+resource "linode_instance_shared_ips" "border_shared_secondaries" {
+  for_each   = local.border_secondaries
+  depends_on = [linode_instance_shared_ips.border_shared_primary]
+
+  linode_id = linode_instance.border[each.key].id
+  addresses = local.border_addresses[each.key]
+}
+
diff --git a/terraform/linode/nix-infect-cloud-init.yaml b/terraform/linode/nix-infect-cloud-init.yaml
new file mode 100644
index 0000000..370d657
--- /dev/null
+++ b/terraform/linode/nix-infect-cloud-init.yaml
@@ -0,0 +1,24 @@
+#cloud-config
+fqdn: ${fqdn}
+hostname: ${hostname}
+runcmd:
+  - rm -f /etc/systemd/network/05-eth0.network
+  - netplan generate
+  - netplan apply
+  - NIX_CHANNEL=nixos-23.05 /usr/local/bin/nixos-infect 2>&1 | tee /tmp/infect.log
+write_files:
+  - path: /usr/local/bin/nixos-infect
+    permissions: "0755"
+    encoding: gzip+base64
+    content: ${nixos_infect}
+  - path: /etc/netplan/01-netcfg.yaml
+    content: |
+      network:
+        version: 2
+        renderer: networkd
+        ethernets:
+          eth0:
+            dhcp4: false
+            accept-ra: true
+            nameservers:
+              addresses: ${resolvers}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces support for Linode by adding a new cloud-init configuration file that automates network configuration and installs the NixOS infect utility.  
- Adds a cloud-init YAML file for Linode deployments  
- Configures network settings and custom file writes with the NixOS infect utility

<details>
<summary>Files not reviewed (3)</summary>

* **terraform/linode/.env.template**: Language not supported
* **terraform/linode/.terraform.lock.hcl**: Language not supported
* **terraform/linode/main.tf**: Language not supported
</details>



state: COMMENTED
author_association: NONE
submitted_at: 2025-05-01T10:51:26Z
user: login: tosuke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T10:52:29Z

Review comments:
diff_hunk: @@ -0,0 +1,24 @@
+#cloud-config
+fqdn: ${fqdn}
+hostname: ${hostname}
+runcmd:
+  - rm /etc/systemd/network/05-eth0.network
path: terraform/linode/nix-infect-cloud-init.yaml
user: login: Copilot
body: Consider using 'rm -f' instead of 'rm' to avoid errors if the file does not exist.
```suggestion
  - rm -f /etc/systemd/network/05-eth0.network
```
created_at: 2025-05-01T10:51:26Z
author_association: NONE
diff_hunk: @@ -0,0 +1,24 @@
+#cloud-config
+fqdn: ${fqdn}
+hostname: ${hostname}
+runcmd:
+  - rm /etc/systemd/network/05-eth0.network
path: terraform/linode/nix-infect-cloud-init.yaml
user: login: tosuke
body: fixed https://github.com/tosuke-homelab/infra/pull/2/commits/9c08a04fca85085c43dd87bb277bac738c425614
created_at: 2025-05-01T10:52:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-02T12:04:24Z
body: 
## Plan Result (linode)

[CI link](https://github.com/tosuke-homelab/infra/actions/runs/15257784713)


<pre><code>No changes. Your infrastructure matches the configuration.</code></pre>






<!-- github-comment: {"Command":"plan","JobName":"plan","PRNumber":2,"Program":"tfcmt","SHA1":"5d7c309a0cc48bda41440865bcd161522ce07d5d","Target":"linode","Vars":{},"WorkflowName":"On Pull Request"} -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-26T15:44:51Z
body: 
## :white_check_mark: Apply Succeeded (gcloud)

[CI link](https://github.com/tosuke-homelab/infra/actions/runs/15257770598)



<pre><code>Apply complete! Resources: 0 added, 0 changed, 0 destroyed.</code></pre>

<details><summary>Details (Click me)</summary>

```hcl

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

```

</details>

<!-- github-comment: {"Command":"apply","JobName":"apply","PRNumber":2,"Program":"tfcmt","SHA1":"5d7c309a0cc48bda41440865bcd161522ce07d5d","Target":"gcloud","Vars":{},"WorkflowName":"On Merge Group"} -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-26T15:44:52Z
body: 
## :white_check_mark: Apply Succeeded (linode)

[CI link](https://github.com/tosuke-homelab/infra/actions/runs/15257770598)



<pre><code>Apply complete! Resources: 0 added, 0 changed, 0 destroyed.</code></pre>

<details><summary>Details (Click me)</summary>

```hcl
Acquiring state lock. This may take a few moments...
Releasing state lock. This may take a few moments...

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

```

</details>

<!-- github-comment: {"Command":"apply","JobName":"apply","PRNumber":2,"Program":"tfcmt","SHA1":"5d7c309a0cc48bda41440865bcd161522ce07d5d","Target":"linode","Vars":{},"WorkflowName":"On Merge Group"} -->
author_association: NONE