Deploy Manifest #
After our Kubernetes cluster is successfully running, the next operational step we most often do is managing workloads on top of it. We need to deploy our application components, from web servers, backend services, databases, to external network routing rules (Ingress). In the traditional Kubernetes world, we’re used to running the kubectl apply -f manifest.yaml command. However, when we talk about large-scale automation and multi-environment deployments (like Dev, Staging, and Prod), manual kubectl commands quickly trigger configuration inconsistencies.
Ansible provides a dedicated kubernetes.core collection designed to bridge the container orchestration world with Ansible’s automation power. Through the modules in this collection, we can write Kubernetes manifests declaratively inside playbooks, leverage the Jinja2 templating engine for dynamic manifests, integrate sensitive data directly from Ansible Vault, and ensure the deployment flow runs idempotently and safely. This article will deeply discuss how we manage the entire Kubernetes manifest lifecycle using Ansible.
Setting Up Ansible Collection Dependencies for Kubernetes #
Before our Ansible playbook can interact with the Kubernetes API Server, there are several software dependencies we must install on the control node where Ansible runs. Ansible’s Kubernetes modules don’t call the kubectl CLI behind the scenes, but communicate directly using the REST API through the official Kubernetes Python library.
# Command to install the Kubernetes collection for Ansible
ansible-galaxy collection install kubernetes.core
# Command to install the required Python modules on the control node
pip install kubernetes PyYAML
[!NOTE] Make sure the
kubernetesPython library is installed in the same Python environment used by Ansible. If we’re using a Python virtual environment, make sure to activate it first before running thepipinstallation.
WHEN TO USE THE K8S MODULE?
✓ We want to deploy manifests whose configuration changes dynamically per environment.
✓ We need to combine Kubernetes resource creation with external infrastructure provisioning (like cloud storage or databases).
✓ We want to secure secret credentials (DB passwords, API keys) using Ansible Vault.
✓ We need automatic rollback automation if the deployment process fails.
WHEN IT'S BETTER NOT TO USE IT:
✗ For one-off local manifest testing (faster to use plain kubectl apply).
✗ When our team doesn't yet understand basic Kubernetes object structures (better to learn Kubernetes YAML first).
K8s Authentication Flow and Connection Options in Ansible #
The kubernetes.core.k8s module supports various authentication methods to connect to the Kubernetes API Server. Choosing the right authentication method greatly determines the security level and flexibility of our automation.
flowchart TD
A["Ansible Playbook Run"] --> B{"Choose Authentication Method"}
B -->|"Local / CI-CD"| C["Kubeconfig File (admin.conf)"]
B -->|"In Cluster (Pod)"| D["Service Account Token (In-Cluster)"]
B -->|"Production Cloud"| E["Dynamic Provider (EKS/GKE Token)"]1. Using Kubeconfig (The Most Common Method) #
This method is the easiest to use. Ansible reads the cluster configuration file (usually located at ~/.kube/config or a custom path) to get the API Server address, certificate authority, and access token.
# Example setting the kubeconfig path in a group variable
# group_vars/all.yml
---
k8s_kubeconfig: "{{ playbook_dir }}/kubeconfig/admin.conf"
2. Using the Token and API Host Directly #
If we run Ansible from an external CI/CD system that doesn’t have a physical kubeconfig file, we can inject the API Server host and access token dynamically from environment variables:
- name: Deploy the namespace using a direct access token
kubernetes.core.k8s:
host: "https://10.0.1.10:6443"
validate_certs: false
api_key: "{{ vault_k8s_api_token }}"
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: production-ci
Creating Namespaces Dynamically #
Namespaces are Kubernetes’ way of dividing one physical cluster into several virtual logical clusters. It’s highly recommended to separate each application or environment into its own namespace to avoid object naming collisions and make security isolation easier.
Here’s how to deploy a Namespace inline using the definition parameter in Ansible:
# playbooks/deploy-app.yml
---
- name: Prepare the Application Work Environment
hosts: localhost
connection: local
gather_facts: false
vars:
app_namespace: "finance-prod"
env_name: "production"
k8s_kubeconfig: "{{ playbook_dir }}/kubeconfig/admin.conf"
tasks:
- name: Create the Namespace for the application declaratively
kubernetes.core.k8s:
kubeconfig: "{{ k8s_kubeconfig }}"
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ app_namespace }}"
labels:
managed-by: ansible
environment: "{{ env_name }}"
team: backend
Managing Configuration with ConfigMap and Secret from Ansible Vault #
Modern applications must separate application code from configuration (the Twelve-Factor App principle). In Kubernetes, we use ConfigMap for regular configuration and Secret for sensitive configuration.
The Danger of Hardcoding Secrets #
Storing database passwords, private keys, or API tokens in plaintext in Git repositories is one of the most critical security holes in IT operations. Many developers fall into the trap of manually base64-encoding values then storing them directly in YAML files. Remember, base64 is not encryption, it’s just a data representation format. Anyone with access to the Git code can easily decode those tokens.
Solution: Ansible Vault and Kubernetes Secret Collaboration #
Using Ansible, we can store all sensitive data in a fully encrypted state using Ansible Vault. When the playbook runs, Ansible decrypts that data in memory instantly, then deploys it to Kubernetes as a Secret using the stringData field. Kubernetes handles the base64 encoding process automatically when the data is stored in the etcd database.
Let’s look at a direct comparison between the anti-pattern approach and the recommended approach.
# ANTI-PATTERN: Storing pre-base64-encoded secrets hardcoded in Git manifest files
# Anyone reading this file can easily decode the password using the base64 -d command
- name: Deploy the secret the wrong way (DON'T DO THIS)
kubernetes.core.k8s:
kubeconfig: "{{ k8s_kubeconfig }}"
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: app-db-secret-bad
namespace: "{{ app_namespace }}"
type: Opaque
data:
# ✗ Password publicly exposed in the Git repository
database-password: "c2FuZ2F0LXJhaGFzaWE="
# CORRECT: Using Ansible Vault for advanced encryption and stringData for convenience
# We use Ansible Vault's strong encryption and prevent log leaks with no_log: true
- name: Deploy database credentials with Ansible Vault (Best Solution)
kubernetes.core.k8s:
kubeconfig: "{{ k8s_kubeconfig }}"
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: app-db-secret-good
namespace: "{{ app_namespace }}"
type: Opaque
stringData:
# ✓ Safe values decrypted in memory by Ansible at runtime
database-password: "{{ vault_production_db_password }}"
api-token: "{{ vault_payment_api_token }}"
# DON'T FORGET: Prevent decrypted text output from being exposed on the console terminal screen
no_log: true
For non-sensitive data, we deploy a ConfigMap that dynamically takes port and host configuration values:
- name: Deploy the backend application ConfigMap
kubernetes.core.k8s:
kubeconfig: "{{ k8s_kubeconfig }}"
state: present
definition:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: "{{ app_namespace }}"
data:
APP_PORT: "8080"
LOG_LEVEL: "info"
DATABASE_HOST: "postgres-service.database.svc.cluster.local"
Deploying the Application with a Zero-Downtime Strategy #
Deployments are responsible for creating and updating our application Pod replicas. So our application can be updated safely without causing user downtime (zero-downtime), we must configure the RollingUpdate strategy precisely.
Inside the Deployment manifest, we must set two key parameters:
maxSurge: Determines how many new Pod replicas may be created above the targeted replica count during the update process. We set it to1so the new Pod is created first before the old Pod is shut down.maxUnavailable: Determines how many old Pod replicas may be unavailable during the update process. We set it to0to guarantee our application capacity is always 100% fulfilled during the version transition.
Additionally, we must define Liveness and Readiness Probes so Kubernetes can accurately detect when our containers are truly ready to accept network traffic.
- name: Deploy the Backend Application with a Rolling Update Strategy
kubernetes.core.k8s:
kubeconfig: "{{ k8s_kubeconfig }}"
state: present
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-app
namespace: "{{ app_namespace }}"
labels:
app: backend-app
version: "2.1.0"
spec:
replicas: 3
selector:
matchLabels:
app: backend-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0 # Zero-downtime guaranteed
template:
metadata:
labels:
app: backend-app
version: "2.1.0"
spec:
containers:
- name: app-container
image: "registry.company.com/backend:2.1.0"
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-db-secret-good
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
Exposing the Application Using Service and Ingress #
So users outside the cluster can access our backend application, we must create a network path connecting outside internet traffic to Pods inside the cluster. We do this by combining a Service object (as an internal load balancer) and an Ingress object (as the HTTP/HTTPS route manager at the front gate).
flowchart TD
Internet["Outside Internet Traffic"] --> Ingress["Ingress Resource (domain.com)"]
Ingress -->|"HTTP Path Routing"| Service["Service (ClusterIP)"]
Service -->|"Load Balancing"| Pod1["Pod Replica 1"]
Service -->|"Load Balancing"| Pod2["Pod Replica 2"]Here’s the Ansible playbook for deploying a Service and Ingress for our backend application:
- name: Deploy the Internal ClusterIP Service
kubernetes.core.k8s:
kubeconfig: "{{ k8s_kubeconfig }}"
state: present
definition:
apiVersion: v1
kind: Service
metadata:
name: backend-service
namespace: "{{ app_namespace }}"
labels:
app: backend-app
spec:
type: ClusterIP
selector:
app: backend-app
ports:
- protocol: TCP
port: 80 # Port listened on the Service network
targetPort: 8080 # Our application container port
- name: Deploy the Ingress for External Domain Routing
kubernetes.core.k8s:
kubeconfig: "{{ k8s_kubeconfig }}"
state: present
definition:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: "{{ app_namespace }}"
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
rules:
- host: "app.company.com"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backend-service
port:
number: 80
Ensuring the Rollout Cycle Completes with k8s_rollout_status #
One common mistake we often encounter in deployment automation is applying a manifest then immediately assuming the process is done (the fire-and-forget pattern). If our new container image experiences a crash loop during boot, the Kubernetes API Server still accepts the manifest, but our application on the ground is broken and inaccessible.
To prevent this, Ansible provides the k8s_rollout_status module. This module blocks playbook execution and waits until all new Pod replicas under the Deployment are reported healthy (per the readiness probe specification). If the update process gets stuck or fails, this task detects the failure and the playbook stops so we can immediately trigger corrective action (rollback).
- name: Wait until the backend Deployment rollout process completes successfully
kubernetes.core.k8s_rollout_status:
kubeconfig: "{{ k8s_kubeconfig }}"
name: backend-app
namespace: "{{ app_namespace }}"
kind: Deployment
timeout: 300 # Wait a maximum of 5 minutes
- name: Get the current Pod information for verification
kubernetes.core.k8s_info:
kubeconfig: "{{ k8s_kubeconfig }}"
kind: Pod
namespace: "{{ app_namespace }}"
label_selectors:
- "app=backend-app"
register: active_pods_info
- name: Validate the health status of all Pods
assert:
that:
- active_pods_info.resources | length == 3
- active_pods_info.resources | selectattr('status.phase', 'equalto', 'Running') | list | length == 3
fail_msg: "Danger! Some Pods are reported unhealthy!"
success_msg: "Success! All Pod replicas are running with Running status."
Cleaning Up Unneeded Resources (State Absent) #
In the application development cycle, sometimes we must remove old components or delete old environment resources so they don’t waste our cluster capacity. We can do this easily using the state: absent parameter on the k8s module.
- name: Clean up the old deprecated Deployment
kubernetes.core.k8s:
kubeconfig: "{{ k8s_kubeconfig }}"
state: absent
kind: Deployment
name: legacy-frontend
namespace: "{{ app_namespace }}"
- name: Remove all old Services based on the label selector
kubernetes.core.k8s:
kubeconfig: "{{ k8s_kubeconfig }}"
state: absent
kind: Service
namespace: "{{ app_namespace }}"
label_selectors:
- "app=legacy-frontend"
Summary #
- Install the
kubernetes.corecollection and thekubernetesPython library on the Ansible control node as the main prerequisite for API interaction.- Use the
kubernetes.core.k8smodule declaratively to deploy Kubernetes resources with built-in idempotency validation.- Always separate code from configuration using ConfigMap objects for general data and Secret for sensitive credential data.
- Must use Ansible Vault to encrypt sensitive passwords before storing them in the Git repository, and deploy using the
stringDataparameter.- Make sure to set
no_log: trueon tasks deploying sensitive data so passwords don’t leak to terminal log output.- Apply the RollingUpdate strategy with
maxSurge: 1andmaxUnavailable: 0to guarantee application deployments run without downtime (zero-downtime).- Connect Service (ClusterIP) and Ingress objects to safely expose our internal applications to the outside public domain network.
- Leverage the
k8s_rollout_statusmodule after deploying a Deployment to monitor the health transition of new Pod replicas before continuing the workflow.