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