Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 11 — Terraform Basics — Provider, Resource, State

Đã đọc 0/14 mục · còn khoảng 13 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Ở bài trước, chúng ta đã nói về Infrastructure as Code (IaC) như một triết lý: mô tả hạ tầng bằng mã nguồn thay vì click chuột. Nhưng triết lý thì hay, còn khi ngồi xuống gõ phím thì bạn cần một công cụ cụ thể. Trong bài này, chúng ta đi thẳng vào Terraform — công cụ IaC phổ biến nhất thế giới hiện nay, được dùng ở gần như mọi công ty công nghệ tại Việt Nam có hạ tầng cloud, từ startup fintech đến các ngân hàng số như Timo, Cake, hay các công ty thương mại điện tử như Tiki, Sendo.

Vì sao Terraform quan trọng đến vậy? Vì nó là "ngôn ngữ chung" giữa bạn và nhà cung cấp cloud. Thay vì học riêng cách bấm nút trên AWS Console, GCP Console, rồi Azure Portal — mỗi nơi một kiểu — bạn học một cú pháp duy nhất là HCL (HashiCorp Configuration Language) và Terraform dịch nó ra lệnh gọi API cho từng nhà cung cấp.

Trong bài này, chúng ta tập trung vào ba khái niệm nền tảng mà nếu không nắm chắc thì bạn sẽ mãi loay hoay: Provider (cầu nối tới cloud), Resource (thứ bạn muốn tạo), và State (bộ nhớ ghi lại những gì Terraform đã tạo). Nắm được ba thứ này, bạn đã hiểu 70% cách Terraform vận hành. Ba bài sau (Modules, Best Practices, và so sánh với Ansible) đều xây dựng trên nền này.

Khái niệm cốt lõi

Cấu trúc file và khối terraform

Một dự án Terraform bắt đầu từ các file có đuôi .tf. Theo quy ước, người ta thường tách thành main.tf (khai báo resource chính), variables.tf (biến đầu vào), outputs.tf (giá trị đầu ra), nhưng thực ra Terraform gộp tất cả file .tf trong cùng thư mục lại thành một cấu hình duy nhất — nên tên file chỉ để con người dễ đọc.

Khối đầu tiên bạn gần như luôn gặp là khối terraform, nơi khai báo phiên bản và các provider cần dùng:

# main.tf
terraform {
  required_version = ">= 1.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

Ở đây, required_version khóa phiên bản Terraform CLI để tránh chuyện đồng đội mỗi người dùng một phiên bản rồi sinh lỗi khó lường. required_providers khai báo rằng dự án này cần provider AWS, lấy từ registry hashicorp/aws, và giới hạn phiên bản ~> 5.0 (nghĩa là chấp nhận 5.x nhưng không nhảy sang 6.0). Toán tử ~> gọi là "pessimistic constraint" — cho cập nhật vá lỗi nhỏ nhưng chặn thay đổi lớn có thể phá vỡ cấu hình.

Provider — cầu nối tới nhà cung cấp

Provider là plugin giúp Terraform nói chuyện với một API cụ thể. AWS provider biết cách gọi API của AWS, Google provider gọi GCP, và có cả provider cho Cloudflare, GitHub, thậm chí cho Grafana hay Datadog. Sau khi khai báo trong required_providers, bạn cấu hình nó:

provider "aws" {
  region = "ap-southeast-1"  # Singapore, gần Việt Nam nhất
}

Region ap-southeast-1 (Singapore) là lựa chọn phổ biến cho các công ty Việt Nam vì độ trễ thấp và tuân thủ dữ liệu khu vực. Provider sẽ tự tìm credentials theo thứ tự: biến môi trường (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY), rồi file ~/.aws/credentials, rồi IAM role nếu chạy trên EC2. Nguyên tắc vàng: không bao giờ hardcode credentials vào file .tf vì file đó thường được commit lên Git.

Resource — thứ bạn thực sự muốn tạo

Resource là trái tim của Terraform. Mỗi resource mô tả một thành phần hạ tầng: một máy ảo, một bucket lưu trữ, một database, một record DNS. Cú pháp luôn theo dạng:

resource "<loại_resource>" "<tên_địa_phương>" {
  # thuộc tính
}

Ví dụ tạo một máy chủ EC2:

resource "aws_instance" "web_server" {
  ami           = "ami-0df7a207adb9748c7"
  instance_type = "t3.micro"

  tags = {
    Name        = "web-server-prod"
    Environment = "production"
    ManagedBy   = "terraform"
  }
}

Ở đây aws_instance là loại resource (do AWS provider cung cấp), còn web_server là tên địa phương bạn tự đặt để tham chiếu trong code. Cần phân biệt rõ: tên web_server chỉ tồn tại trong Terraform, còn Name = "web-server-prod" trong tags mới là tên hiển thị trên AWS Console.

Các resource có thể tham chiếu lẫn nhau, và đây là điểm mạnh của Terraform. Ví dụ, gắn một security group vào instance:

resource "aws_security_group" "web_sg" {
  name        = "web-server-sg"
  description = "Allow HTTP and HTTPS"

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "web_server" {
  ami                    = "ami-0df7a207adb9748c7"
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.web_sg.id]
}

Cú pháp aws_security_group.web_sg.id tạo ra một dependency ngầm (implicit dependency): Terraform hiểu rằng phải tạo security group trước, lấy ID của nó, rồi mới tạo instance. Bạn không cần tự sắp xếp thứ tự — Terraform dựng ra một đồ thị phụ thuộc (dependency graph) và tự tính toán thứ tự đúng.

State — bộ nhớ của Terraform

Đây là khái niệm khiến nhiều người mới bối rối nhất, nhưng lại quan trọng nhất. State là một file (mặc định tên terraform.tfstate, định dạng JSON) ghi lại ánh xạ giữa những gì bạn viết trong code và những gì thực sự tồn tại trên cloud.

Vì sao cần state? Hãy tưởng tượng bạn viết code tạo một EC2 instance rồi chạy terraform apply. Lần sau bạn sửa instance_type từ t3.micro thành t3.small và chạy lại. Làm sao Terraform biết là bạn muốn sửa instance cũ chứ không phải tạo mới một cái nữa? Câu trả lời nằm ở state: Terraform nhớ rằng "resource aws_instance.web_server trong code tương ứng với instance có ID i-0abc123 trên AWS". Nhờ đó nó so sánh và chỉ áp dụng thay đổi cần thiết.

Quy trình mỗi lần chạy terraform plan hay apply:

  • Đọc code hiện tại (trạng thái mong muốn).
  • Đọc state file (trạng thái Terraform tin là đang tồn tại).
  • Gọi API cloud để refresh (trạng thái thực tế).
  • So sánh và tính ra kế hoạch: tạo, sửa, hay xóa cái gì.
Vấn đề lớn nhất với state là khi làm việc nhóm. Nếu state nằm trên máy cá nhân của bạn, đồng nghiệp không thể thấy. Hai người chạy apply cùng lúc sẽ giẫm chân nhau. Giải pháp là remote state — lưu state ở nơi chia sẻ được, thường là S3 kèm khóa (locking) qua DynamoDB:

terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "prod/web/terraform.tfstate"
    region         = "ap-southeast-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

dynamodb_table cung cấp cơ chế state locking: khi một người đang apply, người khác chạy sẽ bị chặn lại với thông báo "state đang bị khóa", tránh xung đột. encrypt = true mã hóa state khi lưu — điều bắt buộc vì state có thể chứa dữ liệu nhạy cảm như mật khẩu database ở dạng plaintext.

Tình huống thực tế

Tình huống 1 — Startup fintech ở TP.HCM và bài học state trên laptop

Một startup fintech khoảng 15 người ở quận 1, TP.HCM, mới chuyển từ tạo hạ tầng thủ công sang Terraform. Bạn DevOps đầu tiên viết code rất tốt, quản lý được 40 resource trên AWS: VPC, RDS PostgreSQL, mấy con EC2, và một Application Load Balancer. Nhưng state file terraform.tfstate nằm ngay trên laptop của bạn ấy.

Ba tháng sau, laptop bị hỏng ổ cứng. State biến mất. Code thì còn trên Git, nhưng khi bạn thứ hai clone về và chạy terraform plan, Terraform không biết gì về 40 resource đang chạy — nó tưởng chưa có gì cả và lên kế hoạch tạo mới toàn bộ. Nếu chạy apply, bạn sẽ có 40 resource trùng lặp, hoặc lỗi vì tên bị trùng, RDS đã tồn tại. Đội phải mất gần một tuần dùng lệnh terraform import để nhập thủ công từng resource về state mới.

Bài học: State là tài sản sống còn, không phải file rác. Ngay từ resource đầu tiên, hãy cấu hình remote backend (S3 + DynamoDB) với versioning bật sẵn. Chi phí gần như bằng không, còn cái giá của việc mất state thì rất đắt.

Tình huống 2 — Công ty thương mại điện tử và version constraint

Một công ty thương mại điện tử tầm trung ở Hà Nội có team 6 người dùng Terraform. Họ không khóa phiên bản provider — chỉ ghi aws = { source = "hashicorp/aws" } mà không có version. Mọi thứ chạy tốt trong nhiều tháng.

Rồi một sáng thứ Hai, một bạn junior chạy terraform init trên máy mới, tự động tải về AWS provider phiên bản mới nhất — vừa nhảy một major version. Cú pháp một thuộc tính trong resource aws_s3_bucket đã thay đổi (AWS provider từng tách logic bucket ra nhiều resource riêng ở phiên bản 4.0). terraform plan báo hàng loạt thay đổi lạ, và tệ hơn, bạn junior đó apply luôn, khiến cấu hình bucket lưu ảnh sản phẩm bị xáo trộn — một số ảnh trên website mất quyền truy cập công khai trong 40 phút giữa giờ cao điểm mua sắm.

Bài học: Luôn khóa phiên bản provider bằng ~> và commit file .terraform.lock.hcl vào Git. File lock này đảm bảo mọi người trong đội dùng đúng cùng một phiên bản provider, bất kể chạy init khi nào.

Tình huống 3 — Agency dùng chung một tài khoản AWS cho nhiều khách hàng

Một agency ở Đà Nẵng làm dịch vụ, quản lý hạ tầng cho 5 khách hàng khác nhau nhưng ban đầu để tất cả state trong cùng một file với cùng một key trên S3. Kết quả: mỗi lần apply cho khách A, Terraform lại nhìn thấy resource của khách B, C, D và có nguy cơ đụng chạm. Blast radius (phạm vi ảnh hưởng khi có lỗi) quá lớn.

Họ tách lại: mỗi khách hàng một thư mục riêng, mỗi thư mục có key state riêng như client-a/terraform.tfstate, client-b/terraform.tfstate. Giờ đây một lỗi ở dự án khách A không thể lan sang khách B, và mỗi plan chỉ hiển thị đúng resource liên quan.

Bài học: Chia nhỏ state theo ranh giới hợp lý (theo môi trường, theo dịch vụ, theo khách hàng) để giảm blast radius. Một state khổng lồ chứa mọi thứ vừa chậm vừa nguy hiểm.

Hướng dẫn từng bước

Hãy cùng dựng một hạ tầng tối thiểu từ đầu để bạn chạm tay vào cả ba khái niệm.

Bước 1 — Cài đặt và chuẩn bị. Cài Terraform CLI (phiên bản 1.5 trở lên) và AWS CLI. Cấu hình credentials bằng biến môi trường, không hardcode:

export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."

Bước 2 — Tạo file main.tf với khối terraform, provider, và một resource đơn giản (một S3 bucket để tập cho an toàn, chi phí gần như bằng 0):

terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-southeast-1"
}

resource "aws_s3_bucket" "demo" {
  bucket = "vietnamcos-terraform-demo-2026"

  tags = {
    ManagedBy = "terraform"
  }
}

Bước 3 — Chạy terraform init. Lệnh này tải provider AWS về thư mục .terraform/ và tạo file .terraform.lock.hcl. Đây là bước bắt buộc trước tiên trong mọi dự án.

Bước 4 — Chạy terraform plan. Terraform sẽ hiển thị kế hoạch: dấu + nghĩa là tạo mới. Hãy đọc kỹ output này — đây là thói quen quan trọng nhất của một DevOps engineer giỏi. Đừng bao giờ apply mà chưa đọc plan.

Bước 5 — Chạy terraform apply. Terraform hỏi lại yes/no, gõ yes để xác nhận. Sau khi xong, mở file terraform.tfstate bằng editor và quan sát: bạn sẽ thấy JSON ghi lại bucket vừa tạo cùng ID thực tế của nó.

Bước 6 — Thử sửa và chạy lại. Thêm một tag mới vào bucket, chạy terraform plan lại. Lần này bạn sẽ thấy dấu ~ (sửa đổi) thay vì +. Đây chính là lúc state phát huy tác dụng: Terraform biết bucket đã tồn tại và chỉ cập nhật tag.

Bước 7 — Dọn dẹp bằng terraform destroy để xóa toàn bộ resource, tránh phát sinh chi phí ngoài ý muốn. State sẽ được cập nhật, đánh dấu các resource đã bị xóa.

Lỗi thường gặp & mẹo

Commit state hoặc credentials lên Git. Đây là lỗi kinh điển và nguy hiểm nhất. State chứa dữ liệu nhạy cảm ở dạng plaintext. Luôn thêm vào .gitignore: .tfstate, .tfstate.*, .terraform/. Ngược lại, file .terraform.lock.hcl thì nên commit.

Sửa tay trên Console rồi quên. Khi bạn vào AWS Console sửa trực tiếp một resource do Terraform quản lý, bạn tạo ra "state drift" — trạng thái thực tế khác với state. Lần plan sau Terraform sẽ đòi hoàn tác thay đổi thủ công của bạn. Nguyên tắc: khi đã dùng Terraform quản lý resource nào, đừng động vào nó bằng tay nữa.

Không dùng state locking. Nếu backend không có locking (ví dụ local state hay S3 không kèm DynamoDB), hai người apply đồng thời có thể làm hỏng state. Luôn bật locking khi làm việc nhóm.

Nhầm giữa tên địa phương và tên thật. Nhớ rằng tên trong resource "aws_instance" "web" chỉ dùng nội bộ Terraform; muốn đặt tên hiển thị trên cloud phải dùng thuộc tính như tags hoặc name.

Mẹo dùng terraform fmt và terraform validate. Chạy terraform fmt để tự động căn chỉnh code cho đẹp và nhất quán, terraform validate để bắt lỗi cú pháp trước khi plan. Đưa cả hai vào CI pipeline.

Mẹo dùng -target một cách thận trọng. Bạn có thể chạy terraform apply -target=aws_instance.web để chỉ áp dụng cho một resource, hữu ích khi debug. Nhưng đừng lạm dụng vì nó dễ làm state không đồng bộ với ý định thật.

Bài tập thực hành

  • Dựng hạ tầng cơ bản: Tạo một thư mục dự án, viết main.tf khai báo provider AWS (region ap-southeast-1) và tạo một S3 bucket. Chạy đủ chu trình init → plan → apply → destroy. Trước mỗi bước, tự hỏi: "Terraform sẽ làm gì tiếp theo?"
  • Quan sát state: Sau khi apply, mở file terraform.tfstate và tìm cho được ID thật của resource. Sửa một thuộc tính trong code, chạy plan, và xác định trong output dòng nào cho biết đây là hành động sửa (~) chứ không phải tạo mới (+).
  • Thiết lập remote backend: Tạo trước một S3 bucket và một DynamoDB table thủ công, sau đó cấu hình khối backend "s3" với locking. Chạy terraform init lại và quan sát Terraform đề nghị di chuyển state từ local lên remote.
  • Tự tạo drift: Sau khi apply, vào AWS Console đổi một tag của bucket bằng tay. Chạy terraform plan và giải thích tại sao Terraform muốn hoàn tác thay đổi đó. Đây là bài tập giúp bạn thấm khái niệm state drift.
  • Suy nghĩ thiết kế: Với công ty của bạn (hoặc một công ty giả định), hãy vẽ ra cách chia state: bao nhiêu file, phân theo tiêu chí gì (môi trường, dịch vụ), và giải thích lựa chọn dựa trên blast radius.

Tóm tắt

Trong bài này, chúng ta đã nắm được ba trụ cột của Terraform. Provider là plugin làm cầu nối tới API của nhà cung cấp cloud, được khai báo trong khối required_providers và nên khóa phiên bản bằng ~>. Resource là đơn vị hạ tầng bạn khai báo — máy ảo, bucket, database — với cú pháp resource "loại" "tên", và các resource tự động tính thứ tự phụ thuộc qua tham chiếu lẫn nhau. State là bộ nhớ ánh xạ giữa code và hạ tầng thực tế; nó cho phép Terraform biết cái gì cần tạo, sửa, xóa, và bắt buộc phải lưu ở remote backend có locking khi làm việc nhóm.

Ba tình huống thực tế cho thấy hậu quả rất thật khi lơ là: mất state vì để trên laptop, sự cố production vì không khóa phiên bản provider, và blast radius quá lớn vì gộp chung state. Thói quen cốt lõi cần rèn ngay: luôn đọc plan trước khi apply, không bao giờ commit state hay credentials, và không sửa tay resource đã do Terraform quản lý.

Ở bài tiếp theo, chúng ta sẽ nâng cấp từ những file .tf phẳng này lên Terraform Modules & Workspaces — cách đóng gói và tái sử dụng hạ tầng một cách chuyên nghiệp cho nhiều môi trường.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi