12/02/2022 - Week 1 - Setup Ansible repo
Trong khi thế giới đã thay đổi rất nhiều, cách vận hành hệ thống cũng đã thay đổi so với 10 năm về trước, thì tui vẫn xài những thứ gọi là boring-tech và xài rất nhiều trong nhiều dự án kể cả cá nhân lẫn công việc và một trong số đó là Ansible để quản trị hệ thống. Tuần này, sẽ nói về cách tui setup một Ansible repo mà tui đang xài để quản lý, cài đặt, cấu hình các dịch vụ trên các VM.
Nhắc lại một chút về nguyên tắc khi viết weekly-summary đó là KHÔNG NÓI XẠO, có sao nói vậy, đang làm gì thì nói vậy, không làm thì nói không làm, làm dở thì nói làm dở, một số chỗ nếu dính NDA không thể nói cũng sẽ ghi cụ thể.
Tui hay chia hạ tầng nói chung thành 2 phần như kiểu của Gruntwork, với một phần sẽ có cách tiếp cận riêng:
- Phần Infra phía dưới, bao gồm những thứ tui tự định nghĩa là từ OS trở xuống dưới như Network, subnet, routing, VM … Ở thời điểm hiện tại, vẫn xài Terraform quản trị miễn là provider đó có hỗ trợ, ví dụ xưa thì AWS, giờ thì Linode.
- Phần System phía trên, từ OS trở lên trên như package, services, config … Phần này là Ansible cái mà sẽ nói trong tuần này.
Tui còn nhớ lần đầu tiên tui viết Ansible là vào năm 2016 thì phải, trước đó thì để setup một VM tui vẫn manual hoặc bash-script lúc đó bash-script có một vài cái rất khó chịu:
- Không scale được, kiểu chạy parallel trên multi-VM không được
- Viết logic rất khó, càng phức tạp càng khó hoặc viết được nhưng code đọc thì không hiểu gì cả. (vì bash hỗ trợ kiểu dữ liệu đơn giản, các kiểu phức tạp như array, hash, map không có hoặc mấy cái grep/awk/sed quá nhiều logic sẽ khó hiểu)
- Khó test, có cái bats-core nhưng thú thật là không muốn dùng tí nào
- <Còn nhiều>, nhưng lâu lâu vẫn xài vì nó nhanh, tiện nếu logic đơn giản
Sau đó chuyển sang Ansible vì nghe một người bạn, cảm giác lần đầu là không khoái lắm, vì sao thấy nó phức tạp quá. Và rất khó chịu ở chỗ là lâu lâu chạy lại playbook thì lúc nào cũng lỗi, lỗi lại phải sửa tay nên rất bực bội, lúc đó cảm thấy rất khó test nên cuối cùng cũng không dùng nhiều.
Một số lỗi rất thường gặp tại thời điểm đó mà tui hay bị:
- Mistake về package name, config option hoặc path -> do không có testing, nên cứ khi nào update code, chạy trên production là rất dễ dính lỗi (ví dụ thay vì package name là của dịch vụ SSH client là
openssh-clienttrên Ubuntu, nhưng lại làopenssh-clientstrên CentOS, nếu sai tên package thì sẽnot found, hoặc như path default config của nginx thay vì/etc/nginxlại gõ nhầm thành/etc/ngnx-> khi render config thì sẽpath not foundhoặc như Redis version 6 họ đã bỏ từ khóaslavevà thay bằngreplica, nếu upgrade version Redis mà không test thì sẽ không start được dịch vụ. - Setup môi trường test đồng nhất và tự động khó, mong đợi là cái VM test nó phải giống cái VM được tạo bởi cloud provider, thế thì mới không có bug được. Hồi đó suy nghĩ tới việc cài 1 cái máy ảo VirtualBox Ubuntu, xong snapshot lại mỗi lần test thì reset lại snapshot hoặc xài Vagrant để boot một cái VM và run playbook vào đó. Nhưng nói chung nó vẫn ko tự động lắm.
- Test bằng cơm, tức là có môi trường, chạy playbook và nhìn output.
1. Setup môi trường Python
Ansible và nhiều thư viện liên quan viết bằng Python, nên nó cũng tương tự các mã nguồn Python khác, nên cần có môi trường để development, test, ở đây dùng các thứ như sau:
pyenvđể quản lý multi-version Python (ko dùng Python của system, cũng ko tự build)poetryđể quản lý thư viện Python, xưa xài cáirequirement.txtthông quapip3nhưng nó thủ công quá, một phần muốn có cái kiểu.lockđể lock version. Xàipoetrynhiều lúc cũng bực mình, vì lỗi lúc nào cũng giống nhau, lỡ có bug gì cái là rất khó để tìm giải pháp, nói chung xài tạm được chứ cũng ko ưa lắm =)) ai thích có thể thử cái khácdirenvđể tự động load một số ENV variable khicdvào source code -> cái này thấy rất ngon
Cụ thể như sau
> brew install pyenv
# Nhét đủ 4 cái vào bash_profile, bashrc gì đó, lên docs xem nhé
export PYENV_ROOT="$HOME/.pyenv
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
# Đang xài Python 3.8.12
> pyenv install 3.8.12
> pyenv global 3.8.12
> python3 --version
Python 3.8.12
# Upgrade pip và cài poetry trước nhé
> pip install --upgrade pip
> pip install poetry
> brew install direnv
# config direnv
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
# Cài layout poetry cho direnv
> add to ~/.direnvrc
layout_poetry() {
if [[ ! -f pyproject.toml ]]; then
log_error 'No pyproject.toml found. Use `poetry new` or `poetry init` to create one first.'
exit 2
fi
# create venv if it doesn't exist
poetry run true
export VIRTUAL_ENV=$(poetry env info --path)
export POETRY_ACTIVE=1
PATH_add "$VIRTUAL_ENV/bin"
}
Thêm một tí là để direnv sẽ tự update PS1 khi cd và source code, thì thêm cái này trong .bashrc
show_virtual_env() {
if [[ -n "$VIRTUAL_ENV" && -n "$DIRENV_DIR" ]]; then
echo "($(basename $VIRTUAL_ENV))"
fi
}
export -f show_virtual_env
PS1='$(show_virtual_env)'$PS1
=> Dùng được cho tất cả các dự án Python khác nhé, không phải mỗi Ansible
2. Init Ansible repo
> cd ~/code/me
> mkdir -pv gh-infra
> pyenv local 3.8.12
> cat .python-version
3.8.12
Sau mấy bước trên, trong thư mục code sinh ra file .python-version chỉ version Python của mã nguồn
> poetry init
This command will guide you through creating your pyproject.toml config.
Package name [gh-infra]:
Version [0.1.0]:
Description []: Ansible demo
Author [xluffy <[email protected]>, n to skip]:
License []:
Compatible Python versions [^3.8]:
Would you like to define your main dependencies interactively? (yes/no) [yes] yes
You can specify a package in the following forms:
- A single name (requests)
- A name and a constraint (requests@^2.23.0)
- A git url (git+https://github.com/python-poetry/poetry.git)
- A git url with a revision (git+https://github.com/python-poetry/poetry.git#develop)
- A file path (../my-package/my-package.whl)
- A directory (../my-package/)
- A url (https://example.com/packages/my-package-0.1.0.tar.gz)
Search for package to add (or leave blank to continue): ansible=2.9.27
Adding ansible=2.9.27
Add a package: ansible-lint=^5.3.2
Adding ansible-lint=^5.3.2
Add a package: flake8=^4.0.1
Adding flake8=^4.0.1
Add a package: yamllint=^1.26.3
Adding yamllint=^1.26.3
Add a package: prettytable=^3.0.0
Adding prettytable=^3.0.0
Add a package: cryptography=^36.0.1
Adding cryptography=^36.0.1
Add a package: pytest=^6.2.5
Adding pytest=^6.2.5
Add a package: black=^21.12b0
Adding black=^21.12b0
Add a package: molecule=3.5.2
Adding molecule=3.5.2
Add a package: mitogen=0.2.9
Adding mitogen=0.2.9
Add a package: yamlfix=^0.8.0
Adding yamlfix=^0.8.0
Add a package: molecule-vagrant=^1.0.0
Adding molecule-vagrant=^1.0.0
Add a package: autopep8=^1.6.0
Adding autopep8=^1.6.0
Add a package: pytest-testinfra=^6.5.0
Adding pytest-testinfra=^6.5.0
Khởi tạo và add các thư viện cần thiết, sẽ ra một file pyproject.toml
Tạo .envrc, file cấu hình trong repo của direnv và allow content
> cat .envrc
layout_poetry
export FORCE_COLOR=1
export PY_COLORS=1
direnv: error /Users/quanggg/code/me/gh-infra/.envrc is blocked. Run `direnv allow` to approve its content
> direnv allow
Sau khi chạy lệnh trên sẽ thấy điều kì diệu của direnv, nó đã tạo cho chúng ta một VIRTUAL_ENV và edit dùm PS1, sau đó chỉ việc xài poetry để cài thư viện đã khai báo ở trên.
> direnv allow
direnv: loading ~/code/me/gh-infra/.envrc
Creating virtualenv gh-infra-ckHUeV7x-py3.8 in /Users/quanggg/Library/Caches/pypoetry/virtualenvs
direnv: export +FORCE_COLOR +POETRY_ACTIVE +PY_COLORS +VIRTUAL_ENV ~PATH
(gh-infra-ckHUeV7x-py3.8):: You are quanggg -at- xluffys-MacBook-Air [~/code/me/gh-infra]
> poetry install
Updating dependencies
Resolving dependencies... (21.0s)
Writing lock file
Package operations: 72 installs, 0 updates, 0 removals
• Installing six (1.16.0)
• Installing markupsafe (2.0.1)
• Installing pycparser (2.21)
• Installing python-dateutil (2.8.2)
• ...
# Sau bước này ra cái file `poetry.lock` lock version
Bây giờ mỗi lần cd ra ngoài source code và cd vào source code, direnv sẽ hỗ trợ load/unload ENV variable và môi trường ảo Python giúp, không phải làm thêm gì cả. Cần thêm thư viện gì thì cứ poetry add, ai clone source về thì sau mục #1 phía trên chỉ cần poetry install là xong, đủ đồ chơi.
> cd ~
direnv: unloading
> cd -
/Users/quanggg/code/me/gh-infra
direnv: loading ~/code/me/gh-infra/.envrc
direnv: export +FORCE_COLOR +POETRY_ACTIVE +PY_COLORS +VIRTUAL_ENV ~PATH
(gh-infra-ckHUeV7x-py3.8):: You are quanggg -at- xluffys-MacBook-Air [~/code/me/gh-infra]
3. Ansible orgs and tooling
- typos để ource code spell checker, tệp
_typos.toml - Molecule và molecule-vagrant để tạo máy ảo test và viết code test, đi kèm là VirtualBox
- Makefile để generate một vài info như inventory
mitogenđể chạy Ansible lẹ hơn, nhưng chưa hỗ trợ Ansible lớn hơn 0.2.9 nên vẫn xài Ansible 0.2.9 nhéansible-vaultđể support symmetric encryption thông qua.vault_passyamllintvàansible-lintđể lintyamlfile và ansible module có theo rule không, hỗ trợ develop thống nhất, chuẩn.flake8,black,autopep8hỗ trợ develop python theo coding style, auto fix ….pytest,pytest-testinfrađể hỗ trợ viết code test infra và Ansible roledocsđể chứa hướng dẫn về cách xài, cách development, ad-hoc task chạy sao, migration thì như thế nàosecretschứa cái gì cần giữ bí mật, đừng commit lên nhé, nếu không mang tính bí mật thuộc về một cá nhân nào đó thì có thể dùngansible-vaultđể encrypt giốngid_ed25519.vaultcũng được
Cấu trúc mã nguồn như sau:
> tree
.
├── Makefile
├── _typos.toml
├── ansible.cfg
├── ansible_cache
├── docs
│ ├── migrations
│ │ ├── 001-migrate-abc-srv.md
│ │ └── 001-migrate-redis-srv.md
│ ├── runbooks
│ │ ├── ansible-run-specify-tasks.md
│ │ └── debug-jinja2-render.md
│ ├── setup.md
│ └── testing.md
├── hosts.txt
├── inventories
│ ├── 000_cross_env_vars.yml
│ └── prod
│ ├── group_vars
│ │ └── all
│ │ ├── 000_cross_env_vars.yml -> ../../../000_cross_env_vars.yml
│ │ └── 001_remote_env_vars.yml
│ ├── host_vars
│ └── hosts
├── poetry.lock
├── pyproject.toml
├── roles
├── run_molecule.py
└── secrets
├── id_ed25519
└── id_ed25519.pub
└── id_ed25519.vault
Cài VirtualBox và vagrant hỗ trợ tạo máy ảo testing, test x86_64 và Ubuntu thôi nhé, tui ko còn ưa CentOS nữa.
> VirtualBox lên trang chủ tải
> brew install vagrant
# Xài chính Ubuntu 20.04 lên test trên đây, box này y chang VM AWS hoặc các cloud provider khởi tạo nhé
> vagrant box add ubuntu/focal64
> vagrant box list
ubuntu/focal64 (virtualbox, 20220208.0.0)
3.1 Role common
- Được include bởi tất cả các playbook, lên tất cả các server đều sẽ có thuộc tính giống nhau nhờ role này (tunning, hardening, security, audit, monitoring …)
- Hỗ trợ testing với
molecule
Cấu trúc lược bớt như sau, cụ thể tí xem source trong repo nhé:
> tree
.
├── Makefile
├── defaults
│ └── main
│ ├── main.yml
│ ├── node_exporter.yml
│ └── vault.yml
├── files
│ └── bench-local-dns
├── handlers
│ └── main.yml
├── molecule
│ └── default
│ ├── converge.yml
│ ├── molecule.yml
│ ├── prepare.yml
│ └── tests
│ ├── __pycache__
│ │ └── test_default.cpython-38-pytest-6.2.5.pyc
│ └── test_default.py
├── tasks
│ ├── apt.yml
│ ├── build.yml
│ ├── ps_mem.yml
│ ├── resolved.yml
│ ├── root.yml
│ ├── sshd.yml
│ └── utils.yml
└── templates
├── 00-header.sh.j2
├── 10-network-perf.conf.j2
├── 10-network-security.conf.j2
├── 10-system-memory.conf.j2
├── 10-system-security.conf.j2
├── 20auto-upgrades.j2
├── sshd_config.j2
├── system.conf.j2
├── uptimed.conf.j2
├── user.conf.j2
└── vimrc.j2
Cách để khởi tạo và các command của molecule thì lên docs coi nhé, không thì copy tương tự cũng chạy được, nói nhanh thì có những thứ sau:
molecule/defaultgọi là kịch bản (scenario)molecule.ymlmô tả về môi trường, cách testplatformlà vagrant, máy ảo virtualbox phía trên, Ubuntu 20.04ubuntu/focal64, memory máy ảo là 1280MB, thuộc groupcommonlintthì xàiyamllint,ansible-linttestymlfile (tasks, handler, variables, templates)provisionerlà ansible, chính là cáiconverge.ymlverifierlà./tests/test_default.py
converge.ymltương tự playbookprepare.ymllàm một số thứ trước khi chạy playbookmolecule/default/tests/test_default.py-> code verify
Ví dụ:
Có một task tạo user deploy khi server thuộc group web/app/api như sau
- name: common | create deployment group
group:
name: deploy
state: present
when: >
inventory_hostname in groups.app | default([])
or
inventory_hostname in groups.api | default([])
or
inventory_hostname in groups.web | default([])
tags:
- r_common_based
- r_common_users
- name: common | create deployment user
user:
name: deploy
create_home: yes
state: present
group: deploy
comment: "Deployer user"
shell: "/bin/bash"
when: >
inventory_hostname in groups.app | default([])
or
inventory_hostname in groups.api | default([])
or
inventory_hostname in groups.web | default([])
tags:
- r_common_based
- r_common_users
Viết code test bằng testinfra để kiểm tra user đó có thật sự tồn tại sau khi chạy playbook không như sau, cách này hơi phèn vì check group dựa trên hostname, nhưng nó chạy, vì rules tự đặt là trong hostname có services:
def test_users(host):
"""
Test create user/group
"""
_hostname = os.uname()[1]
if "app" in _hostname or "api" in _hostname or "web" in _hostname:
_g_deploy = host.group("deploy")
_u_deploy = host.user("deploy")
assert _g_deploy.exists
assert _u_deploy.exists
assert _u_deploy.shell == "/bin/bash"
Hoặc muốn test các service sau khi chạy playbook xong sẽ được start và cho phép khởi động khi boot OS
@pytest.mark.parametrize("services", ["sshd", "rsyslog", "chrony", "node_exporter"])
def test_services_running(host, services):
"""
Test services are running + enabled
"""
_s_services = host.service(services)
assert _s_services.is_running
assert _s_services.is_enabled
Xong xuôi, bây giờ chạy test, kịch bản chạy tuần tự sẽ như sau:
> mol matrix test
INFO Test matrix
---
default:
- dependency
- lint
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
- side_effect
- verify
- cleanup
- destroy
- chạy
lint, xem code có đúng chuẩn chưa,ymlcó đúng chuẩn chưa, viết task ansible có theo best practice không (ví dụ ko khuyến khích xài moduleshell/commandnè hoặc có miss attribute gì không, như miss permission của thư mục khi tạo nè) cleanup/destroy, nếu đã có một VM trước đó, thì xóa đi, làm lại từ đầu, test cho chuẩn (giống xóa cái VM trước đó đã test)syntaxkiểm tra syntaxcreatetạo VM thông quavagrantvà VirtualBox, giốngvagrant upthôi, xài cáiVBoxManage list runningvmssẽ thấy VM được tạo ra hoặc mở VirtualBox GUI sẽ thấy có máy ảo mớiprepare, chạy cáiprepare.ymlphía trênconvergechạy playbook của role nàyidempotencemột đặc tính quan trọng của Ansible, nó đảm bảo rằng playbook được chạy đi chạy lại nhiều lần thì vẫn đạt tínhidempotence, ví dụ mấy moduleshell/commandkhông hỗ trợidempotencenên thường không khuyến khích xài, trừ khi bắt buộc, lúc đó sẽ cần add tagsmolecule-idempotence-notestđể skip testverifylà chạy những thứ kiểm tra xem role có chạy đúng không, tệpmolecule/default/tests/test_default.py- Và xong xuôi, cuối cùng thì
cleanup/destroy-> xóa VM instance
Chạy thử (xóa bớt cho đỡ dài dòng) (cứ clone về, setup đủ là chạy được)
> cd roles/common
> mol test
INFO Running default > lint
COMMAND: set -e
yamllint .
ansible-lint --exclude molecule
flake8
PLAY [Destroy] ******************************************************************************************************************************************************************************************************************************************
TASK [Destroy molecule instance(s)] *********************************************************************************************************************************************************************************************************************
Saturday 12 February 2022 16:58:18 +0700 (0:00:00.015) 0:00:00.015 *****
ok: [localhost]
TASK [Populate instance config] *************************************************************************************************************************************************************************************************************************
Saturday 12 February 2022 16:58:20 +0700 (0:00:02.370) 0:00:02.386 *****
ok: [localhost]
TASK [Dump instance config] *****************************************************************************************************************************************************************************************************************************
Saturday 12 February 2022 16:58:20 +0700 (0:00:00.040) 0:00:02.426 *****
skipping: [localhost]
PLAY RECAP **********************************************************************************************************************************************************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
Saturday 12 February 2022 16:58:20 +0700 (0:00:00.041) 0:00:02.467 *****
===============================================================================
Destroy molecule instance(s) --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 2.37s
Dump instance config ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.04s
Populate instance config ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.04s
Playbook run took 0 days, 0 hours, 0 minutes, 2 seconds
INFO Running default > syntax
playbook: /Users/quanggg/code/me/gh-infra/roles/common/molecule/default/converge.yml
INFO Running default > create
PLAY [Create] *******************************************************************************************************************************************************************************************************************************************
TASK [Create molecule instance(s)] **********************************************************************************************************************************************************************************************************************
Saturday 12 February 2022 16:58:21 +0700 (0:00:00.015) 0:00:00.015 *****
Saturday 12 February 2022 16:58:57 +0700 (0:00:00.493) 0:00:36.103 *****
===============================================================================
Create molecule instance(s) --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 35.47s
Dump instance config ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.49s
Populate instance config dict -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.06s
Convert instance config dict to a list ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.06s
Playbook run took 0 days, 0 hours, 0 minutes, 36 seconds
INFO Running default > prepare
Cuối cùng được kết quả như sau

Có một số trick khác hay xài khi development và testing như sau:
mol test --destroy=neversẽ không destroy sau khi test chạy xong (molmặc định sẽ destroy kể cả khi chạy test hoặc chạy playbook fail), sẽ tiện trong development vì quá tìnhcreateVM cũng mất 1-2 phútmol login, có thể login vào VM tương tự nhưvagrant login, hỗ trợ debug trong instancemol --debug testsẽ in ra nhiều thông tin hơn khi chạymoldễ debug hơn- Có thể chạy riêng từng bước như
mol verify,mol createcũng được, xemmol matrix <verify/create/test>để biết là sẽ chạy những bước gì trong đó - 2 biến môi trường
FORCE_COLORvàPY_COLORSởdirenv/.envrcgiúp show color khi chạy test
Dài vậy, nhưng chưa rõ thì vào mã nguồn xem thêm nhé https://github.com/xluffy/gh-infra