Red Hat OpenShift includes an abundance of technologies out of the box that are necessary for effectively managing a fleet of devices at the edge. One of those components, the scheduler, enables these services to be efficiently co-located onto a single platform.
In addition, OpenShift manages many of these services via an Operator, meaning a non-technical team doesn’t need to understand all the specific details about the service. OpenShift, in a sense, helps make managing devices at the edge simpler and cost-effective.
This article details how to go about building this configuration on a single-node OpenShift cluster. Keep in mind that you can apply these same concepts to a 3-node compact and full OpenShift cluster as well.
Why use OpenShift on a single node?
Before we begin, let us step back and ask: Why do this? Well, there are a variety of reasons:
- Having all the components in a single-node OpenShift (SNO) cluster makes it a great way to have a one-stop experience.
- Having all the components in a single OpenShift node provides a quick and easy way to prove out a concept.
- Since it's OpenShift, the SNO concept can be graduated to a large cluster to meet the capacity needs of a production environment.
- Device Edge images are really just YAML files that should be maintained in Git, which gives us a clear path to infrastructure as code (IaC) and proper continuous integration/continuous deployment (CI/CD) all within OpenShift Container Platform.
- Operators are genuinely a great way to reduce the barrier of entry when it comes to installing services and components in OpenShift.
Components
Now that we understand some of the why, let's move forward and lay out what components we will be using in this single-node OpenShift "edge manager in a box." The core set of services we’ll be consuming are:
- An image registry to store our edge images as we compose them.
- Management of local storage for retaining our composed images, databases, etc.
- An instance of Ansible automation controller to drive our automation and leverage existing automation.
- A pipeline technology; we’ll be using Red Hat OpenShift Pipelines.
- A virtualization platform such as Red Hat OpenShift Virtualization (formerly container-native virtualization).
- A virtual machine template to deploy virtual machines from which we can build our images.
These core services, when integrated together, offer the functionality necessary for managing our fleet of device edge devices.
There are different ways to deploy workloads on OpenShift. However, because we’ll be consuming a handful of Operators, we find it's useful to leverage automation to get everything deployed. Ansible has a module in the kubernetes.core collection (k8s) that can be leveraged to talk directly to the Kubernetes API. We’ll use it here to push k8s objects related to installing Operators and creating instances from those Operators.
The wrapper playbook
The first playbook we need to create on our quest for edge device management is a wrapper playbook that will ultimately call all the playbooks to build out our environment. The playbook will look like the following:
---
- name: import playbook to configure the local registry
ansible.builtin.import_playbook: configure-registry.yml
- name: import playbook to setup local storage
ansible.builtin.import_playbook: configure-storage.yml
- name: import playbook to setup controller
ansible.builtin.import_playbook: install-ansible.yml
- name: import playbook to setup pipelines
ansible.builtin.import_playbook: configure-pipelines.yml
- name: import playbook to setup virtualization
ansible.builtin.import_playbook: configure-virtualization.yml
- name: import playbook to setup image builder virtual machine template
ansible.builtin.import_playbook: setup-image-builder-vm-template.yml
This playbook simply imports other playbooks that contain the actual steps necessary to get an Operator installed: create an OperatorGroup, deploy an instance, and more. We won’t go through all of these playbooks, but let’s take a deep dive on the playbook to set up Red Hat Ansible Automation Platform:
---
- name: install controller
hosts:
- sno_clusters
gather_facts: false
module_defaults:
kubernetes.core.k8s:
kubeconfig: "{{ tmpdir.path }}/ocp/auth/kubeconfig"
tasks:
- name: configure storage
delegate_to: localhost
block:
- name: create namespace
kubernetes.core.k8s:
definition: "{{ lookup('file', 'files/namespaces/ansible-automation-platform.yaml') | from_yaml }}"
- name: create operator group
kubernetes.core.k8s:
definition: "{{ lookup('file', 'files/operator-groups/ansible-automation-platform.yaml') | from_yaml }}"
- name: install operator
kubernetes.core.k8s:
definition: "{{ lookup('file', 'files/operators/ansible-automation-platform.yaml') | from_yaml }}"
register: operator_install
until:
- operator_install.result.status.state is defined
- operator_install.result.status.state == 'AtLatestKnown'
retries: 100
delay: 10
- name: create instance of controller
kubernetes.core.k8s:
definition: "{{ lookup('file', 'files/instances/controller.yaml') | from_yaml }}"
The playbook above is grabbing files that contain k8s objects and pushing them into the Kubernetes API. For Ansible Automation Platform specifically, we have a namespace, an Operator group, a subscription, and then an instance of Controller. First, the namespace custom resource YAML:
---
apiVersion: v1
kind: Namespace
metadata:
labels:
openshift.io/cluster-monitoring: "true"
name: ansible-automation-platform
Next, we have the Operator group custom resource YAML:
---
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
name: ansible-automation-platform-operator
namespace: ansible-automation-platform
spec:
targetNamespaces:
- ansible-automation-platform
Then comes the subscription custom resource YAML:
---
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: ansible-automation-platform
namespace: ansible-automation-platform
spec:
channel: 'stable-2.3'
installPlanApproval: Automatic
name: ansible-automation-platform-operator
source: redhat-operators
sourceNamespace: openshift-marketplace
Finally, once the Operator finishes deploying, an instance of the controller custom resource YAML:
---
apiVersion: automationcontroller.ansible.com/v1beta1
kind: AutomationController
metadata:
name: controller
namespace: ansible-automation-platform
spec:
replicas: 1
After a few minutes, we’ll have a running instance of the Ansible automation controller on our OpenShift cluster.
Configuring Ansible automation controller
If we already have an instance of Controller set up and configured, then this part isn’t necessary. However, if we're starting from a completely empty instance of Controller, then we need to apply some base configuration to it so it can start driving automation.
Note: A best practice with automation controller is to store the configuration in code, then leverage automation to deploy the configuration to Controller. Here, we’ll leverage the redhat_cop.controller_configuration collection. First, we’ll need some specific credential types:
- OpenShift kubeconfig: A credential type to inject a kubeconfig into the execution environment of our automation.
- Red Hat Subscription Management credentials: A credential type for storing authentication details for Red Hat Customer Portal.
- Image credentials: A credential type for securely storing the user account credentials we want in our composed images, as opposed to storing these in plain text.
- Ansible controller API credentials: A set of credentials to authenticate to automation controller’s API.
- Kube credentials: A set of credentials that will be used to authenticate to our registry. I’m using OpenShift’s internal registry and the kubeadmin account, but you can substitute a properly scoped account and use a registry of your choosing.
The YAML definitions of these custom credential types:
controller_credential_types:
- name: Openshift Kubeconfig
kind: cloud
inputs:
fields:
- id: kubeconfig
type: string
label: Kubeconfig
#secret: true
multiline: true
injectors:
env:
K8S_AUTH_KUBECONFIG: "{ { tower.filename.kubeconfig }}"
KUBECONFIG: "{ { tower.filename.kubeconfig }}"
file:
template.kubeconfig: "{ { kubeconfig }}"
- name: RHSM Credentials
kind: cloud
inputs:
fields:
- id: rhsm_username
type: string
label: RHSM Hostname
- id: rhsm_password
type: string
label: RHSM Username
secret: true
injectors:
extra_vars:
rhsm_username: "{ { rhsm_username }}"
rhsm_password: "{ { rhsm_password }}"
- name: Image Credentials
kind: cloud
inputs:
fields:
- id: image_username
type: string
label: Image Hostname
- id: image_password
type: string
label: Image Username
secret: true
injectors:
extra_vars:
image_username: "{ { image_username }}"
image_password: "{ { image_password }}"
- name: Ansible Controller API Credentials
kind: cloud
inputs:
fields:
- id: controller_hostname
type: string
label: Controller Hostname
- id: controller_username
type: string
label: Controller Username
- id: controller_password
type: string
label: Controller Password
secret: yes
injectors:
extra_vars:
controller_hostname: "{ { controller_hostname }}"
controller_username: "{ { controller_username }}"
controller_password: "{ { controller_password }}"
controller_validate_certs: "no"
- name: Kubeadmin Credentials
kind: cloud
inputs:
fields:
- id: kubeadmin_username
type: string
label: Kubeadmin username
- id: kubeadmin_password
type: string
label: Kubeadmin password
secret: true
injectors:
extra_vars:
kubeadmin_username: "{ { kubeadmin_username }}"
kubeadmin_password: "{ { kubeadmin_password }}"
Most of these credential types are straightforward; however, the kubeconfig credential type has some additional injectors in the form of a file and an environment variable of the path to that file. In addition, the leading two brackets in the injector configurations are how we tell the collection to send an “unsafe” string to the API without attempting to render it locally. Leveraging our new credential types, we can create the set of credentials we’ll need for our automation:
controller_credentials:
- name: kubeconfig
organization: Default
credential_type: Openshift Kubeconfig
inputs:
kubeconfig: "{{ lookup('file', (tmpdir.path + '/ocp/auth/kubeconfig')) | from_yaml | string }}"
- name: Machine Credentials
organization: Default
credential_type: Machine
inputs:
username: cloud-user
password: "{{ vm_template_password }}"
become_password: "{{ vm_template_password }}"
- name: Ansible Controller API Credentials
credential_type: Ansible Controller API Credentials
organization: Default
inputs:
controller_hostname: "{{ controller_hostname }}"
controller_username: admin
controller_password: "{{ controller_password }}"
- name: RHSM Credentials
credential_type: RHSM Credentials
organization: Default
inputs:
rhsm_username: "{{ rhsm_username }}"
rhsm_password: "{{ rhsm_password }}"
- name: Image Credentials
credential_type: Image Credentials
organization: Default
inputs:
image_username: "{{ image_username }}"
image_password: "{{ image_password }}"
- name: Kubeadmin Credentials
credential_type: Kubeadmin Credentials
organization: Default
inputs:
kubeadmin_username: kubeadmin
kubeadmin_password: "{{ lookup('file', (tmpdir.path + '/ocp/auth/kubeadmin-password')) }}"
Next, we’ll need an execution environment that contains the appropriate collections and Python libraries. We’ll discuss the building of this execution environment later, but for now, this is the definition:
controller_execution_environments:
- name: Image Builder Execution Environment
image: quay.io/device-edge-workshops/helper-ee:latest
pull: always
After our execution environment, we’ll set up two inventories: one scoped for performing “local actions,” where the execution node performs the work without needing to connect to a remote system, and another to contain our image builder system:
controller_inventories:
- name: Image Builder Servers
organization: Default
variables:
k8s_api_address: "api.{{ inventory_hostname }}"
k8s_api_int_address: "api-int.{{ inventory_hostname }}:6443"
ocp_namespace: image-builder
image_registry: 'image-registry.openshift-image-registry.svc.cluster.local:5000'
- name: Local Actions
organization: Default
variables:
k8s_api_address: "api.{{ inventory_hostname }}"
k8s_api_int_address: "api-int.{{ inventory_hostname }}:6443"
ocp_namespace: image-builder
image_registry: 'image-registry.openshift-image-registry.svc.cluster.local:5000'
Be sure to define the inventory variables to correspond to your OpenShift cluster environment. Next, a simple host to use for local actions:
controller_hosts:
- name: localhost
inventory: Local Actions
variables:
ansible_connection: local
ansible_python_interpreter: "{ { ansible_playbook_python }}"
Note: This has the same double spacing as above, meaning we’re sending a variable that will be resolved by Controller when it runs the automation, and not by the playbook configuring Controller right now. After that, a project containing our code:
controller_projects:
- name: Image Builder Codebase
organization: Default
scm_type: git
scm_url: https://github.com/redhat-manufacturing/device-edge-demos.git
Finally, we define our job templates:
controller_templates:
- name: Manage Virtual Machine Connectivity
organization: Default
inventory: Local Actions
project: Image Builder Codebase
playbook: demos/rhde-pipeline/playbooks/manage-vm-connection.yml
execution_environment: Image Builder Execution Environment
ask_variables_on_launch: true
credentials:
- kubeconfig
- name: Manage Host in Controller
organization: Default
inventory: Local Actions
project: Image Builder Codebase
playbook: demos/rhde-pipeline/playbooks/manage-host-in-controller.yml
execution_environment: Image Builder Execution Environment
ask_variables_on_launch: true
credentials:
- kubeconfig
- Ansible Controller API Credentials
- name: Preconfigure Virtual Machine
organization: Default
inventory: Image Builder Servers
project: Image Builder Codebase
playbook: demos/rhde-pipeline/playbooks/preconfigure-virtual-machine.yml
execution_environment: Image Builder Execution Environment
ask_variables_on_launch: true
become_enabled: true
credentials:
- Machine Credentials
- RHSM Credentials
- name: Install Image Builder
organization: Default
inventory: Image Builder Servers
project: Image Builder Codebase
playbook: demos/rhde-pipeline/playbooks/install-image-builder.yml
execution_environment: Image Builder Execution Environment
ask_variables_on_launch: true
become_enabled: true
credentials:
- Machine Credentials
- name: Manage Image Builder Connectivity
organization: Default
inventory: Local Actions
project: Image Builder Codebase
playbook: demos/rhde-pipeline/playbooks/manage-ib-connection.yml
execution_environment: Image Builder Execution Environment
ask_variables_on_launch: true
credentials:
- kubeconfig
- name: Compose Image
organization: Default
inventory: Image Builder Servers
project: Image Builder Codebase
playbook: demos/rhde-pipeline/playbooks/compose-image.yml
execution_environment: Image Builder Execution Environment
ask_variables_on_launch: true
become_enabled: true
credentials:
- Machine Credentials
- Image Credentials
- name: Push Image to Registry
organization: Default
inventory: Image Builder Servers
project: Image Builder Codebase
playbook: demos/rhde-pipeline/playbooks/push-image-to-registry.yml
execution_environment: Image Builder Execution Environment
ask_variables_on_launch: true
become_enabled: true
credentials:
- Machine Credentials
- Kubeadmin Credentials
- name: Deploy Edge Container
organization: Default
inventory: Local Actions
project: Image Builder Codebase
playbook: demos/rhde-pipeline/playbooks/deploy-edge-container.yml
execution_environment: Image Builder Execution Environment
ask_variables_on_launch: true
credentials:
- kubeconfig
A few things to note here: We’re consuming the credentials, inventories, project, and execution environment we created earlier. We’re also allowing some of these job templates to take additional variables when launched, a feature we’ll leverage later when building out our pipeline. Also, all of the referenced playbooks are available on GitHub as a starting point for building your own edge automation.
Interfacing with automation controller
Automation controller has a fully featured RESTful API that can be leveraged to perform basically every controller function, making it very easy to integrate with. However, we will do something a bit more custom, which will simplify our pipeline tasks and allow individual tasks to wait for the corresponding automation to complete.
A quick refresher: Execution environments are container images with roles, collections, Python libraries, and the Ansible bits pre-installed and ready to roll. Since we’re already operating within a container platform, we can reuse those execution environments within our pipeline tasks.
Because we’re building an execution environment, our collections and Python libraries will be included, meaning if we start the container, we can directly call Ansible. To extend the functionality a bit further, we’ll add a few steps to the build process and insert a playbook directly that we can leverage during our pipeline run.
Here’s an example Containerfile for our execution environment:
ARG EE_BASE_IMAGE=registry.redhat.io/ansible-automation-platform-23/ee-minimal-rhel8:latest
ARG EE_BUILDER_IMAGE=registry.redhat.io/ansible-automation-platform-23/ansible-builder-rhel8
FROM $EE_BASE_IMAGE as galaxy
ARG ANSIBLE_GALAXY_CLI_COLLECTION_OPTS=
ARG ANSIBLE_GALAXY_CLI_ROLE_OPTS=
USER root
ADD _build /build
WORKDIR /build
RUN ansible-galaxy role install $ANSIBLE_GALAXY_CLI_ROLE_OPTS -r requirements.yml --roles-path "/usr/share/ansible/roles"
RUN ANSIBLE_GALAXY_DISABLE_GPG_VERIFY=1 ansible-galaxy collection install $ANSIBLE_GALAXY_CLI_COLLECTION_OPTS -r requirements.yml --collections-path "/usr/share/ansible/collections"
FROM $EE_BUILDER_IMAGE as builder
COPY --from=galaxy /usr/share/ansible /usr/share/ansible
ADD _build/requirements.txt requirements.txt
RUN ansible-builder introspect --sanitize --user-pip=requirements.txt --write-bindep=/tmp/src/bindep.txt --write-pip=/tmp/src/requirements.txt
RUN assemble
FROM $EE_BASE_IMAGE
USER root
# Add our customizations here
RUN mkdir /helper-playbooks
COPY run-job-template.yml /helper-playbooks/
COPY --from=galaxy /usr/share/ansible /usr/share/ansible
COPY --from=builder /output/ /output/
RUN /output/install-from-bindep && rm -rf /output/wheels
LABEL ansible-execution-environment=true
We’ve added two steps: creating a directory and placing a playbook into it. This playbook is very simple and only acts as a “go-between” our pipeline and the Controller API, yet allows us to wait for jobs to complete and do a bit of validation of inputs:
---
- name: trigger job template run
hosts: localhost
gather_facts: false
pre_tasks:
- name: assert that vars are defined
ansible.builtin.assert:
that:
- controller_hostname is defined
- controller_username is defined
- controller_password is defined
- controller_validate_certs is defined
- job_template is defined
- name: set vars for role
ansible.builtin.set_fact:
controller_launch_jobs:
- name: "{{ job_template }}"
wait: true
timeout: 14400
extra_vars:
virtual_machine_name: "{{ virtual_machine_name | default('rhel9-vm') }}"
resource_state: "{{ resource_state | default('present') }}"
roles:
- redhat_cop.controller_configuration.job_launch
Once the build is complete, this execution environment will also be consumable for our Device Edge build pipeline.
Creating a pipeline to build Device Edge images
With the automation pieces in place and an execution environment (container image) we can leverage as a simple interface between a pipeline and automation controller, we can start to build out a pipeline that will let us achieve our best practices for Device Edge images—defining them as code (IaC) and testing them before rolling them out to our fleet of devices (CI/CD).
From this point forward, we’re going to treat automation controller as what it is: a platform we can consume to run automation in the proper context and securely, all via the API.
The goal of our pipeline is to kick off a compose of a Device Edge image anytime we update or change our image definition. We’ll need to take some additional steps to set up for and capture our composed image, which the pipeline will also handle. Once those steps are completed, our pipeline will clean up all of the lingering pieces configured to ensure our compose works.
First, Red Hat OpenShift 4.12 includes a tech preview feature to manage virtual machines with OpenShift Pipelines, which allows us to easily spin up and spin down virtual machines as part of our pipeline.
Leveraging our customized execution environment from before, we’ll set up some tasks that will be strung together to form our pipeline. In addition, I’ve created a secret in the namespace of my virtual machine and pipeline that contains the details of my instance of Automation Controller; however, feel free to replace that with a proper secret storage system.
First, a task to expose the SSH port of the created virtual machine:
---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: manage-virtual-machine-connectivity
namespace: image-builder
spec:
params:
- name: virtualMachineName
type: string
description: The name of the virtual machine to expose
default: rhel9-vm
- name: resourceState
type: string
description: Creating or cleaning up
default: present
steps:
- name: expose-virtual-machine
image: quay.io/device-edge-workshops/helper-ee:latest
env:
- name: CONTROLLER_HOSTNAME
valueFrom:
secretKeyRef:
name: controller-auth-account
key: controller_hostname
- name: CONTROLLER_USERNAME
valueFrom:
secretKeyRef:
name: controller-auth-account
key: controller_username
- name: CONTROLLER_PASSWORD
valueFrom:
secretKeyRef:
name: controller-auth-account
key: controller_password
- name: CONTROLLER_VALIDATE_CERTS
valueFrom:
secretKeyRef:
name: controller-auth-account
key: controller_validate_certs
script: |
ansible-playbook /helper-playbooks/run-job-template.yml \
--extra-vars "controller_hostname=$CONTROLLER_HOSTNAME" \
--extra-vars "controller_username=$CONTROLLER_USERNAME" \
--extra-vars "controller_password=$CONTROLLER_PASSWORD" \
--extra-vars "controller_validate_certs=$CONTROLLER_VALIDATE_CERTS" \
--extra-vars "job_template='Manage Virtual Machine Connectivity'" \
--extra-vars "virtual_machine_name=$(params.virtualMachineName)" \
--extra-vars "resource_state=$(params.resourceState)"
A good number of our tasks will look similar, so we can go through this task in detail and then simply make tweaks for later tasks.
From top to bottom, we’ve defined the following:
- A name and namespace for the task.
- Some parameters the task will take, and default values for them. Note that we’ve defined a parameter of
resourceState
—this allows us to reuse this same task to both create and destroy resources, simply by feeding in a different value from the pipeline. - Inserting the values of our Kubernetes secret into the container environment.
- Our execution environment we built earlier.
- A simple script block that calls our helper playbook and feeds in the appropriate variables.
When this task runs, the execution environment is started, ansible-playbook is invoked, and our corresponding variables are fed to the playbook, which communicates with the Controller API.
Our other tasks are similar, with minor tweaks to the job_template
variable so a different job template is called and executed by controller. As an added perk, the collection leveraged within our playbook will wait for controller to complete the job, then return success or failure accordingly, giving our pipeline the necessary visibility.
To view all the tasks, check out the tasks
directory on GitHub. You can create tasks using Ansible (similar to above, where we were configuring OpenShift) or the oc
CLI tool.
With our tasks created, we can build our pipeline:
---
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: build-and-host-device-edge-image
namespace: image-builder
spec:
tasks:
- name: create-vm-from-template
params:
- name: templateName
value: rhel9-image-builder-template
- name: runStrategy
value: RerunOnFailure
- name: startVM
value: 'true'
taskRef:
kind: ClusterTask
name: create-vm-from-template
- name: expose-virtual-machine-ssh
params:
- name: virtualMachineName
value: $(tasks.create-vm-from-template.results.name)
runAfter:
- create-vm-from-template
taskRef:
kind: Task
name: manage-virtual-machine-connectivity
- name: create-host-in-controller
params:
- name: virtualMachineName
value: $(tasks.create-vm-from-template.results.name)
runAfter:
- expose-virtual-machine-ssh
taskRef:
kind: Task
name: manage-host-in-controller
- name: preconfigure-virtual-machine
params:
- name: virtualMachineName
value: $(tasks.create-vm-from-template.results.name)
runAfter:
- create-host-in-controller
taskRef:
kind: Task
name: preconfigure-virtual-machine
- name: install-image-builder
params:
- name: virtualMachineName
value: $(tasks.create-vm-from-template.results.name)
runAfter:
- preconfigure-virtual-machine
taskRef:
kind: Task
name: install-image-builder
- name: expose-image-builder
params:
- name: virtualMachineName
value: $(tasks.create-vm-from-template.results.name)
runAfter:
- install-image-builder
taskRef:
kind: Task
name: manage-image-builder-connectivity
- name: compose-image
params:
- name: virtualMachineName
value: $(tasks.create-vm-from-template.results.name)
runAfter:
- install-image-builder
- expose-image-builder
taskRef:
kind: Task
name: compose-image
- name: push-image-to-registry
params:
- name: virtualMachineName
value: $(tasks.create-vm-from-template.results.name)
runAfter:
- compose-image
taskRef:
kind: Task
name: push-image-to-registry
- name: deploy-composed-image
params:
- name: virtualMachineName
value: $(tasks.create-vm-from-template.results.name)
runAfter:
- push-image-to-registry
taskRef:
kind: Task
name: push-image-to-registry
finally:
- name: cleanup-virtual-machine
params:
- name: vmName
value: $(tasks.create-vm-from-template.results.name)
- name: stop
value: 'true'
- name: delete
value: 'true'
taskRef:
kind: ClusterTask
name: cleanup-vm
- name: cleanup-vm-connectivity
params:
- name: virtualMachineName
value: $(tasks.create-vm-from-template.results.name)
- name: resourceState
value: absent
taskRef:
kind: Task
name: manage-virtual-machine-connectivity
- name: cleanup-image-builder-connectivity
params:
- name: virtualMachineName
value: $(tasks.create-vm-from-template.results.name)
- name: resourceState
value: absent
taskRef:
kind: Task
name: manage-image-builder-connectivity
- name: cleanup-host-in-controller
params:
- name: virtualMachineName
value: $(tasks.create-vm-from-template.results.name)
- name: resourceState
value: absent
taskRef:
kind: Task
name: manage-host-in-controller
Let’s walk through the pipeline step-by-step:
- Create a virtual machine on OpenShift and pass the name to later tasks.
- Expose SSH to the virtual machine externally (this isn’t necessary, but it was useful while building and testing this process out).
- Create a corresponding host entry in automation controller.
- Run some preconfiguration steps on the virtual machine, such as registering to Red Hat Subscription Management.
- Install image builder.
- Compose a Device Edge image.
- Push the composed image to an image registry.
- Deploy the composed image to OpenShift.
- Clean up after ourselves.
With this pipeline in place, we remove the burden of having to constantly run and manage a Red Hat Enterprise Linux image just to run image builder. Instead, all the infrastructure we need is spun up and down on demand, only existing while being consumed, then being destroyed after the work concludes.
Expanding the concepts further
This article is meant to serve as a foundation for building out an "edge manager in a box" capable of best practices for edge device management. As such, there are a few additional things we'd recommend adding to the above, but are out of scope for this specific tutorial:
- Use a legitimate secret store: There are a few places above where simple secret storage is used, and while functional, it is not at all recommended for production use cases.
- Extending the pipeline: Currently, the pipeline really only tests if the image will successfully build. Ideally, this would be extended to provision a "test" system using the new image, and test deploying edge applications onto it before declaring the whole process a success.
- Image builder: Eventually, we want image builder to operate in a container, even a privileged one, which eliminates the need for the virtualization aspects of this workflow.
- Image Registry: While the internal OpenShift Container Platform registry does work, using a scalable robust registry makes sense in production. For a primer, check out this blog post on getting started with Red Hat Quay.