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-client
trên Ubuntu, nhưng lại làopenssh-clients
trên CentOS, nếu sai tên package thì sẽnot found
, hoặc như path default config của nginx thay vì/etc/nginx
lại gõ nhầm thành/etc/ngnx
-> khi render config thì sẽpath not found
hoặc như Redis version 6 họ đã bỏ từ khóaslave
và 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.txt
thông quapip3
nhưng nó thủ công quá, một phần muốn có cái kiểu.lock
để lock version. Xàipoetry
nhiề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 khicd
và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_pass
yamllint
vàansible-lint
để lintyaml
file và ansible module có theo rule không, hỗ trợ develop thống nhất, chuẩn.flake8
,black
,autopep8
hỗ 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àosecrets
chứ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.vault
cũ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/default
gọi là kịch bản (scenario)molecule.yml
mô tả về môi trường, cách testplatform
là vagrant, máy ảo virtualbox phía trên, Ubuntu 20.04ubuntu/focal64
, memory máy ảo là 1280MB, thuộc groupcommon
lint
thì xàiyamllint
,ansible-lint
testyml
file (tasks, handler, variables, templates)provisioner
là ansible, chính là cáiconverge.yml
verifier
là./tests/test_default.py
converge.yml
tương tự playbookprepare.yml
là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,yml
có đú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/command
nè 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)syntax
kiểm tra syntaxcreate
tạo VM thông quavagrant
và VirtualBox, giốngvagrant up
thôi, xài cáiVBoxManage list runningvms
sẽ 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.yml
phía trênconverge
chạy playbook của role nàyidempotence
mộ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/command
không hỗ trợidempotence
nê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 testverify
là 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=never
sẽ không destroy sau khi test chạy xong (mol
mặc định sẽ destroy kể cả khi chạy test hoặc chạy playbook fail), sẽ tiện trong development vì quá tìnhcreate
VM 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 test
sẽ in ra nhiều thông tin hơn khi chạymol
dễ debug hơn- Có thể chạy riêng từng bước như
mol verify
,mol create
cũ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_COLOR
vàPY_COLORS
ởdirenv/.envrc
giú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