Lab 10 — Ansible Application Deployment
Mục tiêu: Sau Lab này, người học có thể sử dụng Ansible để deploy một application hoàn chỉnh lên server đã được provision, quản lý version, configuration, Docker Compose, health check và rollback cơ bản.
Đây là bước chuyển quan trọng từ “Ansible dùng để dựng server” sang “Ansible dùng để vận hành application trên server”.
1. Bối cảnh
Ở Lab 9 — Ansible Server Provisioning, chúng ta đã biến một Ubuntu Server sạch thành:
Fresh Ubuntu Server
│
▼
Ansible
│
├── Packages
├── deploy user
├── SSH
├── Docker
├── Docker Compose
└── /opt/todo
│
▼
Application-ready Server
Nhưng server mới chỉ sẵn sàng để chạy application.
Bây giờ chúng ta cần:
Application
│
▼
Ansible
│
▼
Production Server
│
├── Pull image
├── Generate config
├── Start containers
├── Health check
└── Verify deployment
Đó chính là Application Deployment.
2. Provisioning và Deployment
Đây là hai khái niệm cần phân biệt rõ.
Provisioning
Chuẩn bị server:
Ubuntu
├── User
├── SSH
├── Docker
├── Packages
└── Directories
Deployment
Đưa application lên server:
Application
├── Docker image
├── Configuration
├── docker-compose.yml
├── Container
└── Health check
Có thể hình dung:
Ansible
│
┌─────────┴─────────┐
▼ ▼
Provisioning Deployment
│ │
▼ ▼
Server Ready App Running
3. Scenario của Lab
Chúng ta sẽ sử dụng một application đơn giản:
Todo Application
Kiến trúc:
User
│
▼
Nginx
│
▼
Todo API
│
▼
PostgreSQL
Trong Lab này, để tập trung vào Ansible, application sẽ chạy bằng Docker Compose:
Docker Compose
│
├── todo-api
└── postgres
Sau khi deploy:
Ubuntu Server
│
├── Docker
│
└── /opt/todo
│
├── compose.yml
├── .env
├── config/
├── data/
├── logs/
└── backups/
4. Mục tiêu
Sau Lab này, người học phải hiểu được:
- Deploy application bằng Ansible.
- Quản lý Docker image.
- Quản lý Docker Compose.
- Generate
.env. - Deploy
compose.yml. - Start/stop/restart application.
- Detect configuration changes.
- Sử dụng Handlers.
- Health check application.
- Verify deployment.
- Deploy một version cụ thể.
- Rollback về version trước.
- Hiểu Deployment Strategy.
- Tách application configuration khỏi Ansible Role.
- Không hard-code secret.
- Thiết kế deployment có khả năng repeat.
- Hiểu tư duy deployment trong Production.
5. Kiến trúc Project
Tiếp tục project từ Lab 9:
ansible-lab-10/
│
├── ansible.cfg
├── site.yml
│
├── inventory/
│ └── hosts.ini
│
├── group_vars/
│ └── all.yml
│
└── roles/
├── common/
├── users/
├── ssh/
├── docker/
├── application_server/
│
└── application/
├── defaults/
├── handlers/
├── tasks/
├── templates/
└── vars/
Role mới:
roles/application/
Role này chịu trách nhiệm:
Application Deployment
6. Application Deployment Flow
Deployment của chúng ta sẽ đi theo flow:
Git
│
▼
Application Image
│
▼
Ansible
│
┌──────────┴──────────┐
▼ ▼
Configuration Compose File
│ │
└──────────┬──────────┘
▼
Docker Compose
│
▼
Start Services
│
▼
Health Check
│
▼
Deployment OK
7. Chuẩn bị Docker Image
Giả sử Todo API đã được build thành Docker image:
ghcr.io/example/todo-api:1.0.0
Version tiếp theo:
ghcr.io/example/todo-api:1.1.0
Ansible không nên build application trong quá trình deployment Production.
Thay vào đó:
Developer
│
▼
Git
│
▼
CI
│
├── Test
├── Build
└── Push image
│
▼
Container Registry
│
▼
Ansible
│
▼
Production
Đây là một tư duy quan trọng.
8. Application Variables
File:
group_vars/all.yml
Thêm:
application_name: todo
application_dir: /opt/todo
application_image: ghcr.io/example/todo-api
application_version: "1.0.0"
application_port: 8080
postgres_image: postgres:16
postgres_port: 5432
app_environment: production
Kết quả:
application_image
+
application_version
↓
ghcr.io/example/todo-api:1.0.0
9. Tại sao không hard-code version?
Không nên:
image: ghcr.io/example/todo-api:latest
Trong Production.
Vấn đề:
latest
│
├── hôm nay → 1.0.0
│
└── ngày mai → 1.1.0
Bạn không biết server đang chạy version nào chỉ bằng việc nhìn configuration.
Tốt hơn:
application_version: "1.0.0"
Deployment trở thành:
Version 1.0.0
có thể audit và rollback.
10. Tạo Role Application
ansible-galaxy role init roles/application
Cấu trúc:
roles/application/
├── defaults/
│ └── main.yml
├── handlers/
│ └── main.yml
├── tasks/
│ └── main.yml
└── templates/
11. Application Defaults
File:
roles/application/defaults/main.yml
---
application_name: todo
application_version: "1.0.0"
application_port: 8080
application_dir: /opt/todo
postgres_image: postgres:16
postgres_port: 5432
Điểm cần nhớ:
defaults
↓
có priority thấp
Environment có thể override.
12. Tạo Docker Compose Template
File:
roles/application/templates/compose.yml.j2
services:
todo-api:
image: "{{ application_image }}:{{ application_version }}"
container_name: todo-api
restart: unless-stopped
ports:
- "{{ application_port }}:8080"
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
postgres:
image: "{{ postgres_image }}"
container_name: todo-postgres
restart: unless-stopped
environment:
POSTGRES_DB: "{{ postgres_db }}"
POSTGRES_USER: "{{ postgres_user }}"
POSTGRES_PASSWORD: "{{ postgres_password }}"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test:
- CMD-SHELL
- pg_isready -U "{{ postgres_user }}"
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
13. Generate .env
Template:
roles/application/templates/.env.j2
APP_ENV={{ app_environment }}
APP_PORT={{ application_port }}
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_NAME={{ postgres_db }}
DATABASE_USER={{ postgres_user }}
Sau này secret sẽ được lấy từ:
Ansible Vault
chứ không commit trực tiếp vào Git.
14. Deploy Compose File
Task:
- name: Deploy Docker Compose configuration
ansible.builtin.template:
src: compose.yml.j2
dest: "{{ application_dir }}/compose.yml"
owner: "{{ deploy_user }}"
group: "{{ deploy_user }}"
mode: "0644"
notify: Restart application
15. Deploy Environment
- name: Deploy application environment
ansible.builtin.template:
src: .env.j2
dest: "{{ application_dir }}/.env"
owner: "{{ deploy_user }}"
group: "{{ deploy_user }}"
mode: "0600"
notify: Restart application
Ở đây có một điểm rất quan trọng:
compose.yml
↓
configuration
và:
.env
↓
runtime configuration
được quản lý riêng.
16. Pull Docker Image
Task:
- name: Pull application image
community.docker.docker_image:
name: "{{ application_image }}"
tag: "{{ application_version }}"
source: pull
Cần collection:
ansible-galaxy collection install community.docker
Điều này giúp server có image trước khi start application.
17. Tại sao không dùng docker pull?
Không nên:
- name: Pull image
ansible.builtin.command:
cmd: docker pull ...
Nếu có module phù hợp:
community.docker.docker_image
thì nên ưu tiên module.
Lợi ích:
Module
├── Idempotency
├── Structured arguments
├── Better error handling
└── Ansible semantics
18. Start Application
Có thể sử dụng:
- name: Start application
community.docker.docker_compose_v2:
project_src: "{{ application_dir }}"
state: present
Module này sẽ tương tác với:
docker compose
nhưng theo cách phù hợp với Ansible.
19. Full Application Tasks
File:
roles/application/tasks/main.yml
có thể bắt đầu như sau:
---
- name: Deploy Docker Compose configuration
ansible.builtin.template:
src: compose.yml.j2
dest: "{{ application_dir }}/compose.yml"
owner: "{{ deploy_user }}"
group: "{{ deploy_user }}"
mode: "0644"
notify: Restart application
- name: Deploy application environment
ansible.builtin.template:
src: .env.j2
dest: "{{ application_dir }}/.env"
owner: "{{ deploy_user }}"
group: "{{ deploy_user }}"
mode: "0600"
notify: Restart application
- name: Pull application image
community.docker.docker_image:
name: "{{ application_image }}"
tag: "{{ application_version }}"
source: pull
- name: Start application
community.docker.docker_compose_v2:
project_src: "{{ application_dir }}"
state: present
20. Handler
File:
roles/application/handlers/main.yml
---
- name: Restart application
community.docker.docker_compose_v2:
project_src: "{{ application_dir }}"
state: present
recreate: always
Flow:
compose.yml changed
│
▼
notify
│
▼
Restart application
21. Nhưng có một vấn đề
Nếu chỉ thay đổi:
README
thì application không cần restart.
Nếu thay đổi:
compose.yml
thì cần restart/recreate.
Nếu thay đổi:
.env
thì cần restart/recreate.
Đây chính là lý do chúng ta học Handlers ở Lab 7.
22. Deployment Playbook
Update:
site.yml
---
- name: Deploy Todo Application
hosts: application_servers
become: true
roles:
- common
- users
- docker
- application_server
- application
23. Chạy Deployment
ansible-playbook site.yml
Kỳ vọng:
TASK [application : Deploy Docker Compose configuration]
changed
TASK [application : Deploy application environment]
changed
TASK [application : Pull application image]
changed
TASK [application : Start application]
changed
24. Kiểm tra Container
SSH vào server:
ssh deploy@192.168.56.101
Chạy:
docker ps
Kỳ vọng:
todo-api
todo-postgres
25. Kiểm tra Docker Compose
cd /opt/todo
Sau đó:
docker compose ps
Ví dụ:
NAME STATUS
todo-api Up
todo-postgres Up (healthy)
26. Kiểm tra Logs
API:
docker logs todo-api
PostgreSQL:
docker logs todo-postgres
Hoặc:
docker compose logs todo-api
27. Health Check
Một deployment không nên chỉ kiểm tra:
Container = Running
Vì:
Running ≠ Healthy
Ví dụ:
Container
│
├── Process running
│
└── API broken
Do đó cần health check.
28. Application Health Endpoint
Giả sử API có:
GET /health
và trả:
{
"status": "ok"
}
Ansible có thể kiểm tra:
- name: Check application health
ansible.builtin.uri:
url: "http://127.0.0.1:{{ application_port }}/health"
method: GET
status_code: 200
register: health_check
retries: 10
delay: 5
until: health_check.status == 200
Flow:
Deploy
↓
Start
↓
Wait
↓
GET /health
↓
200?
├── Yes → Success
└── No → Retry
29. Tại sao cần retries?
Application có thể cần:
Docker start
↓
Application boot
↓
Load configuration
↓
Connect DB
↓
Ready
Ví dụ mất:
5–15 seconds
Nếu check ngay:
GET /health
↓
Connection refused
không có nghĩa deployment failed.
Do đó:
retries: 10
delay: 5
30. Deployment Verification
Sau deployment, chúng ta cần verify:
✓ Container running
✓ Database healthy
✓ API responding
✓ Correct version
Ví dụ:
docker compose ps
và:
curl http://localhost:8080/health
31. Verify Version
Application nên expose version:
GET /version
Ví dụ:
{
"version": "1.0.0"
}
Ansible:
- name: Check application version
ansible.builtin.uri:
url: "http://127.0.0.1:{{ application_port }}/version"
return_content: true
register: version_response
Sau đó có thể debug:
- name: Show application version
ansible.builtin.debug:
var: version_response.content
32. Deployment Version 1.0.0
Ban đầu:
application_version: "1.0.0"
Chạy:
ansible-playbook site.yml
Server:
todo-api:1.0.0
33. Deployment Version 1.1.0
Update:
application_version: "1.1.0"
Chạy:
ansible-playbook site.yml
Flow:
1.0.0
│
│ Ansible
▼
1.1.0
Không cần SSH vào server:
docker pull ...
docker stop ...
docker run ...
Ansible xử lý toàn bộ.
34. Idempotency
Sau khi deploy:
ansible-playbook site.yml
lần nữa.
Nếu không có thay đổi:
changed = 0
Đây là deployment tốt.
Không nên:
Every run
↓
Pull
↓
Restart
↓
Recreate
nếu application version và configuration không thay đổi.
35. Một lỗi phổ biến
Không nên viết:
- name: Restart application
ansible.builtin.command:
cmd: docker compose down
- name: Start application
ansible.builtin.command:
cmd: docker compose up -d
Mỗi lần chạy:
down
↓
up
application sẽ restart liên tục.
Đây là anti-pattern cho Idempotency.
36. Desired State
Tư duy đúng:
Ansible
↓
Desired State
↓
Application should be:
├── version 1.1.0
├── running
├── healthy
└── configured
Ansible sẽ đưa server về trạng thái đó.
Không phải:
Ansible
↓
Run commands
↓
Hope everything works
37. Application Update
Giả sử:
Current:
1.0.0
Muốn:
Target:
1.1.0
Chỉ cần:
application_version: "1.1.0"
rồi:
ansible-playbook site.yml
Đây là một trong những lợi ích lớn nhất của việc quản lý application version bằng Ansible.
38. Rollback
Giả sử:
1.1.0
có bug.
Quay về:
1.0.0
chỉ cần:
application_version: "1.0.0"
Sau đó:
ansible-playbook site.yml
Flow:
1.0.0
↓
1.1.0
↓
BUG
↓
1.0.0
39. Nhưng Rollback thực tế phức tạp hơn
Đây là điểm người học cần hiểu ở level cao hơn.
Application version không chỉ có:
Docker image
Mà còn:
Application
Database schema
Configuration
Dependencies
External APIs
Ví dụ:
v1.1.0
↓
Database migration
↓
users.email_new
Nếu rollback application:
v1.0.0
thì schema database có thể không tương thích.
Do đó:
Application rollback ≠ Database rollback
Đây là một bài học Production rất quan trọng.
40. Configuration Deployment
Giả sử thay đổi:
app_environment: production
hoặc:
application_port: 8080
Ansible:
Variable
↓
Jinja2
↓
.env / compose.yml
↓
changed
↓
handler
↓
application recreate
41. Configuration Drift
Giả sử ai đó SSH vào server và sửa:
/opt/todo/compose.yml
Ansible biết configuration mong muốn là:
Git
↓
compose.yml
Server hiện tại:
Modified manually
Khi chạy:
ansible-playbook site.yml
Ansible sẽ đưa server về:
Desired State
Đây là:
Configuration Drift Remediation
42. --diff
Khi thay đổi configuration:
ansible-playbook site.yml --diff
giúp nhìn thấy:
Before
↓
After
Rất hữu ích khi debug deployment.
43. Check Mode trước Deployment
ansible-playbook site.yml --check --diff
Workflow tốt:
Change
↓
Check
↓
Review
↓
Deploy
Thay vì:
Change
↓
Deploy production ngay
44. Tags cho Application
Có thể thêm:
tags:
- application
Ví dụ:
ansible-playbook site.yml --tags application
Hoặc:
ansible-playbook site.yml --tags docker
45. Deploy chỉ một server
Giả sử:
app-01
app-02
app-03
Deploy thử:
ansible-playbook site.yml --limit app-01
Kiểm tra:
app-01
Sau đó mới:
ansible-playbook site.yml --limit app-02
và:
ansible-playbook site.yml --limit app-03
Đây là cách mô phỏng:
Canary Deployment
46. Serial Deployment
Trong Playbook:
---
- name: Deploy application
hosts: application_servers
become: true
serial: 1
roles:
- application
Nếu có 3 server:
app-01
↓
health check
↓
app-02
↓
health check
↓
app-03
An toàn hơn deploy đồng thời.
47. serial: 25%
Với:
100 servers
có thể:
serial: 25%
Deployment:
25
↓
25
↓
25
↓
25
Đây là bước đầu tiên để người học hiểu:
Rolling Deployment.
48. Failure Handling
Giả sử:
app-01 → OK
app-02 → OK
app-03 → FAILED
Không nên tiếp tục vô điều kiện.
Có thể dùng:
serial: 1
và health check.
Flow:
app-01
↓
Healthy
↓
app-02
↓
Healthy
↓
app-03
↓
FAILED
↓
STOP
Đây là deployment an toàn hơn.
49. Application Deployment Architecture
Đến đây hệ thống:
Git
│
▼
Application Code
│
▼
CI
│
Build Docker Image
│
▼
Container Registry
│
▼
Ansible
│
┌─────────┴─────────┐
▼ ▼
Config Version
│ │
└─────────┬─────────┘
▼
Docker Compose
│
▼
Application
│
▼
Health Check
Đây đã bắt đầu giống một hệ thống CI/CD thực tế.
50. Tư duy Senior #1 — Ansible không phải CI
Một hiểu nhầm phổ biến:
“Ansible có thể build application nên Ansible là CI/CD.”
Không hoàn toàn.
Có thể phân chia:
CI
│
├── Test
├── Lint
├── Build
├── Security Scan
└── Push Image
và:
CD
│
├── Deploy
├── Configure
├── Health Check
├── Rollout
└── Rollback
Ansible rất phù hợp với phần:
Deployment / Configuration / Operations
51. Tư duy Senior #2 — Immutable Artifact
CI tạo:
todo-api:1.2.3
Sau đó artifact này được dùng cho:
Dev
Staging
Production
Không nên:
Dev
↓
build source
↓
Production
mà:
Source
↓
Build once
↓
Artifact
↓
Deploy everywhere
Đây là:
Build once, deploy many.
52. Tư duy Senior #3 — Không dùng latest
Không nên:
todo-api:latest
Tốt hơn:
todo-api:1.0.0
todo-api:1.1.0
todo-api:1.2.0
Hoặc tốt hơn nữa:
todo-api@sha256:...
để đảm bảo artifact immutable.
53. Tư duy Senior #4 — Deployment phải Observable
Không chỉ:
ansible SUCCESS
mà phải kiểm tra:
Deployment
↓
Container
↓
Application
↓
Health
↓
Metrics
↓
Logs
Ví dụ:
Ansible says SUCCESS
≠
Application is healthy
Do đó cần:
Health Check
54. Tư duy Senior #5 — Application và Infrastructure tách biệt
Không nên biến Role:
roles/application/
thành:
1000 lines
và chứa tất cả:
Docker
Nginx
Database
Application
Monitoring
Backup
Firewall
Tách:
roles/
├── docker/
├── nginx/
├── application/
├── monitoring/
└── backup/
Mỗi Role có một responsibility rõ ràng.
55. Tư duy Senior #6 — Secret không nằm trong Git
Không commit:
postgres_password: "my-secret"
vào:
group_vars/all.yml
Thay vào đó:
Ansible Vault
↓
Encrypted Variables
↓
Deployment
Phần này sẽ được xử lý sâu trong:
Lab 11 — Ansible Secrets & Vault
56. Tư duy Senior #7 — Deployment phải Repeatable
Bạn phải có thể chạy:
ansible-playbook site.yml
nhiều lần.
Không nên cần nhớ:
Lần trước tôi SSH vào server
↓
chạy command A
↓
sửa file B
↓
restart C
↓
chạy command D
Tất cả phải nằm trong:
Code
57. Exercise 1 — Deploy Todo API
Yêu cầu:
Image:
ghcr.io/example/todo-api:1.0.0
Deploy bằng:
ansible-playbook site.yml
Verify:
docker ps
và:
curl http://localhost:8080/health
58. Exercise 2 — Change Configuration
Thay:
app_environment: production
thành:
app_environment: staging
Chạy:
ansible-playbook site.yml
Yêu cầu:
.env changed
↓
Application recreated
59. Exercise 3 — Upgrade Application
Version:
1.0.0
upgrade:
1.1.0
Chạy:
ansible-playbook site.yml
Verify:
curl http://localhost:8080/version
Kết quả:
{
"version": "1.1.0"
}
60. Exercise 4 — Rollback
Rollback:
1.1.0 → 1.0.0
Chỉ thay:
application_version: "1.0.0"
Sau đó:
ansible-playbook site.yml
Verify:
curl http://localhost:8080/version
61. Exercise 5 — Break the Application
Cố tình deploy:
application_version: "99.99.99"
Version không tồn tại.
Chạy:
ansible-playbook site.yml
Quan sát:
Pull image
↓
FAILED
Sau đó sửa:
application_version: "1.0.0"
và deploy lại.
Mục tiêu:
Học cách xử lý deployment failure thay vì chỉ học happy path.
62. Exercise 6 — Health Check Failure
Cố tình cấu hình sai:
application_port: 9999
Application thực tế chạy:
8080
Deployment có thể hoàn thành một phần nhưng:
Health check
↓
FAILED
Người học phải tìm ra:
Port mismatch
và sửa lại.
63. Exercise 7 — Configuration Drift
SSH vào server:
ssh deploy@server
Sửa:
/opt/todo/compose.yml
thủ công.
Sau đó:
ansible-playbook site.yml --diff
Quan sát:
Server configuration
↓
Drift detected
↓
Ansible
↓
Desired state restored
64. Exercise 8 — Multi-server Deployment
Inventory:
[application_servers]
app-01
app-02
app-03
Dùng:
serial: 1
Deploy:
ansible-playbook site.yml
Quan sát thứ tự:
app-01
↓
health check
↓
app-02
↓
health check
↓
app-03
65. Exercise 9 — Canary Deployment
Không deploy toàn bộ server.
Chỉ:
ansible-playbook site.yml --limit app-01
Verify:
app-01
↓
Version 1.1.0
↓
Healthy
Sau đó:
ansible-playbook site.yml --limit app-02,app-03
Đây là mô hình đơn giản của:
Canary Release.
66. Exercise 10 — Production Deployment Challenge
Bạn nhận requirement:
Deploy Todo API v2.0.0
Server:
app-01
app-02
app-03
Yêu cầu:
1. Pull image 2.0.0
2. Deploy configuration
3. Start application
4. Health check
5. Deploy từng server
6. Nếu health check fail → stop rollout
7. Không restart server không cần thiết
8. Có thể rollback về 1.1.0
Người học phải tự thiết kế Playbook.
67. Checklist hoàn thành Lab 10
Người học cần có khả năng:
- [ ] Phân biệt Provisioning và Deployment.
- [ ] Tạo Application Role.
- [ ] Deploy Docker image.
- [ ] Quản lý application version.
- [ ] Không sử dụng
latest. - [ ] Generate Docker Compose bằng Jinja2.
- [ ] Generate
.env. - [ ] Start application bằng Docker Compose.
- [ ] Sử dụng Docker Ansible modules.
- [ ] Sử dụng Handlers.
- [ ] Hiểu Idempotency trong Deployment.
- [ ] Health check application.
- [ ] Verify application version.
- [ ] Detect configuration drift.
- [ ] Upgrade application.
- [ ] Rollback application.
- [ ] Sử dụng
--check. - [ ] Sử dụng
--diff. - [ ] Sử dụng
--limit. - [ ] Hiểu
serial. - [ ] Hiểu Canary Deployment.
- [ ] Hiểu Rolling Deployment.
- [ ] Hiểu Build Once, Deploy Many.
- [ ] Không commit secret vào Git.
- [ ] Biết tại sao không nên dùng
latest. - [ ] Biết tại sao
Container Runningchưa đồng nghĩa vớiApplication Healthy.
68. Kết quả sau Lab 10
Sau Lab 9:
Fresh Server
↓
Ansible
↓
Application-ready Server
Sau Lab 10:
Application
↓
Docker Image
↓
Ansible
↓
Server
↓
Docker Compose
↓
Application Running
↓
Health Check
↓
Production Deployment
Và roadmap lúc này đã trở thành:
Lab 1 → Fundamentals
Lab 2 → Inventory & SSH
Lab 3 → Ad-Hoc Commands
Lab 4 → Playbook Fundamentals
Lab 5 → Variables & Facts
Lab 6 → Templates & Jinja2
Lab 7 → Handlers & Idempotency
Lab 8 → Roles
Lab 9 → Server Provisioning
Lab 10 → Application Deployment
Lab 11 → Secrets & Vault
Lab 12 → Troubleshooting
Lab 13 → Multi-Environment
Lab 14 → Final Project
Điểm quan trọng nhất của Lab 10: người học không chỉ biết viết một Playbook để “chạy application”, mà phải bắt đầu suy nghĩ theo tư duy Production Deployment: versioned artifact → desired state → configuration → rollout → health check → failure handling → rollback. Đây là nền tảng rất quan trọng để bước sang Lab 11 — Ansible Secrets & Vault và sau đó tiến tới CI/CD.
All Rights Reserved