Evaluate requirements¶
Before building the operator, you first want to read documentation about the software architecture you want to deploy to evaluate which services and components need to be created with Kubernetes in which order and how they need to be configured. For example, with an OpenStack component, this usually includes the following as a bare minimum:
Stateful components like database and messaging broker
Stateless services (K8s deployments)
Execution of commands for the initial setup (K8s jobs or preStart hooks)
Configuration files (K8s secrets or configMaps)
K8s service for the API service
When you add a subresource to the custom resource class, you will need to know which statemachine class
is used by Yaook to manage it.
The following table lists examples for frequently used mappings between K8s resources (including Yaook custom resources) and classes from the statemachine module.
Component |
Templated class |
|---|---|
Database |
sm.TemplatedMySQLService |
Database user |
sm.SimpleMySQLUser |
Messaging broker |
sm.TemplatedAMQPServer |
Messaging broker user |
sm.SimpleAMQPUser |
Keystone user |
sm.StaticKeystoneUser or sm.StaticKeystoneUserWithParent |
K8s Deployment |
sm.TemplatedDeployment |
K8s Statefulset |
sm.TemplatedStatefulSet |
K8s Job |
sm.TemplatedJob |
K8s Certificate |
sm.TemplatedCertificate |
K8s Secret |
sm.CueSecret |
You can find other availabe classes inside ./yaook/statemachine/resources/k8s_workload.py and ./yaook/statemachine/resources/yaook_infra.py, or
you can inspect other operators to see how these handle certain scenarios.
Note that most custom resources managed by different operators like the sm.MySQLService are not shared by multiple OpenStack components and a new instance should be created
for each OpenStack CR. There are some exception to this rule, with the central KeystoneDeployment being one example.
If you need another external component that can be referenced by other Yaook operators, add it to the infra operator under ./yaook/op/infra instead of creating a new operator.
Example evaluation¶
We will now use the Barbican installation guide as an example to find out which Yaook classes we can use to automate these steps. Note that this guide only serves as demonstration and is not intended for copy-pasting as parameters are subject to change and many are left out to focus on main concepts. The instructions were originally taken from here. For other OpenStack components a similiar guide should be part of the OpenStack documentation.
The prerequisites section first instructs us to setup a database with required priviledges for the database user. To achieve this,
we add the following to the class of our CR:
class Barbican(sm.ReleaseAwareCustomResource):
...
db = sm.TemplatedMySQLService(...)
db_service = sm.ForeignResourceDependency(...)
db_api_user_password = sm.AutoGeneratedPassword(...)
db_api_user = sm.SimpleMySQLUser(
password_secret=db_api_user_password,
)
Likewise, we also need to create an AMQP server and user so services can communicate with each other:
class Barbican(sm.ReleaseAwareCustomResource):
...
mq = sm.TemplatedAMQPServer(...)
mq_service = sm.ForeignResourceDependency(...)
mq_api_user_password = sm.AutoGeneratedPassword(...)
mq_api_user = sm.SimpleAMQPUser(
password_secret=mq_api_user_password,
)
Next, the OpenStack CLI is used to create the OpenStack user barbican along with its role mappings. In Yaook, we will add the following:
class Barbican(sm.ReleaseAwareCustomResource):
...
keystone = sm.KeystoneReference()
keystone_user = sm.StaticKeystoneUser(
keystone=keystone,
username="barbican"
)
And instead of running
openstack service create --name barbican --description "Key Manager" key-manager
we create the Keystone endpoint using the sm.TemplatedKeystoneEndpoint class:
class Barbican(sm.ReleaseAwareCustomResource):
...
keystone_user_credentials = \
yaook.op.common.keystone_user_credentials_reference(
keystone_user
)
keystone_endpoint = sm.Optional(
condition=yaook.op.common.publish_endpoint,
wrapped_state=sm.TemplatedKeystoneEndpoint(
template="barbican-keystone-endpoint.yaml",
add_dependencies=[keystone],
)
)
Now, we need to create a configuration file to prepare connectivity to the MySQLService, the AMQPServer and Keystone. The configuration is placed inside a K8s secret which we can then mount to the deployments, statefulsets and jobs.
If a configuration does not contain sensitive credentials, the class sm.CueConfigMap is more appropriate.
We first need to create a cue template, whose directory
needs to be referenced with the sm.CueSecret, and which will be placed inside ./yaook/op/cue/pkg/yaook.cloud/barbican_config_by_yaook/defaults.cue, in this particular case.
For each connected component, we also need to pass a separate CueLayer as parameters, where each of these
layer requires customized parameters like credentials or service references that are used to assemble the configuration.
Depending on your component and use case, you might have to add additional layers (if you look at the final Barbican class, there are actually more than three layers).
class Barbican(sm.ReleaseAwareCustomResource):
...
config = sm.CueSecret(
# "barbican-config-" references the directory
metadata=("barbican-config-", True),
add_cue_layers=[
sm.KeystoneAuthLayer(
target="barbican",
credentials_secret=keystone_user_credentials,
endpoint_config=keystone_internal_api,
),
sm.DatabaseConnectionLayer(
target="barbican",
service=db_service,
database_name=DATABASE_NAME,
username=API_SVC_USERNAME,
password_secret=db_api_user_password,
config_section="database",
),
sm.AMQPTransportLayer(
target="barbican",
service=mq_service,
username=API_SVC_USERNAME,
password_secret=mq_api_user_password,
),
...
]
Before the Barbican services can be started, the database needs to be populated via barbican-manage db upgrade which can be executed inside a Kubernetes job.
Here, we also need to add our Docker image as dependency to access the command line utility, and we need to add the configuration we just created to the dependencies in order to gain database access.
The latter will also ensure that the configuration secret is created before this job.
class Barbican(sm.ReleaseAwareCustomResource):
barbican_docker_image = sm.ReleaseAwareVersionedDependency({
...
"2025.1": sm.ConfigurableVersionedDockerImage(
'barbican-2025.1',
sm.YaookSemVerSelector(),
),
},
targetfn=lambda ctx: sm.version_utils.get_target_release(ctx)
)
...
db_sync = sm.Optional(
condition=sm.optional_non_upgrade(),
wrapped_state=sm.TemplatedJob(
template="barbican-job-db-sync.yaml",
add_dependencies=[config, ...],
versioned_dependencies=[barbican_docker_image]
)
)
In the end the guide instructs us to start the Barbican services:
service barbican-keystone-listener restart
service barbican-worker restart
service apache2 restart
Instead of using Apache, we will run these services inside separate Kubernetes deployments using the images we created in the beginning and overwrite the
spec.command with the corresponding command inside the jinja templates.
Again, to ensure that the database sync job finished before the deployment was created, db_sync is added to the add_dependencies property of
each sm.TemplatedDeployment, and again we need the config to create a volume mount inside the jinja templates.
class Barbican(sm.ReleaseAwareCustomResource):
...
keystone_listener = sm.TemplatedDeployment(
template="barbican-deployment-keystone-listener.yaml",
add_dependencies=[
config, db_sync, ...],
...
)
api_deployment = sm.TemplatedDeployment(
template="barbican-deployment-api.yaml",
add_dependencies=[
config, db_sync, ...],
...
)