4.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

KubeVirt 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 4.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 define our configuration. Create a file called cloudinit-userdata.yaml in the folder labs/lab04 with the following content:

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

This will set the password of the default user (fedora for Fedora Core) to kubevirt and configure the password to never expire.

We need to create the secret from this configuration. You can use the following command to create it:

kubectl create secret generic lab04-cloudinit --from-file=userdata=labs/lab04/cloudinit-userdata.yaml --namespace lab-<username>

The output should be:

secret/lab04-cloudinit created

Inspect the secret with:

kubectl get secret lab04-cloudinit -o yaml --namespace lab-<username>
apiVersion: v1
data:
  userdata: I2Nsb3VkLWNvbmZpZw[...]
type: Opaque
kind: Secret
metadata:
  name: lab04-cloudinit
[...]

Task 4.2.2: Create a VirtualMachine using cloud-init

Create a file vm_lab04-cloudinit.yaml in the folder labs/lab04 and start with the following VM configuration:

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

Make sure you create your VM with:

kubectl apply -f labs/lab04/vm_lab04-cloudinit.yaml --namespace lab-<username>

Task 4.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

Start the newly-created VM. This might take a couple of minutes:

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

Connect to the console and log in as soon as the prompt shows up:

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 4.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 (cloudinit-userdata.yaml in the folder labs/lab04) 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 recreate your secret:

kubectl delete secret lab04-cloudinit --namespace lab-<username>
kubectl create secret generic lab04-cloudinit --from-file=userdata=labs/lab04/cloudinit-userdata.yaml --namespace lab-<username>

Next, we need to restart our VM to pick up the changes in the cloud-init configuration:

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

Task 4.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:

Create the following Kubernetes Service (file: service-cloudinit.yaml folder: labs/lab04):

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

And create it with:

kubectl apply -f  labs/lab04/service-cloudinit.yaml --namespace lab-<username>

Test your working webserver from your webshell:

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 4.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:

Create a file called ingress-cloudinit.yaml in the folder labs/lab04 with the following content:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: lab04-cloudinit
spec:
  rules:
    - host: lab04-cloudinit-lab-<username>.<appdomain>
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: 
                name: lab04-cloudinit
                port: 
                  name: http
  tls:
    - hosts:
      - lab04-cloudinit-lab-<username>.<appdomain>

Create the Ingress by executing:

kubectl apply -f  labs/lab04/ingress-cloudinit.yaml --namespace lab-<username>

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: