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 :DCompose 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 | push code |
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 | git push |
vãi lồn :D
nên nó thường là một asynchronous job system, architecture đại khái:
1 | git push |
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 | average wait > 2 minutes |
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 | demand tăng -> add 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 | có địa chỉ |
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 | Playwright script |
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 | Chromium -> translate thành command mà Chromium 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 | move tới x=100, y=200 |
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 | prepare state |
ví dụ:
1 | open login page |
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 | TEST_EMAIL |
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 | open login |
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 | login once |
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 | button Login |
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 | element visible? |
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 | API trả 500 |
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 | trace |
để 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:
- test against staging/dev environment có sẵn.
- trong CI dựng cả stack FE + BE + DB, chờ các service ready rồi mới run Playwright.
- 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 | 1,000 tests |
RAM nổ mẹ luôn :D
Playwright Test dùng bounded parallelism. ví dụ:
1 | 1,000 tests |
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 | worker 1 |
đâ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 | 1 worker |
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 | ít workers |
suite lớn thì có thể scale ngang bằng sharding:
1 | 1,000 tests |
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 | browser fingerprint |
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 | prepare state |
thay vì QA mỗi release tự nhớ và click 100 flows bằng tay:
1 | login |
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 | create VPC |
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 | CDK code |
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 | AWS account |
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 | create EC2? |
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 | người / CI chạy cdk deploy |
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 | CloudFormation template đã 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 | add resource |
đặ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 | database migration đã chạy |
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 | CDK nói infrastructure = A |
đó 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 | ECS |
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 | VPC |
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 | describe desired infra |
nó tồn tại vì cùng một lý do với Docker, CI/CD, Playwright: reduce surprise. thay vì:
1 | "hình như lần trước tôi click cái checkbox này" |
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 | container nào cần chạy |
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 | ECS control plane |
nên:
1 | EC2 = cơ bắp |
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 | desired tasks = 3 |
một task chết:
1 | 3 -> 2 |
deploy version mới theo kiểu rolling update:
1 | old 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 | tasks cần thêm capacity |
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 | task CPU |
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 | CloudWatch = observe |
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 | mình thấy machine |
với Fargate:
1 | tao cần: |
thế thôi.
Underlying của Fargate
Fargate vẫn chạy trên infrastructure AWS. mental model:
1 | ECS: |
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 | instance type |
Fargate nói: đừng tự quản đống host đó nữa. tập trung vào:
1 | container |
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 | CloudWatch Logs |
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 | process |
vẫn check được trong phạm vi container. còn muốn debug trực tiếp:
1 | host OS |
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 | EC2 = compute |
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 | EC2 |
flow:
1 | developer |
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 | log/development.log |
Java, Python, Node… có cách riêng. nhưng cuối cùng vẫn là:
1 | application |
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 | CPU |
ở OS level thì Linux đã cung cấp rất nhiều thông tin. ví dụ:
1 | /proc |
monitoring tool đọc mấy thông số đó rồi:
1 | collect |
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 | Rails đang xử lý 500 requests/sec |
thì application hoặc phần được gắn vào nó phải ghi nhận những con số đó. ví dụ:
1 | requests_total += 1 |
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 | trace #abc |
mỗi đoạn, gọi là một span, ghi lại:
1 | trace ID |
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 | trace abc |
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 | if CPU > 90% for 5 minutes |
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 | file |
ví dụ:
1 | /proc |
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 | OS/kernel/application |
sau đó muốn:
1 | store |
thì làm tiếp.
Gom lại cho dễ hình dung
1 | logs |
CloudWatch, Datadog, Grafana, OpenTelemetry… tham gia vào những đoạn khác nhau của luồng:
1 | write |
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 | HTTP |
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 | computer |
đế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 | 22 -> SSH |
đâ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 | VPC: 10.0.0.0/16 |
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 | public subnets |
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 | public subnet |
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 | ECS trong private subnet |
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 | VPC = mạng ảo của mình |
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 | listen trên port |
nó giống cảnh sát giao thông ở ngã tư. ví dụ:
1 | /api -> target group A |
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 | start v2 |
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 | ECS start task mới |
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 | drop old column |
old container vẫn còn sống và có thể cần column đó. nên thường phải:
1 | thêm schema mới, giữ schema 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.