This post is a follow-up of Terraforming a Linode: hello world.

In a future post, we will continue from here by using Ansible to install and set up Miniflux in our new Linode.

Before we extensively use Ansible to configure our VPS instance, first let’s set up a basic integration between Terraform and Ansible.

First of all, here’s an overview of where I stopped last time. There were a couple of lightweight modifications since then. I’ll explain some of them below.

% cat variables.tf
variable "github_username" {
  type    = string
  default = "thiagowfx"
}

variable "linode_hostname" {
  type    = string
  default = "coruscant"
}

variable "linode_region" {
  type    = string
  default = "eu-central"
}

All variables were moved to a variables.tf file. This is to follow standard terraform conventions / recommendations for module structures. Furthermore, it becomes easier to manage variables when they are all stored in a single place.

The main module file now looks like this:

% cat main.tf
terraform {
  required_providers {
    http = {
      source = "hashicorp/http"
    }
    linode = {
      source = "linode/linode"
    }
  }
}

provider "linode" {}

data "http" "github_keys" {
  url = "https://api.github.com/users/${var.github_username}/keys"
}

locals {
  keys = jsondecode(data.http.github_keys.response_body)[*].key
}

resource "linode_instance" "nanode" {
  type             = "g6-nanode-1"
  image            = "linode/alpine3.19"
  label            = var.linode_hostname
  region           = var.linode_region
  authorized_keys  = local.keys
  backups_enabled  = "false"
  booted           = "true"
  watchdog_enabled = "true"
}

I removed the token from the linode provider. Now it is supplied via the LINODE_TOKEN environment variable. In order to automatically populate that variable, I use direnv. There’s an .envrc file that provides its value, like so:

#!/bin/sh
# terraform init

export LINODE_TOKEN="my-token-here"

I also created a repository for this project: https://github.com/thiagowfx/knol. That’s enough for preliminaries, now let’s go back to Ansible.

The first component we’ll need is an Ansible inventory file, containing the IP address of the host we’ll manage. It could look like this:

[all]
1.2.3.4 ansible_user=root

…wherein 1.2.3.4 is the IP address of our VPS.

That said, due to the fact the VPS instance is created dynamically, maintaining that IP address manually would be tedious. Therefore, let’s have Terraform manage it.

We can do so with a local_file. Heck, we could even use a template_file, however it would be overkill as there are only two simple lines in our inventory at this point. A local_file is created upon terraform apply and deleted upon terraform destroy. Therefore it doesn’t even need to be tracked by our VCS:

resource "local_file" "ansible_inventory" {
  content  = <<-EOF
[all]
${linode_instance.nanode.ip_address} ansible_user=root
EOF
  filename = "inventory.ini"
  file_permission = "0644"
}

Once we run terraform (plan + apply), an inventory.ini file should be created with the above contents.

Because the IP address is ephemeral and dynamic, we should have a straightforward way to see its value. A terraform output is perfect for that:

% cat outputs.tf
output "ip_address" {
  value = linode_instance.nanode.ip_address
}

Later on (after terraforming) we will be able to use terraform output to see the server IP address.

We have the inventory file. Now we need a playbook. A playbook contains a sequence of tasks to be applied to our server.

Let’s start with a basic playbook that just installs and starts nginx:

---
- hosts: all
  tasks:
    - name: Install the web server (nginx)
      community.general.apk:
        name: nginx
        state: present
    - name: Start the web server
      service:
        name: nginx
        state: started

Save this to a playbook.yml file.

After terraforming, we should now be able to run ansible:

% ansible-playbook -i inventory.ini playbook.yml

In order to make this setup more ergonomic, let’s create a Makefile:

TERRAFORM := terraform

all: terraform ansible

ansible:
	ansible-playbook -i inventory.ini playbook.yml

terraform:
	$(TERRAFORM) init
	$(TERRAFORM) plan
	$(TERRAFORM) apply

clean:
	$(TERRAFORM) destroy

.PHONY: all ansible terraform clean

Then we can just run make terraform or make ansible for granular steps. Or just make to run everything in the right order.

I extracted the terraform binary to its own variable because it facilitates the use of OpenTofu (a fork) in lieu of terraform.

And that’s it for today! In a future post, we’ll look into extending our Ansible usage to fully bootstrap Miniflux on the server.