Configure Ubuntu with Cloud Init
Suppose you have a new computer, with no OS installed. Before you can actually work on it, you definitely need an OS (let’s say, Ubuntu) with a specific set of packages: maybe a Chromium-based browser, Docker, and git, to say a few.
Cloud-init introduction
The first option you have is cloning your entire hard disk, but this means bringing all the garbage you have in your current computer into the new one. If you want to start from a plain Ubuntu, I suggest you a way to configure it very easily: Cloud-init.
Cloud-init is an open source initialization tool that was designed to make it easier to get your systems up and running with a minimum of effort, already configured according to your needs.
In this post, I will not deepen all the aspects of Cloud-init, you can find a lot of examples and the available modules in the official documentation.
Run cloud-init
After installing Ubuntu, cloud-init has already run at least once and will not execute again automatically. Ubuntu determines this based on certain “artifacts” that Cloud-init leaves in the filesystem. To remove these artifacts and trick Cloud-init into thinking it has not been executed yet, run:
1
sudo cloud-init clean --logs
Our brand-new Ubuntu needs:
- The APT repositories (if necessary)
- The packages to install
- Custom configurations
So, create a YAML file that adheres to the Cloud-init format, like this one (user-data.yaml):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#cloud-config
apt:
preserve_sources_list: false
sources:
vscode:
source: "deb [arch=amd64] https://packages.microsoft.com/repos/code stable main"
keyid: "BC528686B50D79E339D3721CEB3E94ADBE1229CF"
docker:
# !!! IMPORTANT !!! the source depends on the ubuntu version, so must be changed on a new major version
source: "deb [arch=amd64] https://download.docker.com/linux/ubuntu noble stable"
keyid: "9DC858229FC7DD38854AE2D88D81803C0EBFCD88"
virtualbox:
# !!! IMPORTANT !!! the source depends on the ubuntu version, so must be changed on a new major version
source: "deb [arch=amd64] https://download.virtualbox.org/virtualbox/debian noble contrib"
keyid: "B9F8D658297AF3EFC18D5CDFA2F683C52980AECF"
package_reboot_if_required: true
package_update: true
package_upgrade: true
packages:
- curl
- ca-certificates
- thunderbird
- snap:
- [kubectl, --classic]
- [helm, --classic]
- code
- telegram-desktop
- vivaldi
# --- BEGIN Docker ---
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
# --- END Docker ---
- git-all
- postman
- virtualbox-7.2
- wireshark
# --- BEGIN LibreOffice ---
- libreoffice-calc
- libreoffice-writer
- libreoffice-gnome
# --- END LibreOffice ---
- xclip
- trash-cli
- remmina
# --- BEGIN virtualization ---
- virt-viewer
- qemu-kvm
- libvirt-daemon-system
- libvirt-clients
- bridge-utils
- virt-manager
# --- END virtualization ---
runcmd:
# Docker - add 1000 user to docker group
- |
DEFAULT_USER=$(id -un 1000)
groupadd docker
usermod -aG docker "$DEFAULT_USER"
# Libvirt and KVM - add 1000 user to libvirt and kvm groups
- |
DEFAULT_USER=$(id -un 1000)
adduser "$DEFAULT_USER" libvirt
adduser "$DEFAULT_USER" kvm
write_files:
# Custom aliases
- content: |
alias clip='xclip -selection clipboard'
alias rm='trash'
owner: maluz:maluz
path: /home/maluz/.bash_aliases
permissions: '0744'
defer: true
# git config
- content: |
[user]
email = marco.luzzara@hotmail.it
name = Marco Luzzara
[alias]
hist = git log --all --decorate --oneline --graph
git = !git
sua = status -uall
[init]
defaultBranch = main
owner: maluz:maluz
path: /home/maluz/.gitconfig
permissions: '0664'
defer: true
# kubectl, helm, and docker bash completion
- content: |
source <(kubectl completion bash)
source <(helm completion bash)
source <(docker completion bash)
path: /home/maluz/.bashrc
append: true
owner: maluz:maluz
defer: true
# Templates
- content: ''
path: '/home/maluz/Templates/Empty File'
owner: maluz:maluz
defer: true
You can update the packages section with the packages you want to install, and sources with the APT repositories to add. While it is straightforward to install a new package, adding an APT repository requires some efforts:
sourceproperty: it contains the repository endpoint. You can always find this line in the package documentation. For example, Docker repository is:deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable. In the Cloud-init file, I have omitted thesigned-byproperty because Cloud-init retrieves that from thekeyidproperty.keyidproperty: is the GPG key identifier that Cloud-init will use to verify and import the repository’s signing key. First, request the GPG key from the endpoint, then pipeline the output togpg --show-key. For example, Docker GPG key is returned from:1
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --show-key
The string 40-character long is the value of
keyid.
As for the runcmd section, you should reserve a line for each configuration procedure. To execute multiple commands for a single configuration procedure, use YAML multiline string.
Cloud-init is always executed as root, so you do not need to prepend sudo to the commands. For this reason, the runcmd scripts have been adapted to modify the default user (1000) environment.
The write_files section can be used to write/append content to a file, such as ~/.bash_aliases, which contains custom aliases. Make sure to set the adapt the owner and path according to the default user name. defer: true is important because it allows to
Defer writing the file until ‘final’ stage, after users were created, and packages were installed.
If in the cloud-init logs (/var/log/cloud-init.log) you see a message like Skipping module named cc_write_files, no/empty 'write_files' key in configuration, try removing the defer option.
Finally, you can run cloud-init modules with:
1
2
3
4
5
6
7
8
9
# Install the apt repositories
sudo cloud-init single --name cc_apt_configure --file user-data.yaml
# install the specified packages
sudo cloud-init single --name cc_package_update_upgrade_install --file user-data.yaml
# run the runcmd scripts
sudo cloud-init single --name cc_runcmd --file user-data.yaml
sudo cloud-init single --name scripts_user --file user-data.yaml
# write files
sudo cloud-init single --name cc_write_files --file user-data.yaml
Then reboot.
To re-run a module, the cloud-init clean is mandatory.
Comments