1. 1. Docker: để không surprise khi run ứng dụng
    1. 1.1. Vậy vấn đề gốc, làm sao để đóng gói?
    2. 1.2. Vài cái file / concept quan trọng
  2. 2. CI/CD: khỏi phải nhớ 15 bước mỗi lần release
    1. 2.1. CI và CD
    2. 2.2. CI/CD provider thực sự làm gì?
    3. 2.3. Bên trong CI/CD provider
    4. 2.4. Queue và worker
    5. 2.5. Security
    6. 2.6. Cuối cùng vẫn là giảm surprise
  3. 3. Playwright: robot user + observer
    1. 3.1. Trước hết, browser là gì?
    2. 3.2. Playwright không phải browser
    3. 3.3. click() thực ra là gì?
    4. 3.4. Vậy Playwright dùng để test kiểu gì?
    5. 3.5. Còn data thì sao?
    6. 3.6. Test login và test sau login là hai chuyện khác nhau
    7. 3.7. Playwright còn gì cần care?
      1. 3.7.1. Locator
      2. 3.7.2. Auto-wait
      3. 3.7.3. Assertions
      4. 3.7.4. Isolation
      5. 3.7.5. Network mocking
      6. 3.7.6. Trace / screenshot / video
    8. 3.8. Một điểm rất quan trọng: test environment và parallelism
      1. 3.8.1. Còn 1,000 test cases thì sao?
      2. 3.8.2. Worker thực ra cũng khá đắt
    9. 3.9. Bot detection thì sao?
    10. 3.10. Để khỏi mỗi release lại click tay 100 flows
  4. 4. AWS CDK: describe infrastructure bằng code
    1. 4.1. Bản chất của CDK
    2. 4.2. Vậy CDK cần gì để deploy?
    3. 4.3. IAM có hai tầng đáng care
    4. 4.4. CDK khác Terraform/Pulumi ở đâu?
    5. 4.5. Thứ quan trọng nhất: diff
    6. 4.6. Rollback
    7. 4.7. Drift
    8. 4.8. Dependency
    9. 4.9. Construct
    10. 4.10. Pain points thật sự
    11. 4.11. Để khỏi nhớ lần trước đã click cái gì
  5. 5. ECS và Fargate
    1. 5.1. ECS underlying là gì?
    2. 5.2. Từ image tới task và service
    3. 5.3. Desired state
    4. 5.4. Scaling
      1. 5.4.1. Task scaling
      2. 5.4.2. Compute scaling
    5. 5.5. ECS monitor kiểu gì?
      1. 5.5.1. Container còn sống không?
      2. 5.5.2. Workload có cần scale không?
    6. 5.6. Vậy Fargate là cái gì?
    7. 5.7. Underlying của Fargate
    8. 5.8. Vậy tại sao cần Fargate?
    9. 5.9. Debug thì sao?
    10. 5.10. Category cho sạch
    11. 5.11. Bóc hết buzzword
  6. 6. Observability: muốn biết thì phải có dữ liệu
    1. 6.1. Logs
    2. 6.2. Metrics
    3. 6.3. Application metrics
    4. 6.4. Traces
    5. 6.5. Alerts
    6. 6.6. Nhìn từ Linux
    7. 6.7. Gom lại cho dễ hình dung
  7. 7. Network và deployment: data đi đúng chỗ, đổi version đúng lúc
    1. 7.1. Từ signal tới request
    2. 7.2. IP, router và port
    3. 7.3. VPC và subnet
      1. 7.3.1. Public/private nằm ở route, không phải cái tên
      2. 7.3.2. Security group: ai được nói chuyện với ai?
    4. 7.4. Load balancer: request này đưa cho backend nào?
    5. 7.5. Zero-downtime deployment
      1. 7.5.1. ECS và ALB phối hợp thế nào?
      2. 7.5.2. Database: old và new phải sống chung được
    6. 7.6. Cuối cùng là mấy câu hỏi này
  8. 8. Cuối cùng, mỗi thằng giảm một kiểu surprise

Docker, CI/CD, Testing, IaC, Auto-scale... tất cả ra đời chỉ vì nỗi sợ surprise và uncertainty

· ~36 phút đọc

nhìn đống tên trong title thì có vẻ mỗi thằng giải quyết một chuyện chẳng liên quan. nhưng thử nhìn từ nỗi sợ “đang chạy ngon mà tự nhiên vỡ” thì thấy chúng có một điểm chung: mỗi công cụ giảm một loại surprise khác nhau, từ environment, quy trình release, hành vi application cho tới infrastructure.

không phải dùng đủ bộ là uncertainty biến mất :D chỉ là bớt thứ phải đoán, bớt bước phải nhớ, và dễ biết cái gì vừa thay đổi hơn.

Docker: để không surprise khi run ứng dụng

đi vào Docker, để không surprise khi run ứng dụng kiểu:

á đĩ mẹ nó run được ở máy của em

thì bản chất vẫn là make the environment consistent cho mọi môi trường (máy tôi, máy anh, máy của mấy đứa kia).

Vậy vấn đề gốc, làm sao để đóng gói?

muốn đóng gói thì… đóng gói thôi :D như chơi lego vậy, phải có từng component riêng lẻ, ghép lại thành hình thù.

máy tôi run Ruby 3.0 thì máy anh run 3.0, máy tôi dùng Postgres 14 thì máy anh cũng vậy.

biến số lớn còn lại là?… hệ điều hành và các system dependencies. mỗi thằng dùng một môi trường khác nhau thì có thể tạo ra vô số lỗi bất ngờ.

giải quyết → đơn giản bằng… ép environment của application về cùng một chuẩn luôn :D với Linux container, Docker đóng gói Linux userspace và các dependencies vào image, còn kernel vẫn dùng chung với host Linux.

trên Linux thì container dùng chung kernel của host nên rất nhẹ. còn trên macOS, để chạy Linux container, Docker phải chạy thêm một Linux VM ở bên dưới, nên cũng là một lý do mà trên macOS run Docker nó thường nặng và lag hơn, trong khi trên Ubuntu cấu hình yếu hơn vẫn có thể mượt hơn hẳn.

nhưng cùng image không có nghĩa là mọi môi trường giống hệt nhau. CPU architecture, kernel, config, secrets và dữ liệu vẫn có thể khác. Docker chuẩn hoá được một phần lớn environment, không xoá hết biến số.

Vài cái file / concept quan trọng

vậy là có đủ gia vị cần thiết:

  • Dockerfile → file mô tả cần dùng component nào cho con lego này: base image nào, cài package gì, copy source code nào, chạy command gì.

  • Docker Compose → nhiều lego kết hợp thì cần một file compose.yaml để define cách chúng phối hợp với nhau: app, postgres, redis, network, volume, dependency… thì chính là nó. như 5 anh em siêu nhân lúc gặp boss cuối thì lại kết hợp làm 1 thực thể, lúc này phải biết thằng nào là tay, thằng nào là chân, thằng nào cần thằng nào chạy trước :D

    Compose thường define services, và mỗi service có thể dùng một image có sẵn hoặc tự build image từ Dockerfile.

  • volume → vùng lưu trữ nằm ngoài lifecycle của container, data được giữ nguyên kể cả container bị xoá rồi tạo lại, miễn là không xoá luôn volume.

  • image → sản phẩm được build từ Dockerfile, là một package đã chứa environment và application cần thiết để chạy. nó nằm trên disk và bản thân nó không chạy.

  • container → chỉ là một instance của image, có thể đang chạy hoặc đã dừng. khi run thì ngoài disk, nó bắt đầu ăn thêm RAM và CPU. trên Linux nó không cần một virtual machine riêng cho từng container; nó dùng chung kernel của host và isolate process/network/filesystem…

đơn giản là vậy :D

CI/CD: khỏi phải nhớ 15 bước mỗi lần release

CI/CD nghe thì fancy, nhưng thực ra rất đơn giản: run một chuỗi công việc tự động để khỏi phải làm bằng tay, từ đó giảm surprise.

manual process luôn có nhiều biến số:

  • hôm nay tôi nhớ chạy test, ngày mai tôi quên
  • thằng A deploy theo cách này, thằng B deploy theo cách khác
  • có người nhớ build production mode, có người quên
  • có người chạy migration trước, có người deploy trước rồi mới nhớ migration :D

CI/CD đơn giản là: có event xảy ra → chạy một procedure đã được define sẵn. ví dụ:

1
2
3
4
5
6
7
push code
-> install dependencies
-> lint
-> test
-> build image
-> push image
-> deploy

thay vì mỗi lần release lại có một người ngồi nhớ 15 bước, ta encode 15 bước đó vào một file.

Bitbucket thì bitbucket-pipelines.yml. GitHub thì GitHub Actions workflow. CircleCI, GitLab CI… cũng vậy. syntax khác nhau, provider khác nhau, nhưng cách làm vẫn giống nhau:

event X xảy ra → chạy command A → B → C

CI và CD

CI: code được tích hợp thường xuyên → check ngay xem có gì vỡ không. ví dụ build, lint, unit test, integration test, Playwright… mục tiêu là detect surprise càng sớm càng tốt.

CD: sau khi mọi thứ pass → package/deploy bằng cùng một procedure. Continuous Delivery thì giữ bản build sẵn sàng để release, có thể vẫn chờ người duyệt; Continuous Deployment thì tự deploy khi đủ điều kiện. mục tiêu là tránh surprise ở release.

nên CI/CD về bản chất là reduce human variance bằng automation.

CI/CD provider thực sự làm gì?

repo không tự nhiên có CPU để chạy test :D nó chỉ gửi một cái order cho CI/CD provider:

“tao cần Linux, Node 22, Docker, rồi chạy mấy command này”

repo cũng chỉ order được thứ mà cửa hàng có. provider support Linux thì dùng Linux. có ARM runner thì mới order chạy native trên ARM được. provider không có cái gì thì YAML có viết thần chú cũng đéo summon ra được :D

nên có thể tưởng tượng CI/CD provider như một cửa hàng xây dựng:

  • file cấu hình pipeline là đơn đặt hàng + hướng dẫn thi công
  • runner là chỗ thực sự chạy mấy command đó

có provider cho một isolated slot sạch sẽ, có nơi dùng shared infrastructure, nhưng shared infrastructure vẫn có thể cấp environment riêng cho từng job. có thể dùng VM, container, Kubernetes pod, bare metal… tuỳ implementation của provider.

nếu muốn tự quản môi trường chạy thì dùng self-hosted runner. lúc đó CI/CD provider chủ yếu làm control plane, còn cái máy thực sự chạy code là của mình.

Bên trong CI/CD provider

CI/CD provider thường xử lý job bất đồng bộ. chứ thử tưởng tượng:

1
2
3
4
git push
-> giữ nguyên HTTP request
-> build 20 phút
-> trả response

vãi lồn :D

nên nó thường là một asynchronous job system, architecture đại khái:

1
2
3
4
5
6
7
8
git push
-> event/webhook
-> tạo job
-> queue
-> scheduler
-> runner/worker pick job
-> build/test/deploy
-> trả log + result

thế nên nhìn sâu xuống thì phần thực thi của CI/CD platform thực chất là distributed background job system chuyên chạy code từ repository. fancy UI, YAML, dashboard, artifact… là lớp product ở trên.

Queue và worker

giả sử cùng lúc có 100,000 repo push code. provider không thể có vô hạn machine đứng chờ sẵn. nó có queue, job vào queue và chờ runner available.

để điều phối và scale worker, provider có thể monitor những thứ rất cơ bản:

  • queue depth
  • average queue wait time
  • worker utilization
  • job arrival rate
  • average job duration

ví dụ:

1
2
3
4
5
average wait > 2 minutes
-> scale up workers

worker utilization thấp trong một thời gian
-> scale down workers

vì giữ 10,000 VM ngồi chơi cũng đốt tiền vãi cả lồn. nên bên dưới CI/CD lại quay về một bài toán rất cũ:

1
2
demand tăng -> add workers
demand giảm -> remove workers

tất nhiên phải có cooldown/hysteresis, không thì vừa scale up xong queue giảm lại scale down, rồi 30 giây sau scale up tiếp :D

Security

runner thực chất chỉ là một cái máy đang chạy code mà repository yêu cầu, nên chuyện security cũng bắt đầu từ mấy câu hỏi rất cơ bản:

  • code này chạy ở đâu?
  • chạy với quyền gì?
  • được access secret nào?
  • network access tới đâu?
  • sau khi chạy xong còn state gì lại không?

ephemeral isolated runner:

1
tạo environment sạch -> chạy job -> destroy

có thêm chi phí khởi tạo và dọn environment, nhưng giảm surprise giữa các job rất nhiều.

persistent runner thì có thể tiết kiệm chi phí khởi tạo và tận dụng cache tốt hơn, nhưng nếu isolation ngu thì job trước có thể để lại file, credential, process, cache độc… cho job sau. shared infrastructure không tự động có nghĩa là các job dùng chung state.

case ngu nhất:

persistent runner + untrusted PR + production credentials + quyền rộng

địt con đĩ mẹ, lúc đó pipeline không còn là automation nữa mà thành remote code execution service :D

Cuối cùng vẫn là giảm surprise

đừng để con người lặp lại manual process nếu máy có thể làm chính xác cùng một procedure mỗi lần.

ở phía user:

1
event -> pipeline -> commands

ở phía provider:

1
event -> queue -> scheduler -> workers -> autoscaling

CI giảm surprise khi code thay đổi, CD giảm surprise khi release. isolated runner giảm surprise giữa các lần build. queue giúp hấp thụ job dồn vào, autoscaling điều chỉnh số worker theo workload.

cuối cùng fancy word vẫn chỉ là make the process predictable.

Playwright: robot user + observer

pipeline tự chạy rồi, nhưng làm sao biết user vẫn login, search hay checkout được? với Playwright, ta viết script để browser làm lại hành vi của user, rồi check xem application có phản ứng đúng không.

Trước hết, browser là gì?

browser đơn giản là một chương trình:

1
2
3
4
5
6
có địa chỉ
-> connect tới web server
-> nhận data
-> parse HTML/CSS/JS
-> xử lý logic
-> render kết quả

headless browser cũng làm gần như y chang, chỉ khác là không mở visible window ra UI của OS cho người nhìn.

nó vẫn parse, chạy JS, tính layout, xử lý event, network request, cookie, storage… nên headless browser không phải browser giả. nó vẫn là browser, chỉ là không show cái cửa sổ lên màn hình.

Playwright không phải browser

Playwright là tool/library để control browser. mental model:

1
2
3
4
Playwright script
-> Playwright API
-> browser-specific backend/protocol
-> Chromium / Firefox / WebKit

ví dụ dev viết:

1
await page.locator("#login").click();

Playwright expose một abstraction chung là click(), nhưng bên dưới:

1
2
3
Chromium -> translate thành command mà Chromium hiểu
Firefox -> translate thành command mà Firefox hiểu
WebKit -> translate thành command mà WebKit hiểu

đa số use case dev không cần care đoạn dưới. giống như mình gọi một interface chung, còn implementation phía sau khác nhau.

click() thực ra là gì?

ví dụ:

1
await page.mouse.click(100, 200);

Playwright có thể biến intent đó thành:

1
2
3
move tới x=100, y=200
-> mouse down
-> mouse up

với Chromium thì backend gửi command theo protocol của Chromium, với Firefox thì gửi command theo protocol của Firefox. syntax/protocol/payload phía dưới có thể khác nhau, nhưng intent phía trên vẫn là click chỗ này. Playwright che sự khác nhau đó đi.

Vậy Playwright dùng để test kiểu gì?

các bước cơ bản:

1
2
3
prepare state
-> perform user action
-> verify result

ví dụ:

1
2
3
4
5
open login page
-> fill email
-> fill password
-> click Login
-> expect dashboard xuất hiện

nó chính xác là robot user + observer. robot làm hành động, observer check consequence.

nếu chỉ click mà không assert gì thì đó chỉ là browser automation, chưa chắc đã là useful test.

Còn data thì sao?

Playwright không tự nhiên biết:

  • user nào để login
  • password nào
  • database có data gì
  • account nào đang active
  • order nào cần test

nên test system vẫn phải prepare test data. ví dụ test login:

1
2
TEST_EMAIL
TEST_PASSWORD

credentials không nên hardcode trong repo. local có thể lấy từ env, CI có thể lấy từ secret variables, môi trường test gần production có thể lấy từ Secrets Manager / Vault…

lại quay về nguyên tắc cũ: secret cần tồn tại, nhưng không cần nằm trong source code.

Test login và test sau login là hai chuyện khác nhau

nếu đang test chính login flow:

1
2
3
4
open login
-> fill credentials
-> submit
-> verify login success

thì phải đi qua login thật. nhưng nếu có 100 tests phía sau login mà không cần kiểm tra lại login flow, cứ login lại 100 lần là ngu :D có thể:

1
2
3
login once
-> lưu auth state bằng storageState
-> các tests khác reuse

hoặc thậm chí gọi login API trực tiếp rồi đưa auth state phù hợp vào browser. vì mục tiêu của test page dashboard không phải là test lại login.

file auth state cũng có thể chứa cookie/token đăng nhập, nên không commit vào repo.

Playwright còn gì cần care?

Locator

cần tìm đúng element để interact. ví dụ:

1
2
3
button Login
input Email
link Settings

nên ưu tiên locator gần với cách user nhìn application hơn là selector mong manh, ví dụ getByRole() hoặc getByLabel().

Auto-wait

browser UI không phải lúc nào cũng ready ngay lập tức. button có thể chưa render, API có thể chưa trả, animation có thể chưa xong.

Playwright có logic wait để tránh kiểu:

1
sleep 3 seconds rồi cầu nguyện :D

locator action tự chờ các điều kiện cần để thao tác, nhưng không có nghĩa là tự biết mọi API hay business flow đã xong. phần đó vẫn cần chờ hoặc assert đúng trạng thái mong đợi.

Assertions

sau khi robot làm action thì cần verify:

1
2
3
4
5
element visible?
text đúng?
URL đúng?
button disabled?
dashboard xuất hiện?

Isolation

test A không nên làm bẩn test B. Playwright Test mặc định tạo browser context riêng cho mỗi test, nhưng data trên backend thì vẫn phải tự tách hoặc dọn.

điều cốt lõi là test phải predictable. nếu test fail vì thằng test trước để lại cookie hoặc data thì lại surprise :D

Network mocking

đôi lúc không muốn phụ thuộc backend thật. ví dụ muốn test:

1
2
3
4
API trả 500
network chậm
empty response
specific data

thì Playwright có thể intercept/mock network.

Trace / screenshot / video

local test fail thì còn nhìn được. CI test fail lúc 3 giờ sáng thì không có ai ngồi nhìn browser, nên Playwright có thể lưu:

1
2
3
4
trace
screenshot
video
network info

để debug lại sau.

Một điểm rất quan trọng: test environment và parallelism

Playwright là thằng điều khiển browser. nếu test dashboard thật sự cần login qua hệ thống thật thì browser phải gọi tới một backend nào đó.

có 3 cách chuẩn bị môi trường test:

  1. test against staging/dev environment có sẵn.
  2. trong CI dựng cả stack FE + BE + DB, chờ các service ready rồi mới run Playwright.
  3. mock API response.

cách 3 thường đơn giản và deterministic hơn ở phần response mình kiểm soát, nhưng chủ yếu chỉ cover FE/UI behavior. nó không cover được những surprise thật giữa:

1
FE <-> BE <-> DB

ví dụ payload mismatch, auth, CORS, schema DB, migration, serialization…

Còn 1,000 test cases thì sao?

nếu có 1,000 tests mà chạy từng cái một thì ngồi chờ tới mùa quýt. nhưng 1,000 tests không có nghĩa là:

1
2
1,000 tests
-> 1,000 browser tabs cùng lúc

RAM nổ mẹ luôn :D

Playwright Test dùng bounded parallelism. ví dụ:

1
2
1,000 tests
20 workers

thì tại một thời điểm tối đa khoảng 20 tests chạy song song, nếu suite có đủ việc có thể chạy độc lập. worker chạy xong test này thì lấy test tiếp theo. hình dung đơn giản:

1
2
3
4
5
6
7
8
9
worker 1
-> test 1
-> test 21
-> test 41

worker 2
-> test 2
-> test 22
-> test 42

đây chỉ là minh hoạ chia việc, không phải thứ tự cố định của scheduler.

reuse auth không có nghĩa là để mấy test song song cùng sửa data của một account rồi giẫm lên nhau :D

Worker thực ra cũng khá đắt

một Playwright Test worker thường có browser instance riêng. nên:

1
20 Playwright workers

thực tế có thể còn nặng hơn:

1
20 tabs trong cùng một Chrome

vì 20 tabs còn share một phần browser infrastructure, còn 20 workers có thể là 20 browser instances riêng.

thứ Playwright Test thường reuse là browser instance trong mỗi worker, còn mỗi test được tạo một BrowserContext riêng. mental model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1 worker
-> 1 browser

browser
-> tạo context test A
-> chạy test A
-> destroy context

-> tạo context test B
-> chạy test B
-> destroy context

-> tạo context test C
-> chạy test C
-> destroy context

BrowserContext giống một isolated/incognito profile, nhẹ hơn nhiều so với launch browser mới. nên parallelism vẫn là trade-off:

1
2
3
4
5
6
7
8
9
ít workers
-> chậm

nhiều workers
-> nhanh hơn nếu còn đủ resource

quá nhiều workers
-> CPU/RAM/network/DB contention
-> test flaky hoặc còn chậm hơn

suite lớn thì có thể scale ngang bằng sharding:

1
2
3
1,000 tests
-> shard qua nhiều CI machines
-> mỗi machine chạy một số workers

lại quay về đúng cách làm quen thuộc: queue + workers + bounded concurrency.

UI automation không có magic gì cả. mỗi test thật sự vẫn cần compute để chạy browser. muốn chạy nhanh hơn thì hoặc dùng nhiều resource hơn, hoặc chia workload thông minh hơn.

Bot detection thì sao?

web server có thể đoán browser đang được automate. nó không nhất thiết nhìn thấy:

1
isPlaywright = true

mà hệ thống bot detection thường nhìn nhiều signal từ request và JavaScript phía client:

1
2
3
4
5
6
7
browser fingerprint
network fingerprint
IP
behavior
request rate
mouse/keyboard pattern
session pattern

rồi suy luận user thật hay bot. nên headless browser không có nghĩa là automatically bị detect, nhưng cũng không có nghĩa là không thể bị detect.

Để khỏi mỗi release lại click tay 100 flows

Playwright là một abstraction layer để script điều khiển Chromium / Firefox / WebKit, và test flow vẫn chỉ là:

1
2
3
4
prepare state
-> imitate user
-> observe result
-> assert expected behavior

thay vì QA mỗi release tự nhớ và click 100 flows bằng tay:

1
2
3
4
5
6
login
checkout
search
settings
logout
...

ta encode chúng thành script. machine sẽ làm lại cùng một procedure mỗi lần, không mệt, không quên, không “hôm nay em chưa test case đó”.

và nếu application bắt đầu behave khác expectation ở những case đã được viết và chạy thì Playwright báo surprise ngay :D

AWS CDK: describe infrastructure bằng code

application có code, test có script, còn infrastructure thì sao? AWS CDK cho mình describe infrastructure bằng code → convert thành CloudFormation → AWS tạo/update resources.

Bản chất của CDK

thay vì ngồi AWS Console rồi click:

1
2
3
4
5
6
create VPC
create subnet
create security group
create ECS
create RDS
...

ta viết code mô tả:

tao muốn hệ thống cuối cùng trông như này

CDK không trực tiếp biến TypeScript thành EC2 hay RDS. flow thật là:

1
2
3
4
5
6
CDK code
-> synth
-> CloudFormation template
-> CloudFormation
-> AWS APIs
-> resources thật

nên CDK thực chất là abstraction layer phía trên CloudFormation.

Vậy CDK cần gì để deploy?

vẫn phải có:

1
2
3
AWS account
AWS credentials
IAM permissions

không có quyền IAM phù hợp thì CDK cũng đéo deploy được gì :D vì cuối cùng vẫn phải có một identity được AWS cho phép:

1
2
3
4
5
create EC2?
create RDS?
modify IAM?
create S3?
delete resource?

CDK giúp mình không phải tự gọi từng AWS API. còn synth template thuần local thì không nhất thiết cần credentials, trừ khi code cần lookup thông tin từ AWS.

IAM có hai tầng đáng care

trong setup dùng deployment role, thường sẽ có:

1
2
3
4
5
người / CI chạy cdk deploy
-> có quyền deploy / assume deployment role

CloudFormation execution role
-> có quyền thực sự tạo/update/delete infrastructure

câu hỏi cốt lõi vẫn là: ai đang yêu cầu AWS làm gì, và identity đó được quyền làm tới đâu?

CDK khác Terraform/Pulumi ở đâu?

về concept thì không khác nhiều. tất cả đều giải quyết manual infrastructure → infrastructure described as code.

AWS CDK chọn một đường rất AWS-native:

1
code -> CloudFormation -> AWS

nên nếu hệ thống đã xác định AWS thì khá hợp. đổi lại là lock-in AWS mạnh, nhưng nếu vốn đã build toàn bộ trên AWS thì nhiều lúc đây chẳng phải pain point gì cả.

Thứ quan trọng nhất: diff

AI giờ viết code được rồi. cái mình cần care không phải nhớ syntax, mà là:

nó sắp làm cái đéo gì với infrastructure của mình?

nên cdk diff quan trọng hơn việc thuộc lòng CDK API. mental model:

1
2
3
4
5
6
7
CloudFormation template đã deploy
vs
desired template từ code

-> diff
-> review
-> deploy

cdk diff không phải công cụ kiểm tra toàn bộ trạng thái thực tế trên AWS. nếu ai đó sửa tay gây drift thì cần kiểm tra drift riêng.

ví dụ phải nhìn được:

1
2
3
4
5
add resource
modify resource
replace resource
delete resource
change IAM policy

đặc biệt chữ replace rất đáng sợ với stateful resource.

Rollback

CDK inherit phần lớn rollback từ CloudFormation. deploy fail thì CloudFormation thường cố quay về stable state trước đó, nhưng:

infra rollback != data rollback

ví dụ:

1
2
3
4
database migration đã chạy
data đã bị mutate
S3 object đã bị delete
external service đã bị gọi

thì rollback stack không magically reverse hết những thứ đó. đây là một trong những pain point lớn nhất của IaC: stateless infra khá dễ rollback, stateful system thì không.

Drift

một surprise rất ngu:

1
2
3
4
CDK nói infrastructure = A
nhưng ai đó vào AWS Console click tay sửa thành B

-> code state != real world

đó là drift. IaC hoạt động đẹp nhất khi rule là infrastructure changes đi qua code. càng nhiều người click tay production thì IaC càng mất ý nghĩa.

Dependency

resource không độc lập. ví dụ:

1
2
3
4
5
6
7
8
ECS
-> cần network

application
-> cần database

database
-> cần subnet/security group

CDK/CloudFormation build dependency graph từ các reference và dependency được khai báo, rồi tạo theo đúng order. dependency của application không phải lúc nào cũng được tự suy ra, cần thì phải khai báo rõ.

hiểu đơn giản là resource A cần B tồn tại trước → dependency edge. cũng lại giống Lego thôi. tay chưa có thì chưa gắn được bàn tay :D

Construct

construct có thể đại diện cho một resource, hoặc package nhiều resource thành một Lego lớn hơn. ví dụ thay vì mỗi project đều manually define:

1
2
3
4
5
6
VPC
subnets
security groups
load balancer
ECS service
logging

ta có thể wrap thành:

1
MyWebService(...)

bên trong nó tạo cả đống resource. điểm hay là reuse, điểm nguy hiểm là abstraction quá sâu → dev không còn biết nó đang tạo cái gì.

nên với AI era, tôi quan tâm generated construct phải dễ inspect và dễ diff hơn là abstraction nhìn fancy.

Pain points thật sự

CDK syntax không phải vấn đề lớn. mấy thứ đáng care hơn là:

  • IAM có quá rộng không?
  • diff có replacement/delete resource nguy hiểm không?
  • stateful data có survive rollback không?
  • có ai sửa infra bằng tay gây drift không?
  • construct có hide quá nhiều resource không?
  • dependency giữa resources có đúng không?
  • deployment role có blast radius lớn tới đâu?

Để khỏi nhớ lần trước đã click cái gì

gom lại flow của CDK:

1
2
3
4
5
describe desired infra
-> synthesize CloudFormation
-> diff với template đã deploy
-> review
-> CloudFormation gọi AWS APIs để apply changes

nó tồn tại vì cùng một lý do với Docker, CI/CD, Playwright: reduce surprise. thay vì:

1
2
3
"hình như lần trước tôi click cái checkbox này"
"staging khác prod chỗ nào nhỉ?"
"thằng nào vừa sửa security group?"

ta có:

1
code -> diff -> review -> deploy

AI có thể viết code. thứ con người cần care hơn là review intent, permissions, diff, state và blast radius.

ECS và Fargate

ECS nghe ban đầu rất dễ bị hiểu nhầm là một dạng EC2 nâng cấp chuyên chạy container. thực ra không phải. EC2 và ECS không cùng category.

EC2 là compute, nói đơn giản là một cái máy có CPU, RAM, disk, network. còn ECS là phần mềm orchestration do AWS tự host và vận hành.

nó không phải cái máy để app chạy. nó là thằng control:

1
2
3
4
5
6
container nào cần chạy
chạy bao nhiêu bản
chạy ở compute nào
container chết thì làm gì
deploy version mới thế nào
scale lên/xuống ra sao

nói cực ngắn: orchestration = điều phối lifecycle, placement và các dependency được khai báo.

ECS underlying là gì?

ECS bản chất vẫn là software. software thì đương nhiên phải chạy trên server nào đó. AWS tự host ECS control plane trên infrastructure nội bộ của họ. mình không thấy và cũng không cần care server đó là gì.

flow nếu dùng ECS + EC2:

1
2
3
4
ECS control plane
-> scheduler quyết định task chạy ở đâu
-> ECS agent trên EC2 nhận instruction
-> container runtime chạy container

nên:

1
2
3
EC2 = cơ bắp
ECS = bộ não
ECS agent = thằng tay chân nằm trên EC2

container thật vẫn consume CPU/RAM trên EC2. ECS chỉ điều phối.

Từ image tới task và service

có image rồi thì ECS cần biết chạy nó kiểu gì:

  • Task definition → bản mô tả image nào, CPU/RAM bao nhiêu, port nào, env/secrets gì… một task definition có thể mô tả một hoặc nhiều container chạy cùng nhau.
  • Task → một lần chạy theo task definition đó.
  • Service → duy trì số task mong muốn và điều phối việc thay task khi deploy.

nên image không phải task, task definition cũng chưa phải thứ đang chạy. nó là bản mô tả; tạo task thì mới đem bản mô tả đó ra thực thi.

Desired state

khi dùng ECS service, concept rất đơn giản:

tao muốn hệ thống cuối cùng trông như này

ví dụ:

1
2
3
4
5
desired tasks = 3
actual = 2

-> thiếu 1
-> schedule thêm 1

một task chết:

1
2
3 -> 2
-> ECS tạo lại 1

deploy version mới theo kiểu rolling update:

1
2
3
old tasks
-> gradually replace
-> new tasks

ECS service liên tục desired state vs actual state → reconcile. task chạy riêng lẻ ngoài service thì không mặc định được service scheduler bù lại như vậy.

Scaling

có hai tầng khác nhau.

Task scaling

traffic tăng, nếu đã cấu hình scaling policy phù hợp:

1
3 tasks -> 10 tasks

ECS tăng số task cần chạy.

Compute scaling

nếu chạy trên EC2 mà cluster không còn đủ CPU/RAM, với capacity provider đã bật managed scaling:

1
2
3
4
tasks cần thêm capacity
-> Auto Scaling Group tăng EC2
-> EC2 join cluster
-> ECS đặt task lên EC2 mới

traffic giảm thì có thể scale theo chiều ngược lại. nên ECS không chỉ restart container, nó còn coordinate workload với compute capacity bên dưới nếu mình setup capacity provider / autoscaling.

ECS monitor kiểu gì?

phải tách hai chuyện.

Container còn sống không?

ECS có control plane + ECS agent / runtime state để biết task/container đang RUNNING hay STOPPED. còn healthy/unhealthy thì cần health check tương ứng, chứ process còn chạy chưa chắc app đã khoẻ.

cái này không cần CloudWatch mới biết.

Workload có cần scale không?

đây mới là nơi metrics tham gia. ví dụ:

1
2
3
4
task CPU
memory
ALB request count
custom metrics

metrics vào CloudWatch. Application Auto Scaling dùng metrics và policy mình cấu hình để điều chỉnh:

1
desiredCount 3 -> 6

rồi ECS làm đúng phần việc của nó: cố duy trì 6 tasks trong giới hạn capacity hiện có.

nên AWS thực ra ghép nhiều tool lại:

1
2
3
4
CloudWatch = observe
Application Auto Scaling = decide theo policy
ECS = orchestrate
EC2/Fargate = compute

Vậy Fargate là cái gì?

Fargate không phải một orchestrator khác. trong setup này ECS vẫn là thằng orchestration. Fargate chỉ giải quyết câu hỏi: compute để chạy task lấy ở đâu?

với EC2:

1
2
3
4
5
mình thấy machine
mình chọn instance type
mình quản ASG
mình quản OS/AMI
mình care cluster capacity

với Fargate:

1
2
3
4
5
tao cần:
1 vCPU
2 GB RAM

còn server nào chạy thì AWS tự lo

thế thôi.

Underlying của Fargate

Fargate vẫn chạy trên infrastructure AWS. mental model:

1
2
3
4
5
6
7
8
9
ECS:
"tao cần task này chạy với 1 vCPU + 2 GB RAM"

Fargate:
-> nhận request
-> allocate compute nội bộ
-> dựng isolated runtime
-> chạy container
-> report state lại

server vật lý/VM phía dưới vẫn phải tồn tại, chỉ là AWS hide nó khỏi customer. nên Fargate về bản chất là abstraction layer che compute infrastructure đi.

giống như đứa bé mặc thêm cái áo. compute vẫn ở đó, mình chỉ không còn phải nhìn và quản host nữa.

Vậy tại sao cần Fargate?

chủ yếu để reduce friction. ECS + EC2 chạy được phần lớn workload mà mình chạy trên ECS + Fargate, nhưng với EC2 mình còn phải care:

1
2
3
4
5
6
7
8
9
instance type
AMI
patch OS
ASG
cluster capacity
idle instance
bin packing
Spot / On-Demand
drain instance

Fargate nói: đừng tự quản đống host đó nữa. tập trung vào:

1
2
3
4
5
6
container
CPU
RAM
network
secrets
desired count

vẫn phải care chi phí, quota và lựa chọn Fargate / Fargate Spot, chứ không phải compute vô hạn miễn phí :D

trade-off rất obvious:

  • EC2 = nhiều control hơn, nhiều trách nhiệm hơn
  • Fargate = ít control hơn, ít friction hơn

Fargate không mạnh hơn EC2. nó chỉ remove một tầng khỏi đầu mình.

Debug thì sao?

Fargate không cho SSH vào underlying host. hợp lý thôi vì host không thuộc về mình. nhưng code chạy trong container, nên vẫn có thể dùng:

1
2
CloudWatch Logs
ECS Exec -> shell vào container

miễn là đã cấu hình logging, bật ECS Exec với quyền/network phù hợp và image có shell cần dùng.

đa số app-level debugging như:

1
2
3
4
5
process
env
filesystem
network
runtime config

vẫn check được trong phạm vi container. còn muốn debug trực tiếp:

1
2
3
4
host OS
kernel
container runtime
host networking

thì Fargate không cho. nhưng app bình thường thì thường không cần.

Category cho sạch

đây là phần dễ nhầm nhất:

1
2
3
4
EC2        = compute
ECS = orchestrator
Kubernetes = orchestrator
Fargate = managed compute, giấu tầng host

nên ECS gần category với Kubernetes hơn là EC2. còn Fargate không nên được coi là “một loại EC2 khác”. nó là cách AWS hide compute layer khỏi mình.

Bóc hết buzzword

1
2
3
4
5
6
7
8
9
EC2
= máy

ECS
= phần mềm quản lifecycle + placement của containers,
phối hợp với autoscaling

Fargate
= giấu cái máy đi

flow:

1
2
3
4
5
6
7
8
9
10
11
developer
-> define desired workload

ECS
-> orchestration

EC2 hoặc Fargate
-> provide compute

container
-> chạy application

Elastic Container Service, Fargate, capacity provider, service auto scaling nghe ghê gớm, nhưng cuối cùng vẫn chỉ là:

cần bao nhiêu process chạy, chạy ở đâu, chết thì làm gì, thiếu resource thì thêm ở đâu.

Observability: muốn biết thì phải có dữ liệu

muốn biết hệ thống đang làm gì thì phải đọc dữ liệu mà OS và application đang tạo ra hoặc cung cấp. Observability bắt đầu từ chuyện đó.

về cơ bản có vài loại dữ liệu, rồi từ đó mới theo dõi và cảnh báo.

Logs

log là một chuỗi thông tin được ghi lại theo thời gian. ở application level thì mỗi framework/runtime làm khác nhau. ví dụ Rails thường có:

1
2
3
log/development.log
log/test.log
log/production.log

Java, Python, Node… có cách riêng. nhưng cuối cùng vẫn là:

1
2
3
4
application
-> ghi log
-> tool đọc log
-> search / filter / display

production có thể không ghi trực tiếp xuống .log, mà đẩy qua stdout/socket rồi collector lấy đi. nhưng cách hình dung vẫn vậy: thằng nào đó write data, thằng khác read data.

Metrics

metrics là mấy con số:

1
2
3
4
5
6
7
CPU
RAM
disk
network
request count
error count
latency

ở OS level thì Linux đã cung cấp rất nhiều thông tin. ví dụ:

1
2
3
/proc
/sys
cgroups

monitoring tool đọc mấy thông số đó rồi:

1
2
3
4
collect
-> aggregate
-> store
-> draw graph

CPU 90% không phải monitoring tool tự nghĩ ra. kernel có các bộ đếm thời gian CPU, tool lấy chênh lệch giữa các lần đo để tính mức sử dụng.

với CloudWatch, nguồn dữ liệu tuỳ metric: CPU của EC2 có thể đến từ hạ tầng AWS, còn RAM bên trong máy thường cần agent thu thập. không phải mọi metric đều do CloudWatch trực tiếp đọc từ /proc.

Application metrics

OS chỉ biết những gì xảy ra ở level của OS. ví dụ:

1
process này đang dùng 80% CPU

nhưng nếu muốn biết:

1
2
3
Rails đang xử lý 500 requests/sec
checkout fail 20 lần
payment latency 800ms

thì application hoặc phần được gắn vào nó phải ghi nhận những con số đó. ví dụ:

1
2
3
requests_total += 1
payment_errors += 1
request_duration = 320ms

sau đó monitoring tool lấy dữ liệu bằng cách đọc endpoint, nhận dữ liệu được đẩy tới, hoặc tính từ logs/traces.

cuối cùng vẫn là write → read. counter, gauge, histogram… là các cách biểu diễn khác nhau: đếm tích luỹ, giá trị hiện tại, hay phân bố các lần đo. chọn đúng thì mới tổng hợp và đọc được ý nghĩa của dữ liệu.

Traces

trace là đánh dấu một request để biết nó đi qua đâu và mất bao lâu ở từng chỗ. ví dụ:

1
2
3
4
5
6
trace #abc

frontend
-> API
-> payment service
-> database

mỗi đoạn, gọi là một span, ghi lại:

1
2
3
4
5
6
7
trace ID
span ID
parent span ID
start time
end time
service
result

trace context cần được truyền qua các service để các span biết mình thuộc cùng một luồng. sau đó tool đọc lại và ghép theo các ID đó. ví dụ thời gian từng đoạn:

1
2
3
4
5
trace abc

API xử lý riêng 50ms
payment 700ms
database 30ms

thế là thấy payment chiếm nhiều thời gian. các span có thể lồng nhau hoặc chạy song song, nên không cứ cộng mọi duration là ra tổng thời gian request.

vẫn là ghi dữ liệu → đọc dữ liệu → ghép lại.

Alerts

alert còn đơn giản hơn:

1
2
3
4
5
6
7
8
if CPU > 90% for 5 minutes
-> báo

if error rate > 5%
-> báo

if server không response
-> báo

tool kiểm tra dữ liệu theo chu kỳ hoặc khi có dữ liệu mới. nếu data khớp điều kiện đã cấu hình thì trigger action: read → compare → action.

Nhìn từ Linux

Linux có một cách hình dung rất hữu ích:

everything is a file

không phải mọi thứ literally là file .txt trên SSD. nhưng rất nhiều tài nguyên và trạng thái có thể được truy cập qua:

1
2
3
4
5
6
file
pseudo-file
file descriptor
pipe
socket
device

ví dụ:

1
2
3
/proc
/sys
/dev

các pseudo-file như trong /proc có thể được kernel tạo nội dung lúc đọc, không cần ghi sẵn xuống disk. nên ở mức thấp:

1
2
3
4
5
OS/kernel/application
-> ghi / cung cấp data

monitoring tool
-> đọc data

sau đó muốn:

1
2
3
4
5
store
search
aggregate
draw graph
alert

thì làm tiếp.

Gom lại cho dễ hình dung

1
2
3
4
5
6
7
8
9
10
11
logs
= ghi lại chuyện gì đã xảy ra

metrics
= ghi lại / cung cấp các con số

traces
= ghi lại đường đi và thời gian của request

alerts
= đọc dữ liệu rồi check condition

CloudWatch, Datadog, Grafana, OpenTelemetry… tham gia vào những đoạn khác nhau của luồng:

1
2
3
4
5
6
write
-> read
-> store
-> search
-> aggregate
-> display

không phải tool nào cũng làm hết: OpenTelemetry chủ yếu giúp tạo, thu thập và xuất telemetry; Grafana thường dùng để truy vấn và hiển thị dữ liệu từ các nguồn; CloudWatch/Datadog cung cấp nhiều phần thu thập, lưu trữ, truy vấn và cảnh báo.

nếu bóc xuống thao tác nền tảng thì vẫn là ghi và đọc. nhìn từ góc đó, mọi thứ còn lại phần lớn là dệt gấm thêu hoa lên hai việc ấy.

nhưng có dữ liệu chưa chắc đã hiểu được hệ thống. phải ghi đúng thứ cần biết, nối được chúng với nhau thì lúc có surprise mới lần ra chuyện gì đang xảy ra.

Network và deployment: data đi đúng chỗ, đổi version đúng lúc

Từ signal tới request

máy tính muốn nói chuyện với nhau thì cuối cùng vẫn phải truyền tín hiệu. data là bits 010101..., network hardware mã hoá chúng thành tín hiệu rồi gửi đi; bên nhận giải mã ngược lại.

Môi trường Tín hiệu
Cáp đồng Tín hiệu điện
Cáp quang Ánh sáng
Wi-Fi Sóng vô tuyến

nhưng truyền được bits chưa đủ. muốn nói chuyện thực tế thì phải giải quyết thêm:

Câu hỏi Thứ giải quyết
Gửi frame tới interface nào trên mạng cục bộ? MAC
Packet đi tới địa chỉ nào, qua mạng nào? IP và routing
Data tới đầu nhận nào trong máy? Port, kết hợp địa chỉ IP và giao thức
Cần truyền tin cậy, đúng thứ tự? TCP
Application trao đổi request/response theo quy tắc gì? Ví dụ HTTP

một HTTPS request dùng HTTP/1.1 hoặc HTTP/2 có thể hình dung từ trên xuống:

1
2
3
4
5
6
HTTP
-> TLS
-> TCP
-> IP
-> Ethernet / Wi-Fi
-> tín hiệu vật lý

bên nhận xử lý ngược lại. HTTP/3 dùng QUIC trên UDP, không phải HTTP nào cũng đi qua TCP.

IP, router và port

IP là địa chỉ mạng, giống địa chỉ nhà. ví dụ 192.168.1.10, 10.0.0.5.

hai private network tách biệt có thể cùng dùng 10.0.0.5 mà không conflict. nhưng nối hai mạng có dải IP trùng nhau thì routing sẽ thành vấn đề. public IP thuộc không gian địa chỉ được phân bổ trên Internet, không thể tuỳ ý dùng trùng như vậy; một public IP vẫn có thể đại diện cho nhiều máy phía sau NAT hoặc load balancer.

router trả lời câu hỏi: packet này cần đi tiếp theo hướng nào? nó tra route phù hợp với destination IP để chọn next hop:

1
2
3
4
5
computer
-> home router
-> ISP router
-> các router trung gian
-> destination

đến đúng địa chỉ rồi, port giúp OS chuyển data tới socket mà application đang dùng. hình dung IP là địa chỉ toà nhà, port là số phòng. những port thường gặp:

1
2
3
22   -> SSH
443 -> HTTPS
5432 -> PostgreSQL

đây là quy ước phổ biến, không phải app bị buộc phải chạy đúng port đó. port cũng không phải process ID.

VPC và subnet

VPC là một mạng ảo được cô lập về mặt logic trong AWS. bên trong, các thành phần như ALB, ECS task và RDS có thể dùng private IP để nói chuyện với nhau.

subnet chia dải địa chỉ của network thành các phần nhỏ hơn. ví dụ:

1
2
3
VPC:           10.0.0.0/16
public subnet: 10.0.1.0/24
private subnet:10.0.2.0/24

trong AWS, mỗi subnet nằm trong một Availability Zone. muốn trải hệ thống qua nhiều AZ thì cần nhiều subnet. một cách bố trí phổ biến:

1
2
3
4
5
6
7
public subnets
-> internet-facing ALB

private subnets
-> ECS
-> RDS
-> Redis

Public/private nằm ở route, không phải cái tên

public subnet có route trực tiếp tới Internet Gateway. private subnet thì không:

1
2
3
public subnet
-> Internet Gateway
-> Internet

với IPv4, có route đó chưa đủ để một EC2 nhận kết nối từ Internet: còn cần public IP và các rule mạng cho phép.

nếu resource trong private subnet cần chủ động kết nối ra Internet, một cách phổ biến với IPv4 là:

1
2
3
4
ECS trong private subnet
-> public NAT Gateway trong public subnet
-> Internet Gateway
-> Internet

NAT cho phép kết nối outbound và lưu lượng trả về tương ứng, không tự mở cửa để Internet kết nối tuỳ ý vào ECS.

Security group: ai được nói chuyện với ai?

security group là firewall có trạng thái, gắn với resource/network interface. nó có rule cho phép, không có rule deny riêng; traffic không được cho phép thì bị chặn.

ví dụ cấu hình:

Kết nối Rule
Internet → ALB :443 Cho phép HTTPS
ALB → ECS :3000 Cho phép từ security group của ALB
ECS → RDS :5432 Cho phép từ security group của ECS
Internet → RDS :5432 Không cho phép

vì có trạng thái, traffic trả về của kết nối được cho phép không cần mở thêm một rule đối ứng chỉ để nhận response.

gom lại:

1
2
3
4
VPC            = mạng ảo của mình
subnet = chia mạng thành các khu
route = packet đi theo hướng nào
security group = kết nối nào được phép

Load balancer: request này đưa cho backend nào?

load balancer không phải một loại compute đặc biệt. compute là nơi chạy phần mềm cân bằng tải. với một HTTP load balancer như ALB, có thể hình dung:

1
2
3
4
5
6
listen trên port
-> nhận request
-> đọc rule
-> chọn backend phù hợp, đang healthy
-> forward
-> chuyển response về client

nó giống cảnh sát giao thông ở ngã tư. ví dụ:

1
2
/api    -> target group A
/images -> target group B

ngoài ra còn có health check, TLS termination, sticky session, connection reuse. không phải load balancer nào cũng đọc HTTP path; loại hoạt động ở tầng TCP/UDP xử lý theo thông tin kết nối thay vì URL.

AWS ALB là managed load balancer: AWS vận hành hạ tầng bên dưới. ALB có thể public hoặc internal, không nhất thiết phải listen ra Internet.

Zero-downtime deployment

nếu chỉ có một process phục vụ request và làm:

1
stop v1 -> start v2

thì sẽ có một khoảng không có process để phục vụ. cách tránh khoảng trống đó là đừng bỏ bản cũ trước khi bản mới sẵn sàng.

ví dụ đang có ba task v1, ta thay dần:

1
2
3
4
5
6
7
start v2
-> kiểm tra v2 sẵn sàng phục vụ
-> đưa v2 vào nhận traffic
-> ngừng đưa request mới tới v1 cần thay
-> chờ request đang chạy trên v1 hoàn tất
-> stop v1
-> lặp lại cho các task còn lại

flow cốt lõi là start new → verify → route → drain old → stop old.

ECS và ALB phối hợp thế nào?

với ECS service gắn ALB, ECS quản lifecycle của task, còn ALB route request tới các target đủ điều kiện:

1
2
3
4
5
6
ECS start task mới
-> đăng ký target với ALB
-> health check xác nhận target healthy
-> ALB có thể gửi traffic tới target mới
-> ECS thay dần task cũ
-> deregister/drain target cũ và dừng task

ALB không tự quyết định deploy version nào. ECS điều phối theo cấu hình deployment; ALB xử lý routing, health check và draining.

vậy orchestrator không chỉ có mỗi chuyện “container chết → restart”. nó quản start, stop, placement, health, scale và thay version, phối hợp với những thành phần khác.

nhưng rolling update không tự bảo đảm zero downtime. vẫn phải có đủ capacity để giữ bản cũ trong lúc khởi động bản mới, health check phản ánh khả năng phục vụ thật, và app xử lý graceful shutdown đúng. hết thời gian drain/stop mà request chưa xong thì vẫn có thể bị cắt.

Database: old và new phải sống chung được

app version cũ và mới thường chạy cùng lúc một khoảng thời gian. đó là lý do migration không được ngu kiểu:

1
2
drop old column
-> deploy new code

old container vẫn còn sống và có thể cần column đó. nên thường phải:

1
2
3
4
5
thêm schema mới, giữ schema cũ
-> deploy code tương thích cả hai
-> chuyển dữ liệu
-> chuyển hoàn toàn sang cách dùng mới
-> khi không còn code phụ thuộc mới xoá phần cũ

old và new phải coexist được, kể cả trong khoảng thời gian mình còn cần rollback. migration cũng phải tránh lock bảng lâu hoặc tạo tải quá lớn, chứ tương thích schema thôi chưa đủ.

Cuối cùng là mấy câu hỏi này

Câu hỏi Phần chịu trách nhiệm
Data đi đâu? Routing
Ai được nói chuyện với ai? Firewall/security group
Request nên vào backend nào? Load balancer
Task nào cần chạy, thay hay dừng lúc nào? Orchestrator
Đổi version mà không gián đoạn phục vụ thế nào? Old/new chạy song song, kiểm tra readiness, chuyển traffic và drain

nhìn theo cách đó, đống network/cloud/devops này bớt mù mờ hơn: đưa data đúng chỗ, quản lifecycle đúng lúc và giảm surprise.

Cuối cùng, mỗi thằng giảm một kiểu surprise

  • Docker → bớt chuyện “máy em chạy được”, chuẩn hoá environment của application.

  • CI/CD → bớt chuyện quên bước hoặc mỗi người release một kiểu, đưa quy trình vào automation.

  • Playwright → bớt chuyện deploy xong mới biết user không login được, kiểm tra những hành vi đã được viết thành test.

  • CDK → bớt chuyện không nhớ đã click gì trên Console, đưa infrastructure vào code để diff và review.

  • ECS → bớt chuyện task chết mà không ai bù lại, duy trì workload theo desired state. Fargate thì bớt phần host phải tự quản, còn autoscaling điều chỉnh task/capacity theo policy.

  • Observability → bớt chuyện hệ thống có vấn đề mà không biết vì sao, dùng logs, metrics và traces để lần ra nguyên nhân, rồi đặt cảnh báo phù hợp.

  • Network và deployment an toàn → kiểm soát đường đi của request, quyền kết nối và cách thay version mà không cắt ngang việc user đang làm.

không có thằng nào bảo đảm hệ thống không bao giờ vỡ. config sai thì automation vẫn chạy cái sai đó rất đều :D

cái đáng lấy không phải là thêm một đống tên vào tech stack, mà là biết mình đang giảm biến số nào, còn biến số nào phải tự chịu trách nhiệm.

Home