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:

  1. 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 a memcached-apb, you would probably use memcached-operator
  • <group> is the API group for your Kubernetes Custom Resource Definition. For example, if I own the domain example.com, I might use the group apps.example.com.
  • <version> is the API version for your Kubernetes Custom Resource Definition. v1alpha1 is a common starting value, with v1beta1 implying a fair amount of API stability and v1 implying no breaking API changes at all.
  • <kind> is the kind of your resource. For example, if you are creating a memcached-operator, your kind would likely be Memcached

So for the example memcached-operator, the command would be:

  1. 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:

  1. FROM quay.io/operator-framework/ansible-operator:v0.9.0
  2. COPY watches.yaml ${HOME}/watches.yaml
  3. COPY roles/ ${HOME}/roles/
  4. 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:

  1. ---
  2. - version: v1alpha1
  3. group: apps.example.com
  4. kind: Memcached
  5. playbook: /opt/ansible/playbooks/provision.yml
  6. finalizer:
  7. name: finalizer.memcached.apps.example.com/v1alpha1
  8. 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:

  1. ---
  2. - version: v1alpha1
  3. group: apps.example.com
  4. kind: Memcached
  5. playbook: /opt/ansible/playbooks/provision.yml
  6. finalizer:
  7. name: finalizer.memcached.apps.example.com/v1alpha1
  8. playbook: /opt/ansible/playbooks/deprovision.yml
  9. - version: v1alpha1
  10. group: apps.example.com
  11. kind: MemcachedBinding
  12. playbook: /opt/ansible/playbooks/bind.yml
  13. finalizer:
  14. name: finalizer.memcachedbinding.apps.example.com/v1alpha1
  15. 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 by meta.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 the meta.name of a <kind>Binding resource
  • namespace: This is accessible via the meta.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:

  1. version: v1alpha1
  2. group: apps.example.com
  3. kind: PostgreSQL

the following task:

  1. - name: encode bind credentials
  2. asb_encode_binding:
  3. fields:
  4. DB_TYPE: postgres
  5. DB_HOST: "{{ app_name }}"
  6. DB_PORT: "5432"
  7. DB_USER: "{{ postgresql_user }}"
  8. DB_PASSWORD: "{{ postgresql_password }}"
  9. DB_NAME: "{{ postgresql_database }}"

would become:

  1. - name: Create bind credential secret
  2. community.kubernetes.k8s:
  3. definition:
  4. apiVersion: v1
  5. kind: Secret
  6. metadata:
  7. name: '{{ meta.name }}-credentials'
  8. namespace: '{{ meta.namespace }}'
  9. data:
  10. DB_TYPE: "{{ 'postgres' | b64encode }}"
  11. DB_HOST: "{{ app_name | b64encode }}"
  12. DB_PORT: "{{ '5432' | b64encode }}"
  13. DB_USER: "{{ postgresql_user | b64encode }}"
  14. DB_PASSWORD: "{{ postgresql_password | b64encode }}"
  15. DB_NAME: "{{ postgresql_database | b64encode }}"
  16. - name: Attach secret to CR status
  17. operator_sdk.util.k8s_status:
  18. api_version: apps.example.com/v1alpha1
  19. kind: PostgreSQL
  20. name: '{{ meta.name }}'
  21. namespace: '{{ meta.namespace }}'
  22. status:
  23. 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

  1. #!/usr/bin/env python
  2. import yaml
  3. def extract_params(all_params):
  4. properties = {}
  5. required = set()
  6. for param in all_params:
  7. name = param['name']
  8. name_parts = name.split('_')
  9. camel_name = name_parts[0] + ''.join([x.title() for x in name_parts[1:]])
  10. if param.get('required') is True:
  11. if camel_name not in properties:
  12. required.add(camel_name)
  13. elif camel_name in required and param.get('required') is False:
  14. required.remove(camel_name)
  15. properties[camel_name] = {
  16. "type": param["type"],
  17. "description": param.get("description", param.get("title", ""))
  18. }
  19. return {
  20. "validation": {"openAPIv3Schema": {
  21. "properties": {
  22. "spec": {
  23. "required": list(required),
  24. "properties": properties
  25. }
  26. }
  27. }}
  28. }
  29. def main():
  30. with open('apb.yml', 'r') as f:
  31. apb_meta = yaml.safe_load(f.read())
  32. for field in ['parameters', 'bind_parameters']:
  33. print("Converting {0} to OpenAPI spec".format(field))
  34. print(yaml.dump({field: extract_params([
  35. param for x in apb_meta['plans'] for param in x.get(field, [])
  36. ])}, default_flow_style=False))
  37. if __name__ == '__main__':
  38. main()

It will parse the parameters and bind_parameters from your apb.yml, and output OpenAPI validation blocks that can be included in your CustomResourceDefinitions. For example, when run through the convert.py script, the following apb.yml:

  1. version: 1.0.0
  2. name: keycloak-apb
  3. description: Keycloak - Open Source Identity and Access Management
  4. bindable: True
  5. async: optional
  6. tags:
  7. - sso
  8. - keycloak
  9. metadata:
  10. displayName: Keycloak (APB)
  11. imageUrl: "https://github.com/ansibleplaybookbundle/keycloak-apb/raw/master/docs/imgs/keycloak_ico.png"
  12. documentationUrl: "http://www.keycloak.org/documentation.html"
  13. providerDisplayName: "Red Hat, Inc."
  14. dependencies:
  15. - 'docker.io/jboss/keycloak-openshift:3.4.3.Final'
  16. - 'centos/postgresql-95-centos7:9.5'
  17. serviceName: keycloak
  18. plans:
  19. - name: ephemeral
  20. description: Deploy keycloak without persistence
  21. free: True
  22. metadata:
  23. displayName: Keycloak ephemeral
  24. parameters:
  25. - name: admin_username
  26. required: True
  27. default: admin
  28. type: string
  29. title: Keycloak admin username
  30. - name: admin_password
  31. required: True
  32. type: string
  33. display_type: password
  34. title: Keycloak admin password
  35. - name: apb_keycloak_uri
  36. required: False
  37. type: string
  38. title: Keycloak URL
  39. 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
  40. - name: keycloak_users
  41. required: False
  42. type: string
  43. display_type: textarea
  44. title: Users
  45. description: JSON defining the users to add to the realm and their memberships
  46. - name: keycloak_roles
  47. required: False
  48. type: string
  49. display_type: textarea
  50. title: Roles
  51. description: JSON defining the roles to add to the realm
  52. bind_parameters:
  53. - name: service_name
  54. display_group: Provision
  55. required: True
  56. title: Name of the service to bind
  57. type: string
  58. - name: redirect_uris
  59. display_group: Provision
  60. required: True
  61. title: Redirect URIs
  62. 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/*
  63. type: string
  64. - name: web_origins
  65. display_group: Provision
  66. title: Web Origins
  67. description: Web Origins to allow CORS
  68. type: string
  69. - name: sso_url_name
  70. default: SSO_URL
  71. display_group: Binding
  72. title: Keycloak URL Variable name
  73. description: How the application will refer to the Keycloak URL
  74. type: string
  75. - name: sso_realm_name
  76. default: SSO_REALM
  77. display_group: Binding
  78. title: Keycloak Realm Variable name
  79. description: How the application will refer to the Keycloak Realm
  80. type: string
  81. - name: sso_client_name
  82. default: SSO_CLIENT
  83. display_group: Binding
  84. title: Keycloak Client Variable name
  85. description: How the application will refer to the Keycloak Client name
  86. type: string

will produce this output:

  1. Converting parameters to OpenAPI spec
  2. parameters:
  3. validation:
  4. openAPIv3Schema:
  5. properties:
  6. spec:
  7. properties:
  8. adminPassword:
  9. description: Keycloak admin password
  10. type: string
  11. adminUsername:
  12. description: Keycloak admin username
  13. type: string
  14. apbKeycloakUri:
  15. description: URL where the applications should redirect to for authentication.
  16. Must be resolvable by the browser and pods. Leave empty to use the
  17. host generated by the route
  18. type: string
  19. keycloakRoles:
  20. description: JSON defining the roles to add to the realm
  21. type: string
  22. keycloakUsers:
  23. description: JSON defining the users to add to the realm and their memberships
  24. type: string
  25. required:
  26. - adminUsername
  27. - adminPassword
  28. Converting bind_parameters to OpenAPI spec
  29. bind_parameters:
  30. validation:
  31. openAPIv3Schema:
  32. properties:
  33. spec:
  34. properties:
  35. redirectUris:
  36. description: Valid Redirect URIs a browser can redirect to after a successful
  37. login/logout. Simple wildcards are allowed. e.g. https://myservice-myproject.apps.example.com/*
  38. type: string
  39. serviceName:
  40. description: Name of the service to bind
  41. type: string
  42. ssoClientName:
  43. description: How the application will refer to the Keycloak Client name
  44. type: string
  45. ssoRealmName:
  46. description: How the application will refer to the Keycloak Realm
  47. type: string
  48. ssoUrlName:
  49. description: How the application will refer to the Keycloak URL
  50. type: string
  51. webOrigins:
  52. description: Web Origins to allow CORS
  53. type: string
  54. required:
  55. - serviceName
  56. - 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 January 1, 0001