Skip to content

ansible k8s

ghdrako edited this page May 11, 2023 · 33 revisions

To interact with the Kubernetes cluster using the kubernetes.core Ansible collection. The kubernetes.core collection is not included in the Ansible Core, which includes only the ansible.builtin collection.

Prerequisities

You can verify the presence of the kubernetes.core Ansible collection using the following ansible-galaxy command:

$ ansible-galaxy collection list

The collection requires Python 3 installed in your system and the kubernetes Python library.

pip3 install PyYAML jsonpatch kubernetes

to interact with the Helm package manager, you also need the helm Python library.

pip3 install --use-pep517 helm

Install

  • manually
ansible-galaxy collection install kubernetes.core
  • automatic: Require creating a requirements.yml file, a particular YAML file with the list of all the Ansible collections to install in the target system.
$ cat requirements.yml 
---
collections:
- name: cloud.common
- name: kubernetes.core
ansible-galaxy install -r collections/requirements.yml

Prepare venv

$ python3 -m venv <env-name>
$ source <env-name>/bin/activate
(<env-name>) $ pip3 install --upgrade pip setuptools
$ python -m ensurepip --upgrade   # if pip not exist in instalation
(<env-name>) $ pip3 install PyYAML jsonpatch kubernetes
(<env-name>) $ pip3 install --use-pep517 helm
(<env-name>) $ pip3 freeze > requirements.txt
(<env-name>) $ ansible-galaxy collection install cloud.common
(<env-name>) $ ansible-galaxy collection install kubernetes.core
$ deactivate

After the manual or automatic Ansible collection installation process, your system is configured to interact with the kubernetes.core Ansible collection.

**Tip **the kubernetes.core ansible collection supports the ansible turbo mode. by default, this feature is disabled. it requires an additional cloud.common ansible collection installed. the AnsibleTurboModule class is inherited from the standard AnsibleModule class, which spawns a little python daemon, and the module logic runs inside this python daemon. the result is a drastic improvement in performance because python modules are loaded one time, and ansible can reuse an existing authenticated session. the daemon run in a single process exposes a local UniX socket for communication and kills itself after 15 seconds. You can set the ENABLE_TURBO_MODE variable to true or 1 using the environment statement in the play section of the ansible playbooks.

Once you have successfully installed the Python and Ansible dependencies, you need to execute the authentication to your cluster in order to execute API requests via the command-line tool.

For the Kubernetes cluster, you need the kubectl command-line utility. For the OpenShift cluster, the oc command-line utility is needed instead.

Authentication

In a Kubernetes cluster, obtaining a valid authentication token requires the setup of a context for your cluster with the kubectrl command-line utility.

$ kubectl config set-credentials developer/foo.example.com –-username=developer --password=developer
  • set-credentials developer/foo.example.com Creates a new credential named developer/foo.example.com (prefer to use the same as cluster name) Then you need to configure the cluster connection server:
$ kubectl config set-cluster foo.example.com –-insecure-skip-tls-verify=true –-server=https://foo.example.com
  • –-insecure-skip-tls-verify=true: Skips TLS certificates validation
  • –-server=https://foo.example.com: Specifies the cluster URI

Then you can join in a context the cluster and credentials information:

$ kubectl config set-context default/foo.example.com/developer --user=developer/foo.example.com 
                                                               --namespace=default --cluster=foo.example.com
  • --set-context default/foo.example.com/developer: Creates a new context named default/foo.example.com/developer (you can specify any name)
  • --user=developer/foo.example.com: Skips TLS certificates validation
  • --namespace=default: Specifies the default namespace (in this case, default)
  • --cluster=foo.example.com: Specifies the cluster configuration name

OpenShift

To obtain the OpenShift cluster using the oc command and specifying the username, password, cluster URI and the port

$ oc login -u developer -p developer https://api.openshift.example.com:6443

As an alternative to a username and password, you can authenticate using a token obtained via the OpenShift console. After successfully logging in to the web user interface, just choose Copy Login Command under your username in the top-right menu area. Once generated, the token is usable via the command-line oc command like the following:

$ oc login --token=sha256~xxxxxx --server=https://api.openshift.example.com:6443

After successfully authenticating, an authentication token is stored in the .kube directory under the config file on the local machine. You can customize the filename and path using the KUBECONFIG environment variable in the terminal.

$ export KUBECONFIG=/home/devops/.kube/config-developer

In the same way, you should instruct Ansible to read the environment variable by adding the following environment lines in the Play area of your Ansible Playbook:

[...]
environment:
KUBECONFIG: "/home/devops/.kube/config-developer"
[...]
$ oc login -u developer -p developer --server=https://api.openshift.example.com:6443

If needed add the additional certification authority file with the --certificate-authority parameter followed by the path on disk of the certificate. You can acquire more information by increasing the verbosity level to six, the highest available, with the --loglevel 6 parameter. A successful login procedure can be verified using this OpenShift command:

$ oc whoami

Using

Report Namespaces

To list all the namespaces can be easily achieved using Ansible Playbook with two tasks. The first task uses the Ansible query lookup plugin and invokes the Ansible kubernetes.core.k8s module to interact with the Kubernetes API and return a list of all the namespaces in the cluster. The result of the Ansible lookup plugin is saved in the projects runtime variable using the ansible.builtin.set_fact Ansible module. You can use this variable to perform further tasks, such as print the result onscreen using the ansible.builtin.debug Ansible module.

$cat ns_list.yml
---
- name: k8s ns
  hosts: all
  tasks:
    - name: list all namespaces
      ansible.builtin.set_fact:
        projects: "{{ query('kubernetes.core.k8s', api_version='v1',
        kind='Namespace') }}"

  - name: display the result
    ansible.builtin.debug:
      msg: "{{ projects }}"

Create a Namespace

The k8s Ansible module is part of the kubernetes.core collection; it’s very powerful and allows you to interact with the Kubernetes API like the kubectl or oc command-line utilities.

  • The name parameter specifies the namespace that you want to create or verify if present.
  • The api_version parameter specifies the Kubernetes API version; the default is v1 for version 1.
  • The kind parameter specifies an object model, which is namespace for this use case.
  • The state parameter determines if an object should be created (the - present option), updated (the - patched option), or deleted (the - absent option)
$cat ns_create.yml 
---
- name: k8s ns
  hosts: all
  vars:
    myproject: "ansible-examples"
  tasks:
    - name: namespace present
      kubernetes.core.k8s:
        api_version: v1
        kind: Namespace
        name: "{{ myproject }}"
        state: present
The Inventory File
localhost ansible_connection=local
$ ansible-playbook -i inventory ns_create.yml

Verify

$ kubectl get namespace | grep ansible-examples
# openshift
$ oc projects | grep ansible-examples

Authentication in playbook

It’s possible to perform the authentication step in the Ansible Playbook using the k8s_auth Ansible module, which is part of the kubernetes.core Ansible collection. If you want to execute the authentication directly in the Ansible Playbook, you need to add a task before executing any command to the cluster. The get access token task connects to the host address with the specified username and password credentials and saves the result in the k8s_auth_results runtime variable. The variable contains the token under the k8s_auth.api_key variable. Every time you use the k8s module, the extra parameter api_key is specified with the value of the token registered from the previous task.

$ cat ns_create_auth.yml 
---
- name: k8s ns
  hosts: all
  vars:
    myproject: "ansible-examples"
    k8s_username: "kubeadmin"
    k8s_password: "password"
    k8s_host: "https://api.k8s:6443"
    k8s_validate: true

    tasks:
      - name: get access token
        kubernetes.core.k8s_auth:
          username: "{{ k8s_username }}"
          password: "{{ k8s_password }}"
          host: "{{ k8s_host }}"
          validate_certs: "{{ k8s_validate }}"
        register: k8s_auth_results
      - name: namespace present
        kubernetes.core.k8s:
          api_key: "{{ k8s_auth_results.k8s_auth.api_key }}"
          api_version: v1
          kind: Namespace
          name: "{{ myproject }}"
          state: present

Store any variables with sensitive data in an Ansible vault that’s encrypted and protected. For OpenShift authentication, use the community.okd.openshift_auth Ansible module in the first task.

Report Namespaces

To achieved using Ansible Playbook with two tasks. The first task uses the Ansible query lookup plugin and invokes the Ansible kubernetes.core.k8s module to interact with the Kubernetes API and return a list of all the namespaces in the cluster. The result of the Ansible lookup plugin is saved in the projects runtime variable using the ansible.builtin.set_fact Ansible module. You can use this variable to perform further tasks, such as print the result onscreen using the ansible.builtin.debug Ansible module.

$cat ns_list.yml 
---
- name: k8s ns
  hosts: all
  tasks:
    - name: list all namespaces
      ansible.builtin.set_fact:
        projects: "{{ query('kubernetes.core.k8s', api_version='v1',
        kind='Namespace') }}"
    - name: display the result
      ansible.builtin.debug:
        msg: "{{ projects }}"

To filter the output in a more synthetic and useful way, use the community.general.json_query filter plugin and JMESPath.

Create Deployment

The pod definition is inside the Deployment YAML manifest document in the following example. The deployment.yaml document is a simple Deployment YAML file that deploys the latest tag of the container named nginx with two replicas listening on port TCP 3000 under the ansible-examples namespace

$cat deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: ansible-examples
spec:
  replicas: 2
  selector:
    matchLabels:
    app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
    - name: nginx
      image: nginx:1.22
      imagePullPolicy: Always
ports:
- containerPort: 3000

Manually create deployment

$ kubectl apply -f deployment.yaml
$ kubectl get deployment nginx
$ kubectl get pods
$ kubectl get pods -n ansible-examples
cat deployment.yml
---
- name: k8s deployment
  hosts: all
  vars:
    myproject: "ansible-examples"
  tasks:
    - name: namespace present
      kubernetes.core.k8s:
        api_version: v1
        kind: Namespace
        name: "{{ myproject }}"
        state: present
    - name: deployment present
      kubernetes.core.k8s:
        src: deployment.yaml
        namespace: "{{ myproject }}"
        state: present

Note You’ll receive a Kubernetes return code 404 error when the namespace is not present and a 403 error when the namespace is terminated.

Note When Kubernetes cannot download the Container image from the Container registry; it reports the statuses ImagePullBackOff and CrashLoopBackOff. these two statuses are connected, meaning that the Kubernetes cluster tried to download the image from the registry but without success. the full workflow is from ErrImagePull, then ImagePullBackOff, and then keep trying. the root cause could be a misspelled image name or tag, a registry issue, a rate limit in the registry (see free vs. pro accounts in Docker hub), a private registry without specifying the secret for the pod, or simply a bit of bad luck about connectivity.

Report Deployments in Namespacethe

Ansible query lookup plugin invokes the kubernetes.core.k8s module to interact with the Kubernetes API and return a list of all the deployments in the cluster. The query lookup plugin (sometimes shortened as q) invokes the kubernetes.core.k8s module, specifying the deployment as the kind parameter and ansible-examples as a namespace parameter. You can list other Kubernetes objects by changing the kind parameter value or other namespaces by changing the namespace parameter value. The lookup plug returns the full JSON received from the Kubernetes cluster

You need to filter the output to extract a single element: the name of the deployment in the namespace. The most useful Ansible filter plugin to extract a single element is the Ansible json_query filter plugin. It’s specifically designed to select a single element or a data structure from a JSON output. Its full name is community.general.json_query, which means that you need the additional community.general Ansible collection installed in your system. The community.general.json_query.

You can install the collection using the ansible-galaxy command-line tool:

$ ansible-galaxy collection install community.general

The Ansible module requires the Python jmespath library to be installed in the system:

$ pip3 install jmespath

With the Ansible json_query filter plugin, you can use the JMESPath popular query language for JSON. Considering the Kubernetes output, you apply the [].metadata.name filter pattern which means extracting for each element of the list (the star between brackets []) the key metadata, and displaying the subkey name specified using the dot notation. The result of the Ansible lookup plugin and the Ansible filter is saved in the deployments runtime variable using the ansible.builtin.set_fact Ansible module. It’s displayed onscreen using the Ansible debug module.

$cat deployment_list.yml
---
- name: k8s deployments
  hosts: all
  tasks:
    - name: list all deployments
      ansible.builtin.set_fact:
      deployments: "{{ query('kubernetes.core.k8s', kind='Deployment',
      namespace='ansible-examples') | community.general.json_query('[*].
      metadata.name') }}"
    - name: display the result
      ansible.builtin.debug:
        msg: "{{ deployments }}"

Output

TASK [display the result]
ok: [localhost] => {
  "msg": [
    "nginx"
  ]
}

Create a Pod

Kubernetes file

$cat pod.yaml  
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
    - name: nginx
      image: nginx:latest
      ports:
        - containerPort: 80

Ansible file

$cat pod.yml 
---
- name: k8s pod
  hosts: all
  vars:
    myproject: "ansible-examples"
  tasks:
    - name: namespace present
      kubernetes.core.k8s:
        api_version: v1
        kind: Namespace
        name: "{{ myproject }}"
        state: present
    - name: pod present
        kubernetes.core.k8s:
        src: pod.yaml
        namespace: "{{ myproject }}"
        state: present

Check

$ kubectl get pods -n ansible-examples
$ oc get pods -n ansible-examples

Create a Secret

Secrets are not encrypted; they are encoded in base64 and stored in etcd on an encrypted volume. Note that when you decide to encrypt your data, you can’t return to the non-encrypted status.

Kubernetes file

$cat secret.yaml 
---
apiVersion: v1
kind: Secret
metadata:
  name: mysecret
  namespace: ansible-examples
  type: Opaque
  data:
    username: YWRtaW4=
    password: bXlzdXBlcnNlY3JldHBhc3N3b3Jk

Ansible Playbook File

$cat secret.yml 
---
- name: k8s secret
  hosts: all
  tasks:
    - name: namespace present
      kubernetes.core.k8s:
      api_version: v1
      kind: Namespace
      name: "{{ myproject }}"
      state: present
    - name: secret present
      kubernetes.core.k8s:
      src: secret.yaml
      state: present

You can create two data fields: username and password, with the value. The value must be encoded as base64.

$ kubectl get secrets -n ansible-examples
$ oc get secrets -n ansible-examples

Create Service

The Kubernetes Service is continuously updated by the pod statuses with of healthy pods. The Kubernetes Service provides a constant IP address and port that act as an entry point to a group of pods. This information doesn’t change for as long as the service exists. Internal and external clients can reach your application running in a group of pods by connecting to the service IP and ports. These connections are routed to one of the pods behind the Kubernetes Service.

$cat service.yaml 
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-example
spec:
  selector:
    app: nginx-example
    ports:
      - protocol: TCP
        port: 80
        targetPort: 3000
$ kubectl apply -f service.yaml
$ kubectl get service nginx-example -n ansible-examples
$ oc get services -n ansible-examples

Ansible Playbook

---
- name: k8s service
  hosts: all
  vars:
    myproject: "ansible-examples"
    tasks:
      - name: k8s service
        kubernetes.core.k8s:
          src: service.yaml
          namespace: "{{ myproject }}"
          state: present

Kubernetes Networking

Kubernetes uses five techniques to track pod status and direct traffic to the appropriate pods:

  • ClusterIP: Accessible only internally within the cluster.
  • NodePort: Exposes a static port (the NodePort) on each node’s IP that can be accessed from outside the cluster by the address NodeIP:NodePort. The NodePort service connects to a ClusterIP service that is created automatically for your NodePort.
  • LoadBalancer: Exposes a load balancer externally. The LoadBalancer service connects to the NodePort and ClusterIP, automatically created.
  • Ingress: Reduces the amount of load balancer for HTTP and HTTPS defining traffic routes.
  • ExternalName: Maps to a DNS name. It returns a CNAME record with its value.

The simplest network configuration is the ClusterIP service type. This is accessible from within the cluster only. A ClusterIP service IP address is assigned from the cluster range. The ClusterIP service routes the traffic to the nodes. Behind the scene, each node runs a kube-proxy container for this task. The kube-proxy container creates the appropriate IPtables firewall rules to redirect the ClusterIP traffic to the appropriate Pod IP address. This type of service is used by frontend pods to redirect traffic to the backend pods or to act like load balancers. In the Kubernetes YAML for ClusterIP service, the most important field is the selector. It determines which pods serve as endpoints for this service.

Kubernetes file

cat ClusterIP.yaml 
---
apiVersion: v1
kind: Service
metadata:
  name: "nginx-example"
  namespace: "mynamespace"
  spec:
    type: ClusterIP
    selector:
      app: "nginx"
      ports:
        - name: http
          protocol: TCP
          port: 80
          targetPort: 9376

The NodePortservice type is similar to a ClusterIP service, but it also opens a port on each node. Opening a port allows access to the service from inside the cluster (using the ClusterIP). External users can connect directly to the node on the NodePort.

A random port is opened on the local node for each service unless the nodeport property specifies a specific port. The kube-proxy container forwards traffic from the port to the service’s cluster IP and then to the pod that’s updating the IPtable rules.

---
apiVersion: v1
kind: Service
metadata:
  name: "nginx-example"
  namespace: "mynamespace"
  spec:
    type: NodePort
    selector:
      app: "nginx"
      ports:
        - name: http
          protocol: TCP
          port: 80
          targetPort: 9376
          nodeport: 25000

The LoadBalancer service type extends the NodePort service type by adding a load balancer in front of all nodes. This means Kubernetes requests a load balancer and registers all the nodes. The load balancer doesn’t detect where the pods for a specific service are running. Consequently, the load balancer adds all worker nodes as backend instances. The Load Balancer service type uses the classic load balancers by default. This means there is a classic load balancer in front of all instances listening to your requests. The load balancer routes the requests to the nodes through an exposed port. You can also use the network load balancer instead of the classic load balancer. The classic load balancer processes the requests one by one when they arrive from the Internet. It then forwards the request to one of the Kubernetes instances on a specific port. When there is a service listening to a specific port, it acts like a second-layer load balancer for the backend pods that handle the requests.

$cat LoadBalancer.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: "nginx-example"
  namespace: "mynamespace"
  spec:
    type: LoadBalancer
    selector:
      app: "nginx"
      ports:
        - name: http
          port: 80
          targetPort: 9376

You can reduce the number of load balancers using the Kubernetes ingress object. An ingress object exposes HTTP and HTTPS routes outside the cluster and routes traffic to your services according to defined traffic rules. Ingress objects use ingress controllers, which fulfill the ingress rules and requests (usually using a load balancer). Using the ingress objects and controllers, you can transition from one load balancing per service to one load balancer per ingress and route to multiple services. Traffic can be routed to the proper service using path-based routing. When using Kubernetes, you have many ingress controller options, for example, Envoy Controllers and NIGNX controllers. The load balancer picks the proper target group based on the path. The load balancer then forwards the request to one of the Kubernetes instances on the application ports. The service listens at a specific port and balances the requests to one of the pods of the application or service

$cat Ingress.yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: "nginx-example"
  namespace: "mynamespace"
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx-example
  rules:
    - http:
      paths:
        - path: /webapp
          backend:
            service:
              serviceName: webapp
              servicePort: 9376

The ExternalName service type is used to connect to a resource outside of the cluster. For example, this could be a database or file system resource that is in the network outside the cluster. The pods that require access to the external resource service require access to the resource to connect to the resource service, which returns the external resource endpoints. This type of service is helpful when you decide to migrate the external resource to the Kubernetes cluster. When the resource is deployed in the cluster, you can update the service type to ClusterIP. The application can continue to use the same resource endpoint. The most important property in the YAML manifest is the externalName field, which specifies where the service maps to.

$cat ExternalName.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: "nginx-example"
  namespace: "mynamespace"
  spec:
    type: ExternalName
    externalName: mydb.example.com

Scaling

Since Kubernetes version 1.21, PodDisruptionBudget (PDB) is a way to limit the number of concurrent disruptions that your application experiences, thus allowing for high availability. Meanwhile, it allows the cluster administrator to manage the nodes of the cluster by manually draining a node or preventing an upgrade from taking too many copies of the application offline. PDB allows you to specify and overprovision in order to guarantee the high availability of your service. PDB is different from auto-scaling because it overprovisions the application on purpose.

$ kubectl scale deployment nginx --replicas=3
$ kubectl get deployments
$ kubectl scale deployment nginx --replicas=1

With Ansible, you can scale a deployment, ReplicaSet, replication controller, and job using the kubernetes.core.k8s_scale Ansible module. The k8s_scale Ansible module is part of the kubernetes.core collection and it interacts with the Kubernetes API like the kubectl or oc command-line utilities.

$cat scale_up.yml
---
- name: k8s scale
  hosts: all
  vars:
    myproject: "ansible-examples"
    mydeployment: "nginx"
    myreplica: 10
    mytimeout: 120
  tasks:
    - name: scale Deployment
      kubernetes.core.k8s_scale:
        api_version: v1
        kind: Deployment
        name: "{{ mydeployment }}"
        namespace: "{{ myproject }}"
        replicas: "{{ myreplica }}"
        wait_timeout: "{{ mytimeout }}"

The Ansible Playbook scale_up.yml changes the replica count to ten of the deployment nginx in the namespace ansible-examples. The Ansible Playbook waits for a timeout maximum of 120 seconds (two minutes) for the operation to complete.

When the pod count is changed by a big number and the time is not sufficient, you might receive a failed status with the following message on the screen: Resource scaling timed out. This simply means that the Kubernetes cluster is taking longer than expected to execute the operation; it’s not a failure message. A more deep analysis of the fatal error message, for example, reveals the operation in progress:

"message": "ReplicaSet \"nginx-689b466988\" is progressing."

Auto-scaling

  • Horizontal scaling adds/remove pods of the same size capacity.
  • Vertical scaling keeps the same number of pods, but you change the size of capacity. Kubernetes Cluster Autoscaler (CA) allocates the right amount of resources to guarantee that your application meets the demands of the traffic. The change is performed for your pods in the auto-scale group in your cluster in order to enable Horizontal Pod Autoscaler (HPA) or Vertical Pod Autoscaler (VPA).

Kubernetes Cluster Autoscaler can be deployed, but it requires permission to interact with your cluster. The Kubernetes Cluster Autoscaler needs a service account to interact with the auto-scaling group.

Name Description
helm Add, update, and delete Kubernetes packages using the package manager Helm
helm_info Obtain information about a Helm package deployed into the Kubernetes cluster
helm_plugin Add, update, and delete Helm plugins
helm_plugin_info Obtain information about your Helm plugins
helm_plugin_info Obtain information about your Helm plugins
helm_repository Add, update, and delete Helm repositories
helm_template Render Helm chart templates
k8s Interact with the Kubernetes (K8s) objects
k8s_cluster_info Obtain Kubernetes clusters, any APIs available, and their respective versions
k8s_cp Copy files and directories to and from a pod
k8s_drain Set drain, cordon, or uncordon for a node in the Kubernetes cluster
k8s_exec Execute a command in a pod
k8s_info Obtain information about Kubernetes objects
k8s_json_patch Apply JSON patch operations to existing Kubernetes objects
k8s_log Fetch logs from a Kubernetes resource
K8s_rollback Roll back Kubernetes object deployments and DaemonSets
k8s_scale Set the scale parameter for a deployment, ReplicaSet, replication controller, or job
k8s_service Add, update, and delete services on Kubernetes
k8s_taint Set the taint attribute for a node in a Kubernetes cluster
kubectl Execute commands in Pods on a Kubernetes cluster (connection plugin)
k8s_config_resource_namer Generate resource names for the given resource of type ConfigMap, Secret (filter plugin)
k8s Fetch containers and services for one or more Kubernetes clusters and group by cluster name, namespace, namespace_services, namespace_pods, and labels (inventory plugin)
k8s Provide access to the full range of Kubernetes APIs (lookup plugin)
kustomize Use the kustomization.yaml file to build a set of Kubernetes resources (lookup plugin)

Test

Clone this wiki locally