Ansible Playbook Bundle Migration Operator
This guide will walk you through migrating your legacy Ansible Playbook Bundles (APB) to a more modern Kubernetes Operator architecture. We will cover how to prepare a new Ansible-based Operator, cover the changes that you will need to make to the Ansible logic in your existing APB, and at the end you will have a functional Operator.
Directory structure and metadata
Generate Operator resources
Before we begin our migration, we need to first generate an Ansible operator using the operator-sdk tool. Run the following command:
operator-sdk new <name> --type=ansible --api-version=<group>/<version> --kind=<kind>`
where:
<name>
is the name for your operator, so if for example you have amemcached-apb
, you would probably usememcached-operator
<group>
is the API group for your Kubernetes Custom Resource Definition. For example, if I own the domainexample.com
, I might use the groupapps.example.com
.<version>
is the API version for your Kubernetes Custom Resource Definition.v1alpha1
is a common starting value, withv1beta1
implying a fair amount of API stability andv1
implying no breaking API changes at all.<kind>
is the kind of your resource. For example, if you are creating amemcached-operator
, yourkind
would likely beMemcached
So for the example memcached-operator
, the command would be:
operator-sdk new memcached-operator --type=ansible --api-version=apps.example.com/v1alpha1 --kind=Memcached
Once this is generated, take the build
, deploy
, and molecule
directories, as well as the watches.yaml
and copy them into your APB directory.
Dockerfile
You now have two Dockerfiles, your original APB Dockerfile
at the top-level, and a build/Dockerfile
for your operator.
In your build/Dockerfile
, ensure that your playbooks and roles are being copied to ${HOME}/roles
and ${HOME}/playbooks
, and that your watches.yaml
is being copied to ${HOME}/watches.yaml
.
If you are installing any additional dependencies, ensure that those are reflected in your build/Dockerfile
as well.
As a sample, your build/Dockerfile
will probably look something like this:
FROM quay.io/operator-framework/ansible-operator:v0.9.0
COPY watches.yaml ${HOME}/watches.yaml
COPY roles/ ${HOME}/roles/
COPY playbooks/ ${HOME}/playbooks/
Once this is done you may remove your original APB Dockerfile
.
watches.yaml
In the watches.yaml
, ensure the playbook for your kind
points to your provision.yml
playbook in the container (likely location for that will be /opt/ansible/playbooks/provision.yml
).
Next, add a finalizer block with a name of: finalizer.<name>.<group>/<version>
, and set the playbook to point to your deprovision.yml
in the container (likely location for that will be /opt/ansible/playbooks/deprovision.yml
). For the memcached-operator we generated above, the watches.yaml would look like this:
---
- version: v1alpha1
group: apps.example.com
kind: Memcached
playbook: /opt/ansible/playbooks/provision.yml
finalizer:
name: finalizer.memcached.apps.example.com/v1alpha1
playbook: /opt/ansible/playbooks/deprovision.yml
Binding
If you have a bind
playbook, add a new entry to your watches.yaml
(you can copy paste the existing entry).
The version
and group
, will remain unchanged, but update the kind
with a Binding
suffix.
For example, if you have a resource with kind: Memcached
, the kind of your new entry will be MemcachedBinding
.
The playbook for this entry should map to your bind
playbook, (likely location /opt/ansible/playbooks/bind.yml
), and if you have an unbind
playbook then set the playbook for the finalizer to point to it (likely location /opt/ansible/playbooks/unbind.yml
). If you don’t have an unbind
playbook, remove the finalizer block for your Binding
resource.
For an APB with both bind
and unbind
playbooks, the watches.yaml
would end up looking like this:
---
- version: v1alpha1
group: apps.example.com
kind: Memcached
playbook: /opt/ansible/playbooks/provision.yml
finalizer:
name: finalizer.memcached.apps.example.com/v1alpha1
playbook: /opt/ansible/playbooks/deprovision.yml
- version: v1alpha1
group: apps.example.com
kind: MemcachedBinding
playbook: /opt/ansible/playbooks/bind.yml
finalizer:
name: finalizer.memcachedbinding.apps.example.com/v1alpha1
playbook: /opt/ansible/playbooks/unbind.yml
You will also need to run operator-sdk add crd --api-version=<group>/<version> --kind=<kind>
to generate a new CRD and example in deploy/crds
.
deploy/crds/
Now that you have all your CRDs created, you can generate the OpenAPI spec for them using your apb.yml
.
The convert.py
script included at the bottom of this document can handle the conversion to the OpenAPI spec, at which point you can copy paste everything from validation:
on into your primary CRD (for the regular parameters
), or into your Binding
CRD (for bind_parameters
).
You may notice that the OpenAPI validation uses camelCase
parameters, while your apb.yml
and Ansible playbooks probably assume snake_case
variables. Ansible Operator
will automatically convert the camelCase
parameters from the Kubernetes resource into snake_case
before passing them to your playbook, so this should not require any change on your part.
Ansible logic
There will be some changes required to your Ansible playbooks/roles/tasks.
Idempotence
One major conceptual difference between the APB model and the Operator model, is that APBs are meant to run provision
once, while operators constantly reconcile to ensure that the state of the cluster matches the state that the user requested.
This means that you will need to ensure that your playbooks are idempotent, and can be run repeatedly with the same parameters without causing an error.
Service Bundle contract and meta variables
Ansible Operator does not respect the Service Bundle contract that exists between APBs and the Ansible Service Broker. The following variables will not be passed in by the Ansible Operator:
cluster
: Operators ideally work on both Kubernetes and OpenShift, so any uses of openshift-specific resources should handle errors and fallback_apb_plan_id
: Operators have no concept of a plan_apb_service_class_id
: This concept is replaced by the group/version/kind specified in your CRD_apb_service_instance_id
: This concept is replaced bymeta.name
, the name of the Custom Resource created by the user requesting the action._apb_last_requesting_user
: There is no analogue to this._apb_provision_creds
: There is no analogue to this._apb_service_binding_id
: This concept is replaced by themeta.name
of a<kind>Binding
resourcenamespace
: This is accessible via themeta.namespace
variable
Instead, the Ansible Operator will pass in a field called meta
, which contains the name
and namespace
of the Custom Resource that the user created.
asb_encode_binding
The asb_encode_binding
module will not be present in the Ansible Operator base image. In order to save credentials after a successful provision, you will need to create a secret
in Kubernetes, and update the status of your custom resource so that people can find it. For example, if we have the following Custom Resource group/version/kind:
version: v1alpha1
group: apps.example.com
kind: PostgreSQL
the following task:
- name: encode bind credentials
asb_encode_binding:
fields:
DB_TYPE: postgres
DB_HOST: "{{ app_name }}"
DB_PORT: "5432"
DB_USER: "{{ postgresql_user }}"
DB_PASSWORD: "{{ postgresql_password }}"
DB_NAME: "{{ postgresql_database }}"
would become:
- name: Create bind credential secret
community.kubernetes.k8s:
definition:
apiVersion: v1
kind: Secret
metadata:
name: '{{ meta.name }}-credentials'
namespace: '{{ meta.namespace }}'
data:
DB_TYPE: "{{ 'postgres' | b64encode }}"
DB_HOST: "{{ app_name | b64encode }}"
DB_PORT: "{{ '5432' | b64encode }}"
DB_USER: "{{ postgresql_user | b64encode }}"
DB_PASSWORD: "{{ postgresql_password | b64encode }}"
DB_NAME: "{{ postgresql_database | b64encode }}"
- name: Attach secret to CR status
operator_sdk.util.k8s_status:
api_version: apps.example.com/v1alpha1
kind: PostgreSQL
name: '{{ meta.name }}'
namespace: '{{ meta.namespace }}'
status:
bind_credentials_secret: '{{ meta.name }}-credentials'
ansible_kubernetes_modules
The ansible_kubernetes_modules
role and the generated modules are now deprecated. The community.kubernetes
Ansible collection supports Ansible 2.9+ and is the supported way to interact with Kubernetes from Ansible. The community.kubernetes.k8s
module takes normal kubernetes manifests, so if you currently rely on the old generated modules some refactoring will be required.
convert.py
The convert.py
script should be run from inside the APB directory, next to the apb.yml
#!/usr/bin/env python
import yaml
def extract_params(all_params):
properties = {}
required = set()
for param in all_params:
name = param['name']
name_parts = name.split('_')
camel_name = name_parts[0] + ''.join([x.title() for x in name_parts[1:]])
if param.get('required') is True:
if camel_name not in properties:
required.add(camel_name)
elif camel_name in required and param.get('required') is False:
required.remove(camel_name)
properties[camel_name] = {
"type": param["type"],
"description": param.get("description", param.get("title", ""))
}
return {
"validation": {"openAPIv3Schema": {
"properties": {
"spec": {
"required": list(required),
"properties": properties
}
}
}}
}
def main():
with open('apb.yml', 'r') as f:
apb_meta = yaml.safe_load(f.read())
for field in ['parameters', 'bind_parameters']:
print("Converting {0} to OpenAPI spec".format(field))
print(yaml.dump({field: extract_params([
param for x in apb_meta['plans'] for param in x.get(field, [])
])}, default_flow_style=False))
if __name__ == '__main__':
main()
It will parse the parameters
and bind_parameters
from your apb.yml
, and output OpenAPI validation blocks that can be included in your CustomResourceDefinition
s. For example, when run through the convert.py
script, the following apb.yml
:
version: 1.0.0
name: keycloak-apb
description: Keycloak - Open Source Identity and Access Management
bindable: True
async: optional
tags:
- sso
- keycloak
metadata:
displayName: Keycloak (APB)
imageUrl: "https://github.com/ansibleplaybookbundle/keycloak-apb/raw/master/docs/imgs/keycloak_ico.png"
documentationUrl: "http://www.keycloak.org/documentation.html"
providerDisplayName: "Red Hat, Inc."
dependencies:
- 'docker.io/jboss/keycloak-openshift:3.4.3.Final'
- 'centos/postgresql-95-centos7:9.5'
serviceName: keycloak
plans:
- name: ephemeral
description: Deploy keycloak without persistence
free: True
metadata:
displayName: Keycloak ephemeral
parameters:
- name: admin_username
required: True
default: admin
type: string
title: Keycloak admin username
- name: admin_password
required: True
type: string
display_type: password
title: Keycloak admin password
- name: apb_keycloak_uri
required: False
type: string
title: Keycloak URL
description: URL where the applications should redirect to for authentication. Must be resolvable by the browser and pods. Leave empty to use the host generated by the route
- name: keycloak_users
required: False
type: string
display_type: textarea
title: Users
description: JSON defining the users to add to the realm and their memberships
- name: keycloak_roles
required: False
type: string
display_type: textarea
title: Roles
description: JSON defining the roles to add to the realm
bind_parameters:
- name: service_name
display_group: Provision
required: True
title: Name of the service to bind
type: string
- name: redirect_uris
display_group: Provision
required: True
title: Redirect URIs
description: Valid Redirect URIs a browser can redirect to after a successful login/logout. Simple wildcards are allowed. e.g. https://myservice-myproject.apps.example.com/*
type: string
- name: web_origins
display_group: Provision
title: Web Origins
description: Web Origins to allow CORS
type: string
- name: sso_url_name
default: SSO_URL
display_group: Binding
title: Keycloak URL Variable name
description: How the application will refer to the Keycloak URL
type: string
- name: sso_realm_name
default: SSO_REALM
display_group: Binding
title: Keycloak Realm Variable name
description: How the application will refer to the Keycloak Realm
type: string
- name: sso_client_name
default: SSO_CLIENT
display_group: Binding
title: Keycloak Client Variable name
description: How the application will refer to the Keycloak Client name
type: string
will produce this output:
Converting parameters to OpenAPI spec
parameters:
validation:
openAPIv3Schema:
properties:
spec:
properties:
adminPassword:
description: Keycloak admin password
type: string
adminUsername:
description: Keycloak admin username
type: string
apbKeycloakUri:
description: URL where the applications should redirect to for authentication.
Must be resolvable by the browser and pods. Leave empty to use the
host generated by the route
type: string
keycloakRoles:
description: JSON defining the roles to add to the realm
type: string
keycloakUsers:
description: JSON defining the users to add to the realm and their memberships
type: string
required:
- adminUsername
- adminPassword
Converting bind_parameters to OpenAPI spec
bind_parameters:
validation:
openAPIv3Schema:
properties:
spec:
properties:
redirectUris:
description: Valid Redirect URIs a browser can redirect to after a successful
login/logout. Simple wildcards are allowed. e.g. https://myservice-myproject.apps.example.com/*
type: string
serviceName:
description: Name of the service to bind
type: string
ssoClientName:
description: How the application will refer to the Keycloak Client name
type: string
ssoRealmName:
description: How the application will refer to the Keycloak Realm
type: string
ssoUrlName:
description: How the application will refer to the Keycloak URL
type: string
webOrigins:
description: Web Origins to allow CORS
type: string
required:
- serviceName
- redirectUris
The block beneath parameters
would be put into the Keycloak
CRD, and the block beneath bind_parameters
would be put in the KeycloakBinding
CRD.
Last modified March 23, 2020: Ansible doc org (#2704) (1739e10c)