github.skymusic.top/operator-framework/operator-sdk@v0.8.2/doc/ansible/dev/developer_guide.md (about)

     1  # Developer guide
     2  
     3  This document provides some useful information and tips for a developer
     4  creating an operator powered by Ansible.
     5  
     6  ## Getting started with the k8s Ansible modules
     7  
     8  Since we are interested in using Ansible for the lifecycle management of our
     9  application on Kubernetes, it is beneficial for a developer to get a good grasp
    10  of the [k8s Ansible module][k8s_ansible_module]. This Ansible module allows a
    11  developer to either leverage their existing Kubernetes resource files (written
    12  in YaML) or express the lifecycle management in native Ansible. One of the
    13  biggest benefits of using Ansible in conjunction with existing Kubernetes
    14  resource files is the ability to use Jinja templating so that you can customize
    15  deployments with the simplicity of a few variables in Ansible.
    16  
    17  The easiest way to get started is to install the modules on your local machine
    18  and test them using a playbook.
    19  
    20  ### Installing the k8s Ansible modules
    21  
    22  To install the k8s Ansible modules, one must first install Ansible 2.6+. On
    23  Fedora/Centos:
    24  ```bash
    25  $ sudo dnf install ansible
    26  ```
    27  
    28  In addition to Ansible, a user must install the [OpenShift Restclient
    29  Python][openshift_restclient_python] package. This can be installed from pip:
    30  ```bash
    31  $ pip install openshift
    32  ```
    33  
    34  ### Testing the k8s Ansible modules locally
    35  
    36  Sometimes it is beneficial for a developer to run the Ansible code from their
    37  local machine as opposed to running/rebuilding the operator each time. To do
    38  this, initialize a new project:
    39  ```bash
    40  $ operator-sdk new --type ansible --kind Foo --api-version foo.example.com/v1alpha1 foo-operator
    41  Create foo-operator/tmp/init/galaxy-init.sh
    42  Create foo-operator/tmp/build/Dockerfile
    43  Create foo-operator/tmp/build/test-framework/Dockerfile
    44  Create foo-operator/tmp/build/go-test.sh
    45  Rendering Ansible Galaxy role [foo-operator/roles/Foo]...
    46  Cleaning up foo-operator/tmp/init
    47  Create foo-operator/watches.yaml
    48  Create foo-operator/deploy/rbac.yaml
    49  Create foo-operator/deploy/crd.yaml
    50  Create foo-operator/deploy/cr.yaml
    51  Create foo-operator/deploy/operator.yaml
    52  Run git init ...
    53  Initialized empty Git repository in /home/dymurray/go/src/github.com/dymurray/opsdk/foo-operator/.git/
    54  Run git init done
    55  
    56  $ cd foo-operator
    57  ```
    58  
    59  Modify `roles/Foo/tasks/main.yml` with desired Ansible logic. For this example
    60  we will create and delete a namespace with the switch of a variable:
    61  ```yaml
    62  ---
    63  - name: set test namespace to {{ state }}
    64    k8s:
    65      api_version: v1
    66      kind: Namespace
    67      state: "{{ state }}"
    68    ignore_errors: true
    69  ```
    70  **note**: Setting `ignore_errors: true` is done so that deleting a nonexistent
    71  project doesn't error out.
    72  
    73  Modify `roles/Foo/defaults/main.yml` to set `state` to `present` by default.
    74  ```yaml
    75  ---
    76  state: present
    77  ```
    78  
    79  Create an Ansible playbook `playbook.yml` in the top-level directory which
    80  includes role `Foo`:
    81  ```yaml
    82  ---
    83  - hosts: localhost
    84    roles:
    85      - Foo
    86  ```
    87  
    88  Run the playbook:
    89  ```bash
    90  $ ansible-playbook playbook.yml
    91   [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
    92  
    93  
    94  PLAY [localhost] ***************************************************************************
    95  
    96  TASK [Gathering Facts] *********************************************************************
    97  ok: [localhost]
    98  
    99  Task [Foo : set test namespace to present]
   100  changed: [localhost]
   101  
   102  PLAY RECAP *********************************************************************************
   103  localhost                  : ok=2    changed=1    unreachable=0    failed=0
   104  
   105  ```
   106  
   107  Check that the namespace was created:
   108  ```bash
   109  $ kubectl get namespace
   110  NAME          STATUS    AGE
   111  default       Active    28d
   112  kube-public   Active    28d
   113  kube-system   Active    28d
   114  test          Active    3s
   115  ```
   116  
   117  Rerun the playbook setting `state` to `absent`:
   118  ```bash
   119  $ ansible-playbook playbook.yml --extra-vars state=absent
   120   [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
   121  
   122  
   123  PLAY [localhost] ***************************************************************************
   124  
   125  TASK [Gathering Facts] *********************************************************************
   126  ok: [localhost]
   127  
   128  Task [Foo : set test namespace to absent]
   129  changed: [localhost]
   130  
   131  PLAY RECAP *********************************************************************************
   132  localhost                  : ok=2    changed=1    unreachable=0    failed=0
   133  
   134  ```
   135  
   136  Check that the namespace was deleted:
   137  ```bash
   138  $ kubectl get namespace
   139  NAME          STATUS    AGE
   140  default       Active    28d
   141  kube-public   Active    28d
   142  kube-system   Active    28d
   143  ```
   144  ## Using Ansible inside of an Operator
   145  Now that we have demonstrated using the Ansible Kubernetes modules, we want to
   146  trigger this Ansible logic when a custom resource changes. In the above
   147  example, we want to map a role to a specific Kubernetes resource that the
   148  operator will watch. This mapping is done in a file called `watches.yaml`.
   149  
   150  ### Custom Resource file
   151  
   152  The Custom Resource file format is Kubernetes resource file. The object has
   153  mandatory fields:
   154  
   155  **apiVersion**:  The version of the Custom Resource that will be created.
   156  
   157  **kind**:  The kind of the Custom Resource that will be created
   158  
   159  **metadata**:  Kubernetes specific metadata to be created
   160  
   161  **spec**:  This is the key-value list of variables which are passed to Ansible.
   162  This field is optional and will be empty by default.
   163  
   164  **annotations**: Kubernetes specific annotations to be appended to the CR. See
   165  the below section for Ansible Operator specific annotations.
   166  
   167  #### Ansible Operator annotations
   168  This is the list of CR annotations which will modify the behavior of the operator:
   169  
   170  **ansible.operator-sdk/reconcile-period**: Used to specify the reconciliation
   171  interval for the CR. This value is parsed using the standard Golang package
   172  [time][time_pkg]. Specifically [ParseDuration][time_parse_duration] is used
   173  which will apply the default suffix of `s` giving the value in seconds.
   174  
   175  Example:
   176  ```
   177  apiVersion: "foo.example.com/v1alpha1"
   178  kind: "Foo"
   179  metadata:
   180    name: "example"
   181  annotations:
   182    ansible.operator-sdk/reconcile-period: "30s"
   183  ```
   184  
   185  ### Testing an Ansible operator locally
   186  
   187  Once a developer is comfortable working with the above workflow, it will be
   188  beneficial to test the logic inside of an operator. To accomplish this, we can
   189  use `operator-sdk up local` from the top-level directory of our project. The
   190  `up local` command reads from `./watches.yaml` and uses `~/.kube/config` to
   191  communicate with a Kubernetes cluster just as the `k8s` modules do. This
   192  section assumes the developer has read the [Ansible Operator user
   193  guide][ansible_operator_user_guide] and has the proper dependencies installed.
   194  
   195  Since `up local` reads from `./watches.yaml`, there are a couple options
   196  available to the developer. If `role` is left alone (by default
   197  `/opt/ansible/roles/<name>`) the developer must copy the role over to
   198  `/opt/ansible/roles` from the operator directly. This is cumbersome because
   199  changes will not be reflected from the current directory. It is recommended
   200  that the developer instead change the `role` field to point to the current
   201  directory and simply comment out the existing line:
   202  ```yaml
   203  - version: v1alpha1
   204    group: foo.example.com
   205    kind: Foo
   206    #  role: /opt/ansible/roles/Foo
   207    role: /home/user/foo-operator/Foo
   208  ```
   209  
   210  Create a Custom Resource Definition (CRD) and proper Role-Based Access Control
   211  (RBAC) definitions for resource Foo. `operator-sdk` auto-generates these files
   212  inside of the `deploy` folder:
   213  ```bash
   214  $ kubectl create -f deploy/crds/foo_v1alpha1_foo_crd.yaml
   215  $ kubectl create -f deploy/service_account.yaml
   216  $ kubectl create -f deploy/role.yaml
   217  $ kubectl create -f deploy/role_binding.yaml
   218  ```
   219  
   220  Run the `up local` command:
   221  ```bash
   222  $ operator-sdk up local
   223  INFO[0000] Go Version: go1.10.3
   224  INFO[0000] Go OS/Arch: linux/amd64
   225  INFO[0000] operator-sdk Version: 0.0.6+git
   226  INFO[0000] Starting to serve on 127.0.0.1:8888
   227  
   228  INFO[0000] Watching foo.example.com/v1alpha1, Foo, default
   229  ```
   230  
   231  Now that the operator is watching resource `Foo` for events, the creation of a
   232  Custom Resource will trigger our Ansible Role to be executed. Take a look at
   233  `deploy/cr.yaml`:
   234  ```yaml
   235  apiVersion: "foo.example.com/v1alpha1"
   236  kind: "Foo"
   237  metadata:
   238    name: "example"
   239  ```
   240  
   241  Since `spec` is not set, Ansible is invoked with no extra variables. The next
   242  section covers how extra variables are passed from a Custom Resource to
   243  Ansible. This is why it is important to set sane defaults for the operator.
   244  
   245  Create a Custom Resource instance of Foo with default var `state` set to
   246  `present`:
   247  ```bash
   248  $ kubectl create -f deploy/cr.yaml
   249  ```
   250  
   251  Check that namespace `test` was created:
   252  ```bash
   253  $ kubectl get namespace
   254  NAME          STATUS    AGE
   255  default       Active    28d
   256  kube-public   Active    28d
   257  kube-system   Active    28d
   258  test          Active    3s
   259  ```
   260  
   261  Modify `deploy/cr.yaml` to set `state` to `absent`:
   262  ```yaml
   263  apiVersion: "foo.example.com/v1alpha1"
   264  kind: "Foo"
   265  metadata:
   266    name: "example"
   267  spec:
   268    state: "absent"
   269  ```
   270  
   271  Apply the changes to Kubernetes and confirm that the namespace is deleted:
   272  ```bash
   273  $ kubectl apply -f deploy/cr.yaml
   274  $ kubectl get namespace
   275  NAME          STATUS    AGE
   276  default       Active    28d
   277  kube-public   Active    28d
   278  kube-system   Active    28d
   279  ```
   280  
   281  ### Testing an Ansible operator on a cluster
   282  
   283  Now that a developer is confident in the operator logic, testing the operator
   284  inside of a pod on a Kubernetes cluster is desired. Running as a pod inside a
   285  Kubernetes cluster is preferred for production use.
   286  
   287  To build the `foo-operator` image and push it to a registry:
   288  ```
   289  $ operator-sdk build quay.io/example/foo-operator:v0.0.1
   290  $ docker push quay.io/example/foo-operator:v0.0.1
   291  ```
   292  
   293  Kubernetes deployment manifests are generated in `deploy/operator.yaml`. The
   294  deployment image in this file needs to be modified from the placeholder
   295  `REPLACE_IMAGE` to the previous built image. To do this run:
   296  ```
   297  $ sed -i 's|REPLACE_IMAGE|quay.io/example/foo-operator:v0.0.1|g' deploy/operator.yaml
   298  ```
   299  
   300  **Note**
   301  If you are performing these steps on OSX, use the following command:
   302  ```
   303  $ sed -i "" 's|REPLACE_IMAGE|quay.io/example/foo-operator:v0.0.1|g' deploy/operator.yaml
   304  ```
   305  
   306  Deploy the foo-operator:
   307  
   308  ```sh
   309  $ kubectl create -f deploy/crds/foo_v1alpha1_foo_crd.yaml # if CRD doesn't exist already
   310  $ kubectl create -f deploy/service_account.yaml
   311  $ kubectl create -f deploy/role.yaml
   312  $ kubectl create -f deploy/role_binding.yaml
   313  $ kubectl create -f deploy/operator.yaml
   314  ```
   315  
   316  Verify that the foo-operator is up and running:
   317  
   318  ```sh
   319  $ kubectl get deployment
   320  NAME                     DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
   321  foo-operator       1         1         1            1           1m
   322  ```
   323  
   324  #### Viewing the Ansible logs
   325  
   326  The `foo-operator` deployment creates a Pod with two containers, `operator` and `ansible`.
   327  The `ansible` container exists only to expose the standard Ansible stdout logs that most Ansible
   328  users will be familiar with. In order to see the logs from a particular container, you can run
   329  
   330  ```sh
   331  kubectl logs deployment/foo-operator -c ansible
   332  kubectl logs deployment/foo-operator -c operator
   333  ```
   334  
   335  The `ansible` logs contain all of the information about the Ansible run and will make it much easier to debug issues within your Ansible tasks,
   336  whereas the `operator` logs will contain much more detailed information about the Ansible Operator's internals and interface with Kubernetes.
   337  
   338  
   339  ## Custom Resource Status Management
   340  The operator will automatically update the CR's `status` subresource with
   341  generic information about the previous Ansible run. This includes the number of
   342  successful and failed tasks and relevant error messages as seen below:
   343  
   344  ```yaml
   345  status:
   346    conditions:
   347      - ansibleResult:
   348        changed: 3
   349        completion: 2018-12-03T13:45:57.13329
   350        failures: 1
   351        ok: 6
   352        skipped: 0
   353      lastTransitionTime: 2018-12-03T13:45:57Z
   354      message: 'Status code was -1 and not [200]: Request failed: <urlopen error [Errno
   355        113] No route to host>'
   356      reason: Failed
   357      status: "True"
   358      type: Failure
   359    - lastTransitionTime: 2018-12-03T13:46:13Z
   360      message: Running reconciliation
   361      reason: Running
   362      status: "True"
   363      type: Running
   364  ```
   365  
   366  Ansible Operator also allows you as the developer to supply custom status
   367  values with the [k8s_status][k8s_status_module] Ansible Module. This allows the
   368  developer to update the `status` from within Ansible with any key/value pair as
   369  desired. By default, Ansible Operator will always include the generic Ansible
   370  run output as shown above. If you would prefer your application *not* update
   371  the status with Ansible output and would prefer to track the status manually
   372  from your application, then simply update the watches file with `manageStatus`:
   373  ```yaml
   374  - version: v1
   375    group: api.example.com
   376    kind: Foo
   377    role: /opt/ansible/roles/Foo
   378    manageStatus: false
   379  ```
   380  
   381  To update the `status` subresource with key `foo` and value `bar`, `k8s_status`
   382  can be used as shown:
   383  ```yaml
   384  - k8s_status:
   385      api_version: app.example.com/v1
   386      kind: Foo
   387      name: "{{ meta.name }}"
   388      namespace: "{{ meta.namespace }}"
   389      status:
   390        foo: bar
   391  ```
   392  
   393  ### Ansible Operator Conditions
   394  The Ansible Operator has a set of conditions which it will use as it performs
   395  its reconciliation procedure. There are only a few main conditions:
   396  
   397  * Running - the Ansible Operator is currently running the Ansible for
   398    reconciliation.
   399  
   400  * Successful - if the run has finished and there were no errors, the Ansible
   401    Operator will be marked as Successful. It will then wait for the next
   402    reconciliation action, either the reconcile period, dependent watches triggers
   403    or the resource is updated.
   404  
   405  * Failed - if there is any error during the reconciliation run, the Ansible
   406    Operator will be marked as Failed with the error message from the error that
   407    caused this condition. The error message is the raw output from the Ansible
   408    run for reconciliation. If the failure is intermittent, often times the
   409    situation can be resolved when the Operator reruns the reconciliation loop.
   410  
   411  Please look over the following sections for help debugging an Ansible Operator:
   412  
   413  
   414  * [View the Ansible logs](../user-guide.md#view-the-ansible-logs)
   415  * [Additional Ansible debug](../user-guide.md#additional-ansible-debug)
   416  * [Testing Ansible Operators with Molecule](testing_guide.md#testing-ansible-operators-with-molecule)
   417  
   418  ### Using k8s_status Ansible module with `up local`
   419  This section covers the required steps to using the `k8s_status` Ansible module
   420  with `operator-sdk up local`. If you are unfamiliar with managing status from
   421  the Ansible Operator, see the [proposal for user-driven status
   422  management][manage_status_proposal].
   423  
   424  If your operator takes advantage of the `k8s_status` Ansible module and you are
   425  interested in testing the operator with `operator-sdk up local`, then it is
   426  imperative that the module is installed in a location that Ansible expects.
   427  This is done with the `library` configuration option for Ansible. For our
   428  example, we will assume the user is placing third-party Ansible modules in
   429  `/usr/share/ansible/library`.
   430  
   431  To install the `k8s_status` module, first set `ansible.cfg` to search in
   432  `/usr/share/ansible/library` for installed Ansible modules:
   433  ```bash
   434  $ echo "library=/usr/share/ansible/library/" >> /etc/ansible/ansible.cfg
   435  ```
   436  
   437  Add `k8s_status.py` to `/usr/share/ansible/library/`:
   438  ```bash
   439  $ wget https://raw.githubusercontent.com/fabianvf/ansible-k8s-status-module/master/k8s_status.py -O /usr/share/ansible/library/k8s_status.py
   440  ```
   441  
   442  ## Extra vars sent to Ansible
   443  The extra vars that are sent to Ansible are managed by the operator. The `spec`
   444  section will pass along the key-value pairs as extra vars.  This is equivalent
   445  to how above extra vars are passed in to `ansible-playbook`. The operator also
   446  passes along additional variables under the `meta` field for the name of the CR
   447  and the namespace of the CR.
   448  
   449  For the CR example:
   450  ```yaml
   451  apiVersion: "app.example.com/v1alpha1"
   452  kind: "Database"
   453  metadata:
   454    name: "example"
   455  spec:
   456    message:"Hello world 2"
   457    newParameter: "newParam"
   458  ```
   459  
   460  The structure passed to Ansible as extra vars is:
   461  
   462  
   463  ```json
   464  { "meta": {
   465          "name": "<cr-name>",
   466          "namespace": "<cr-namespace>",
   467    },
   468    "message": "Hello world 2",
   469    "new_parameter": "newParam",
   470    "_app_example_com_database": {
   471       <Full CRD>
   472     },
   473  }
   474  ```
   475  `message` and `newParameter` are set in the top level as extra variables, and
   476  `meta` provides the relevant metadata for the Custom Resource as defined in the
   477  operator. The `meta` fields can be accesses via dot notation in Ansible as so:
   478  ```yaml
   479  ---
   480  - debug:
   481      msg: "name: {{ meta.name }}, {{ meta.namespace }}"
   482  ```
   483  
   484  [k8s_ansible_module]:https://docs.ansible.com/ansible/2.6/modules/k8s_module.html
   485  [k8s_status_module]:https://github.com/fabianvf/ansible-k8s-status-module
   486  [openshift_restclient_python]:https://github.com/openshift/openshift-restclient-python
   487  [ansible_operator_user_guide]:../user-guide.md
   488  [manage_status_proposal]:../../proposals/ansible-operator-status.md
   489  [time_pkg]:https://golang.org/pkg/time/
   490  [time_parse_duration]:https://golang.org/pkg/time/#ParseDuration