Really!! Are there could native way to deploy, LCM VMs and add Self Serve on top ????
In this post I will describe an art of the possibility using the below tools:
- RHDH: Red Hat Developer Hub (Open source project: Backstage)
- OCP Virtualization: Red Hat OpenShift Virtualization (Open source project: KubeVirt)
- AAP: Red Hat Ansible Automation Platform (Open source project: Ansible / AWX)
- RHEL BootC: Image mode for Red Hat Enterprise Linux (Open source project: bootc)
- GitOps: Red Hat OpenShift GitOps (Open source project: ArgoCD)
- Quay Registry or any other OCI compliant registry
All of these projects can be run on Red Hat OpenShift (Open source project: OKD) OR on other Kubernetes distribution or on VMs (you pick your underlying infra. For this post I have used OpenShift for simplicity of deployment, integrated tools and narrowly focusing on the usecases instead of the deployment of the tools).
The main goal here is to: Easily deploy and lifecycle applications and stuffs in VMs.
Here're the purpose of the tools:
- RHDH to be used as self serve portal for operations teams (VM Management)
- RHDH to be used as self serve portal for application teams (Release VM based application and depedencies)
- OCP Virtualization and BootC is used to treat VMs as like they are container, at least from DevOps perspective, without compromising VM type operations and security (Please don't qoute me here as I am not the SME on this matter)
- RHEL BootC is the used for
- packaging applicatoins with immutability
- works really well with OCP Virtualization
- significantly simplifies the LCM of the application (and underlying depedencies and OS layer) without compromising security posture
- GitOps is used as the tying actions with RHDH (eg: VMs deployment, trigger AAP etc)
Now that the what and the why is out of the way I will describe the how part.
Demo Video:
How do we put an application in RHEL BootC VM:
step1:
Just like we would package an application in a container we will package it in a RHEL BootC container.
See below the Dockerfile where I am packaging a LAMP stack in a BootC container
FROM registry.redhat.io/rhel9/rhel-bootc:9.4 RUN mkdir -p /run/httpd /run/mariadb /var/lib/php/opcache RUN subscription-manager register --activationkey=xxxx --org=xxxx RUN subscription-manager status RUN dnf module enable -y php:8.2 nginx:1.22 && dnf install -y httpd mariadb mariadb-server php-fpm php-mysqlnd && dnf clean all RUN dnf -y install cloud-init && \ ln -s ../cloud-init.target /usr/lib/systemd/system/default.target.wants && \ dnf clean all RUN systemctl enable httpd mariadb php-fpm RUN mkdir -p /etc/firewalld/services RUN systemctl enable firewalld RUN firewall-offline-cmd --add-service=http RUN echo '<h1 style="text-align:center;">{{ appconfig }}</h1> <?php phpinfo(); ?>' >> /usr/local/index.php RUN ln -s /usr/local/index.php /var/www/html/index.php
Note: We can also fit this into an existing SOI process as well. For example, a SOI engineer can create and release the base image from BootC. An application engineer can then base off their image from that.
step2:
This is our base container or golden image from which we can generate distribution for different hypervisors. Fot this post, because I am going to deploy it on OCP Virtualization I am going to generate a qcow2 disk out of it.
podman run --privileged \ -v /var/lib/containers/storage:/var/lib/containers/storage \ -v .:/output \ quay.io/centos-bootc/bootc-image-builder:latest \ --type qcow2 \ <the-container-image>
step3:
To make it a bootable (VM bootable that is) container we will wrap the disk in another container called UBI. Here's a Dockerfile for that:
FROM registry.redhat.io/{{ ubi }}/ubi:{{ ubiversion }} AS builder
ADD --chown=107:107 disk.qcow2 /disk/
RUN chmod 0440 /disk/*
FROM scratch
COPY --from=builder /disk/* /disk/
This is the container we can use as bootable container for OCP Virtualization. This will create a RHEL VM (just like any other RHEL VM) but using container. When I read about it first it also sounded to me "that nuts! How the hell??" But really the credit here goes to OCP Virtualization and UBI image.
At this point if you feel like "I could have done that with any Linux disti, what's so special about bootc?" -- you would be right. The real benefit of RHEL BootC is about "lifecycle the container way". It will make sense in the "How do we LCM it" section.
How do we automate the process:
It is a 3 parts process. We will handle first 2 parts here. The 3rd part deserves its own section.
Part 1: Automating the application release process:
We already know how to do it (as described in the "How do we put an application in RHEL BootC VM" section). We can simply automate the steps in an AAP playbook with few more additional steps to templatise and upload to a container registry. Below is the playbook:
--- - name: Hardcoded BootC to UBI Workflow with Registry Login hosts: localhost connection: local gather_facts: false environment: # STORAGE_DRIVER: overlay STORAGE_OPTS: "ignore_chown_errors=true" tasks: # --- PHASE -1: CLEANUP --- - name: Reset Podman storage to fix driver mismatch ansible.builtin.command: podman system reset --force ignore_errors: true # In case it's already empty # --- PHASE 0: AUTHENTICATION --- - name: Login to Red Hat Registry ansible.builtin.command: > podman login registry.redhat.io -u "xxx" -p "xxxxxxxxxxxxxxx" - name: Login to Quay.io ansible.builtin.command: > podman login quay.io -u "xxxxxxxx" -p "xxxxxxxxxxx" # --- PHASE 1: SETUP WORKSPACE --- - name: Create build directories ansible.builtin.file: path: "{{ item }}" state: directory loop: ["./bootc", "./ubi9", "./output"] # --- PHASE 1.2: INLINE DOCKERFILES (Parameterized) --- - name: Create Inline BootC Dockerfile ansible.builtin.copy: dest: "./bootc/Dockerfile" content: |- FROM registry.redhat.io/rhel9/rhel-bootc:9.4 RUN mkdir -p /run/httpd /run/mariadb /var/lib/php/opcache RUN subscription-manager register --activationkey=xxxxxxx --org=xxxxxxxxx RUN subscription-manager status RUN dnf module enable -y php:8.2 nginx:1.22 && dnf install -y httpd mariadb mariadb-server php-fpm php-mysqlnd && dnf clean all RUN dnf -y install cloud-init && \ ln -s ../cloud-init.target /usr/lib/systemd/system/default.target.wants && \ dnf clean all RUN systemctl enable httpd mariadb php-fpm RUN mkdir -p /etc/firewalld/services RUN systemctl enable firewalld RUN firewall-offline-cmd --add-service=http RUN echo '<h1 style="text-align:center;">{{ appconfig }}</h1> <?php phpinfo(); ?>' >> /usr/local/index.php RUN ln -s /usr/local/index.php /var/www/html/index.php - name: Create Inline UBI9 Wrapper Dockerfile ansible.builtin.copy: dest: "./ubi9/Dockerfile" content: |- FROM registry.redhat.io/{{ ubi }}/ubi:{{ ubiversion }} AS builder ADD --chown=107:107 disk.qcow2 /disk/ RUN chmod 0440 /disk/* FROM scratch COPY --from=builder /disk/* /disk/ # --- PHASE 2: BOOTC BUILD --- # We use --userns=host to fix the 'invalid argument' lchown error - name: 1. Build the BootC Image ansible.builtin.command: > podman build --userns=host --network=host -t localhost/rhel9-bootc-lamp:latest ./bootc/ # --- PHASE 3: QCOW2 EXTRACTION --- - name: Run BootC Image Builder (Privileged) vars: # This gets the actual current directory of the pod at runtime current_dir: "{{ lookup('pipe', 'pwd') }}" ansible.builtin.command: > podman run --rm --privileged --network host -v /var/lib/containers/storage:/var/lib/containers/storage:Z -v {{ lookup('pipe', 'pwd') }}/output:/output:Z registry.redhat.io/rhel9/bootc-image-builder:latest --type qcow2 --local localhost/rhel9-bootc-lamp:latest # --- PHASE 4: UBI WRAPPER BUILD --- - name: Prepare UBI Build Context ansible.builtin.copy: src: "./output/qcow2/disk.qcow2" dest: "./ubi9/disk.qcow2" remote_src: true # --- PHASE 5: UBI WRAPPER BUILD --- - name: 3. Build Final UBI9 Wrapper Image ansible.builtin.command: > podman build --network=host --userns=host -t quay.io/coolproject/my-rh9-bootc:{{ imageversion }} ./ubi9/ # --- PHASE 6: PUSH --- - name: Push Golden Image to Registry ansible.builtin.command: > podman push quay.io/coolproject/my-rh9-bootc:{{ imageversion }}
Notice the templatisation and ignore the passwords and usernames.
A good question here is "why templatise". Well, 1 it is a good practice and 2 we will leverage the template to add "Self Serve" to it.
Here's a screen shot of the container registry hosting the built golden image:
Part 2: Automating the VM deployment process:
Ok, in this example, we are not automating it. We are "self Serv"ing it. But the process remains the same. It can easily be automated using the same way via an automation.
Note: I have used AAP to automate here. But the good thing about this approach it we can literally use any CI tools (eg: Tekton, github actions, azure devops etc) to achieve the same.
How do we add Self Serve to it:
We are using RHDH as the self serve portal. We need few plugins for the RHDH templates which is more related to the topic of "deploying and enabling RHDH on OpenShift". So, rather than describing it in this post here is the link to the github repo where relevant yamls exist.
Once RHDH has the relevant plugings enabled we will implement 2 templates for self serving “the release” and “the deployment” of the applications/VMs. Ofcourse the CI and CD could be a triggered process with gitops. For this example lets make it self serve on demand (rather than auto triggered with git commit; this example has some details to implement the auto trigger using tools like tekton, github action, azure devops etc).
Here're a few screenshot from my RHDH for this:
Template # Self Serve the "release" of the package:
This github repo contains the yamls for the template.
The thing to highlight in this template is that we have a templated an "app config" block through with we can change the configuration of the application being package. Imagine the similar concept to change what we install as dependencies or other apps on VM OR have different template for different category of golden images for the different VMs (really this is only limited to the imagination).
Template # Self Serve deploy VMs from the golden image:
Once golden images for VMs are are created (from the above described) we have the image ready to be deployed. In this case the bootable container is ready in our container registry. All we need to do is to create a deployment definition of a OpenShift Virtualization VirtualMachine CRD. Ofcouse we can easily templatise this using RHDH. Basically through this template we can control which golden image to deploy, vm configuraions (eg: CPU, Memory, Storage etc) and any other thing that we (as the ops team) want to give control to the self serving users (the app team or any other downstream teams).
This github repo contains the yamls for the template.
Here's the screenshot of the template in action:
Here's the VirtualMachine definition is generated:
apiVersion: kubevirt.io/v1 kind: VirtualMachine metadata: name: bootc-hello-world namespace: myvms1 finalizers: - kubevirt.io/virtualMachineControllerFinalize spec: dataVolumeTemplates: - metadata: name: bootc-hello-world spec: source: registry: url: "docker://quay.io/alitestseverything/my-rh9-bootc:hw1" secretRef: quay-registry-secret storage: resources: requests: storage: 15Gi running: true template: metadata: annotations: vm.kubevirt.io/flavor: medium vm.kubevirt.io/os: rhel.9 vm.kubevirt.io/workload: server creationTimestamp: null labels: network.kubevirt.io/headlessService: headless kubevirt.io/domain: bootc-hello-world kubevirt.io/size: medium spec: accessCredentials: - sshPublicKey: propagationMethod: noCloud: {} source: secret: secretName: common-vm-ssh-key architecture: amd64 domain: cpu: cores: 2 sockets: 1 threads: 1 memory: guest: 8Gi devices: disks: - disk: bus: virtio name: rootdisk - disk: bus: virtio name: cloudinitdisk interfaces: - masquerade: {} model: virtio name: default features: acpi: {} smm: enabled: true firmware: bootloader: efi: {} machine: type: q35 resources: {} networks: - name: default pod: {} terminationGracePeriodSeconds: 180 volumes: - dataVolume: name: bootc-hello-world name: rootdisk - cloudInitNoCloud: userData: | #cloud-config chpasswd: expire: false password: xxxx user: xxxx name: cloudinitdisk
Here's the screenshot of VM deployed:
Right now the way the self serve works (eg: triggering the golden image build process, deploying the VM from bootable container etc) together is via GitOps. The RHDH action is to create a pull request with relevant K8s objects such as
- Kind: AnsibleJob to trigger ansible job template for creating golden image. Enhancement note: I think, it is possible to create a RHDH/Backstage custom action to deploy the object directly in the OpenShift by passing the GitOps process in the middle. The reason is that these AnsibleJob definitions are basically garbage object that's not needed after the ansible job is triggered.
- Kind: VirtualMachine to create VMs in the OpenShift cluster. The GitOps should remain here as this is also IaC (which is K8s native).
- etc
Upon approval of the pull request the GitOps (ArgoCD) deploys the artifact in the cluster and the actual object (VM, Jobs etc) is handled by K8s it self as the orchestrator.
How do we LCM it:
Ok, now that we have created a very cool automated process to release golden images and deploy that to VMs lets focus on LCM of the VM or Package. This is where the power of BootC comes in handy. To describe it (to the best of my ability without confusing people), it is a 2 part process.
The process is:
Part -1: Create the bootc container:
This is the release of new version of the package / golden image. The new version could be for a new release of application or upgrade of OS or depedency tools. Regardless, we need to create the bootc container with updated contents. This is described in the "step1" of "How do we put an application in RHEL BootC VM" section (we dont need to do step2 or step3 to create the release).
Part -2: upgrade or deploy with new release:
We have 2 choices here.
Choice - 1: We can perform an upgrade inside the existing VM. This is very cool, because this works slightly differently that traditional VM upgrade process. The way it works is when the upgrade command is executed in the VM is downloads the updated layers of the container image and updates them. This process is faster and simple and when prepared in the operations the right way it can save time.
Here's the code to perform bootc upgrade in the VM:
# download the upgraded layers sudo bootc upgrade# Reboot to apply the upgrade sudo reboot
More details here and here.
We coded another AAP playbook for automating the manual upgrade process. Which could be linked to RHDH for self serve trigger upgrade.
Note: the upgrade could be tied to new release of app as well (because it’s not the traditional VM image and everything is container layers)
Choice - 2: Perform the step1,2 and 3 from "How do we put an application in RHEL BootC VM" section and rather than upgrading a new bootable container image is created and deployed (just like container).
Conclusion:
Hopefully, I was able to articulate it. The question here is that is this a way an organisation can simplify their VMs fleet management? I would imagine the answer is not that simple but it is a good start.
Comments
Post a Comment