Ansible for k8s
用 ansible 来创建 k8s,这类项目已经很多了,在 github 上面随意可以搜到很多,而这篇文章主要介绍的是如果用 ansible 来做日常的 k8s 运维和开发部署。
本文中的例子可以在 https://github.com/u2takey/ansible-k8s 找到
helm 和 kustomize 的问题
从我第一次使用 helm 就觉得这个东西的设计实在古怪:
- 一个包(配置)管理工具依赖服务端(tiller)
- 做的事情很简单,设计却过于复杂(看看 helm 有多少命令和参数就知道了)
- template 可读性很差,表达能力却很弱(受制于go-template的表达能力)
- 99% 的用户不会安装 tiller, 只是拿来做 template 渲染工具,但是就算作为一个简单的渲染工具,helm也不好用
- 其他缺陷:参考
后来出现了 kustomize,kustomize的设计并不依赖服务端,而是想做好本地渲染,然而几次使用之后发现,kustomize 显然还是失败了
- kustomize 的设计有点像 ansible了,然而表达能力还是很差,设计却过于复杂
- 作为了一个 yaml 工具,kustomize 带了太多k8s属性,比如 kustomize 了解什么是 images,什么是 configmap,这个设计显然不够灵活
- 如果 kustomize (很有希望的)像ansible好好学习,能做好一个专用领域的 ansible 工具,易用性能肯定能好很多
求助于 ansible
关于运维和部署,ansible已经积累了太多经验,虽然本质上 ansible 最初的设计还是针对 "hosts", 并不是针对集群,ansible的 k8s module 质量也参差不齐,但是尽管如此,使用时候你会发现,ansible 在运维 k8s 的能力上还是很强。
用 ansible 部署 k8s 集群
这部分不是本文的重点,ansible 部署 k8s 是比较常见的 k8s 部署方式,这里给几个 star 比较高的项目,不再细述。
用 ansible 运维 k8s 服务
ansible 主要可以使用 k8s 模块来管理 k8s 资源
比如创建一个 namespace,可以使用下面的写法
- name: Create a k8s namespace
k8s:
name: testing
api_version: v1
kind: Namespace
state: present
创建 service 的写法
- name: Create a Service object from an inline definition
k8s:
state: present
definition:
apiVersion: v1
kind: Service
metadata:
name: web
namespace: testing
labels:
app: galaxy
service: web
spec:
selector:
app: galaxy
service: web
ports:
- protocol: TCP
targetPort: 8000
name: port-8000-tcp
port: 8000
上面的这种 inline 的写法实际上可读性比较差,更推荐使用 src(读取文件) 或者 definition + lookup + template 语句的办法来创建资源,ansible 的 template 使用 jinja2 来渲染,表达能力很强。
- name: Create a Deployment by reading the definition from a local file
k8s:
state: present
src: /testing/deployment.yml
- name: Read definition file from the Ansible controller file system after Jinja templating
k8s:
state: present
definition: "{{ lookup('template', '/testing/deployment.yml.j2') }}"
一个完整的例子
下面我们看一个完整的例子,这个例子里面我们有两个集群,分别叫 4lr4c3wx
和 8keawqnz
, 并且我们在本地的 admin.conf 里面加入了这两个 context, 所以我们不用在 ansible config里面添加相关密钥信息。
- 使用
ansible-galaxy
初始化 role
➜ ansible-galaxy role init nginx
➜ ll nginx
.rw-r--r-- 1.3k leiwang 4 Jan 15:11 README.md
drwxr-xr-x - leiwang 4 Jan 15:11 defaults
drwxr-xr-x - leiwang 4 Jan 15:11 files
drwxr-xr-x - leiwang 4 Jan 15:11 handlers
drwxr-xr-x - leiwang 4 Jan 15:11 meta
drwxr-xr-x - leiwang 4 Jan 15:11 tasks
drwxr-xr-x - leiwang 4 Jan 15:13 templates
drwxr-xr-x - leiwang 4 Jan 15:11 tests
drwxr-xr-x - leiwang 4 Jan 15:11 vars
- 修改 host 和 ansible.cfg, 指向两个集群.
# ansible.cfg
[defaults]
inventory = hosts.ini
host_key_checking = False
pipelining = True
gathering = smart
fact_caching = jsonfile
fact_caching_timeout = 86400
fact_caching_connection = /tmp/ansible_fact_cache
forks = 20
# host
[test]
4lr4c3wx context=4lr4c3wx
8keawqnz context=8keawqnz
[test:vars]
ansible_connection=local
ansible_python_interpreter=~/miniconda3/bin/python
- 修改 task/main.yml, 这里我们编写两个任务,一是操作 namespace, 二是创建 nginx deployment 和 service,支持 namespace 和 state 参数,支持 state也就是说 支持创建或者删除,对 namespace的操作加上
ignore_errors
, 因为创建或者删除 namespace 时可能会报错,我们希望忽略这种错误。
---
- debug: var=context
- name: set namespace {{ namespace }} to {{ state }}
k8s:
context: "{{ context }}"
api_version: v1
kind: Namespace
name: "{{ namespace }}"
state: "{{ state }}"
ignore_errors: true
- name: set nginx deployment to {{ state }}
k8s:
context: "{{ context }}"
state: "{{ state }}"
definition: "{{ lookup('template', 'nginx.yml.j2') | from_yaml_all | list }}"
namespace: "{{ namespace }}"
- nginx.yml.j2 里nginx deployment和service,实际上这两个 object 分开会更好,这里放在一起只是为了演示,如果放在一起应该用
from_yaml_all
+list
filter来处理
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx
namespace: {{ namespace }}
labels:
version: v2
spec:
# 略
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: {{ namespace }}
spec:
# 略
- playbook include 刚刚编写的 nginx role
---
- name: example k8s playbook
hosts: test
vars:
namespace: default
roles:
- nginx
- 运行 playbook
➜ ansible-playbook main.yml
PLAY [example k8s playbook] *********************************************************************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************************************************************************
ok: [test_cluster_2]
ok: [test_cluster_1]
TASK [nginx : debug] ****************************************************************************************************************************************************************
ok: [test_cluster_1] => {
"context": "4lr4c3wx"
}
ok: [test_cluster_2] => {
"context": "8keawqnz"
}
TASK [nginx : set namespace default to present] *************************************************************************************************************************************
ok: [test_cluster_1]
ok: [test_cluster_2]
TASK [nginx : set nginx deployment to present] **************************************************************************************************************************************
changed: [test_cluster_1]
changed: [test_cluster_2]
PLAY RECAP **************************************************************************************************************************************************************************
test_cluster_1 : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
test_cluster_2 : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
- 使用 kubectl 验证效果,发现两个集群的 nginx deployment和service都已经创建出来了
- clean up: 使用
state=absent
清理刚刚创建的资源
➜ ansible-playbook main.yml -e "state=absent"
用 ansible 来开发 operator
- ansible 不仅仅可以做简单的运维,甚至可以帮助开发 operator. operator或者controller的主要逻辑是 使某种 API Object 成为他预期的状态,ansible 的逻辑也是如此,虽然没有直接用golang 开发的operator 灵活,但是使用 ansible 开发,不用编写一行代码,开发部署效率还是很高的。
- 使用 ansible 开发 operator 依赖 operator-sdk
- operator-sdk 和 kubebuilder 的对比在这里,其中和这篇文章有关的区别是 operator-sdk 支持ansible。
一个完整的例子
这个完整的例子里面,我们创建一个 crd Nginx
, 这个Nginx 对应了一个 Nginx deployment和 一个service
- 安装 operator-sdk,过程略➜ operator-sdk new nginx-operator \\
--api-version=nginx.operator.t.io/v1 \\
--kind=Nginx \\
--type=ansible
INFO[0000] Creating new Ansible operator 'nginx-operator'.
INFO[0000] Created deploy/service_account.yaml
INFO[0000] Created deploy/role.yaml
INFO[0000] Created deploy/role_binding.yaml
INFO[0000] Created deploy/crds/nginx.operator.t.io_nginxes_crd.yaml
INFO[0000] Created deploy/crds/nginx.operator.t.io_v1_nginx_cr.yaml
INFO[0000] Created build/Dockerfile
INFO[0000] Created roles/nginx/README.md
INFO[0000] Created roles/nginx/meta/main.yml
INFO[0000] Created roles/nginx/files/.placeholder
INFO[0000] Created roles/nginx/templates/.placeholder
INFO[0000] Created roles/nginx/vars/main.yml
INFO[0000] Created molecule/test-local/playbook.yml
INFO[0000] Created roles/nginx/defaults/main.yml
INFO[0000] Created roles/nginx/tasks/main.yml
INFO[0000] Created molecule/default/molecule.yml
INFO[0000] Created build/test-framework/Dockerfile
INFO[0000] Created molecule/test-cluster/molecule.yml
INFO[0000] Created molecule/default/prepare.yml
INFO[0000] Created molecule/default/playbook.yml
INFO[0000] Created build/test-framework/ansible-test.sh
INFO[0000] Created molecule/default/asserts.yml
INFO[0000] Created molecule/test-cluster/playbook.yml
INFO[0000] Created roles/nginx/handlers/main.yml
INFO[0000] Created watches.yaml
INFO[0000] Created deploy/operator.yaml
INFO[0000] Created .travis.yml
INFO[0000] Created molecule/test-local/molecule.yml
INFO[0000] Created molecule/test-local/prepare.yml
INFO[0000] Project creation complete. - 使用operator-sdk, 初始化 operator,可以看到创建出了很多资源
- 定义 crd,这里我们的 crd 只支持三个参数
cpu
,memory
,replicas
, 下面是这个 crd object的例子
apiVersion: nginx.operator.t.io/v1
kind: Nginx
metadata:
name: example-nginx
spec:
# Add fields here
replicas: 3
cpu: 100m
memory: 100M
- 修改 tasks/main.yml,类似上一节,使用k8s module创建资源
---
- name: nginx deployment
k8s:
definition: "{{ lookup('template', 'nginx-deployment.yml.j2') }}"
namespace: "{{ meta.namespace }}"
- name: nginx service
k8s:
definition: "{{ lookup('template', 'nginx-service.yml.j2') }}"
namespace: "{{ meta.namespace }}"
- 先测试, 这里我们没有使用 ansible的测试工具,而是做了一个简单的 test playbook, 以测试 role的编写是否有问题
# test.yml
---
- hosts: localhost
vars:
meta:
name: test-nginx
namespace: default
roles:
- nginx
# do test
ansible-playbook test.yml
- 测试正常,修改 deploy 中的 placeholder, buid and push
➜ sed -i 's|{{ REPLACE_IMAGE }}|ccr.ccs.tencentyun.com/mla-library/tool:test|g' deploy/operator.yaml
➜ sed -i 's|{{ pull_policy\\|default('\\''Always'\\'') }}|Always|g' deploy/operator.yaml
➜ operator-sdk build ccr.ccs.tencentyun.com/mla-library/tool:test
➜ docker push ccr.ccs.tencentyun.com/mla-library/tool:test
- 把 crd 创建出来,部署 operator 以及相关的 role account 等kubectl create -f deploy/crds/
kubectl create -f deploy/➜ kubectl get pod
NAME READY STATUS RESTARTS AGE
example-nginx-78cf7659d4-9txlz 0/1 ContainerCreating 0 17s
example-nginx-78cf7659d4-qzwsm 1/1 Running 0 17s
example-nginx-78cf7659d4-td78l 0/1 ContainerCreating 0 17s➜ kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
example-nginx LoadBalancer 10.7.255.124 119.28.109.137 - 观察
Nginx
的创建情况(我们刚刚已经把 exampleNginx一并创建了),可以发现 deployment 和 service都符合预期 - 修改 example-nginx,副本数量改成1, cpu,memory也做修改,观察效果,可以发现 deployment 也很快得到了对应的修改。➜ kubectl edit nginx example-nginx
spec:
cpu: 50m
memory: 50M
replicas: 1➜ kubectl get pod
NAME READY STATUS RESTARTS AGE
example-nginx-69647b9c6-8kgdm 1/1 Running 0 52s➜ kubectl describe deploy example-nginx
Name: example-nginx
Namespace: default
....略
Pod Template:
Labels: app=example-nginx
Containers:
example-nginx:
Image: nginx
Port: <none>
Host Port: <none>
Limits:
cpu: 50m
memory: 50M
Requests:
cpu: 50m
memory: 50M