3.2. Cloud-init

Use cloud-init to initialize your VM

In this section we will use cloud-init to initialize a Fedora Cloud1 VM. Cloud-init is the de-facto standard for providing startup scripts to VMs.

Cloud-init is widely adopted. Some of the known users of cloud-init are:

  • Ubuntu
  • Arch Linux
  • CentOS
  • Red Hat
  • FreeBSD
  • Fedora
  • Gentoo Linux
  • openSUSE

Supported data sources

OpenShift Virtualization supports the cloudInitNoCloud and cloudInitConfigDrive data source methods.

cloudInitNoCloud data source

cloudInitNoCloud is a flexible data source to configure an instance locally. It can work without network access but can also fetch configuration from a remote server. The relevant configuration of a cloudInitNoCloud data source in a VM looks like this:

volumes:
  - name: cloudinitdisk
    cloudInitNoCloud:
      userData: "#cloud-config"
[...]

This volume must be referenced after the VM disk in the spec.template.spec.domain.devices.disks section:

- name: cloudinitdisk
  disk:
    bus: virtio

Using the cloudInitNoCloud attribute gives us the following possibilities to provide our configuration:

  • userData: inline cloudInitNoCloud configuration in the user data format
  • userDataBase64: cloudInitNoCloud configuration in the user data format as a base64-encoded string
  • secretRef: reference to a K8s secret containing cloudInitNoCloud userdata
  • networkData: inline cloudInitNoCloud network data
  • networkDataBase64: cloudInitNoCloud network data as a base64-encoded string
  • networkDataSecretRef: reference to a K8s secret containing cloudInitNoCloud network data

The most convenient for the lab is to use the cloudInitNoCloud user data method.

The user data format recognizes the following headers. Depending on the header, the content is interpreted and executed differently. For example, if you use the #!/bin/sh header the content is treated as an executable shell script.

User data formatContent headerExpected content-type
Cloud config data#cloud-configtext/cloud-config
User data script#!text/x-shellscript
Cloud boothook#cloud-boothooktext/cloud-boothook
MIME multi-partContent-Type: multipart/mixedmultipart/mixed
Cloud config archive#cloud-config-archivetext/cloud-config-archive
Jinja template## template: jinjatext/jinja
Include file#includetext/x-include-url
Part handler#part-handlertext/part-handler

If you want to combine multiple items, you can do that using #cloud-config-archive.

Here is an example how to configure multiple items:

volumes:
  - name: cloudinitdisk
    cloudInitNoCloud:
      userData: |
        #cloud-config-archive
        - type: "text/cloud-config"
          content: |
            timezone: Europe/Zurich
        - type: "text/x-shellscript"
          content: |
            #!/bin/sh
            yum install -y nginx        

Check cloud-init’s network configuration sources for more information about the network data format. Be aware that there is a different format used whenever you use cloudInitNoCloud or cloudInitConfigDrive.

cloudInitConfigDrive data source

The cloudInitConfigDrive data source works identically to the cloudInitNoCloud data source by defining:

volumes:
- name: cloudinitdisk
  cloudInitConfigDrive:
    userData: "#cloud-config"
[...]

The volume must be referenced after the VM disk in the spec.template.spec.domain.devices.disks section:

- name: cloudinitdisk
  disk:
    bus: virtio

When using cloudInitConfigDrive, the network data has to be in the OpenStack Metadata Service Network format.

Task 3.2.1: Create a cloud-init config secret

We are now going to create a Fedora Cloud VM and provide a cloud-init userdata configuration to initialize our VM.

First, we are going to create a secret containing our configuration: On the top left of OpenShift’s web console, make sure you change into OpenShift’s Administrator view, then click on Workloads and Secrets in the menu on the left. Make sure you’re in your own namespace, then click on Create on the right side of the page and choose Key/value secret.

Secret creation

Fill in the following information:

Secret name: lab04-cloudinit
Key: userdata
Value:

  #cloud-config
  password: kubevirt
  chpasswd: { expire: False }

Click on Create to finally create the secret.

Switch back into the Virtualization view.

Task 3.2.2: Create a VirtualMachine using cloud-init

Back in the Virtualization view, create a new VM:

  1. Click on VirtualMachines in the left menu, then on the Create button and choose With YAML
  2. Replace the already filled-in content with the following:
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: lab04-cloudinit
spec:
  runStrategy: Halted
  template:
    metadata:
      labels:
        kubevirt.io/domain: lab04-cloudinit
    spec:
      domain:
        devices:
          disks:
            - name: containerdisk
              disk:
                bus: virtio
          interfaces:
          - name: default
            masquerade: {}
        resources:
          requests:
            memory: 2Gi
      networks:
      - name: default
        pod: {}
      volumes:
        - name: containerdisk
          containerDisk:
            image: quay.io/containerdisks/fedora:43

Extend the VM configuration to include our secret lab04-cloudinit we created earlier.

Solution

Your VirtualMachine configuration should look like this:

apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: lab04-cloudinit
spec:
  runStrategy: Halted
  template:
    metadata:
      labels:
        kubevirt.io/domain: lab04-cloudinit
    spec:
      domain:
        devices:
          disks:
            - name: containerdisk
              disk:
                bus: virtio
            - name: cloudinitdisk
              disk:
                bus: virtio
          interfaces:
          - name: default
            masquerade: {}
        resources:
          requests:
            memory: 2Gi
      networks:
      - name: default
        pod: {}
      volumes:
        - name: containerdisk
          containerDisk:
            image: quay.io/containerdisks/fedora:43
        - name: cloudinitdisk
          cloudInitNoCloud:
            secretRef:
              name: lab04-cloudinit

Finally, click Create to create the VM.

Task 3.2.3: Log in to the VirtualMachine

Start the VM and verify whether logging in with the defined user and password works as expected.

Solution

Connect to the console and log in as soon as the prompt shows up. Either use OpenShift’s web console to do so or execute:

virtctl console lab04-cloudinit --namespace lab-<username>

You might also see the cloud-init execution messages in the console log during startup:

[...]
[  OK  ] Started systemd-logind.service - User Login Management.
[  147.604999] cloud-init[796]: Cloud-init v. 23.4.4 running 'init-local' at Fri, 06 Sep 2024 11:42:25 +0000. Up 147.17 seconds.
         Starting systemd-hostnamed.service - Hostname Service...
[...]

[  210.442576] cloud-init[973]: Cloud-init v. 23.4.4 finished at Fri, 06 Sep 2024 11:43:29 +0000. Datasource DataSourceNoCloud [seed=/dev/vdb][dsmode=net].  Up 210.34 seconds
[...]

Log in using user fedora and the password you defined in the secret you created earlier in this chapter, usually kubevirt.

Task 3.2.4: Enhance your startup script

In the previous section we have created a VM using a cloud-init script. Enhance the startup script with the following functionality:

  • Set the timezone to Europe/Zurich
  • Install the nginx package
  • Write a custom nginx.conf to /etc/nginx/nginx.conf
  • Start the nginx service

For the custom nginx configuration, you can use the following content:

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

events {
  worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    
    access_log  /var/log/nginx/access.log  main;
    
    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 4096;
    
    include             /etc/nginx/mime.types;
    default_type        text/plain;
    
    server {
        listen       8080;
        server_name  _;
        root         /usr/share/nginx/html;
        
        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;
        
        location /health {
            return 200 'ok';
        }
        
        location / {
            set $response 'Hello from ${hostname}\n';
            set $response '${response}GMT time:   $date_gmt\n';
            set $response '${response}Local time: $date_local\n';
        
            return 200 '${response}';
        }
    }
}
Solution

Your cloud-init configuration will look like this:

        #cloud-config
        password: kubevirt
        chpasswd: { expire: False }
        packages:
          - nginx
        timezone: Europe/Zurich
        write_files:
          - content: |
              user nginx;
              worker_processes auto;
              error_log /var/log/nginx/error.log;
              pid /run/nginx.pid;

              events {
                worker_connections 1024;
              }

              http {
                  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for"';

                  access_log  /var/log/nginx/access.log  main;

                  sendfile            on;
                  tcp_nopush          on;
                  tcp_nodelay         on;
                  keepalive_timeout   65;
                  types_hash_max_size 4096;

                  include             /etc/nginx/mime.types;
                  default_type        text/plain;

                  server {
                      listen       8080;
                      server_name  _;
                      root         /usr/share/nginx/html;

                      # Load configuration files for the default server block.
                      include /etc/nginx/default.d/*.conf;

                      location /health {
                        return 200 'ok';
                      }

                      location / {
                        set $response 'Hello from ${hostname}\n';
                        set $response '${response}GMT time:   $date_gmt\n';
                        set $response '${response}Local time: $date_local\n';

                        return 200 '${response}';
                      }
                  }
              }              
            path: /etc/nginx/nginx.conf
        runcmd:
          - systemctl enable nginx
          - systemctl start nginx

You need to edit your secret using above content:

  1. Switch into the Administrator view and choose Workloads then Secrets from the menu on the left
  2. Make sure you’re in the correct project and click on secret lab04-cloudinit
  3. Edit it by clicking on Actions on the top right and choose Edit Secret
  4. Replace the existing Value’s content with the one above
  5. Click Save

Switch back into the Virtualization view, choose VirtualMachines in the menu on the left and restart your VM.

Task 3.2.5: Test your webserver on your virtual machine

We have spawned a virtual machine that uses cloud-init and installs a simple nginx webserver. Let us test the webserver:

In the menu on the left, open Networking and choose Services. Click on the Create Service button on the top right.

Replace the already filled-in content with the following:

apiVersion: v1
kind: Service
metadata:
  name: lab04-cloudinit
spec:
  ports:
    - name: http
      port: 8080
      protocol: TCP
      targetPort: 8080
  selector:
    kubevirt.io/domain: lab04-cloudinit
  type: ClusterIP

Click Create.

Test your working webserver from your web terminal :

curl -s lab04-cloudinit.lab-<username>.svc.cluster.local:8080

You should see output similar to this:

Hello from lab04-cloudinit
GMT time:   Wednesday, 29-Oct-2025 10:53:14 GMT
Local time: Wednesday, 29-Oct-2025 11:53:14 CET

Task 3.2.6: (Optional) Expose the webserver

The nginx webserver is now only accessible within our Kubernetes cluster. In this optional lab we are going to expose it to the internet.

For that, we need to create an Ingress resource:

  1. In the menu on the left, open Networking and choose Ingresses
  2. Click on the button Create Ingress
  3. Replace the already filled-in content with the following:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    route.openshift.io/termination: edge
  name: lab04-cloudinit
spec:
  ingressClassName: openshift-default
  rules:
    - host: lab04-cloudinit-lab-<username>.<appdomain>
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: 
                name: lab04-cloudinit
                port: 
                  name: http
  tls:
    -  {}

Create the Ingress by clicking the Create button.

After that open a new browser tab and enter the URL: https://lab04-cloudinit-lab-<username>.<appdomain>

Congratulations, you’ve successfully exposed an nginx webserver to the internet that is running in a Fedora VM on Kubernetes!

End of lab

References

You can find additional information about cloud-init here: