
Tổ chức dự án CloudFormation để làm việc hiệu quả với Claude Code
Mục đích
Ở các dự án Infra với khách hàng Nhật, mình và team mình thường xuyên viết hạ tầng sử dụng CloudFormation. Khi AI - đặc biệt là Claude Code phát triển, cách code hạ tầng bây giờ cũng nên thay đổi để nâng cao performance.
Khi đi tìm hiểu trên mạng mình cũng chưa gặp blog nào nói về việc tổ chức dự án CloudFormation như thế nào để làm việc hiệu quả với Claude Code.
Mấy hôm trước vô tình đọc 1 bài: How to setup your Claude code project?**** trên linkedin, mình cũng tham khảo và nãy ra ý tưởng - sao không tổ chức project CloudFormation để làm việc tốt với Claude Code.
Bài viết này chia sẻ cách mình tổ chức một dự án CloudFormation thực tế — Demo xây dựng VPC + ALB + EC2 với HTTPS trên AWS — để Claude Code có thể hiểu context, tuân thủ convention, và hỗ trợ mình deploy an toàn mà không cần nhắc nhở lặp lại.
Tư duy cốt lõi
Claude Code đọc file trước khi trả lời. Nếu bạn đưa đúng thông tin vào đúng file, Claude sẽ:
- Biết convention của project mà không cần bạn giải thích
- Tự check security guardrails trước khi viết template
- Hỏi confirm trước khi chạy lệnh nguy hiểm
- Không lặp lại lỗi đã từng gặp
Ngược lại, nếu project không có cấu trúc, Claude phải đoán — và đoán sai.
Cấu trúc thư mục

webapp-infra/
├── .claude/ ← Não của Claude trong project này
│ ├── CLAUDE.md ← Context tổng quan, luôn được đọc
│ ├── CLAUDE.local.md ← Override cá nhân (gitignored)
│ ├── settings.json ← Cấu hình Claude Code
│ ├── outputs-catalog.md ← Danh sách tất cả CloudFormation exports
│ ├── rules/ ← Quy tắc bắt buộc
│ │ ├── cfn-standards.md ← Convention đặt tên, tag, import pattern
│ │ ├── security-guardrails.md ← Những gì tuyệt đối không được làm
│ │ ├── data-protection.md ← DeletionPolicy, UpdateReplacePolicy
│ │ └── env-separation.md ← Dev vs Prod phân tách thế nào
│ ├── skills/ ← Workflow tự động theo ngữ cảnh
│ │ ├── new-stack/SKILL.md ← Khi tạo template mới
│ │ ├── cross-stack-ref/SKILL.md← Khi dùng !ImportValue
│ │ ├── iam-audit/SKILL.md ← Khi tạo/sửa IAM resource
│ │ └── cost-guard/SKILL.md ← Khi thêm resource tốn tiền
│ ├── agents/ ← Agent chuyên biệt
│ │ ├── security-auditor.md
│ │ └── cost-analyzer.md
│ └── commands/ ← Slash commands
│ ├── deploy.md ← /project:deploy
│ ├── validate.md ← /project:validate
│ ├── diff.md ← /project:diff
│ ├── rollback.md ← /project:rollback
│ └── drift.md ← /project:drift
├── templates/
│ ├── networking/webapp-networking.yaml
│ ├── security/webapp-security.yaml
│ └── compute/webapp-compute.yaml
├── parameters/
│ ├── dev-networking.json ← gitignored (chứa account-specific values)
│ ├── dev-security.json
│ ├── dev-compute.json
│ ├── prod-networking.json
│ ├── prod-security.json
│ └── prod-compute.json
├── DEPLOY.md ← Hướng dẫn deploy từ đầu đến cuối
└── .gitignore
File quan trọng nhất: .claude/CLAUDE.md
Đây là file Claude Code đọc mỗi khi bắt đầu conversation. Hãy coi nó như README dành cho AI.
Cấu trúc nên có:
# webapp — AWS Infrastructure Project
## Project Overview
Learning project: VPC + ALB + EC2 with HTTPS on AWS ap-northeast-1 (Tokyo).
All infrastructure managed by CloudFormation. No manual resource creation.
## Stack Topology
Deploy in this exact order — never skip, never reverse:
1. {env}-webapp-networking → templates/networking/webapp-networking.yaml
2. {env}-webapp-security → templates/security/webapp-security.yaml
3. {env}-webapp-compute → templates/compute/webapp-compute.yaml
## Technology Decisions
| Concern | Decision |
|----------------|-------------------------------------------------------|
| EC2 access | SSM Session Manager — no KeyPair, no SSH, no Bastion |
| HTTP traffic | :80 → 301 redirect to :443 (never forward HTTP) |
| NAT Gateway | dev: 1 (cost saving) / prod: 2 per region (HA) |
| Parameters | Separate file per stack in parameters/ |
## Environments
| | dev | prod |
|---|---|---|
| Account ID | XXXXXXXXXXXX | (future) |
| AWS CLI profile | cloudmentor | (future) |
## Naming Convention
- Stack names: {env}-{project}-{layer} → dev-webapp-networking
- Logical IDs: {ResourceType}{Desc} → Ec2WebServer1a (PascalCase, no hyphens)
- Tag Name: {env}-{project}-{type}-{desc} → dev-webapp-sg-alb
## Required Tags — every resource must have all 5
- Name, Environment, Project, ManagedBy, Layer
## Key Files
- outputs-catalog.md — tất cả CloudFormation export names
- parameters/dev-{layer}.json — dev values (gitignored)
- CLAUDE.local.md — personal overrides (gitignored)
Nguyên tắc: CLAUDE.md nên trả lời câu hỏi "project này là gì, ai dùng, deploy ở đâu, convention là gì" — không cần chi tiết kỹ thuật (để vào rules/).
.claude/CLAUDE.local.md — Override cá nhân (gitignored)
File này để mỗi người trong team có thể ghi thông tin cá nhân mà không commit lên git:
# CLAUDE.local.md — gitignored, không commit
## AWS CLI Profile
Default profile: cloudmentor
Account ID: XXXXXXXXXXXX
Region: ap-northeast-1
## Hosted Zone
Domain: cloudmentor.pro
Hosted Zone ID: XXXXXXXX8KTLB7O
Khi Claude hỏi "Hosted Zone ID là gì?" — nó tìm file này trước khi hỏi bạn.
.claude/outputs-catalog.md — Danh bạ CloudFormation Exports
Mỗi khi bạn dùng Fn::ImportValue, Claude cần biết export name chính xác. File này là nguồn sự thật duy nhất:
# Outputs Catalog — webapp
## dev-webapp-networking
| Export Name | Value | Dùng cho |
|---|---|---|
| dev-webapp-networking-VpcId | VPC ID | security, compute |
| dev-webapp-networking-SubnetPublic1aId | Public Subnet AZ-A | ALB |
| dev-webapp-networking-SubnetPrivate1aId | Private Subnet AZ-A | EC2 |
## dev-webapp-security
| Export Name | Value | Dùng cho |
|---|---|---|
| dev-webapp-security-SecurityGroupAlbId | ALB SG ID | ALB |
| dev-webapp-security-IamInstanceProfileEc2Name | EC2 Instance Profile | EC2 |
Quan trọng: Mỗi khi thêm Output mới vào template, cập nhật file này luôn.
.claude/rules/ — Quy tắc bắt buộc
Rules là những gì Claude không bao giờ được vi phạm, bất kể user yêu cầu gì.
cfn-standards.md — Convention kỹ thuật
# Cross-stack Import Pattern
# Scalar property:
VpcId:
Fn::ImportValue: !Sub '${NetworkingStackName}-VpcId'
# List item:
SecurityGroupIds:
- Fn::ImportValue: !Sub '${SecurityStackName}-SecurityGroupEc2Id'
# !ImportValue !Sub trên cùng 1 dòng là INVALID YAML — đừng bao giờ dùng
# Tag Block Pattern
Tags:
- { Key: Name, Value: !Sub '${Environment}-webapp-{type}-{desc}' }
- { Key: Environment, Value: !Ref Environment }
- { Key: Project, Value: webapp }
- { Key: ManagedBy, Value: CloudFormation }
- { Key: Layer, Value: networking }
Resources không hỗ trợ Tags — sẽ báo lỗi E3002 nếu thêm Tags:
AWS::IAM::InstanceProfileAWS::ElasticLoadBalancingV2::ListenerAWS::EC2::VPCGatewayAttachment,Route,SubnetRouteTableAssociationAWS::Route53::RecordSet
Security Group Rule Description chỉ chấp nhận a-zA-Z0-9. _-:/()#,@[]+=&;{}!$* — không dùng em-dash —, dùng hyphen - thay thế. Vi phạm → stack CREATE_FAILED.
security-guardrails.md — Những gì không bao giờ được làm
# IAM — không bao giờ wildcard Action VÀ Resource cùng lúc:
Statement:
- Effect: Allow
Action: '*' # FORBIDDEN
Resource: '*'
# Security Groups — EC2 SG chỉ nhận traffic từ ALB SG:
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref SecurityGroupAlb # GOOD
# CidrIp: 0.0.0.0/0 # FORBIDDEN
# ALB — HTTP :80 phải redirect sang HTTPS:
DefaultActions:
- Type: redirect
RedirectConfig:
Protocol: HTTPS
StatusCode: HTTP_301
.claude/skills/ — Workflow tự động
Skills là workflow Claude tự động áp dụng khi gặp ngữ cảnh phù hợp.
new-stack/SKILL.md
# Skill: new-stack
Invoke khi user yêu cầu tạo template mới.
## Workflow
1. Xác định layer (networking/security/compute/data)
2. Tra outputs-catalog.md — stack này cần import gì?
3. Generate skeleton đúng pattern (Description, Parameters, Conditions, Outputs)
4. Áp dụng 5 tags bắt buộc lên MỌI resource
5. Check security-guardrails.md
6. Check data-protection.md nếu có stateful resource
7. Tạo parameters/dev-{layer}.json
8. Cập nhật outputs-catalog.md
## Checklist trước khi trả kết quả
- [ ] Tất cả Outputs đều có Export
- [ ] Logical IDs dùng PascalCase, không có hyphen
- [ ] Import dùng Fn::ImportValue: !Sub (không dùng !ImportValue !Sub)
- [ ] outputs-catalog.md đã cập nhật
iam-audit/SKILL.md
# Skill: iam-audit
Invoke tự động khi thấy AWS::IAM::Role, Policy, InstanceProfile trong template.
## Checklist
1. Wildcard check: Action: '*' + Resource: '*' → CRITICAL
2. Principal check: không cho Principal: '*'
3. AdministratorAccess trên EC2 role → CRITICAL
4. iam:PassRole → cần comment giải thích lý do
.claude/commands/ — Slash Commands
Commands là workflow có tên, gọi bằng /project:command-name.
deploy.md — /project:deploy
# /project:deploy
Deploy một stack qua changeset workflow — luôn review trước khi apply.
Usage: /project:deploy layer=[networking|security|compute] env=[dev|prod]
## Workflow
1. cfn-lint — validate syntax
2. Tạo changeset
3. Chờ changeset sẵn sàng
4. Hiển thị changeset → DỪNG LẠI, hỏi confirm
(Cảnh báo đặc biệt nếu có Remove hoặc Replacement: True)
5. Execute — chỉ sau khi user confirm
6. Monitor events
## Notes
Không deploy compute trước security, security trước networking.
Bắt đầu hạ tầng mới: Viết những file nào trước?
Thứ tự mình khuyến nghị:
Bước 1 — Định nghĩa project trong CLAUDE.md
Trả lời các câu hỏi:
- Hạ tầng này làm gì?
- Gồm những stack nào, thứ tự deploy?
- Account nào, region nào, profile AWS CLI là gì?
- Convention đặt tên resource, tag?
# my-api — AWS Infrastructure Project
## Stack Topology
1. {env}-my-api-networking
2. {env}-my-api-security
3. {env}-my-api-compute
## Technology Decisions
| EC2 access | SSM Session Manager |
| Database | RDS PostgreSQL |
Bước 2 — Ghi thông tin cá nhân vào CLAUDE.local.md
## AWS CLI Profile
profile: my-profile
region: ap-southeast-1
Account: 123456789012
Bước 3 — Tạo rules/ theo đặc thù của project
Copy từ project mẫu, sửa lại phần nào khác biệt. Đặc biệt chú ý:
cfn-standards.md: naming convention của project bạnsecurity-guardrails.md: thêm rule bảo mật đặc thù (VD: S3 không public, RDS không public)data-protection.md: nếu có RDS/DynamoDB, định nghĩa DeletionPolicy rõ ràng
Bước 4 — Tạo outputs-catalog.md trống, điền dần
# Outputs Catalog — my-api
## {env}-my-api-networking
| Export Name | Value | Dùng cho |
|---|---|---|
| (điền sau khi viết template) | | |
Bước 5 — Viết template đầu tiên (networking)
Lúc này bạn đã có đủ context để prompt Claude Code.
Prompt Claude Code như thế nào?
Tạo template mới
Tôi cần tạo CloudFormation template cho networking layer của project my-api.
Stack sẽ tạo: VPC (10.0.0.0/16), 2 public subnet (AZ-a, AZ-c),
2 private subnet, 1 IGW, 1 NAT GW (dev), route tables.
Đọc CLAUDE.md và rules/ trước khi viết.
Claude sẽ tự động:
- Áp dụng naming convention từ CLAUDE.md
- Check security guardrails
- Thêm đủ 5 tags
- Đề xuất cập nhật outputs-catalog.md
Deploy an toàn
/project:deploy layer=networking env=dev
Claude sẽ chạy changeset workflow, hiển thị những gì sẽ thay đổi, và hỏi confirm trước khi apply.
Thêm resource mới vào stack đã có
Tôi muốn thêm RDS PostgreSQL vào stack compute.
Instance class: db.t3.micro cho dev, db.t3.small cho prod.
Single-AZ cho dev, Multi-AZ cho prod.
Claude sẽ tự động invoke iam-audit skill nếu cần IAM, cost-guard skill để cảnh báo chi phí RDS, và data-protection rule để thêm DeletionPolicy.
Debug lỗi deploy
Stack dev-my-api-compute bị CREATE_FAILED.
Lỗi: [paste error message từ AWS Console hoặc CLI]
Paste nguyên lỗi — Claude sẽ tra cfn-standards.md và security-guardrails.md để tìm pattern phù hợp.
Những lỗi phổ biến và cách tránh
!ImportValue !Sub trên cùng một dòng
# SAI — INVALID YAML, cfn-lint báo E0000:
VpcId: !ImportValue !Sub '${NetworkingStackName}-VpcId'
# ĐÚNG:
VpcId:
Fn::ImportValue: !Sub '${NetworkingStackName}-VpcId'
Ghi vào cfn-standards.md để Claude không bao giờ lặp lại.
Em-dash trong Security Group rule Description
# SAI — AWS API reject, stack CREATE_FAILED:
Description: HTTP from ALB only — never open to internet
# ĐÚNG:
Description: HTTP from ALB only - never open to internet
Tags trên resource không hỗ trợ
AWS::IAM::InstanceProfile, AWS::ElasticLoadBalancingV2::Listener, AWS::Route53::RecordSet không có Tags property. cfn-lint báo E3002.
Profile AWS CLI và region không khớp
Nếu profile default region là ap-southeast-1 nhưng bạn deploy vào ap-northeast-1 — phải explicit --region ap-northeast-1 trên mọi AWS CLI command. Ghi vào CLAUDE.local.md để Claude nhớ.
Kết quả sau khi tổ chức đúng
Với project được tổ chức như trên, mình có thể:
- Mở conversation mới với Claude Code, không cần giải thích gì — Claude đọc CLAUDE.md và hiểu ngay context
- Gõ
/project:deploy layer=compute env=devvà nhận được changeset review đầy đủ - Thêm resource mới mà không lo quên tag, quên DeletionPolicy, hay vi phạm security guardrails
- Onboard người mới vào project chỉ bằng cách cho họ đọc
.claude/folder
Tham khảo
- 📦 Sample project trên GitHub — webapp-infra với đầy đủ 3 stacks, rules, skills, commands
- Claude Code documentation
- AWS CloudFormation Best Practices
- cfn-lint