今天努力了一个下午,终於算是勉强搞出了一组能动的 playbook,这边就来记录一下过程以及就我所知可以改进的地方。
首先来回忆一下,架设一个新的沙盒的流程:
接下来就是要搜寻一下,会需要用到哪些 Ansible 的 module,以上面的流程来说我用到了这些:
来简单介绍一下这几个 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,会拿到不同的密码,以此来避免共用密码的情形发生。
接下来是跟 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,透过 path
跟 state
指定了要在哪里创建资料夹。另外还要记得指定 owner
跟 group
确保之後有权限操作。比较需要注意的是 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 是稍微有点不同的概念,相关说明可以参考官方文件。
最後,就是要实际来部署沙盒的容器了,首先,因为 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,关於他们的比较大致上是这样(来源):
另外,除了上面这些 module,还有一个 ansible.builtin.script 也是用来执行 command 的,不过它是将 managed node 上的 script 先复制一份到 host 上再执行的。
最後还有那个 when
,因为 ansible.builtin.command 在不传 creates
或 removes
参数的情况下是不支援 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 连线与断线:三次握手、四次挥手
昨天介绍了CSS,今天就来介绍大家F12的功能, 写网页都要懂得看F12 首先到我们做的index....
天亮了 昨晚2号玩家死亡 关於迷雾森林故事 颤栗消逝 洛神:昨晚2号玩家被杀死了,邪恶阵营获胜,可以...
今天是第20天了,剩下的这10天我们会针对之前作的去做延伸,话不多说赶快开始吧! 正常来说完成登入後...
有时候,我们都太天真的想像着美好,然而降临我们面前的不只是美好,有时是想不到的冲突,或者双方同时出现...
随着 WordPres 的区块功能不停地强化,市场上出现单个功能的区块 (Block),当然也有组...