Ansible 部署证书

本文主要介绍如何结合 Ansible 与 cmcli 命令行工具自动化部署证书。

本文将结合 Ansible 与 cmcli 命令行工具,进行一次证书部署。由于实际部署的环境不同,请根据实际情况进行的灵活定制。

请注意: 本文不会过多介绍 Ansible 基础知识。基础知识请参考 Ansible 官方文档

准备工作

  1. Python 本文基于 Python 3.7
  2. Ansible 本文基于 Ansible 2.9.11
  3. cmcli 命令行工具

本示例部署证书的环境

  1. Nginx Docker 镜像
  2. Docker SDK for Python, 供 Ansible 的 docker_container 模块调用。可使用 pip 进行安装。
pip3 install docker

本示例文件目录结构

当完成配置完后当前的工作目录包括以下文件

|____hosts 配置Ansible inventory
|____play.yml 申请证书的playbook
|____ansible.cfg 配置文件
|____library
| |____request_cert.py 申请证书的模块

步骤

1. 申请证书

下文的例子,是申请多个单域名证书。可以根据实际情况编写参数。

这里主要是简单是了解 Ansible 的 Module 功能与 cmcli 结合,方便使用脚本,以下为示例。

# request_cert.py

# 调用 cmcli 命令,其中-t为OPEN API Token,-a为账户ID
cmd = 'cmcli enroll -u https://api.certcloud.cn \
-t {token} \
-a {account_id} --app-info "CertCloud-Ansible '


# 执行cmcli的命令
def get_cert(cmd):
    process = subprocess.run(cmd, shell=True)
    code = process.returncode
    while code == 403:
        process = subprocess.run(cmd, shell=True)
        code = process.returncode
    return process


def request():
    module = AnsibleModule(argument_spec=dict(
        domain_num=dict(required=True, type='int'),
        validity=dict(required=True, type='int'), # 证书有效天数,填 1~365
        domain=dict(required=True, type='str') # 申请的域名
    ))
    debug_msg = [module.params['domain_num']]

    basic_domain = module.params['domain']

    threads = [] # 每个域名证书都会开启一个线程申请,可以按照具体情况分配

    for a in range(1, module.params['domain_num']+1):
        domain = str(a)+basic_domain

        # 这里将 key-file 私钥,cert-file 证书都命名为当前申请的域名
        request_cmd = cmd + \
            "--key-file ./certs/{0}.key --cert-file ./certs/{0}.crt --cn {0} \
                --validity {1}".format(
                domain, module.params['validity'])
        debug_msg.append(request_cmd)

        t = threading.Thread(target=get_cert, args=[request_cmd])
        threads.append(t)
        t.start()

    for t in threads:
        t.join(timeout=60)
    module.exit_json(changed=True, meta=debug_msg)

if __name__ == "__main__":
    request()

2. 配置 playbook

以下是 playbook 示例。 其中配置需要的变量都在 hosts 文件中。

# play.yml

# 以下申请证书的过程可以在本地进行,然后部署到远程主机上
- hosts: local
  tasks:
    - name: 准备阶段 - 清空证书文件夹
      file: path={{certs_dir}} state=absent #配置好本地申请的
      tags: request
    - name: 准备阶段 - 创建空的文件夹
      file: path={{certs_dir}} state=directory
      tags: request

    - name: 请求200张证书
      request_cert: # 这里调用我们上面写好的模块
        # 下面的变量,都配置在 hosts 的变量中
        domain: "{{domain}}"
        domain_num: "{{instance_num}}"
        validity: "{{validity}}"
      tags: [request,certs]
      loop_control: 
        label: "申请证书"

# 以下将申请好的证书发送到需要部署证书的主机群
- hosts: Remote 

  tasks:
    - name: 同步证书文件
      become: yes
      become_user: root
      synchronize:
        src: "{{certs_dir}}"
        dest: "{{remote_certs_dir}}"
      tags: request


    # 将 nginx 的容器中的配置改成申请的证书文件
    - name: 在200台服务器上更新200张证书
      shell: "docker exec {{ item }}{{ domain }}-nginx /bin/bash -c 
      'sed -i s?domain?certs/{{ item }}{{ domain }}?g /etc/nginx/nginx.conf &&
      nginx -s reload'" 
      with_sequence: start=1 end={{ instance_num }}
      tags: [replace, request]
      # 以下 async,poll 可选,结合下文 mitogen 插件加快部署速度
      async: 180
      poll: 0
      loop_control: 
        label: "为 https://{{ item }}{{ domain }}:{{item|int+https_port}} 部署证书"

    # 可选,等待证书更新。
    - name: 等待证书更新
      wait_for:
        timeout: 10
      tags: [replace, request]

    # 可选,起一定数量的服务器供实验
    - name: 准备Web服务器
      docker_container:
        name: "{{ item }}{{ domain }}-nginx"
        state: started
        published_ports:
          - "{{item|int+https_port}}:443"
        # 这里给容器里的 Nginx 配置了一些证书的地址,方便替换,可以自行准备需要的容器。
        image: nginx:1.19.1-demo 
        # 这里使用 volume 映射,让所有容器共用一个宿主机文件夹,方便所有容器共用。
        volumes:
          - "{{remote_certs_dir}}/certs:/etc/nginx/certs"
      with_sequence: start=1 end={{ instance_num }}
      tags: create_containers
      async: 180
      poll: 0
      loop_control: 
        label: "创建 https://{{ item }}{{ domain }}:{{item|int+https_port}} 站点"

下面是 hosts 的配置示例

# hosts

[local]
localhost ansible_connection=local

[local:vars]
ansible_python_interpreter=/usr/bin/python3


[all:vars]
certs_dir=./certs
instance_num=200
domain=.cm2.httpsauto.com
https_port=50000
validity=30

# remote-host 是在 ssh config 里配置好的主机信息
[Remote]
remote-host

[Remote:vars]
ansible_python_interpreter=/usr/bin/python3
remote_certs_dir=/root/path

3. 运行 playbook

运行以下命令,可以申请并部署30天的证书:

ansible-playbook demo_play.yml --tags=request

可以通过在命令里加上 extra-vars, 填写 validity 有效期的值,可灵活改变证书申请的有效期。例如申请有效期365天的证书:

ansible-playbook demo_play.yml --tags=request --extra-vars="validity=365"

进阶

单节点加速部署

如果希望加快 Ansible 单节点的部署的速度,可使用 mitogen 插件加速。

先运行:

pip3 install mitogen

然后在 ansible.cfg 中配置 strategy 和 strategy_plugins 的插件信息。

macOS

strategy = mitogen_linear
strategy_plugins = $HOME/Library/Python/3.7/lib/python/site-packages/ansible_mitogen/plugins/strategy 

Linux

strategy = mitogen_linear
strategy_plugins = /usr/local/lib/python3.7/site-packages/ansible_mitogen/plugins/strategy

Windows

strategy = mitogen_linear
strategy_plugins = C:\Python37\Lib\site-packages\site-packages\ansible_mitogen\plugins\strategy

多节点加速部署

可利用 fork、serial 等关键字配置,加快部署速率。具体方法参考官方文档

以上是利用 Ansible 从 CertCloud 申请证书并部署到 Nginx 的示例。其他 Web 服务器也可以参考这篇文章,进行操作。Ansible 十分强大且灵活,请根据自己的需求进行自定义。


February 26, 2021