Back
Featured image of post 初窥门径之---Ansible快速入门与实践

初窥门径之---Ansible快速入门与实践

如果你有多台机器要管理,比如NTP对时,或设置一下时区等,重复操作你肯定很烦。

如果你有多种类型的系统,Ubuntu要这么搞,CentOS又是那样弄,细节之处你肯定想骂娘。

如果你有多个应用要部署,A依赖B,C又要搞一堆前置或后置操作,甚至不知道它部署在哪了,揪头发时你肯定想肯定有更好的办法。

前言

没错。或许如标题剧透,Ansible就是个更好的办法。我曾经听闻良久,但有时人因为懒,原来手工能搞定的事情,就不知觉中重复多时。最近借着捣鼓和整理基础设施及部署,顺便就把Ansible这个强大工具学习和吸收一下吧。

没用它之前,你觉得好像不太需要。对它了解多些后,你觉得用它真好!

本文会从新手到一些高阶用法(我也是个新人,我所理解的高阶:),带你一起初窥门径吧。 PS:本人接触不久,认识可能粗浅,文章若有错误欢迎交流指正,感谢感谢!

简介

我不想去重复某些文档的说明,仅基于个人理解简要介绍一下: Ansible是基于python的一套自动化部署工具,它依赖极低(SSH和python),所以非常适合用它设置系统或安装软件,批量管理机器不在话下。 它基于明确的配置文件格式描述一些行为,颇有点声明式语言的样子,不过所定义的流程还是命令式。

内功心法

我感觉学习和掌握Ansible有四个阶段,就像练内功一样,这四个阶段我就戏称之为:

  • 脚本小子阶段
  • 略通皮毛阶段
  • 初窥门径阶段
  • 登堂入室阶段

脚本小子阶段

你可以基于Ansible的一些基本命令操作和控制执行一些简单的任务了。在Ansible中叫ad-hoc的东西你虽然不明其意,但已经可以做一些事情了,后面我们会演示一些例子,就是这个阶段要学会干的事。

略通皮毛阶段

到这时你会基于自己的需求,能独立写出一个Playbook完成较复杂的任务了。你了解了不少的基础模块,也知道如何引用外部基础模块做些事情了。很快我们的示例也会进展到这个阶段。

初窥门径阶段

遇到一些问题,你会把Ansible作为武器库之一,拿它来帮助你做一些事情,你也更细致的了解到生产环境中的一些做法会是怎么样的。除此之外,社区中大量现有的Roles也帮助你更快速的完成目标。或许我目前处于这个阶段?

登堂入室阶段

你已经不再只是一个使用者了。对于大型工程的Ansible实践和对于Anisble之下的原理,你也了然于胸。

以上阶段也是我们这个文章的路径,而我实际使用时长和场景有限,第4阶段只能随便聊聊。

入门与实践

脚本小子要上路

我学习过程中看过B站这个视频配置管理神器Ansible感觉很不错的,跟着学能半小时到一小时走过脚本小子阶段。

理解清两个概念就是这个阶段的目标:

  • inventory文件的组织
  • ansible的基本命令

在没搞清楚这些和执行过一系列不同类型的ansible命令前,我劝你别直接去玩playbook等东西:)

inventory文件

定义了Ansible要操作目标集合和一些参数,不同机器不同登录端口啊密码等都可以在配置中指定出来,而外部使用Ansible的命令是一样的。如下是个示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 组织方式:功能、地域、环境
# k3s-home-0807
[k3s]
192.168.50.216
192.168.50.217
192.168.50.218
192.168.50.219

# gitlab runner
[runner]
192.168.50.11
192.168.50.12

# 名为 localvm 的嵌套组
[localvm:children]
k3s
runner

# 给嵌套组定义变量,应用于组内所有服务器
[localvm:vars]
ansible_user=kevin
# ansible_password='' 
ansible_ssh_private_key_file=~/.ssh/id_ed25519
ansible_ssh_common_args='-o StrictHostKeyChecking=no'

一个INI格式文件,我们上面定义了一些组:k3s,runner等。也定义一些虚拟的嵌套组localvm等。后续我称这个名称为group

Ansible基本命令

Ansible的基础命令格式很简单,都是一个样式:

1
ansible <group> -m <module-name> -a "args or command"

这里没有指定上面的inventory文件,是因为有个叫Ansible.cfg的默认配置文件,如果在里面指定了,我们就不用在命令中每次指定。它的内容可能是这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# cat ansible.cfg
[defaults]
host_key_checking = false
inventory = ./inventory
command_warnings = False
deprecation_warnings = False
roles_path = ./roles:~/.ansible/roles
nocows = 1
retry_files_enabled = False
interpreter_python='/usr/bin/python3'

[ssh_connection]
control_path = %(directory)s/%%h-%%p-%%r
pipelining = True

基于到底可以执行哪一些命令,不一一细说了,你也不需要一开始就知道,先看懂下面这些命令都是在做啥:

1
2
3
4
5
6
7
8
9
ansible <group> -a "date"

ansible <group> -b -m yum -a "name=chrony state=present"

ansible <group> -b -m service -a "name=chronyd state=started enabled=yes"

ansible <group> -b -a "chronyc tracking"

ansible <group> -a "date"

注意这里比较常用的-b是提权,即类似sudo来做一些事情。

接下来你接到一个任务,把某个group所有机器的时区设置为Asia/Shanghai

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
➜  ansible-home git:(main) ✗ ansible k3s -m timezone -b -a "name=Asia/Shanghai"
192.168.50.216 | SUCCESS => {
    "changed": false
}
192.168.50.217 | SUCCESS => {
    "changed": false
}
192.168.50.218 | SUCCESS => {
    "changed": false
}
192.168.50.219 | SUCCESS => {
    "changed": false
}

你可能直接执行会失败,请查看这里

我也能写Playbook

你会用Ansible执行一些命令,也知道如何查看各种模块文档及使用方式后, 它能提供的生产力有限。如何让这些命令只输入一次?一些复杂的事情能否通过Ansible解决呢?带着这些问题,当我折腾Headscale过程中,发现一堆设置和配置修改,心中一动,何不用Ansible来尝试写个Playbook解决呢?这篇Tailscale 基础教程:Headscale 的部署方法和使用教程的文章有很多操作的细节,我们不想每次要搞这些都要重复做对吧?

初看Playbook

Playbook本质就是把我们一系列的Ansible操作聚合起来。建议你打开上述文章,然后看看我们第一个Playbook怎么写:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
- hosts: <target host>
  become: true
  # 主要参考这里设置的: https://icloudnative.io/posts/how-to-set-up-or-migrate-headscale/
  tasks:
    # 下载headscale
    - name: Download headscale
      get_url:
        url: https://github.com/juanfont/headscale/releases/download/v0.16.4/headscale_0.16.4_linux_amd64
        dest: /tmp/
    
    - name: 设置权限,移动可执行文件
      shell: |
        chmod +x /tmp/headscale_0.16.4_linux_amd64
        cp /tmp/headscale_0.16.4_linux_amd64 /usr/local/bin/headscale        
      # FIXME 测试阶段这步可能报错(正在运行了)
      ignore_errors: yes

    - name: 创建headscale组
      ansible.builtin.group:
        name: headscale
        state: present 

    - name: 创建headscale用户
      ansible.builtin.user:
        name: headscale
        comment: for headscale & tailscale
        uid: 1040
        group: headscale
        home: /home/headscale

    - name: 创建配置目录
      ansible.builtin.file:
        path: /etc/headscale
        state: directory
        mode: '0755'
        owner: headscale
        group: headscale

    - name: 创建目录用来存储数据与证书
      ansible.builtin.file:
        path: /var/lib/headscale
        state: directory
        mode: '0755'
        owner: headscale
        group: headscale

    - name: 创建空的 SQLite 数据库文件
      ansible.builtin.file:
        path: /var/lib/headscale/db.sqlite
        state: touch
        mode: u=rw,g=r,o=r
        owner: headscale
        group: headscale

    - name: 创建headscale配置文件(基于示例)
      get_url:
        # url: https://github.com/juanfont/headscale/raw/main/config-example.yaml 
        url: https://raw.githubusercontent.com/juanfont/headscale/v0.16.4/config-example.yaml
        dest: /etc/headscale/config.yaml
      retries: 3
      # ignore_errors: yes

    - name: 获得机器外网IP
      uri:
        url: https://ip.gs/ip
        return_content: yes
      register: public_ip

    - name: 修改配置 server_url
      # server_url: http://127.0.0.1:8080
      lineinfile:
        path: /etc/headscale/config.yaml
        regexp: '^server_url'
        line: "server_url: http://{{ public_ip.content | trim }}:8080"
    

    # 可使用反向引用, 这里我直接删除原网段
    # - name: 注释原网段
    #   #   - fd7a:115c:a1e0::/48
    #   #   - 100.64.0.0/10
    #   lineinfile:
    #     path: /etc/headscale/config.yaml
    #     regexp: '^(  -.*/[0-9]{2})'
    #     line: '# \g<1>'
    #     backrefs: yes
    - name: 删除原网段
      #   - fd7a:115c:a1e0::/48
      #   - 100.64.0.0/10
      lineinfile:
        path: /etc/headscale/config.yaml
        regexp: '^(  -.*/[0-9]{2})'
        state: absent

    - name: 修改配置ip网段
      # ip_prefixes
      lineinfile:
        path: /etc/headscale/config.yaml
        # regexp: '^ip_prefixes'
        insertafter:  '^ip_prefixes'
        line: "  - 100.66.0.0/16"

    - name: 关闭magic_dns
      lineinfile:
        path: /etc/headscale/config.yaml
        regexp: '(\s*)(magic_dns: )(true)'
        line: '\1\2 false'
        backrefs: yes
    
    - name: 修改unix_socket文件目录(不然会报权限问题)
      lineinfile:
        path: /etc/headscale/config.yaml
        regexp: '^unix_socket:'
        line: 'unix_socket: /var/run/headscale/headscale.sock'


    - name: 拷贝service文件
      ansible.builtin.copy:
        src: files/headscale.service
        dest: /etc/systemd/system/headscale.service

    - name: 启动headscale服务
      ansible.builtin.service:
        name: headscale
        enabled: yes
        state: started

    - name: 等待5秒,之后再检查headscale服务
      ansible.builtin.wait_for:
        timeout: 5

    - name: 检查headscale服务状态
      shell: |
        systemctl status headscale
        netstat -lpn | grep 8080        

从下载文件,用户创建和配置文件修改,拷贝远程文件等各种操作,我们原本在机器上操作的都可以通过模块组合起来。

重构得有层次感

上面一个大而全的yaml文件把事情做完了,但这样不方便维护和管理,于是官方尝试让大家按他们推荐的目标结构组织起来,比如这样,我们要做的只是把上面的东西按功能分拆为多个文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
➜  roles git:(main) ✗ tree headscale
headscale
├── README.md
├── defaults
│   └── main.yml
├── files
│   └── headscale.service
├── handlers
│   └── main.yml
├── meta
├── tasks
│   ├── config.yml
│   ├── install.yml
│   └── main.yml
├── templates
└── vars

比如把tasks分为了多步骤等。在Ansible中这样定义的一系列行为,可以被称为Role。它更像是一种包装的概念,比如安装某个服务可以封装为一个Role,能够方便被引用。

到这里,如果你用ansible headscale关键词搜索,会发现类似的事情早有人做过了,甚至做得更好,比如这个。看看它,也是一个更深入了解和学习Role和Playbook概念的过程。

游刃有余就算是初窥门径了吗

到现在为止,我们好像懂一些Ansible了,对它能做啥也有些了解了。接下来要提升认知层面的几个维度才能算进入第三阶段。 其一是更多种场景和更高复杂度的工程实践,其二是对Ansible这种基于配置的描述能力和组织能力的认识。

更多场景之roles使用

以前在安装一些常用的软件时会需要去网上搜索下载链接,未来你会多一种方式,并且能够管理起来复用,比如你想安装常用的kubectlhelm。你甚至都不需要打开搜索引擎,可借助内置于Ansible包的ansible-galaxy工具查找用可用的roles:

1
ansible-galaxy search kubectl

然后写一个类似如下的yaml(playbook),定义了要在本地安装这两个软件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# cat deploy/deploy-kubectl-helm.yml
- name: setup dev env.
  hosts: localhost
  remote_user: root
  become: true
  tasks:
    - name: install kubectl
      ansible.builtin.include_role:
        name: alvistack.kube_kubectl
      tags: alvistack.kube_kubectl

    - name: install helm
      ansible.builtin.include_role:
        name: averagebit.helm
      tags: averagebit.helm

之后执行:

1
ansible-playbook deploy/deploy-kubectl-helm.yml

很快软件就安装好了,可能做同样事情的其它人还在寻找它们的下载链接呢!!你Ansible在手,回车我有,事了拂衣去,深藏身与名:)

更多场景之密钥管理

我们有一些机密信息,比如sshkey,又或者密码等不方便明文写在配置中,这时应该怎么做呢?我们可以借助如vault等密钥管理工具,将敏感信息保存在上面,在执行时,通过ansible的vault相关模块,从中动态获取到key,使用后将其销毁。以下是个示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
- name: get ssh_key from vault
  hosts: localhost
  vars:
    # secret v2, 使用path模式需要添加
        path: "{{ vault_path_of_ssh_key }}"
        auth_method: approle
        role_id: "{{ lookup('ansible.builtin.env', 'role_id') }}"
        secret_id: "{{ lookup('ansible.builtin.env', 'secret_id') }}"
      register: secret
    # - name: Display the secret data
    #   ansible.builtin.debug:
    #     msg: "{{ secret.data.data.data.private_key}}"
    - name: create ssh-key directory
      ansible.builtin.shell: "mkdir -p {{ ssh_key_dir }}"
    - name: Save the ssh-key to file
      ansible.builtin.copy:
        content: "{{ secret.data.data.data.private_key }}"
        dest: "{{ ssh_key_dir }}/id_ed25519"
        mode: 0600

这样我们的代码中将不会保存敏感信息,只需要在环境变量中有访问vault的密钥(流水线中可以是参数)就OK了。当然Ansible也带了内置的vault解决方案供使用。

更多场景中NAS中管理docker

有了Ansible之后,之前零散在NAS(一台QNAP TS453BMini)中起的几个容器,都有了统一管理的可能,我把它在git仓库中管理了起来。不再担心不记得当时启动参数了,也不再需要修改后在机器上手工重启等,都交给流水线搞定,提交,生效,就这么简单。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- name: setup apps on nas
  hosts: nas
  vars:
    - ansible_ssh_private_key_file: /tmp/vault-ssh-key/id_ed25519
  tasks:
    - name: copy Docker Compose files
      copy:
        src: files/{{ item }}
        dest: "{{ remote_path }}/"
      loop:
        - plex.yaml
        - pg.yaml
        - webdav.yaml
        - transmission.yaml
        - apt-proxy.yaml
        - pushover.yaml

    #use files parameter to use multiple docker-compose.yml files
    - name: deploy Docker Compose stack
      community.docker.docker_compose:
        project_src: "{{ remote_path }}/"
        files:
          - plex.yaml
          - pg.yaml
          - webdav.yaml
          - transmission.yaml
          - apt-proxy.yaml
          - pushover.yaml

这只要借助于docker_compose的模块就可以搞定了,NAS中常用的一些容器现在部署模式升级了:) 当然有一些小坑,无论你用QNAP或群晖,在接Ansible时都遇到一点点小问题,不在此聊,有兴趣留言可继续交流。

认知的变化

在云原生背景下,很多时候会讲声明式语义,会讲As Code理念,我认为Ansible都能沾上一些边。它的一些命令如软件的安装与卸载是借助于state=presentstate=absent来定义,这有点声明式的意味。

而我们把整个安装部署都用Ansible描述的话,不论是基础设施上的分类与定义(在inventory中),还是软件或配置的描述都清晰准确的as code呈现着。这比传统的基于运维同学在终端输入命令操作会上升一个层次,而Ansible交给我们的是需要探究如何更好的组织文件,方便后续维护工作。

当然,这不是真正的声明式,不像k8s的operator,那个给使用者提供的DSL更加声明式,而把过程化的行为放置在了operator内部。这对Ansible也是无可奈何的事情,因为总需要有地方描述业务细节,只能在Roles内部了,复杂度不在A就在B而已。

理解了原理能叫登堂入室了吗

网上有一些原理讲解的资料,比如:

这里有篇文章有图片可更直观看到这个过程:ansible架构原理及工作流程

这块我也没太深入,就暂且不表吧:)

总结

为什么在乔峰手里,太祖长拳也能对战一流高手?童子功练就的太祖长拳它长年累月的进化后,尽得其精要。Ansible有可能就是你可练的太祖长拳:D

比如用它来管理你开发机的初始化,全自动一键式,可复用,这或许比基于cloud-init方案更加具备移植性。又比如像上面用它在NAS中管理容器服务等,或者作为k8s集群服务部署模型的一个补充。

相信保持实践和思考,你(我)的长拳也能虎虎生威:)