SPDX-License-Identifier | path | slug | date | title | short_description | tags | author | author_link | author_img | author_description | language | available_languages | header_img | cta | |||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
MIT |
/tutorials/howto-hcloud-kickstart-centos |
howto-hcloud-kickstart-centos |
2020-04-15 |
How-to: Kickstart CentOS servers on Hetzner Cloud |
This how-to describes how to Kickstart a CentOS 7 server using Ansible. |
|
Jeroen Hoekx |
en |
|
header-7 |
cloud |
Hetzner Cloud provides official pre-built CentOS images.
The installimage
script can be used to provision custom images.
A much simpler approach exists that also grants the same control over package selection and disk partitioning without any external resources or manual image creation steps.
CentOS and other Red Hat Enterprise Linux derivatives support a standard way to achieve this through Kickstart installations.
This how-to describes how to Kickstart a CentOS 7 server using Ansible.
Prerequisites
- Hetzner Cloud API Token
- Visit Hetzner Cloud Console at https://console.hetzner.cloud, select your project, and create a new API Token.
- Ansible 2.9 is installed and you have basic knowledge about it
hcloud-python
installed (pip install hcloud
)
Ansible requires systems to be in the inventory before they can be configured.
Create a file hosts
with a group containing the servers to Kickstart:
[hcloud_servers]
<your_host>
Since the VM will be rebooted in rescue mode during the install process, disable host key checking in ansible.cfg
:
[defaults]
host_key_checking = False
inventory = hosts
For production deployments, one improvement could be to revoke the rescue mode SSH host key after launching the Kickstart.
Export your Hetzner Cloud API token so it is available in the environment:
$ export HCLOUD_TOKEN=<your-token>
Finally, make your public SSH key available so it can be injected in the system.
Create group_vars/all.yml
:
ssh_keys:
- name: <a-name-for-your-key>
key: "<your-public-ssh-key>"
If there is already an SSH key in your Hetzner Cloud project, be sure to use the same name and key.
Create a playbook hcloud-kickstart.yml
and create a server as explained in the how-to on hcloud Ansible modules.
First register the SSH key(s):
- name: Provision Hetzner Cloud Servers
hosts: hcloud_servers
user: root
gather_facts: no
vars:
hcloud_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
tasks:
- name: Create SSH keys
local_action:
module: hcloud_ssh_key
api_token: "{{ hcloud_token }}"
name: "{{ item.name }}"
public_key: "{{ item.key }}"
loop: "{{ ssh_keys }}"
run_once: yes
Then check if the server already exists and ensure it is created:
- name: Get VM information
local_action:
module: hcloud_server_info
api_token: "{{ hcloud_token }}"
name: "{{ inventory_hostname_short }}"
register: info_vm
- name: Create VM
local_action:
module: hcloud_server
api_token: "{{ hcloud_token }}"
name: "{{ inventory_hostname_short }}"
location: fsn1
server_type: cx11-ceph
image: centos-7
ssh_keys: "{{ ssh_keys|map(attribute='name')|list }}"
register: create_vm
Why the explicit check if the server exists?
We want this playbook to be idempotent, meaning that it can be run multiple times without affecting any servers that are already deployed.
The hcloud_server
module has built-in safeguards that will prevent it from trying to recreate an existing server, but it is not possible to know if a server was newly created based on its return values.
Later on in the playbook we want to make sure that only brand new servers are being kickstarted since this is a rather destructive operation.
I prefer that it does not happen when I change the size of my server.
Finally update the in-memory inventory with the IP address of the server.
In case the server is new, also add the server to the unprovisioned
group.
This group will be used later on to target only the servers we need to Kickstart.
- name: Register the ip address
add_host:
hostname: '{{ hostvars[item].inventory_hostname }}'
ansible_host: '{{ hostvars[item].info_vm.hcloud_server_info[0].ipv4_address }}'
when: hostvars[item].info_vm.hcloud_server_info|count == 1
with_items: "{{ ansible_play_hosts }}"
- name: Register the new ip address
add_host:
hostname: '{{ hostvars[item].inventory_hostname }}'
ansible_host: '{{ hostvars[item].create_vm.hcloud_server.ipv4_address }}'
groups:
- unprovisioned
when: hostvars[item].info_vm.hcloud_server_info|count == 0
with_items: "{{ ansible_play_hosts }}"
add_host
requires the use of with_items
to loop over all hosts because it runs only once per play.
Each server that needs to be Kickstarted is now part of the unprovisioned
group.
First boot them in rescue mode. Add this new play to the existing hcloud-kickstart.yml
playbook:
- name: Kickstart the system
hosts: unprovisioned
user: root
gather_facts: no
vars:
hcloud_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
tasks:
- name: Restart in rescue mode
local_action:
module: hcloud_server
api_token: "{{ hcloud_token }}"
name: "{{ inventory_hostname_short }}"
ssh_keys: "{{ ssh_keys|map(attribute='name')|list }}"
rescue_mode: linux64
state: restarted
- name: Wait for the rescue system to be available
wait_for_connection:
timeout: 60
The CentOS installer will search any filesystem labeled OEMDRV
for a file called ks.cfg
.
So we need to wipe the existing disk, create the required filesystem and make the server boot the CentOS installer.
Create a script prepare-kickstart.sh
:
#!/bin/bash
set -e
MIRROR=http://mirror.hetzner.de/centos/7/os/x86_64
### Wipe all partitions on the existing disk
wipefs --all --force /dev/sda
### Create and partition to allow kickstarts
parted -s /dev/sda mklabel msdos
parted -s /dev/sda mkpart primary 4M 200M
mkfs.ext4 -q -L OEMDRV /dev/sda1
mount /dev/sda1 /boot
### Download kernel and installer initrd to kickstart partition
curl -o /boot/vmlinuz $MIRROR/isolinux/vmlinuz
curl -o /boot/initrd.img $MIRROR/isolinux/initrd.img
### Install and configure Grub to load the installer initrd
grub-install --no-floppy /dev/sda
cat >/boot/grub/grub.cfg <<EOF
set default=0
set timeout=5
menuentry "CentOS Kickstart" {
set root=(hd0,1)
linux /vmlinuz
initrd /initrd.img
}
EOF
Upload and run this script:
- name: Upload the partitioning script
template:
src: prepare-kickstart.sh
dest: prepare-kickstart.sh
- name: Wipe the drive and prepare for Kickstart
command: /bin/bash prepare-kickstart.sh
Then create the Kickstart in centos-7.ks
.
Update the root password. Preferably use the encrypted variant as described in the Kickstart reference.
network --bootproto=dhcp --hostname={{ inventory_hostname_short }}
### Use the Hetzner mirror for fast installations
url --url="http://mirror.hetzner.de/centos/7/os/x86_64"
repo --name="mirror-updates" --baseurl=http://mirror.hetzner.de/centos/7/updates/x86_64
### Enable the firewall and allow only SSH
services --disabled=ip6tables,iptables,netfs,rawdevices --enabled=network,sshd
firewall --enable --ssh
### Install in text mode and reboot after installation
install
text
skipx
reboot
firstboot --disable
### Do not overwrite data on any data disk
ignoredisk --only-use=sda
### Set locale and timezone
keyboard --vckeymap=us
lang en_US.UTF-8
timezone Etc/UTC --isUtc --nontp
### Set a root password
auth --enableshadow --passalgo=sha512
rootpw <your-password>
### Partition the disk
zerombr
clearpart --all --initlabel
bootloader --location=mbr --boot-drive=sda
### Use LVM
part /boot --fstype=xfs --size=500 --asprimary --fsoptions="defaults" --ondrive=sda
part pv.01 --size=1 --grow --ondrive=sda
volgroup vg_{{ inventory_hostname_short }}_root pv.01 --pesize=32768
logvol / --fstype=xfs --name=lv_root --vgname=vg_{{ inventory_hostname_short }}_root --size=4096 --fsoptions="defaults,relatime"
logvol /var --fstype=xfs --name=lv_var --vgname=vg_{{ inventory_hostname_short }}_root --size=4096 --fsoptions="defaults,relatime"
logvol swap --fstype=swap --name=lv_swap --vgname=vg_{{ inventory_hostname_short }}_root --size=1024 --fsoptions="defaults"
### Do not install packages that are not required
%packages --nobase
@core --nodefaults
libselinux-python
-*-firmware
-NetworkManager*
-alsa*
-audit
-cron*
-iprutils
-kexec-tools
-microcode_ctl
-plymouth*
-postfix
-rdma
-tuned
-wpa_supplicant
%end
%post
### Install the SSH key
mkdir -m0700 /root/.ssh/
cat <<EOF >/root/.ssh/authorized_keys
{% for key in ssh_keys %}
{{ key.key }}
{% endfor %}
EOF
chmod 0600 /root/.ssh/authorized_keys
restorecon -R /root/.ssh/
### Enable tmpfs
systemctl enable tmp.mount
%end
Finally template the Kickstart and reboot:
- name: Template the Kickstart
template:
src: centos-7.ks
dest: /boot/ks.cfg
- name: Wait for the Kickstart to finish
reboot:
The server is now available for further configuration through Ansible:
- name: Access the newly Kickstarted system
hosts: hcloud_servers
user: root
tasks:
- ping:
Run the playbook:
$ ansible-playbook hcloud-kickstart.yml
Since we've taken care of creating an idempotent playbook, rerunning the playbook will not Kickstart a server again.
We've just created a fully customized CentOS 7 server in less than 100 lines of Ansible YAML. It was up and running in a few minutes.
An Ansible role containing these tasks is available on Github, along with a ready-to-run example.
Kickstarting CentOS 8 servers is a trivial extension by changing the CentOS versions used throughout the various files and adapting the Kickstart configuration. The same steps are able to Kickstart Hetzner Dedicated Servers as well.