Skip to content

Latest commit

 

History

History
386 lines (296 loc) · 11.9 KB

File metadata and controls

386 lines (296 loc) · 11.9 KB
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.
Ansible
Hetzner Cloud
hcloud
CentOS
Jeroen Hoekx
en
en
header-7
cloud

Introduction

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
  • Ansible 2.9 is installed and you have basic knowledge about it
  • hcloud-python installed (pip install hcloud)

Step 1 - Prepare Ansible

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.

Step 2 - Create the server

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.

Step 3 - Kickstart

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:

Step 4 - Further configuration

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.

Conclusion

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.

License: MIT