Day 27:开始撰写 Playbook

今天努力了一个下午,终於算是勉强搞出了一组能动的 playbook,这边就来记录一下过程以及就我所知可以改进的地方。

首先来回忆一下,架设一个新的沙盒的流程:

  1. ssh 连上机器
  2. clone 沙盒 repo(如果不存在的话)
  3. 更新 repo
  4. 设定或是更新 token(第一次或是有需要的时候)
  5. 把沙盒跑起来或是重开
  6. 替 NOJ 设定新的沙盒(填 token 跟 URL)

接下来就是要搜寻一下,会需要用到哪些 Ansible 的 module,以上面的流程来说我用到了这些:

  • ansible.builtin.copy
  • ansible.builtin.pip
  • ansible.builtin.set_fact
  • ansible.builtin.file
  • ansible.builtin.git
  • ansible.builtin.template
  • ansible.builtin.command
  • community.docker.docker_image_info
  • community.docker.docker_compose

来简单介绍一下这几个 module 的用途。

设定变数

首先第一个部分是要来设定变数,因为目前的其中一个需求是要能够设定沙盒使用的 token,但若是直接帮所有的沙盒都使用同样的 token 那可能会造成一些安全性的隐忧。另外因为有些值在整份 playbook 里面会出现多次,所以把他们定义成变数也能避免重复。

在 playbook 里面要定义变数的话可以使用 vars 这个 key,我写出来的设定长得会像这样:

vars:
  token: "{{ lookup('password', 'secret/' + inventory_hostname + '/token') }}"
  project_dir: /srv/normal-oj-sandbox
  venv_dir: /tmp/.sandbox-venv
  req_path: /tmp/sandbox-requirements.txt

之後只要使用 {{ var_name }} 这样的格式,就可以使用变数的值了。

那,撇开其他变数不看,先来看看 token 这个变数,他长得比较特别一点,也有一组 {{ }} 把中间的值框起来,但是这里的 lookup 不是变数,在 Ansible 里面这称作 Lookups,是一种从外部取值的方式。而 lookup 里面放的第一个参数,代表的是 lookup plugin,像是 这边的 password 就是一个 Ansible 内建的 lookup plugin,用来产生随机的密码,并把它存进指定的档案内。然後在後面,我使用了 inventory_hostname 这个 Ansible 的内建变数,来确保不同的 host,会拿到不同的密码,以此来避免共用密码的情形发生。

处理 repo 相关

接下来是跟 git repo 相关的,因为我是把沙盒的 repo 放在 /srv 底下,因此需要先使用 root 创建一个可以给一般使用者存取的资料夹,这部分就需要使用 ansible.builtin.file,它的用途是进行 host 上的档案相关的操作。task 会长这样:

name: Create project dir
become: yes
ansible.builtin.file:
  path: "{{ project_dir }}"
  owner: bogay
  group: bogay
  mode: 0744
  state: directory

上面透过了 become: yes 来让我可以使用 root 权限执行这个 task,透过 pathstate 指定了要在哪里创建资料夹。另外还要记得指定 ownergroup 确保之後有权限操作。比较需要注意的是 mode,需要设定成 0744 而不是一般档案比较常看到的 0644,多出来的 x 权限是为了可以 cd 进去而加上的。

创建完资料夹,下一步就是要取得 source code,这时候需要使用的是 ansible.builtin.git,来做 git 相关的操作。task 定义如下:

name: Checkout sandbox repo
ansible.builtin.git:
  repo: "https://github.com/Normal-OJ/Sandbox.git"
  dest: "{{ project_dir }}"
  version: 56a1bf
  force: yes

设定更新 repo 的相关参数,就可以 checkout 到指定的版本,不过这边因为我後面会动到 repo 底下的档案,因此需要加上 force: yes 来确保它会更新,不然整次执行就会失败。

更新完 repo 之後,还需要把设定档复制进去,使用的是 ansible.builtin.template 这个 module,它会使用 Jinja2 作为模板引擎,并且把使用当前 playbook 的变数去处理我们预先定义的模板,把他们复制到 host 上。task 定义如下:

name: Copy configs
ansible.builtin.template:
  src: "{{ item.src }}"
  dest: "{{ project_dir }}/{{ item.dest }}"
  mode: 0644
loop:
  - src: docker-compose.yml.j2
    dest: docker-compose.yml
  - src: .config/dispatcher.json
    dest: .config/dispatcher.json
  - src: .config/submission.json.j2
    dest: .config/submission.json

就是把 control node 的 src,复制到 managed node 的 dest。比较特别的是这里的 loop,因为我有多个档案要复制,可是撰写好几个 task 就显得有点冗长,loop 可以帮助我们把参数定义成一个阵列,然後依序把他们填进 task 里面去执行。

另外,在比较旧版的 Ansible,是使用 with_* 的语法来做到回圈的,关於他们的比较,可以参考官方文件的说明

至此,算是把所有 repo 相关的设定准备好了。

安装依赖

因为这份 playbook 需要使用到一些 docker 相关的 module,因此需要安装额外的依赖,像是 docker-py 还有 docker compose,所以需要先安装这些东西,因为两者刚好都能透过 pip 安装,因此我们需要使用 ansible.builtin.pip 来执行 pip,但是因为我有将依赖写成 requirements.txt,因此需要先将档案复制到 host 上。这部分的 task 定义如下:

- name: Copy requirements.txt
  ansible.builtin.copy:
    src: requirements.txt
    dest: "{{ req_path }}"
    mode: 0644
- name: Setup python environment
  ansible.builtin.pip:
    requirements: "{{ req_path }}"
    virtualenv: "{{ venv_dir }}"
    virtualenv_command: 'python3 -m venv'

这边因为我不希望影响到整个 host,所以有使用 virtualenv 来隔离环境,话说在这边安装的时候有遇到 No module named pkg_resources 之类的错误,後来在这边找到应该是因为 setuptools 的问题,或许在写 playbook 时也要考虑这个情形。

到这边就安装好相关的依赖了,可以进行後续的操作。(其实还有 docker 啦,这就是这份 playbook 没处理好的部分之一)

下一步是要让 Ansible 使用 virtualenv 内的 Python interpreter,毕竟刚刚那些依赖是装在他身上。我们除了修改 ansible.cfg 以外,也可以透过修改 ansible_python_interpreter 这个变数来修正。而要修改变数,我们可以使用 ansible.builtin.set_fact。task 定义如下:

name: Update intepreter path
ansible.builtin.set_fact:
  ansible_python_interpreter: "{{ venv_dir }}/bin/python"

这样在之後的 task 中,Ansible 就都会使用我们现在所定义的 interpreter 了。BTW,这边的 "fact" 在 Ansible 中跟 variable 是稍微有点不同的概念,相关说明可以参考官方文件

创建 container

最後,就是要实际来部署沙盒的容器了,首先,因为 NOJ 上执行 submission 也是透过 docker container,所以需要先确保对应的 docker image 存在。这可以透过 community.docker.docker_image_info 来检查,task 如下:

name: Check sandbox image existence
community.docker.docker_image_info:
  name:
    - noj-c-cpp
    - noj-py3
register: inspect_res

这边的 register 也是 Ansible 里面宣告变数的方式,会把这个 module 的回传值存进 inspect_res 里面,让後面的 task 可以用来检查。

下一步是当 image 不存在的时候要去 build image,需要的是 ansible.builtin.command,帮我执行预先写好的 shell script,task 如下:

name: Build sandbox image
ansible.builtin.command:
  chdir: "{{ project_dir }}"
  cmd: build.sh
when: inspect_res.images | length != 2

这边用到两个参数,chdir 是指定命令要在哪边执行,而 cmd 就是要执行的命令。不过其实在 Ansible 里面还有其他几个同样也可以执行命令的 module,关於他们的比较大致上是这样(来源):

  • ansible.builtin.command:执行指令,但不会透过 shell,因此一些像是环境变数或是 pipe、redirect 之类的特性皆不能使用。适合执行简单命令的情形,另外它通常相较其他选项来得安全且一致(不受 host 环境影响)。
  • ansible.builtin.shell:透过 shell 执行命令,基本上就像在本机上执行那样。
  • ansible.builtin.raw:也是透过 shell 执行命令,但不透过 Python interpreter,而是直接使用 ssh。通常情况下不建议使用这个 module,但在少数案例中我们还是会需要它,像是在 host 上安装 Python(不然其他 module 就没办法使用了)。

另外,除了上面这些 module,还有一个 ansible.builtin.script 也是用来执行 command 的,不过它是将 managed node 上的 script 先复制一份到 host 上再执行的。

最後还有那个 when,因为 ansible.builtin.command 在不传 createsremoves 参数的情况下是不支援 check mode 的,这就意味着他每次都会执行。但这并非必要,我们只需要在那两个 docker image 不存在时再执行就好,此时就需要加上条件判断。when 後面接的是一个表达式,当它是 true 的时候才会执行这个 task。inspect_res.images | length != 2 这句的意思是,inspect_res.images 的个数不等於 2,其中 | 表示 jinja 的 filter,而 length 是其中一个内建的 filter,用来取元素数量。

最後一步,就是要把沙盒跑起来,需要使用 community.docker.docker_compose,顾名思义,这个 module 就是用来管理 docker compose 的服务的。task 定义如下:

name: Run service
community.docker.docker_compose:
  project_src: "{{ project_dir }}"

这边只需要指定 project_src 一个参数,让 Ansible 知道在哪边找 docker-compose.yml 就好。

小结

终於是写完了,因为刚学 Ansible 所以需要花不少时间去翻阅相关文件,对我来说算是不小的挑战。而且目前写出来的 playbook 我想也是相当粗糙,没有考虑到足够多的场景,接下来应该会再研究看看如何进行模组化。


<<:  [Day26] Business Logic Vulnerabilities - 商业逻辑漏洞

>>:  Day25【Web】TCP 连线与断线:三次握手、四次挥手

全端入门Day17_前端程序撰写之F12

昨天介绍了CSS,今天就来介绍大家F12的功能, 写网页都要懂得看F12 首先到我们做的index....

[第二十四只羊] 迷雾森林舞会XVIII 游戏角色设定again_final_final

天亮了 昨晚2号玩家死亡 关於迷雾森林故事 颤栗消逝 洛神:昨晚2号玩家被杀死了,邪恶阵营获胜,可以...

Flutter基础介绍与实作-Day20 旅游笔记的实作(1)

今天是第20天了,剩下的这10天我们会针对之前作的去做延伸,话不多说赶快开始吧! 正常来说完成登入後...

Day 3 就是你了!

有时候,我们都太天真的想像着美好,然而降临我们面前的不只是美好,有时是想不到的冲突,或者双方同时出现...

24 | 【进阶教学】什麽是 WordPress 区块组合套件外挂?

随着 WordPres 的区块功能不停地强化,市场上出现单个功能的区块 (Block),当然也有组...