Skip to main content

Lesson: First-Boot Bootstrap and Code-Native IaC

What you'll learn

  • What cloud-init is and how it configures a fresh VM on its first boot.
  • The shape of a cloud-init user-data file (packages, users, files, commands).
  • How the lab itself uses cloud-init to turn the golden template into a working VM.
  • What Pulumi is: IaC in real programming languages instead of a config language.
  • When code-native IaC (Pulumi) is appealing versus HCL (Terraform).

Skill gained: you can read and write a basic cloud-init file like the ones the lab uses, and explain what Pulumi offers compared to Terraform.

The lesson

1. cloud-init: the first-boot bootstrap tool

When a fresh Linux cloud image boots for the very first time, it's generic — no users beyond the default, no hostname you chose, nothing installed. cloud-init is the standard tool that runs once, at first boot, reads a config you supply (called user-data), and turns that generic image into the machine you wanted: sets the hostname, creates users, installs SSH keys, installs packages, writes files, runs commands.

This is bootstrap — the third IaC job from Chapter 1. It happens automatically, before you ever log in.

  Golden image boots for the 1st time
          │
          ▼
   cloud-init reads user-data
          │
   ┌──────┼───────┬───────────┬────────────┐
   ▼      ▼       ▼           ▼            ▼
 set    create  install    write       run
 hostname users  packages   files     commands
          │
          ▼
   VM is ready — cloud-init marks "done", won't repeat on reboot

2. This is exactly how the lab works

You don't have to imagine this — the lab uses it right now. Every lab VM starts from one golden template. On first boot it's configured by a small cloud-init snippet attached per-VM (the lab calls these per-VM snippets). That snippet is the difference between "generic template" and "this is the PostgreSQL server at 10.100.100.13." It's real Infrastructure as Code you can open and read in your own environment, and it's the best starting point for this whole module.

The gateway in front of those VMs is pfSense; the VMs sit on the 10.100.100.0/24 network and are reached through the Jumpbox bastion.

3. Anatomy of a cloud-init user-data file

A user-data file is YAML and must start with the line #cloud-config (cloud-init uses it to recognize the format). Here is a realistic one:

#cloud-config
hostname: web01
fqdn: web01.example.com

# Create a login user with sudo and an SSH key
users:
  - name: omniops
    groups: [sudo]
    shell: /bin/bash
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    ssh_authorized_keys:
      - ssh-ed25519 AAAA...REPLACE_WITH_YOUR_PUBLIC_KEY

# Update the package list and install software
package_update: true
packages:
  - nginx
  - htop

# Write a file onto the new machine
write_files:
  - path: /etc/motd
    content: |
      Welcome to web01 — managed by cloud-init.

# Run shell commands at the end of first boot
runcmd:
  - systemctl enable --now nginx

Walking through the keys:

  • hostname / fqdn — name the machine.
  • users — create accounts; ssh_authorized_keys installs your public key so you can log in without a password. (Never put private keys or real passwords here — use <REDACTED> in any shared example.)
  • package_update + packages — refresh the package index and install software.
  • write_files — drop config files in place.
  • runcmd — a list of shell commands run near the end of boot, for anything the structured keys don't cover.

4. Why "runs once" matters

cloud-init tracks, per instance, whether it has already run. On normal reboots it does not repeat the user-data — it's a bootstrap tool, not an ongoing-configuration tool. That's the key distinction from Ansible:

  • cloud-init = get the machine to a usable baseline at birth, once.
  • Ansible = keep configuring and re-configuring the machine over its whole life.

If you need a change after first boot, you don't edit user-data on a running VM — you use a config-management tool, or you rebuild the VM from an updated snippet. This "rebuild from the snippet" habit is exactly the IaC mindset.

5. Pulumi: IaC in a real programming language

Terraform uses its own language, HCL. Pulumi does the same job as Terraform — provisioning infrastructure declaratively, with providers and state — but you write it in a general-purpose programming language you may already know: Python, TypeScript, Go, or C#. It's the "code, not a config language" alternative.

The same VM-and-tag idea, in Pulumi with TypeScript:

import * as aws from "@pulumi/aws";

const web = new aws.ec2.Instance("web", {
  ami:          "ami-0abcd1234",
  instanceType: "t3.micro",
  tags: { Name: "web-server", Env: "lab" },
});

export const publicIp = web.publicIp;   // an output, like Terraform's output

Notice the shape is familiar: a resource, arguments, and an exported output. Under the hood Pulumi still talks to providers and keeps state, just like Terraform.

6. Why pick Pulumi (or not)?

Appealing because you get real language features for free:

  • Loops and conditionals in normal syntax (for, if) instead of HCL's special constructs.
  • Functions, classes, and your editor's autocomplete and type-checking.
  • One language for both app code and infrastructure, if your team prefers that.

The trade-off:

  • A general-purpose language gives you more rope — you can write tangled, hard-to-review infra logic. HCL's limitations keep things flat and predictable.
  • Terraform has a larger community, more examples, and is what you'll most often meet first in the wild.
        Same job (provisioning, declarative, state, providers)
        ┌────────────────────────────────────────────────────┐
        │  Terraform  ->  HCL (a purpose-built config language)│
        │  Pulumi     ->  Python / TypeScript / Go / C#        │
        └────────────────────────────────────────────────────┘

For a beginner: learn the concepts (providers, resources, state, plan/apply) once, and they carry across both tools. The language is just the surface.

7. Putting Chapter 4 together

  • cloud-init handles first-boot bootstrap — and it's the IaC you can touch in the lab today.
  • Pulumi is a sibling of Terraform for provisioning, differing mainly in how you write it.
  • Neither replaces the others; each owns a slice of the lifecycle. Chapter 5 shows how to choose and combine all of them.

Dig deeper

Search terms

  • cloud-init user-data example #cloud-config
  • cloud-init runcmd write_files users
  • cloud-init first boot how it works
  • pulumi vs terraform for beginners
  • pulumi typescript ec2 instance example

Check yourself

  1. When does cloud-init run, and how many times per instance?
  2. What must the first line of a cloud-config user-data file be, and why?
  3. Name three things a cloud-init user-data file can set up on a fresh VM.
  4. How does the lab use cloud-init, and why is that useful for learning IaC?
  5. What does Pulumi do differently from Terraform, and what is one trade-off of that difference?