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óa slave và thay bằng replica, 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ái requirement.txt thông qua pip3 nhưng nó thủ công quá, một phần muốn có cái kiểu .lock để lock version. Xài poetry 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ác
  • direnv để tự động load một số ENV variable khi cd 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
  • yamllintansible-lint để lint yaml 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 role
  • docs để 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ào
  • secrets 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ùng ansible-vault để encrypt giống id_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 test
    • platform là vagrant, máy ảo virtualbox phía trên, Ubuntu 20.04 ubuntu/focal64, memory máy ảo là 1280MB, thuộc group common
    • lint thì xài yamllint, ansible-lint test yml file (tasks, handler, variables, templates)
    • provisioner là ansible, chính là cái converge.yml
    • verifier./tests/test_default.py
  • converge.yml tương tự playbook
  • prepare.yml làm một số thứ trước khi chạy playbook
  • molecule/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 module shell/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 syntax
  • create tạo VM thông qua vagrant và VirtualBox, giống vagrant up thôi, xài cái VBoxManage list runningvms sẽ thấy VM được tạo ra hoặc mở VirtualBox GUI sẽ thấy có máy ảo mới
  • prepare, chạy cái prepare.yml phía trên
  • converge chạy playbook của role này
  • idempotence 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ính idempotence, ví dụ mấy module shell/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 tags molecule-idempotence-notest để skip test
  • verify là chạy những thứ kiểm tra xem role có chạy đúng không, tệp molecule/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

gh-infra-molecule-test-verify.png

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ình create VM cũng mất 1-2 phút
  • mol login, có thể login vào VM tương tự như vagrant login, hỗ trợ debug trong instance
  • mol --debug test sẽ in ra nhiều thông tin hơn khi chạy mol dễ debug hơn
  • Có thể chạy riêng từng bước như mol verify, mol create cũng được, xem mol matrix <verify/create/test> để biết là sẽ chạy những bước gì trong đó
  • 2 biến môi trường FORCE_COLORPY_COLORSdirenv/.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