一、从运维痛点说起:为什么要把Ansible和K8s凑到一起
做过云原生运维的人,大概率都遇过这俩麻烦:一是新应用部署时,得先改一堆配置文件(比如数据库地址、端口、密码),再手动推到K8s集群里,错一个参数整个应用就起不来;二是集群里的应用状态变了(比如某台节点挂了、应用扩容了),得挨个改相关配置,慢不说还容易漏。
以前大家要么用Ansible单独管服务器配置,要么用K8s的原生功能管容器,但俩工具各干各的,衔接不上。Ansible擅长改配置、批量执行任务,但对K8s的内部状态(比如Pod运行状态、自定义资源)摸得不透;K8s的原生功能(比如ConfigMap)能存配置,但改配置得手动触发,没法自动跟着集群状态变。
有没有办法让俩工具搭伙干活?比如让Ansible帮着生成配置,再让K8s自动把配置灌到应用里,还能实时同步集群状态?答案是可以,就是把Ansible和K8s结合,用自定义模块和Operator模式来实现。
二、核心技术拆解:先搞懂俩关键概念
要理解这套方案,得先搞明白两个核心的东西:自定义模块和Operator模式,咱们用大白话讲清楚。
2.1 自定义模块:给Ansible装“K8s专属工具包”
Ansible本身有很多内置模块(比如copy、file),但这些模块只支持通用操作,不认识K8s的特殊资源(比如自定义的App资源、Pod的运行状态)。自定义模块就是给Ansible装一个“K8s专属工具包”,让Ansible能直接和K8s集群对话,比如读取K8s里的自定义配置、往K8s里创建资源。
举个例子,内置的copy模块只能把文件拷到服务器上,自定义的k8s_config模块就能直接把生成的配置存到K8s的ConfigMap里,还能检查这个ConfigMap是不是符合要求。
2.2 Operator模式:让K8s自己“管自己”
Operator模式是K8s的一种扩展方式,简单说就是给K8s装一个“专属管理员”。这个管理员会盯着K8s里的特定资源(比如咱们定义的App资源),一旦资源的状态变了(比如用户改了App的配置、集群节点变了),管理员就会自动触发对应的操作,比如更新应用的配置、扩容缩容。
比如咱们要部署一个Java应用,就可以定义一个叫MyApp的自定义资源,Operator管理员会盯着MyApp的状态:如果MyApp里的配置改了,管理员就会自动把新配置推到应用里;如果集群里的某台节点挂了,管理员就会自动把应用迁到其他节点,还同步更新相关配置。
三、具体实现:一套完整的示例
咱们用一个具体的场景来演示:部署一个Java SpringBoot应用,要求:1. 应用的配置(比如数据库地址、端口)由Ansible生成,自动存到K8s的ConfigMap里;2. 应用能自动读取ConfigMap里的配置;3. 如果K8s集群里的应用实例数变了,Ansible自动同步配置,Operator自动更新应用状态。
3.1 准备工作:先搭基础环境
首先得准备好环境:一台装了Ansible的机器(版本2.10以上),一个K8s集群(版本1.20以上),还要装kubectl和k8s的Python客户端(用来让Ansible和K8s对话)。
先装依赖:
# 安装K8s的Python客户端,让Ansible能和K8s集群通信
pip install kubernetes
# 确认kubectl能正常连接K8s集群
kubectl cluster-info
3.2 第一步:写Ansible自定义模块,用来生成并同步配置
自定义模块的作用是:根据K8s集群的状态(比如应用的实例数、数据库的IP)生成应用的配置,然后存到K8s的ConfigMap里。
咱们写一个叫k8s_config_generator的自定义模块,放在Ansible的library目录下(这个目录是Ansible放自定义模块的地方)。
# Ansible自定义模块:k8s_config_generator
# 作用:生成SpringBoot应用的配置,存到K8s的ConfigMap里
from ansible.module_utils.basic import AnsibleModule
from kubernetes import client, config
def main():
# 定义模块的参数:namespace(K8s命名空间)、app_name(应用名)、db_ip(数据库IP)、db_port(数据库端口)
module = AnsibleModule(
argument_spec=dict(
namespace=dict(type='str', required=True),
app_name=dict(type='str', required=True),
db_ip=dict(type='str', required=True),
db_port=dict(type='int', required=True),
instance_count=dict(type='int', required=True) # 应用实例数,从K8s集群获取
)
)
# 加载K8s集群的配置(让模块能连接K8s)
config.load_kube_config()
v1 = client.CoreV1Api()
# 生成SpringBoot应用的配置(application.yml)
config_content = f"""
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://{module.params['db_ip']}:{module.params['db_port']}/testdb
username: root
password: 123456
# 把实例数写到配置里,用来测试同步
app:
instance-count: {module.params['instance_count']}
"""
# 定义要创建的ConfigMap的结构
config_map = client.V1ConfigMap(
metadata=client.V1ObjectMeta(
name=f"{module.params['app_name']}-config",
namespace=module.params['namespace']
),
data={"application.yml": config_content}
)
# 检查ConfigMap是否存在,存在就更新,不存在就创建
try:
existing_cm = v1.read_namespaced_config_map(name=f"{module.params['app_name']}-config", namespace=module.params['namespace'])
# 对比新旧配置,不一样就更新
if existing_cm.data["application.yml"] != config_content:
v1.replace_namespaced_config_map(name=f"{module.params['app_name']}-config", namespace=module.params['namespace'], body=config_map)
module.exit_json(changed=True, msg="ConfigMap updated successfully")
else:
module.exit_json(changed=False, msg="ConfigMap is up to date")
except client.exceptions.ApiException as e:
# 如果ConfigMap不存在,就创建
if e.status == 404:
v1.create_namespaced_config_map(namespace=module.params['namespace'], body=config_map)
module.exit_json(changed=True, msg="ConfigMap created successfully")
else:
module.fail_json(msg=f"Error accessing ConfigMap: {e}")
if __name__ == "__main__":
main()
这个模块的逻辑很简单:先拿到输入的参数(数据库IP、端口、应用实例数),生成SpringBoot的配置文件,然后和K8s里已有的ConfigMap对比,不一样就更新,一样就不操作。
3.3 第二步:写Ansible Playbook,调用自定义模块
Playbook是Ansible的任务剧本,用来批量执行任务。咱们写一个Playbook,先从K8s集群获取应用的实例数,然后调用刚才写的自定义模块生成配置。
# Ansible Playbook:deploy_springboot_app.yml
# 作用:从K8s获取应用实例数,调用自定义模块生成配置
- name: Get current SpringBoot app instance count from K8s
hosts: localhost # 因为要连接K8s,所以在本地执行
tasks:
# 第一步:从K8s获取SpringBoot应用的Deployment,得到实例数
- name: Get Deployment info
k8s_info:
api_version: apps/v1
kind: Deployment
name: my-springboot-app
namespace: default
register: deployment_info # 把结果存到deployment_info变量里
# 第二步:调用自定义模块生成配置
- name: Generate and sync app config to K8s ConfigMap
k8s_config_generator:
namespace: default
app_name: my-springboot-app
db_ip: 192.168.1.100 # 假设数据库的IP是这个
db_port: 3306
instance_count: "{{ deployment_info.resources[0].spec.replicas }}" # 从Deployment里拿实例数
这个Playbook分两步:第一步先从K8s的Deployment里拿到应用的实例数(Deployment是K8s里管应用实例的资源,spec.replicas就是实例数);第二步把这个实例数传给自定义模块,生成配置。
3.4 第三步:写Operator,实现状态自动同步
Operator的作用是盯着K8s里的Deployment,一旦Deployment的实例数变了,就自动触发Ansible Playbook,更新ConfigMap里的配置。
咱们用Python写一个简单的Operator(用kopf框架,这个框架能快速写K8s Operator)。
首先装kopf:
pip install kopf
然后写Operator的代码:
# Operator代码:springboot_config_operator.py
# 作用:盯着Deployment,实例数变了就触发Ansible Playbook
import kopf
import subprocess
# 定义要盯的Deployment的标签(只盯名字是my-springboot-app的Deployment)
@kopf.on.update('apps/v1', 'deployments', field='spec.replicas')
def on_deployment_update(spec, name, namespace, **kwargs):
# 当Deployment的spec.replicas(实例数)变了,执行这个函数
if name == 'my-springboot-app':
print(f"Deployment {name} replicas changed to {spec['replicas']}, triggering Ansible Playbook...")
# 执行Ansible Playbook,更新配置
result = subprocess.run(
['ansible-playbook', 'deploy_springboot_app.yml'],
capture_output=True,
text=True
)
if result.returncode != 0:
# 如果执行失败,报错
raise kopf.TemporaryError(f"Ansible Playbook failed: {result.stderr}", delay=10)
print("Ansible Playbook executed successfully, config updated.")
这个Operator的逻辑很简单:用kopf的装饰器,盯着所有Deployment的spec.replicas字段,一旦这个字段变了,就检查是不是咱们的my-springboot-app应用,如果是,就自动执行Ansible Playbook,更新ConfigMap里的配置。
3.5 第四步:部署SpringBoot应用,测试效果
首先写SpringBoot应用的Deployment和Service,把ConfigMap里的配置挂载到应用里:
# K8s Deployment:my-springboot-app-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-springboot-app
namespace: default
spec:
replicas: 2 # 初始实例数是2
selector:
matchLabels:
app: my-springboot-app
template:
metadata:
labels:
app: my-springboot-app
spec:
containers:
- name: my-springboot-app
image: openjdk:11-jre-slim # 用Java11的镜像,假设应用已经打包成jar包
ports:
- containerPort: 8080
# 把ConfigMap里的application.yml挂载到应用的配置目录
volumeMounts:
- name: app-config
mountPath: /app/config
readOnly: true
volumes:
- name: app-config
configMap:
name: my-springboot-app-config # 挂载的ConfigMap就是Ansible生成的那个
然后部署这个Deployment:
kubectl apply -f my-springboot-app-deployment.yml
启动Operator:
kopf run springboot_config_operator.py
现在测试效果:把应用的实例数从2改成3:
kubectl scale deployment my-springboot-app --replicas=3
这时候Operator会自动检测到Deployment的实例数变了,触发Ansible Playbook,Playbook会调用自定义模块,把ConfigMap里的application.yml里的instance-count从2改成3。咱们可以查看ConfigMap验证:
kubectl get configmap my-springboot-app-config -o yaml
能看到instance-count已经变成3了,应用下次重启的时候,就会自动读取这个新的配置。
四、这套方案的适用场景、优缺点和注意事项
4.1 适用场景
这套方案适合哪些情况呢? 一是复杂应用的部署:比如应用的配置依赖多个外部资源(数据库、中间件、其他服务),需要根据这些资源的状态动态生成配置; 二是集群状态频繁变化的场景:比如应用经常扩容缩容、节点经常变更,需要自动同步配置; 三是需要标准化配置的场景:比如多个团队部署相同的应用,用Ansible统一生成配置,避免手动改配置出错; 四是混合环境的场景:比如一部分服务在K8s里,一部分在传统服务器上,用Ansible统一管配置,Operator管K8s里的状态。
4.2 优缺点
优点很明显: 一是自动化程度高:配置生成、同步、应用更新全自动化,不用人工操作; 二是灵活:自定义模块能实现任意复杂的配置生成逻辑,Operator能盯着任意K8s资源; 三是兼容性好:Ansible能对接各种传统服务器和云服务,Operator能扩展K8s的能力; 四是可复用:自定义模块和Operator可以用到多个应用上,不用重复写代码。
缺点也有: 一是复杂度高:比单独用Ansible或K8s的原生功能要复杂,需要同时懂Ansible、K8s、Python(写自定义模块和Operator); 二是维护成本高:如果集群规模大,要维护多个自定义模块和Operator,出问题了不好排查; 三是性能问题:如果集群里的资源变化频繁,Operator频繁触发Ansible Playbook,可能会影响集群性能。
4.3 注意事项
用这套方案的时候,要注意这几点: 一是权限控制:Ansible和Operator都需要访问K8s集群的权限,要给它们分配最小必要的权限,比如Ansible只能读Deployment、写ConfigMap,不能随便改其他资源; 二是幂等性:自定义模块和Playbook要保证幂等,就是重复执行不会出问题,比如多次调用自定义模块,不会重复创建ConfigMap; 三是状态同步的延迟:Operator检测到资源变化,触发Ansible Playbook,再更新ConfigMap,这个过程有延迟,要考虑应用能不能接受这个延迟; 四是错误处理:要做好错误处理,比如Ansible Playbook执行失败了,Operator要能重试,不能直接崩溃; 五是版本管理:自定义模块、Operator、Playbook都要做版本管理,方便回滚和维护。
五、总结
以前Ansible和K8s是“各干各的”,现在把它们结合起来,用自定义模块让Ansible能生成并同步K8s的配置,用Operator让K8s能自动触发Ansible的任务,实现了配置的自动化生成、同步和应用状态的自动更新。
这套方案虽然有复杂度,但解决了传统运维里的很多痛点,适合复杂应用和动态集群的场景。如果你的团队经常遇到配置改来改去、集群状态变化频繁的问题,不妨试试这套方案。
Comments