At Red Hat, we do many in-person and virtual workshops for customers, partners, and other open source developers. In most cases, the workshops are of the "bring your own device" variety, so we face a range of hardware and software setups and corporate endpoint-protection schemes, as well as different levels of system knowledge.
In the past few years, we've made heavy use of Red Hat CodeReady Workspaces (CRW). Based on Eclipse Che, CodeReady Workspaces is an in-browser IDE that is familiar to most developers and requires no pre-installation or knowledge of system internals. You only need a browser and your brain to get hands-on with this tech.
We've also built a set of playbooks for Red Hat Ansible to automate our Quarkus workshop. While they are useful, the playbooks are especially helpful for automating at-scale deployments of CodeReady Workspaces for Quarkus development on Kubernetes. In this article, I introduce our playbooks and show you how to use them for your own automation efforts.
Automating at scale: An overview
While we use CodeReady Workspaces and the Ansible playbooks introduced in this article to automate our Quarkus workshop, many companies use CodeReady Workspaces to automate the onboarding of new developers at scale. In that case, using CodeReady Workspaces also helps to protect corporate intellectual property (that is, source code) and minimize the "works on my machine" excuse for bugs.
Regardless of whether you are running a workshop or an onboarding process, making the experience as smooth as possible requires considerable setup. For a workshop, you need to deploy Red Hat OpenShift, scale it to meet the demands of the number of users expected, and install and configure CodeReady Workspaces for every user. For the best experience, you should also "pre-warm" each workspace so that it is already running by the time you are done with your intro slides. You will also need to install any Operators that you will use as part of the workshop.
In the next sections, I'll go through each of these steps and the Ansible playbooks that we've built to automate them. Most of the setup can be applied to creating custom stacks that follow both company policy and IT policy, and also meet developers' needs.
Installing OpenShift
OpenShift is used for hybrid cloud infrastructure, so for at-scale deployments, you aren't "installing" it in the classic sense of downloading a zip file, unzipping it, and running it on your desktop. You can do that type of install with CodeReady Containers, but running locally is just not an option when you are supporting tens or hundreds of developers in a workshop.
You can easily provision OpenShift on several different public and private clouds. While deploying OpenShift is out of scope for this article, we found it useful to deploy an extra OpenShift worker node for every five students, where each node has 64GiB of memory. That setup supports a positive workshop experience for every student.
For the Quarkus Workshop, we have students doing native Quarkus builds, which require extra memory. Each student also deploys their own Kafka clusters and a few other items. So, we just run through the workshop once, leave everything running, and then add everything up to determine the amount of memory needed per user. Keep in mind that for CodeReady Workspaces, we use the "per-workspace" persistent volume claims (PVC) strategy, where each workspace (and therefore each user) gets its own storage. If you choose to follow that strategy, you will need to ensure that you have enough storage space. The more CPU you can afford, the better.
Once you have OpenShift installed, you will need to create users. You can use basic Linux shell scripting and the oc
CLI to override the default OpenShift authentication mechanism and supply an htpasswd
file containing your users (including an admin user). You will also need the htpasswd
utility and the yq
utility (version 3 or higher) for this Bash script:
#!/bin/bash NUMUSERS=20 TMPHTPASS=$(mktemp) for i in {1..$NUMUSERS} ; do htpasswd -b ${TMPHTPASS} "user$i" 'somepassword' done htpasswd -b ${TMPHTPASS} admin 'adminpassword' $ oc -n openshift-config delete secret workshop-user-secret $ oc -n openshift-config create secret generic workshop-user-secret --from-file=htpasswd=${TMPHTPASS} $ oc -n openshift-config get oauth cluster -o yaml | \ yq d - spec.identityProviders | \ yq w - -s htpass-template.yaml | \ oc apply -f - sleep 20 # don't shoot the messenger, Operators are "eventually consistent" $ oc adm policy add-cluster-role-to-user cluster-admin admin
The htpass-template.yaml
template used with yq
(version 3) looks like:
spec.identityProviders[+]: name: htpassidp type: HTPasswd mappingMethod: claim htpasswd: fileData: name: workshop-user-secret
Running the script with this template merges a new identity provider into the OpenShift auth flow so that users can log in. You could also use Ansible to set up this authorization process, but I haven't yet found the time to convert it.
Deploying CodeReady Workspaces
We use the CodeReady Workspaces Operator for this installation. To automate the installation, we use a bit of Ansible in an Ansible playbook. If the namespace does not already exist, we use the k8s module to create one, along with the OperatorGroup and Subscription (idempotency and all):
# create codeready namespace - name: create codeready namespace k8s: state: present kind: Project api_version: project.openshift.io/v1 definition: metadata: name: "codeready" annotations: openshift.io/description: "" openshift.io/display-name: "CodeReady Project" # deploy codeready operator - name: Create operator subscription for CodeReady k8s: state: present merge_type: - strategic-merge - merge definition: "{{ lookup('file', item ) | from_yaml }}" loop: - ./files/codeready_operatorgroup.yaml - ./files/codeready_subscription.yaml # wait for CRD to be a thing - name: Wait for CodeReady CRD to be ready k8s_facts: api_version: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition name: checlusters.org.eclipse.che register: r_codeready_crd retries: 200 delay: 10 until: r_codeready_crd.resources | list | length == 1 # deploy codeready CR - name: Create CR for CodeReady k8s: state: present merge_type: - strategic-merge - merge definition: "{{ lookup('file', item ) | from_yaml }}" loop: - ./files/codeready_cr.yaml # wait for CodeReady to be up - name: wait for CRW to be running uri: url: https://codeready-codeready.{{ route_subdomain }}/dashboard/ validate_certs: false register: result until: result.status == 200 retries: "120" delay: "15"
The bits of code that are waiting on the custom resource definition (CRD) are important: If you try to create a custom resource (CR) based on a CRD before the CRD is known to the system, it will fail. Furthermore, it takes non-zero time to gain that knowledge once the Operator is installed.
At the end, we also use the uri module to wait for CodeReady Workspaces itself, as we do some additional configuration next.
OperatorGroup
The OperatorGroup is defined in codeready_operatorgroup.yaml
. It is pretty simple, but it's required for Operators to be able to, well, operate:
apiVersion: operators.coreos.com/v1 kind: OperatorGroup metadata: generateName: codeready- annotations: olm.providedAPIs: CheCluster.v1.org.eclipse.che name: codeready-operator-group namespace: codeready spec: targetNamespaces: - codeready
Subscription
The Subscription in codeready_subscription.yaml
is also basic:
apiVersion: operators.coreos.com/v1alpha1 kind: Subscription metadata: name: codeready-workspaces namespace: codeready spec: channel: latest installPlanApproval: Automatic name: codeready-workspaces source: redhat-operators sourceNamespace: openshift-marketplace
CheCluster object
Finally, once the Operator registers its CRDs in Kube, we can create the CheCluster
object in codeready_cr.yaml
. Creating the CheCluster
kicks off the install:
apiVersion: org.eclipse.che/v1 kind: CheCluster metadata: name: codeready-workspaces namespace: codeready spec: server: cheImageTag: '' cheFlavor: codeready devfileRegistryImage: '' pluginRegistryImage: '' tlsSupport: true selfSignedCert: false serverMemoryRequest: '2Gi' serverMemoryLimit: '6Gi' customCheProperties: CHE_LIMITS_WORKSPACE_IDLE_TIMEOUT: "0" database: externalDb: false chePostgresHostName: '' chePostgresPort: '' chePostgresUser: '' chePostgresPassword: '' chePostgresDb: '' auth: openShiftoAuth: false identityProviderImage: '' externalIdentityProvider: false identityProviderURL: '' identityProviderRealm: '' identityProviderClientId: '' storage: pvcStrategy: per-workspace pvcClaimSize: 1Gi preCreateSubPaths: true
Note the memory limits, which are tuned for the containers in our custom CodeReady Workspaces stack. We also set the CHE_LIMITS_WORKSPACE_IDLE_TIMEOUT
here. It is rather annoying to walk away for a short time and find that your lab has timed out and needs a refresh (or requires you to log in again) when you return. Of course, neither of these settings should be used in production.
Tuning Keycloak
It is not possible to use OpenShift's built-in authentication mechanism to pre-create and pre-start workspaces. Doing that would require each user to log in to OpenShift and link the user's account details to Red Hat Single Sign-O. (That, by the way, is why you see openShiftoAuth: false
in the CheCluster resource.)
The workaround to this issue is to create the same set of users in CodeReady Workspaces, again using Ansible:
- name: create codeready users include_tasks: add_che_user.yaml vars: user: "{{ item }}" with_list: "{{ users }}"
In this example, users
is just an array of usernames in Ansible (for instance, [user1, user2, ...]
). We loop through and add the user in add_che_user.yaml
, which uses the CodeReady Workspaces REST API to get credentials for the SSO admin user and create the users:
- name: Get codeready SSO admin token uri: url: https://keycloak-codeready.{{ route_subdomain }}/auth/realms/master/protocol/openid-connect/token validate_certs: false method: POST body: username: "{{ codeready_sso_admin_username }}" password: "{{ codeready_sso_admin_password }}" grant_type: "password" client_id: "admin-cli" body_format: form-urlencoded status_code: 200,201,204 register: codeready_sso_admin_token - name: Add user {{ user }} to Che uri: url: https://keycloak-codeready.{{ route_subdomain }}/auth/admin/realms/codeready/users validate_certs: false method: POST headers: Content-Type: application/json Authorization: "Bearer {{ codeready_sso_admin_token.json.access_token }}" body: username: "{{ user }}" enabled: true emailVerified: true firstName: "{{ user }}" lastName: Developer email: "{{ user }}@no-reply.com" credentials: - type: password value: "{{ workshop_che_user_password }}" temporary: false body_format: json status_code: 201,409
This playbook has a few variables:
route_subdomain
is the default OpenShift subdomain for your cluster (useoc whoami --show-cluster
to discover the cluster).workshop_che_user_password
is your user's desired password.codeready_sso_admin_username/codeready_sso_admin_password
is the admin username and password for the Keycloak instance used by CodeReady Workspaces.
To programmatically discover the Keycloak admin username and password from the deployed Keycloak's environment variables, you can use a little more Ansible code and the k8s_facts module:
- name: Get codeready keycloak deployment k8s_facts: kind: Deployment namespace: codeready name: keycloak register: r_keycloak_deployment - name: set codeready username fact set_fact: codeready_sso_admin_username: "{{ r_keycloak_deployment.resources[0].spec.template.spec.containers[0].env | selectattr('name','equalto','SSO_ADMIN_USERNAME') |map (attribute='value') | list | first }}" - name: set codeready password fact set_fact: codeready_sso_admin_password: "{{ r_keycloak_deployment.resources[0].spec.template.spec.containers[0].env | selectattr('name','equalto','SSO_ADMIN_PASSWORD') |map (attribute='value') | list | first }}"
Next up, we increase the SSO token expiration and SSO session timeout (again, this lets us avoid irritating logouts during a workshop):
- name: Increase codeready access token lifespans uri: url: https://keycloak-codeready.{{ route_subdomain }}/auth/admin/realms/codeready validate_certs: false method: PUT headers: Content-Type: application/json Authorization: "Bearer {{ codeready_sso_admin_token.json.access_token }}" body: accessTokenLifespan: 28800 accessTokenLifespanForImplicitFlow: 28800 actionTokenGeneratedByUserLifespan: 28800 ssoSessionIdleTimeout: 28800 ssoSessionMaxLifespan: 28800 body_format: json status_code: 204
Pre-warming user workspaces
Finally, we're ready to pre-create and pre-warm the CRW workspaces:
- name: Pre-create and warm user workspaces include_tasks: create_che_workspace.yaml vars: user: "{{ item }}" with_list: "{{ users }}"
We will repeat a loop similar to what we did to create the user workspaces. This time, we call create_che_workspace.yaml
, which uses the CodeReady Workspaces REST API:
- name: "Get Che {{ user }} token" uri: url: https://keycloak-codeready.{{ route_subdomain }}/auth/realms/codeready/protocol/openid-connect/token validate_certs: false method: POST body: username: "{{ user }}" password: "{{ workshop_che_user_password }}" grant_type: "password" client_id: "admin-cli" body_format: form-urlencoded status_code: 200 register: user_token - name: Create workspace for {{ user }} from devfile uri: url: "https://codeready-codeready.{{ route_subdomain }}/api/workspace/devfile?start-after-create=true&namespace={{ user }}" validate_certs: false method: POST headers: Content-Type: application/json Authorization: "Bearer {{ user_token.json.access_token }}" body: "{{ lookup('template', './templates/devfile.json.j2') }}" body_format: json status_code: 201,409 register: workspace_def
About the devfile
If you are wondering about the devfile.json.j2
, it is an Ansible Jinja2 template of a CodeReady devfile.
You can find the devfile for this example here. The interesting parts are:
"components": [ { "id": "redhat/quarkus-java11/latest", "type": "chePlugin" },
Note that the devfile includes the Quarkus plugin for the workspace, which provides IDE features like autocompletion and other tidbits:
"image": "image-registry.openshift-image-registry.svc:5000/openshift/quarkus-stack:2.1",
Here, we reference a Che stack that has been pre-generated and deployed into OpenShift as an ImageStream using:
apiVersion: image.openshift.io/v1 kind: ImageStream metadata: name: quarkus-stack namespace: openshift spec: tags: - annotations: description: Quarkus stack for Java and CodeReady Workspaces iconClass: icon-java supports: java tags: builder,java version: "2.1" from: kind: DockerImage name: quay.io/openshiftlabs/quarkus-workshop-stack:2.1 name: "2.1"
We built the stack using a Dockerfile that also includes utilities (oc
, kn
, tkn
, and GraalVM). It runs a couple of test builds to pre-populate the Maven .m2
repository in the image so that users don't download the Internet every time they start the workshop. By pre-pulling this image into OpenShift, we significantly reduce workspace startup time. There is also the Image Puller, which I have not yet used. It looks promising for eliminating some of this logic.
Conclusion
In summary, automating CodeReady Workspace deployments at scale can significantly improve how students experience your workshops. Doing as much as possible up front lets students get to the learning, without waiting for installations, warm up, and so on.
This article introduced some of the Ansible playbooks we've created to automate and improve user experiences with our workshops. Additional options include:
- Deploying other Operators (Strimzi, Jaeger, and so on).
- Creating custom Keycloak realms for the workshop.
- Verifying other components of the workshop are correctly deployed.
Have a look at the Deploy Quarkus Workshop into an OpenShift 4 Cluster playbook. There might be other bits that you can use! Also, if you're interested in the onboarding new developers example, check out the article CodeReady Workspaces Delivers Kubernetes-Native IDE.
Last updated: September 26, 2024