Giới thiệu
Trong bài này chúng ta sẽ tìm hiểu về một vấn đề rất quan trọng là ta xử lý thế nào khi Terraform State khác biệt với hạ tầng thực tế.
Ví dụ ta dùng Terraform để tạo hạ tầng trên AWS, sau khi Terraform tạo hạ tầng xong thì nó sẽ tạo ra tệp tin State để lưu lại trạng thái của hạ tầng. Nếu có ai đó không dùng Terraform mà truy cập thẳng lên AWS Web Console để thay đổi hạ tầng, lúc này trạng thái của hạ tầng trong tệp tin State khác với hạ tầng thực tế. Ta giải quyết vấn đề này như thế nào?
Tạo hạ tầng
Ta làm một ví dụ nhỏ là tạo EC2 và Security Group cho phép truy cập port 22 của EC2. Sau đó ta giả lập một sự thay đổi bên ngoài Terraform bằng cách dùng AWS CLI tạo thêm một SG cho phép truy cập port 80 và gán nó vào EC2.
Tạo một tệp tin tên là main.tf
với đoạn code như sau.
provider "aws" {
region = "us-west-2"
}
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
owners = ["099720109477"]
}
resource "aws_security_group" "allow_ssh" {
name = "allow-ssh"
ingress {
from_port = "22"
to_port = "22"
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0",
]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "allow-ssh"
}
}
resource "aws_instance" "server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.medium"
vpc_security_group_ids = [
aws_security_group.allow_ssh.id
]
lifecycle {
create_before_destroy = true
}
tags = {
Name = "Server"
}
}
output "ec2" {
value = aws_instance.server.id
}
Chạy câu lệnh Terraform để tạo các resource trên.
terraform init && terraform apply -auto-approve
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
instance_id = "i-082e7dcd35b327dbb"
Mở AWS Console ta sẽ thấy con EC2 vừa tạo.
Thay đổi
Tiếp theo ta dùng AWS CLI tạo Security Group và gán nó vào EC2, tạo SG.
aws ec2 create-security-group --group-name "allow-http" --description "allow http" --region us-west-2 --output text
Ta sẽ thấy SG Id được in ra terminal, nhớ sao chép giá trị đó lại.
sg-026401f9c4e93a37a
Cập nhật SG cho phép truy cập port 80.
aws ec2 authorize-security-group-ingress --group-name "allow-http" --protocol tcp --port 80 --cidr 0.0.0.0/0 --region us-west-2
Gán SG vào EC2.
current_security_groups=$(aws ec2 describe-instances --instance-ids $(terraform output -raw instance_id) --query Reservations[*].Instances[*].SecurityGroups[*].GroupId --region us-west-2 --output text)
aws ec2 modify-instance-attribute --instance-id $(terraform output -raw instance_id) --groups $current_security_groups sg-026401f9c4e93a37a --region us-west-2
Lúc này thì hạ tầng trên AWS đã khác biệt với Terraform State, ta chạy câu lệnh plan
sẽ thấy.
terraform plan
Terraform will perform the following actions:
# aws_instance.server will be updated in-place
~ resource "aws_instance" "server" {
id = "i-0531f02acb4fa3c2b"
tags = {
"Name" = "Server"
}
~ vpc_security_group_ids = [
- "sg-026401f9c4e93a37a",
# (1 unchanged element hidden)
]
# (29 unchanged attributes hidden)
# (7 unchanged blocks hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
Nếu ta chạy apply
thì Terraform sẽ quay lại hạ tầng của chúng ta về trước lúc ta thêm SG sg-026401f9c4e93a37a
vào, nhưng cái ta muốn bây giờ là cần Terraform State phải giống với hạ tầng thực tế. Ta sẽ có hai cách để làm việc này:
terraform refresh
terrform apply -refresh-only
Terraform Refresh
Các bạn đừng làm theo cách này nhé.
Cách đầu tiên là ta sử dụng câu lệnh refresh
. Khi ta chạy câu lệnh terraform refresh
thì Terraform thực hiện đọc trạng thái của hạ tầng mà nó đang quản lý, sau đó nó cập nhật lại Terraform State cho giống với hạ tầng.
terraform refresh
data.aws_ami.ubuntu: Reading...
aws_security_group.allow_ssh: Refreshing state... [id=sg-08326a64e6951fcbf]
data.aws_ami.ubuntu: Read complete after 0s [id=ami-0123376e204addb71]
aws_instance.server: Refreshing state... [id=i-0531f02acb4fa3c2b]
Outputs:
instance_id = "i-0531f02acb4fa3c2b"
Bây giờ Terraform State đã giống với hạ tầng trên AWS. Tiếp theo ta sẽ cập nhật lại code bằng tay, vì Terraform không có câu lệnh nào để ta có thể thay đổi được code trong tệp tin cấu hình giống với hạ tầng.
Cập nhật lại tệp tin main.tf
.
...
resource "aws_instance" "server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.medium"
vpc_security_group_ids = [
aws_security_group.allow_ssh.id,
"sg-026401f9c4e93a37a"
]
lifecycle {
create_before_destroy = true
}
tags = {
Name = "Server"
}
}
...
Ở chỗ vpc_security_group_ids
ta thêm SG sg-026401f9c4e93a37a
vào. Ta chạy câu lệnh plan sẽ thấy Terraform State đã giống hạ tầng hiện tại.
terraform plan
data.aws_ami.ubuntu: Reading...
aws_security_group.allow_ssh: Refreshing state... [id=sg-08326a64e6951fcbf]
data.aws_ami.ubuntu: Read complete after 0s [id=ami-0123376e204addb71]
aws_instance.server: Refreshing state... [id=i-0531f02acb4fa3c2b]
No changes. Your infrastructure matches the configuration.
Vậy là ta đã xử lý được khác biệt của Terraform State và hạ tầng thực tế. Nhưng terraform refresh
là câu lệnh cũ và không được khuyến kích sử dụng, vì khi ta chạy refresh ta không biết được resource nào đã thay đổi trong tệp tin State.
Nên từ phiên bản Terraform v0.15.4
đã có một câu lệnh mới hơn để giúp ta giải quyết vấn đề này, đó là câu lệnh refresh only
. Các bạn nên sử dụng nó trong các dự án thực tế.
Terraform Reresh Only
Giống với với câu lệnh refresh
thì refresh only
cũng thực thi đọc trạng thái của hạ tầng mà nó quản lý. Nhưng thay vì cập nhật luôn Terraform State thì nó cho phép ta thấy resource nào sẽ thay đổi và ta có chấp nhận cập nhật lại State không. Ta chạy câu lệnh refresh only
như sau.
terraform apply -refresh-only
Terraform detected the following changes made outside of Terraform since the last "terraform
apply" which may have affected this plan:
# aws_instance.server has changed
~ resource "aws_instance" "server" {
id = "i-0531f02acb4fa3c2b"
tags = {
"Name" = "Server"
}
~ vpc_security_group_ids = [
+ "sg-026401f9c4e93a37a",
# (1 unchanged element hidden)
]
# (29 unchanged attributes hidden)
# (7 unchanged blocks hidden)
}
This is a refresh-only plan, so Terraform will not take any actions to undo these. If you were
expecting these changes then you can apply this plan to record the updated values in the
Terraform state without changing any remote objects.
Would you like to update the Terraform state to reflect these detected changes?
Terraform will write these changes to the state without modifying any real infrastructure.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value:
Nó sẽ hiển thị cho ta biết là SG sg-026401f9c4e93a37a
đã được thêm vào EC2, ta có muốn cập nhật lại State cho nó giống với hạ tầng hiện tại không? Nếu các bạn gõ yes
thì nó sẽ cập nhật lại tệp tin State.
Would you like to update the Terraform state to reflect these detected changes?
Terraform will write these changes to the state without modifying any real infrastructure.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
instance_id = "i-0531f02acb4fa3c2b"
Tiếp theo thì ta cũng cần phải cập nhật lại tệp tin cấu hình bằng tay.
vpc_security_group_ids = [
aws_security_group.allow_ssh.id,
"sg-026401f9c4e93a37a"
]
Chạy câu lệnh plan
ta thấy Terraform State đã phản ánh đúng hạ tầng hiện tại. Nhưng hiện tại ta đang để giá trị của vpc_security_group_ids
là giá trị sg-026401f9c4e93a37a
. Có cách nào ta chuyển thằng này thành một resource trong tệp tin cấu hình luôn không?
Câu trả lời là có, và hiện tại chưa có công cụ nào chuyển toàn bộ hạ tầng của ta thành tệp tin cấu hình của Terraform mà hoàn hảo cả, tất cả mọi thứ ta đều phải cần làm bằng tay.
Terraform import
Để quản lý một resource của hạ tầng mà chưa có trong tệp tin Terraform, ta làm các bước sau đây:
- Khai báo cấu hình của resource đó trong tệp tin Terraform
- Dùng câu lệnh
terraform import
để import resource vào tệp tin State
Cập nhật lại tệp main.tf
thêm vào SG của sg-026401f9c4e93a37a
.
...
resource "aws_security_group" "allow_http" {
name = "allow-http"
description = "allow http"
ingress {
from_port = "80"
to_port = "80"
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0",
]
}
tags = {
Name = "allow-http"
}
}
...
Tiếp theo ta sẽ chạy câu lệnh import
.
terraform import aws_security_group.allow_http sg-026401f9c4e93a37a
aws_security_group.allow_http: Importing from ID "sg-026401f9c4e93a37a"...
aws_security_group.allow_http: Import prepared!
Prepared aws_security_group for import
aws_security_group.allow_http: Refreshing state... [id=sg-026401f9c4e93a37a]
Import successful!
Để xem cách import của các resource khác nhau thì các bạn xem trên docs của provider aws.
Tiếp theo ta cập nhật lại vpc_security_group_ids
của aws_instance.server
không cần phải fix cứng giá trị nữa.
...
vpc_security_group_ids = [
aws_security_group.allow_ssh.id,
aws_security_group.allow_http.id,
]
...
Code hoàn chỉnh.
provider "aws" {
region = "us-west-2"
profile = "kala"
}
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
owners = ["099720109477"]
}
resource "aws_security_group" "allow_ssh" {
name = "allow-ssh"
ingress {
from_port = "22"
to_port = "22"
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0",
]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "allow-ssh"
}
}
resource "aws_security_group" "allow_http" {
name = "allow-http"
description = "allow http"
ingress {
from_port = "80"
to_port = "80"
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0",
]
}
tags = {
Name = "allow-http"
}
}
resource "aws_instance" "server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.medium"
vpc_security_group_ids = [
aws_security_group.allow_ssh.id,
aws_security_group.allow_http.id,
]
lifecycle {
create_before_destroy = true
}
tags = {
Name = "Server"
}
}
output "instance_id" {
value = aws_instance.server.id
}
Giờ ta chạy câu lệnh apply
thì thấy có một số thay đổi nhỏ ở phần tag của SG, các bạn đừng quan tâm giá trị này mà cứ gõ yes
là được.
terraform apply
Terraform will perform the following actions:
# aws_security_group.allow_http will be updated in-place
~ resource "aws_security_group" "allow_http" {
id = "sg-026401f9c4e93a37a"
name = "allow-http"
+ revoke_rules_on_delete = false
~ tags = {
+ "Name" = "allow-http"
}
~ tags_all = {
+ "Name" = "allow-http"
}
# (6 unchanged attributes hidden)
# (1 unchanged block hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_security_group.allow_http: Modifying... [id=sg-026401f9c4e93a37a]
aws_security_group.allow_http: Modifications complete after 1s [id=sg-026401f9c4e93a37a]
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
Outputs:
instance_id = "i-0531f02acb4fa3c2b"
Done 😁. Nhớ chạy destroy resource
nhé.
terraform destroy -auto-approve
Kết luận
Vậy là ta đã tìm xong cách làm sao để xử lý sự khác biệt giữa Terraform State và hạ tầng thực tế. Chúng ta nên sử dụng cách refresh only
thay vì cách refresh
. Đây là bài cuối của loạt bài “Chinh phục Terraform”. Cảm ơn các bạn đã theo dõi.
Nếu bài viết có gì sai hoặc cần cập nhật thì liên hệ Admin.
Tham gia nhóm chat của DevOps VN tại Telegram.
Kém tiếng Anh và cần nâng cao trình độ giao tiếp: Tại sao bạn học không hiệu quả?