Creating a Reproducible Remote Desktop with Terraform

Desk Dance

© Photo by Nat Weerawong on Unsplash

© Photo by Nat Weerawong on Unsplash

Article from Issue 303/2026
Author(s):

The Terraform orchestration tool lets you create a remote desktop configuration that you can easily bring up and tear down, which could save you money and add flexibility to your environment.

The concept of a remote desktop has been around for a long time. The idea is to store a regular desktop configuration on some remote server and let the user connect to it using a remote desktop tool like RDP or VNC. One common approach is to set up a physical server with beefy hardware and then provide a connection to it from a thin client. This option has many downsides, such as the possibility of hardware issues or electrical outages.

An alternative approach is to treat your remote desktop the same way you treat cloud resources – that is, building it on demand and using it only when you actually need it. This solution is certainly cheaper than running RDP 24/7. Plus, the fact that the desktop only exists when you need it also reduces the possibility of a cyberattack. An added advantage is that building on demand lets you scale vertically within minutes.

I wanted a remote desktop that could be quickly created and destroyed, but with a state that persisted between the sessions. Some more specific requirements included:

  • It must be flexible in terms of location and server size, to the greatest extent allowed by the cloud service provider.
  • The desktop state must be captured in an image taken at shutdown, so the desktop will appear as you left it the next time this server is started.
  • User data should persist outside the server on a connected hard drive. This user data will be encrypted at rest.
  • There should be a bastion server to connect to the remote desktop – this way, the remote desktop server is not exposed to the Internet directly.

As an experiment, I will use a Raspberry Pi – of course, you could use another system, but I love the idea of having the setup on my Raspberry Pi. The latest Raspberry Pi OS (formerly Raspbian) should do fine; of course, feel free to adapt the commands accordingly. I will use Hetzner for hosting because it is inexpensive and they have a data center located close to me (Finland). Hetzner also supports automation via Terraform.

The Terraform declarative orchestration tool lets you declare what resources you'd like to create (i.e., "I want to have a server connected to the network, with storage, etc."). You provide the description, and Terraform figures out how to create those resources with a provider of your choice.

If you change your list of requirements and run it again, Terraform will compare the current state of the infrastructure with your desired result and only create additional resources (or modify existing resources) that are needed.

This example shows how to put Terraform to work on a simple and practical solution. Of course, you could do something similar using a Bash scripts, but why not make use of a powerful and versatile configuration tool that will be easier to adapt if you migrate to a different provider?

Installing

Start by installing Terraform, as shown in Listing 1. (See the Terraform website [1] for more on installing Terraform with different operating systems.)

Listing 1

Installing Terraform

# Install prerequisites
sudo apt-get update
sudo apt-get install -y gnupg curl jq
# Add HashiCorp GPG key
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
# Add the official HashiCorp repo
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com \
  $(. /etc/os-release && echo $VERSION_CODENAME) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Install the latest Terraform
sudo apt-get update
sudo apt-get install -y terraform
# Verify that Terraform has been installed
terraform -version

To automate the setup, you'll need an API token from Hetzner. Create your own account on hetzner.com. Once everything is set up, go to the Hetzner Cloud Console [2].

Your console contains all your resources (cloud VMs, firewalls, networks, and so forth) split into "projects." Create a new project and open it. On the left side, you'll see a list of your resources (Servers, Volumes, Floating IPs). At the bottom of the list is a Security button. Click the Security button, and you will see SSH keys, S3 credentials, and other options. Click API tokens, and you'll arrive at a page where you can click Generate API token (Figure 1). Enter an appropriate description and make sure you select Read & Write under permissions. Copy the key.

Figure 1: Make sure to store your API token, because you will only see it once.

Initial Setup

The next step is to start setting things up. Create a directory called office. All the work will happen in this directory. Start with the basic Terraform and Hetzner setup. Create a providers.tf file with the content in Listing 2.

Listing 2

providers.tf

terraform {
  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "~> 1.44" # Or latest stable
    }
  }
  backend "local" {
    path = "./out/terraform.tfstate"
  }
}
provider "hcloud" {
  token = var.hcloud_token
}

Listing 2 tells Terraform that it should use Hetzner as a provider. You will also want to store the Terraform state in the out folder in a terraform.tfstate file. The state file basically stores information about resources that Terraform creates. There is a bit more to it, but you can refer to the Terraform documentation [3] for further info.

Create a variables.tf file, where you can store variables used in the configuration (Listing 3).

Listing 3

variables.tf

variable "hcloud_token" {
  type        = string
  description = "Hetzner API token"
  sensitive   = true   # This prevents it from being output in the terminal
}
variable "image_id" {
  type        = string
  default     = ""
  description = "Optional snapshot ID for virtual office server"
}
variable "bastion_server_type" {
  type        = string
  default     = "cx23"    # Note that we can provide default reasonable value. This can be overridden consequently.
  description = "Hetzner server type for bastion (e.g., cpx11)"
}
variable "office_server_type" {
  type        = string
  default     = "cx23"
  description = "Hetzner server type for office (e.g., cpx41)"
}
variable "location" {
  type        = string
  default     = "hel1"
  description = "Hetzner location slug (e.g., hel1)"
}
variable "up" {
  type = bool
  default = true
  description = "Whether to create servers or destroy them."
}

Don't put your token in a .tf file (in case this code ends up in a repository). Instead, create an office.tfvars file and put your token there (Listing 4).

Listing 4

office.tfvars

hcloud_token="<your-token>"

Now it is time to test the setup. You aren't done yet, but it is a good idea to verify that everything is OK. Make sure you are still in the office directory and run the commands in Listing 5.

Listing 5

Testing Setup

terraform init -var-file=office.tfvars
terraform plan -var-file=office.tfvars

After the first command, you should see the message "Terraform has been successfully initialized". During this init process, Terraform downloads the extra files that it needs to be able to work for a given configuration (listed in providers.tf).

After the second command, you should see "No changes. Your infrastructure matches the configuration." In this case, the configuration is pretty much empty – you haven't defined any resources yet, thus the infrastructure (nonexistent) matches the empty setup.

I should mention that you can name your files differently as long as they have the .tf extension. You can also move resources between them. The file structure described in this article works for me, but you could even put it all into one main.tf if it makes more sense or is easier to maintain. Of course, the .tfvars file still has to be separate.

SSH Keys

You can ask Terraform to generate your SSH keys, to avoid having to manually do this; after all, this is an automated setup. Before I show how to do that, I will take a quick detour to describe how Terraform creates things. To put it very simply, you just need to list what you want to create – an SSH key, a server, a network. Each item you create is called a resource and each resource has a type (key/server/etc.), a name (bastion server, personal ssh key, etc.) and a number of parameters. For a server, the parameters might include the type and location. For an SSH key, a parameter might be the algorithm used to generate it. For a local file, a parameter might be the content that you want to put in it.

When you run the plan command, Terraform reviews the previous infrastructure it has created (or none, if you are running it for the first time), and compares that to all the resources you've described. It can then deduce:

  • Which new resources need to be created
  • Which resources have to be updated (e.g., a cloud disk can be "increased" in size)
  • Which resources have to be replaced (e.g., if you can decide to move your server to another data center, an old server would be destroyed and new one created)
  • Which resources have to be deleted.

Terraform's goal is to make your real infrastructure match all the resource descriptions that you list in your .tf files.

After this review, Terraform will print out its plan and quit. In order to actually apply changes, you will need to run the apply command. Terraform will then undertake the same planning process it used with the plan command, but in the end, it will ask you to approve your changes manually.

Listing 6 shows the ssh.tf file. I have commented on every resource you need.

Listing 6

ssh.tf

# This is the actual SSH key, consisting of private key and public key
resource "tls_private_key" "ssh_key" {
  algorithm = "ED25519"
}
# This resource persists the generated SSH private key into a local file
resource "local_file" "private_key" {
  content  = tls_private_key.ssh_key.private_key_openssh
  filename = "${path.module}/out/ssh/id_ed25519"
  file_permission = "0600"
}
# This resource persists the generated SSH public key into a local file
resource "local_file" "public_key" {
  content  = tls_private_key.ssh_key.public_key_openssh
  filename = "${path.module}/out/ssh/id_ed25519.pub"
}
# This part is very important. When you create a server on Hetzner, you can specify to use the SSH key instead of password, but you need to upload your key first.
# This resource represents an uploaded key -- when created by Terraform, the actual key gets persisted on Hetzner's side.
resource "hcloud_ssh_key" "default" {
  name       = "terraform-key"
  public_key = tls_private_key.ssh_key.public_key_openssh
}
# This resource represents a local file with the SSH config, so that you can use it for SSH-ing into the servers.
# Notice that the bastion host refers to an IP address from somewhere else -- I will introduce that in a moment.
# As for the "office" host, it has a pre-defined IP that I'll configure elsewhere.
# Don't mind the "count" part -- it will be explained later; but generally, this means that we do NOT create this resource
# IF "up" variable is false or not defined.
resource "local_file" "ssh_config" {
  count = var.up ? 1 : 0
  content  = <<-EOT
    Host bastion
        HostName ${hcloud_server.bastion[0].ipv4_address}
        User root
        IdentityFile ${local_file.private_key.filename}
        IdentitiesOnly yes
    Host office
        HostName 10.0.1.10
        User root
        IdentityFile ${local_file.private_key.filename}
        IdentitiesOnly yes
        ProxyJump bastion
  EOT
  filename = "${path.module}/out/ssh/config"
}

You are not done yet, though, so do not run Terraform yet. It is time to set up the network and servers.

Buy this article as PDF

Download Article PDF now with Express Checkout
Price $2.95
(incl. VAT)

Buy Linux Magazine

Related content

  • Appointment Scheduler

    If you have a business that requires customers to make an appointment in advance for services, letting them request the appointment via Easy!Appointments can free up your phone line.

  • Sky Server

    Are you ready to get started with the cloud? Microsoft's Azure Cloud Services provides easy access to an Ubuntu virtual machine.

  • News

    In the news: System76 Developing a New Desktop Environment; Hetzner Opens New Location in the USA; KDE Plasma 5.24 Introduces Fingerprint Reader Support; Ubuntu 21.10 Released and Finally Includes Gnome 40; Hive Ransomware Hitting Linux and FreeBSD Systems; and SUSE Reaches Beyond the Edge with SUSE Linux Enterprise Micro 5.1.

  • Honeypots

    Use Cowrie as a honeypot to capture attack data and learn more about your attacker's methods.

  • PiVPN

    With PiVPN, a system administrator can build a small private network and let end users attach to it themselves – and use it for running games.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News