28/07/2026
Một chút "thu hoạch" sau hành trình làm Software Engineer
Từ viết code đến lúc tự tay làm hệ thống sập... rồi tự cứu lại.
---
Trước đây, mình từng nghĩ công việc của một Software Engineer khá đơn giản:
- Nhận yêu cầu.
- Viết code.
- Chạy được tính năng.
- Push lên GitHub.
- Done.
Nhưng sau một khoảng thời gian trực tiếp xây dựng một hệ thống tương đối đầy đủ, mình mới nhận ra:
«Viết được code chỉ là bước đầu. Làm sao để đoạn code đó chạy ổn định trong một hệ thống thật mới là phần khó nhất.»
---
Hệ thống mình làm gồm khá nhiều thành phần
- Frontend
- Backend API
- Chat Service
- PostgreSQL
- Redis
- DynamoDB
- SQS
- Docker
- Kubernetes
- GitHub Actions
- AWS
Nghe thì khá "hoành tráng".
Nhưng thực tế triển khai lại là chuỗi ngày...
«"Ủa, sao nó không chạy?"»
«"Ủa, nãy còn chạy mà?"»
«"Ủa, trên máy mình chạy được mà?"»
Và câu quen thuộc nhất:
«"Code không đổi, sao hệ thống lại lỗi?"»
---
1. Production khác Local rất nhiều
Ở local, mọi thứ khá dễ.
- Database chạy trong Docker Compose.
- Redis cũng ở đó.
- Service gọi nhau bằng tên container.
- Có lỗi thì restart rồi xem log.
Nhưng khi lên Kubernetes và AWS...
Mọi thứ thay đổi hoàn toàn.
Một service muốn gọi service khác phải đúng:
- DNS
- Namespace
- Port
- Network Policy
Pod muốn kết nối database phải có:
- Connection String đúng
- Security Group đúng
- Route đúng
Secret cũng không thể để thẳng trong file YAML rồi push lên Git.
Lúc đó mình mới hiểu:
«Ứng dụng chạy được không chỉ phụ thuộc vào code, mà còn phụ thuộc vào toàn bộ môi trường xung quanh nó.»
---
2. Docker dạy mình rằng...
«"Máy mình chạy được" không còn là lý do hợp lệ.»
Ban đầu Docker tưởng rất đơn giản.
- Viết Dockerfile
- Build Image
- Run Container
Nhưng rồi đủ loại lỗi xuất hiện:
- Container chạy nhưng Health Check fail
- App khởi động trước Database
- Trùng Port
- Mount thiếu Volume
- Environment Variable không nhận
- Docker Desktop và WSL không kết nối
Có hôm chạy Smoke Test thì nhận ngay:
«The command 'docker' could not be found in this WSL 2 distro.»
Code không sai.
Script cũng không sai.
Sai ở... môi trường.
Sau đó mình bắt đầu có thêm một checklist mới:
- Code đang chạy ở đâu?
- Container nhận biến môi trường nào?
- Service phía dưới đã sẵn sàng chưa?
- Network có hoạt động không?
---
3. CI/CD dạy mình rằng...
Automation chỉ hữu ích khi mình hiểu nó đang tự động hóa điều gì.
Lúc đầu nhìn GitHub Actions tự động:
- Lint
- Test
- Build
- Scan
- Push ECR
- Deploy
Cảm giác rất "xịn".
Đến khi Workflow đỏ...
Mới biết còn nhiều thứ phía sau.
Có lần fail chỉ vì:
- ShellCheck Warning
- Windows Smart App Control chặn Python
- File ".env" có BOM
- Smoke Test fail
- Branch không đúng điều kiện
- Secret thiếu
Lúc đó mới hiểu:
«CI/CD không phải một file YAML thần kỳ.»
Nó là tập hợp của rất nhiều giả định.
Chỉ cần một giả định sai...
Cả Pipeline sẽ dừng.
---
4. IAM dạy mình rằng...
Có chữ Allow chưa chắc đã được phép.
Mình từng gặp:
- AccessDenied
- implicitDeny
- iam:PassRole denied
- ecr:GetAuthorizationToken denied
- AssumeRoleWithWebIdentity failed
Role có.
Policy có.
ARN nhìn cũng đúng.
Nhưng AWS vẫn từ chối.
Sau đó mới biết còn có:
- Trust Policy
- Principal
- Condition
- GitHub Repository
- Branch
- Resource "*"
Từ đó mình bỏ thói quen đoán quyền.
Thay vào đó dùng:
aws iam simulate-principal-policy
Để kiểm tra từng Action.
Một bài học rất đáng giá:
«Đừng đoán quyền. Hãy kiểm chứng quyền.»
---
5. Kubernetes dạy mình rằng...
Pod Running chưa chắc ứng dụng đang khỏe.
Mình từng gặp:
- Pod Running nhưng API chết
- Pod Running nhưng Redis không kết nối
- Pod Running nhưng Migration fail
- Pod Running nhưng không ra Internet
- Pod Running nhưng Health Check đỏ
Kubernetes chỉ nói:
«Container còn tồn tại.»
Còn ứng dụng có hoạt động đúng không...
Mình vẫn phải kiểm tra:
- Log
- Readiness Probe
- Liveness Probe
- Service
- Ingress
- Dependency
---
6. NAT Gateway
Đây là lần mình "tiết kiệm" đáng nhớ nhất.
Nghĩ rằng:
«"Ít người dùng, tắt NAT Gateway chắc không sao."»
Sau đó...
- Pod không ra Internet
- API lỗi
- Deploy fail
- GitHub Actions lỗi
- Private Subnet gần như bị cô lập
Kiểm tra Code.
Không lỗi.
Kiểm tra IAM.
Không lỗi.
Cuối cùng...
Mới nhớ chính mình vừa tắt NAT.
Bật lại.
Mọi thứ hoạt động bình thường.
Sau lần đó mình hiểu thật sự:
- Public Subnet
- Private Subnet
- NAT Gateway
- Internet Gateway
- Route Table
- VPC Endpoint
---
7. Database
Database dạy mình rằng:
«Dữ liệu không thể xử lý bằng niềm tin.»
Ví dụ:
- Check tồn tại
- Chưa có thì Insert
Ở Local rất ổn.
Nhưng khi có hai Request cùng lúc...
Cả hai đều thấy dữ liệu chưa tồn tại.
Kết quả:
Insert trùng.
Từ đó mình bắt đầu chú ý hơn tới:
- Unique Constraint
- Transaction
- Rollback
- HTTP 409 Conflict
- Optimistic Locking
- Idempotency Key
---
8. Message Queue
Một bài học mình rất thích.
Nếu:
- Commit Database xong
- Gửi SQS thất bại
=> Event mất.
Nếu:
- Gửi SQS trước
- Database Rollback
=> Event sai.
Sau đó mình tìm hiểu Outbox Pattern.
Event được ghi cùng Transaction.
Dispatcher gửi sau.
Nếu fail thì Retry.
Consumer Deduplicate.
Người dùng không nhìn thấy.
Nhưng hệ thống đáng tin cậy hơn rất nhiều.
---
9. AWS
AWS dạy mình rằng:
Tạo được Resource...
Không có nghĩa là xong.
Ví dụ:
RDS vừa tạo chỉ mới ở trạng thái:
«creating»
Sau đó còn phải:
- available
- Endpoint
- Security Group
- Secret
- Connection
EKS, ECR, SageMaker...
Đều như vậy.
Mình bắt đầu phân biệt rõ:
- Resource được khai báo
- Resource đang tạo
- Resource sẵn sàng
- Ứng dụng kết nối được
- Health Check thành công
Đó là 5 trạng thái hoàn toàn khác nhau.
---
Điều mình học được nhiều nhất
Không phải Docker.
Không phải Kubernetes.
Không phải AWS.
Cũng không phải Terraform.
Mà là...
Cách xử lý vấn đề có hệ thống.
Khi hệ thống lỗi, mình không còn sửa ngẫu nhiên nữa.
Mình chia nhỏ:
- Lỗi ở Application hay Infrastructure?
- Trước hay sau Deploy?
- Container có chạy không?
- DNS có Resolve không?
- Network có Route không?
- IAM có quyền không?
- Dependency đã sẵn sàng chưa?
- Log cuối cùng thành công là gì?
Mình tìm điểm gãy đầu tiên.
Không phải triệu chứng cuối cùng.
---
Nhìn lại
Trước đây...
Hệ thống lỗi.
Mình nghĩ:
«"Chắc code sai."»
Bây giờ mình biết...
Lỗi có thể nằm ở:
- Code
- Config
- Environment Variable
- Container
- Network
- Database
- Permission
- Infrastructure
- CI/CD
Hoặc...
Chỉ đơn giản là một Service chưa sẵn sàng.
---
Ngày trước mình chỉ quan tâm:
«Feature chạy được.»
Bây giờ mình quan tâm thêm:
- Nếu request gửi hai lần thì sao?
- Nếu database commit nhưng queue lỗi thì sao?
- Nếu Pod Restart thì sao?
- Nếu Deploy thất bại thì Rollback thế nào?
- Nếu Service ngoài chết thì hệ thống phản ứng ra sao?
Có lẽ...
Đó là lúc mình bắt đầu hiểu hơn về công việc của một Software Engineer.
Không chỉ là người viết code.
Mà là người chịu trách nhiệm cho cách một hệ thống vận hành, thất bại và phục hồi.
---
Tổng kết
Viết code giúp mình tạo ra tính năng.
Debug giúp mình hiểu hệ thống.
Triển khai giúp mình hiểu hạ tầng.
Sự cố giúp mình hiểu những phần trước đây chỉ đọc trên tài liệu.
Và những lần tự tay làm hệ thống lỗi...
Lại là những lần giúp mình nhớ kiến thức lâu nhất.
Đến hiện tại, hệ thống vẫn chưa hoàn hảo.
Vẫn còn nhiều thứ phải tối ưu.
Nhưng sau mỗi lần:
- Workflow đỏ
- Pod Crash
- IAM từ chối
- Migration Fail
- Network mất kết nối
Mình không còn quá hoang mang.
Mình bắt đầu biết nên kiểm tra từ đâu.
Và có lẽ...
Đó là "thu hoạch" lớn nhất của mình sau hành trình này.
«Một Software Engineer không phải là người không bao giờ làm hệ thống lỗi.»
«Mà là người sau mỗi lần hệ thống lỗi đều hiểu nó hơn, sửa nó tốt hơn và thiết kế để lỗi đó khó lặp lại hơn.»
---
Còn hiện tại...
Pipeline đã xanh.
Pod đã Running.
API trả về 200 OK.
Database kết nối bình thường.
Thì...
Mình xin phép chưa đụng vào Production thêm một lúc. 😄
Vì đôi khi...
Cách tốt nhất để giữ Production ổn định là đừng "tối ưu thêm một chút" vào cuối ngày.