软件配置管理实践——基于Ansible和Nacos

配置管理的预备知识

在《持续交付》第二章的小结中,作者写道:配置管理是本书其他内容的基础。没有配置管理,根本谈不上持续集成、发布管理以及部署流水线。

可以看出,配置的管理是多么的重要。在小结中,作者使用以下几个问题来判断你的配置管理做得是否好:

  • 是否仅依靠保存于版本控制系统的数据(除了生产数据),就可以从无到有重建生产系统?
  • 是否可以将应用程序回滚到以前某个正确的状态?
  • 是否确保在测试、试运行和正式上线时以同样的方式创建部署环境?

如果你的配置管理流程比较好的话,所有的问题都应该是肯定的。

经过笔者长时间的一线配置管理的经验,要想更好实践《持续交付》中的配置管理,笔者认为配置管理中心应该能做到:

  1. 配置项的定义与配置的存储的解耦。比如应用可能会从Nacos中读取,也可以从环境变量里读取。
  2. 配置项之间的相互引用:消除重复配置项。比如MySQL的连接字符串,可能同时在应用a的配置和应用b的配置中出现。
  3. 配置项版本化:配置项变更跟踪;
  4. 配置项的定义与配置的格式的解耦。比如同一个配置项在应用a使用的是XML格式,但是应用b使用的是YAML格式。
  5. 配置项的自动化校验:当用户引入一个错误的配置项时,我们应该可以知道。
  6. 配置项的作用域功能。
  7. 配置项分组功能:比如按环境对配置项进行分组。
  8. 配置项加密功能。

目前没有一个配置中心能实现以上所有的功能。但是,基于Ansible再配合其它工具,我们可以很低成本地实现以上大部分功能,虽然有些粗糙。

基于Ansible和Nacos的实践

本文以Ansible结合Nacos为例介绍如何更好的进行配置管理。

在本案例中,Ansible担任了配置中心的角色,实现配置项的定义,描述配置项与配置格式(XML、YAML、Properties)之间关系,描述配置项与配置存储(Nacos)之间的关系。Nacos纯粹只是一个存储配置的角色。

Ansible介绍

Ansible通常被认为是一款自动化运维工具。用户通过YAML文件描述机器或者应用的最终状态,Ansible负责实现该状态。它通过Python技术栈的Jinja2模板引擎实现了我们所需要的绝大部分配置管理功能。所以,基于它结合Jenkins这类自动化平台即可实现一个低成本的灵活的配置管理中心。

说回来,配置中心的实现的核心其实是文本模板引擎。


软件配置管理实践——基于Ansible和Nacos_第1张图片
ansible.png

Nacos介绍

软件配置管理实践——基于Ansible和Nacos_第2张图片
nacos.png

Nacos是阿里巴巴开源的一个动态服务发现、配置管理和服务管理平台。不过,它的配置管理功能离上文谈到的配置中心还差太远。但是至少作为配置存储的实现,还是可以的。

说回来,由于我们的配置管理是基于Ansible的,所以,按道理,我们是可以很容易切换配置中心的。

整体架构

软件配置管理实践——基于Ansible和Nacos_第3张图片
art.png

通过Ansible的配置功能,我能实现大部分配置中心应该实现的功能。但是,我们的应用程序不会直接读取Ansible工程中的配置,而是从Nacos中读,所以,我们通过一个Ansible任务自动化将配置发布到Nacos中。最后,再结合Jenkins实现流水线。

Ansible工程的准备

实际工作中,我们会为每个环境创建一个Git工程项目。这个Git工程项目是一个典型的Ansible工程结构,如下:

.
├── Jenkinsfile.groovy
├── README.md
├── group_vars
│   ├── all
│   │   ├── 01.all.yaml
│   │   ├── 02.nacos-config.yaml
│   │   └── app-config
│   │       ├── demo.json.yaml
│   │       ├── application.yaml.yaml
│   │       ├── demo.properties.yaml
├── host_vars
├── hosts
└── playbook-nacos.yaml

如果我们要对比不同环境的配置,只需要通过Beyond compare这类工具就可以实现。就这样可以低成本的实现了一个大的配置管理平台应该有的功能。

以下是各个文件的介绍:

group_vars目录是Ansible的约定目录,用于存放组级别的配置变量及全局配置变量。我们在group_vars目录下建立一个all/app-config子目录,子目录中存储各种类型配置,是为了人类能更好查找到自己的配置。all/app-config子目录下,每一个YAML文件中只定义一个全局配置。这里实现了配置格式与配置项定义的解耦。这是整个方案中,最难理解的地方。

group_vars/all目录下的文件示例:

demo.json.yaml

domo_json_nacos_config: |
    {
        "message": "hello json"
    }

application.yaml.yaml

application_yaml_nacos_config: |
    ---
    server: 
        port: 8080

demo.properties.yaml

demo_properties_nacos_config: |
    spring.datasource.url={{ global_db.url }}

注意,global_db.url是全局级别的配置项,它的值可以被多个配置文件引用。

02.nacos_config.yaml

此文件用于定义Nacos中的配置文件与我们的Ansible之间的关系。nacos_config配置项列表中的每一项都是一个Nacos更新配置的REST API的一次请求。只不过,我们通过YAML实现声明式了。

nacos_config:
  - dataId: demo.json
    group: demoGroup
    type: json
    content: "{{ demo_json_nacos_config | from_json | to_nice_json(indent=2) }}"
    tenant: "{{ nacos_namespace }}"

  - dataId: application.yaml.yaml
    group: demoGroup
    type: yaml
    content: "{{ application_yaml_nacos_config}}"
    tenant: "{{ nacos_namespace }}"

  - dataId: demo.properties
    group: demoGroup
    type: properties
    content: "{{ demo_properties_nacos_config }}"
    tenant: "{{ nacos_namespace }}"

Nacos提供了更新配置的REST API如下:

curl -X POST 'http://127.0.0.1:8848/nacos/v1/cs/configs' \
-d 'dataId=nacos.example&group=com.alibaba.nacos&content=contentTest'

我们现在的问题就是如何将nacos_config.yaml中的配置变成请求Nacos REST API。有两个方案:

  • 方案一:在Jenkins pipeline中,使用groovy语言读取nacos_config.yaml文件,然后拼装REST请求。
  • 方案二:写一个Ansible任务,它会读取nacos_config.yaml的变量,然后通过Ansible的uri模块发起REST请求。

我们选择了方案二。因为方案二能做到与Jenkins的解耦。将来,我们切换到其它的自动化平台,难度不会太大。而且方案二能让我们脱离自动化平台,在本地手工执行。

以下就是我们的Ansible任务的写法:

---
- name: "nacos config deploy"
  uri:
    url: "http://{{nacos_server_addr}}/nacos/v1/cs/configs"
    method: POST
    body_format: "raw"
    return_content: yes
    body:
      tenant: "{{item.tenant | urlencode}}"
      group: "{{item.group|urlencode}}"
      dataId: "{{item.dataId | urlencode}}"
      type: "{{item.type | urlencode}}"
      content: "{{item.content | urlencode}}"
    status_code: 200
    headers:
      content_type: "Content-Type:application/x-www-form-urlencoded; charset=UTF-8"
  with_items: "{{nacos_config}}"
  when: nacos_config is defined
  no_log: "{{ansible_global_no_log | default(True)}}"
 

这里有一个Nacos的坑,1.x的版本下,它只支持urlencode的方式。如果配置文件太大,会请求失败。简单看了下Nacos2.x,REST API似乎没什么变化。

结合Jenkins的流水线

当Ansible工程准备好之后,剩下的问题就是如何自动化执行Ansible。这是Jenkins的强项,就很容易做到了。以下是核心代码:

container('ansible') {
    withCredentials([file(credentialsId: "ansible-vault", variable: 'vaultFile'),
                     sshUserPrivateKey(credentialsId: "ssh-keyfile", keyFileVariable: 'keyfile')
    ]) {
        sh """
            ansible-playbook -i ./hosts \
            --vault-password-file ${env.vaultFile} \
            --extra-vars 'ansible_ssh_private_key_file=${keyfile}' \
            ${WORKSPACE}/playbook.yaml
        """
    }
} //end container

至此,只要提交配置仓库代码,就可以自动化发布配置到Nacos中。

后记

以上实践,在我们项目组已经运行1年多,目前运行良好。除了基础设施,我们已经做到了《持续交付》中的:仅依靠保存于版本控制系统的数据,就能重建系统;能快速的回滚到某个正确的版本;在多个环境中以同样的方式部署环境。

本文虽然介绍的是基于Nacos,但是其思想也适用于行业中的其它的“配置中心”。希望能对你有帮助。如果你有什么疑问,可以留言。

你可能感兴趣的:(软件配置管理实践——基于Ansible和Nacos)