+19

Terraform Series - Bài 12 - Ansible with Terraform

Giới thiệu

Chào các bạn tới với series về Terraform, ở bài trước chúng ta đã tìm hiểu về A/B Testing Deployment. Ở bài này chúng ta sẽ tìm hiểu về một chủ đều khá thú vị là sử dụng kết hợp Terraform với Ansible.

image.png

Problem

Trước khi tìm hiểu về cách dùng Ansible trong Terraform, ta sẽ nói qua một vấn đề mà ta sẽ gặp khi ta xài Terraform, đó là sau khi ta tạo hạ tầng bằng Terraform xong rồi thì làm sao ta có thể config cho nó? Ví dụ là khi bạn dùng Terraform để tạo EC2 trên AWS xong, ta muốn cài những thứ mà ta hay xài là nano, net-tools, docker thì ta làm thế nào?

Đối với EC2 thì ta có thể xài user_data, nhưng nếu ta xài user_data thì cho dù đoạn script trong user_data đó có chạy thành công hay thất bại thì Terraform cũng báo lại cho ta là resource EC2 đã tạo thành công. Nhưng thứ ta muốn lại là ta tạo EC2 xong, ta chắc chắn rằng tất cả đoạn script mà ta cần chạy trên con EC2 đó thành công hết thì Terraform mới báo lại cho ta là đã tạo resource thành công.

Để giải quyết vấn đề đó thì Terraform sinh ra tính năng tên là Provisioners.

Provisioners

Provisioners là một tính năng cho phép ta thực thi đoạn script ở dưới máy local, hoặc chạy đoạn script ở trên remote resource. Thường được sử dụng để cấu hình hạ tầng sau khi hạ tầng được tạo ra. Provisioners có hai loại là:

  • local-exec: dùng để chạy script ở dưới máy local mà Terraform đang chạy, ta sẽ dùng này để chạy Ansible.
  • remote-exec: dùng để chạy script ở máy remote. Ví dụ ta tạo EC2 xong thì ta sẽ dùng remote-exec để chạy đoạn script ở trên con EC2 mới tạo.

Ví dụ ta sẽ dùng remote-exec để cài Apache HTTP Server lên con EC2, tạo một file tên là main.tf.

provider "aws" {
  region = "us-west-2"
}

Tiếp theo, ta tạo SSH key pair cho con EC2 ta cần tạo.

provider "aws" {
  region = "us-west-2"
}

resource "tls_private_key" "key" {
  algorithm = "RSA"
}

resource "aws_key_pair" "key_pair" {
  key_name   = "ansible-key"
  public_key = tls_private_key.key.public_key_openssh
}

Cấu hình Security Group cho phép ssh vào EC2.

...
resource "aws_security_group" "allow_ssh" {
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    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"]
  }
}

Tạo EC2.

...
data "aws_ami" "ami" {
  most_recent = true

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-2.0.*-x86_64-gp2"]
  }

  owners = ["amazon"]
}

resource "aws_instance" "ansible_server" {
  ami                    = data.aws_ami.ami.id
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.allow_ssh.id]
  key_name               = aws_key_pair.key_pair.key_name

  tags = {
    Name = "Apache Server"
  }
}

Oke, giờ ta sẽ dùng remote-exec như sau.

...
resource "aws_instance" "ansible_server" {
  ami                    = data.aws_ami.ami.id
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.allow_ssh.id]
  key_name               = aws_key_pair.key_pair.key_name

  provisioner "remote-exec" {
    inline = [
      "sudo yum update -y",
      "sudo yum install -y httpd.x86_64",
      "sudo systemctl start httpd",
      "sudo systemctl enable httpd"
    ]

    connection {
      type        = "ssh"
      user        = "ec2-user"
      private_key = tls_private_key.key.private_key_pem
      host        = self.public_ip
    }
  }

  tags = {
    Name = "Apache Server"
  }
}

Ở trên ta xài provisioner với loại remote-exec, để provisioner có thể kết nối được với máy remote, ta cần phải cấu hình authentication cho nó ở block connection.

provisioner "remote-exec" {
    ...

    connection {
      type        = "ssh"
      user        = "ec2-user"
      private_key = tls_private_key.key.private_key_pem
      host        = self.public_ip
    }
}

Block inline sẽ chứa những câu lệnh ta cần thực thi ở máy remote, ở trên là những câu cli dùng để cài apache server.

provisioner "remote-exec" {
    inline = [
      "sudo yum update -y",
      "sudo yum install -y httpd.x86_64",
      "sudo systemctl start httpd",
      "sudo systemctl enable httpd"
    ]

    ...
}

Giờ ta chạy câu lệnh init và apply nào.

$ terraform init && terraform apply

Lúc này bạn sẽ thấy sau khi EC2 được tạo ra, terraform sẽ kết nối tới nó và chạy các câu CLI, sau đó nếu các câu lệnh chạy xong hết thì terraform mới báo lại là EC2 đã được tạo thành công.

...
aws_instance.ansible_server: Provisioning with 'remote-exec'...
aws_instance.ansible_server (remote-exec): Connecting to remote host via SSH...
aws_instance.ansible_server (remote-exec):   Host: 35.86.209.174
aws_instance.ansible_server (remote-exec):   User: ec2-user
aws_instance.ansible_server (remote-exec):   Password: false
aws_instance.ansible_server (remote-exec):   Private key: true
aws_instance.ansible_server (remote-exec):   Certificate: false
aws_instance.ansible_server (remote-exec):   SSH Agent: false
aws_instance.ansible_server (remote-exec):   Checking Host Key: false
aws_instance.ansible_server (remote-exec):   Target Platform: unix
aws_instance.ansible_server (remote-exec): Connected!
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

ec2 = "44.235.74.32"

Oke, ta đã sử dụng provisioner để cấu hình cho EC2 thành công 😁. Nếu ta EC2 của ta chỉ cần cấu hình đơn giản thì ta chỉ cần dùng remote-exec để chạy mấy câu CLI đơn giản như vậy là được, nhưng nếu EC2 của ta cần cấu hình phức tạp hơn nhiều thì ta không thể chỉ sử dụng mấy câu CLI được, mà ta cần dùng công cụ gọi là Configuration Management.

Ansible

Khi ta dùng Terraform, ta chỉ sử dụng nó cho việc provisioning hạ tầng, còn việc cấu hình hạ tầng thì ta không nên dùng Terraform vì đó không phải lĩnh vực của nó, mà ta nên dùng configuration management. Trong những công cụ configuration management thì có lẽ Ansible là được sử dụng rộng rãi nhất. Mô hình phổ biến như sau.

Để sử dụng Ansible trong Terraform, đầu tiên ta phải dùng remote-exec để cài Ansible lên trên remote server, sau đó ta sẽ dùng local-exec để thực thi Ansible playbook ở dưới máy local.

Ví dụ ta tạo EC2 và dùng Ansible để cài Nginx lên trên nó, tạo hai file tên là main.tfplaybook.yaml.

provider "aws" {
  region  = "us-west-2"
}

resource "tls_private_key" "key" {
  algorithm = "RSA"
}

resource "local_sensitive_file" "private_key" {
  filename        = "${path.module}/ansible.pem"
  content         = tls_private_key.key.private_key_pem
  file_permission = "0400"
}

resource "aws_key_pair" "key_pair" {
  key_name   = "ansible"
  public_key = tls_private_key.key.public_key_openssh
}

resource "aws_security_group" "allow_ssh" {
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    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"]
  }
}

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_instance" "ansible_server" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.allow_ssh.id]
  key_name               = aws_key_pair.key_pair.key_name

  provisioner "remote-exec" {
    inline = [
      "sudo apt update -y",
      "sudo apt install -y software-properties-common",
      "sudo apt-add-repository --yes --update ppa:ansible/ansible",
      "sudo apt install -y ansible"
    ]

    connection {
      type        = "ssh"
      user        = "ubuntu"
      private_key = tls_private_key.key.private_key_pem
      host        = self.public_ip
    }
  }

  tags = {
    Name = "Ansible Server"
  }
}

output "ec2" {
  value = aws_instance.ansible_server.public_ip
}

Code ở trên cũng tương tự khi nãy, chỉ khác ở một chỗ là ta thêm vào một resource là local_sensitive_file, dùng để output ra file pem mà ta sẽ dùng nó cho Ansible.

...
resource "local_sensitive_file" "private_key" {
  filename        = "${path.module}/ansible.pem"
  content         = tls_private_key.key.private_key_pem
  file_permission = "0400"
}
...

Sau đó để xài ansible, ta dùng local-exec như sau.

...
resource "aws_instance" "ansible_server" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.allow_ssh.id]
  key_name               = aws_key_pair.key_pair.key_name

  provisioner "remote-exec" {
    inline = [
      "sudo apt update -y",
      "sudo apt install -y software-properties-common",
      "sudo apt-add-repository --yes --update ppa:ansible/ansible",
      "sudo apt install -y ansible"
    ]

    connection {
      type        = "ssh"
      user        = "ubuntu"
      private_key = tls_private_key.key.private_key_pem
      host        = self.public_ip
    }
  }

  provisioner "local-exec" {
    command = "ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -u ubuntu --key-file ansible.pem -T 300 -i '${self.public_ip},', playbook.yaml"
  }

  tags = {
    Name = "Ansible Server"
  }
}

Đây là đoạn ta sẽ thực thi Ansible.

provisioner "local-exec" {
    command = "ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -u ubuntu --key-file ansible.pem -T 300 -i '${self.public_ip},', playbook.yaml"
}

Nhớ là dưới máy ta cần cài Ansible trước nhé. Sau đó ta cập nhật file playbook.yaml là chứa code của Ansible.

- name: Install Nginx
  hosts: all
  become: true
  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
    - name: Add index page
      template:
        src: index.html
        dest: /var/www/html/index.html
    - name: Start Nginx
      service:
        name: nginx
        state: started

Nếu bạn chạy ansible trên centos thì sửa chỗ apt thành yum nhé. Tiếp theo ta tạo một file index.html để Ansible copy nó lên trên server.

<!DOCTYPE html>
<html>
  <style>
    body {
      background-color: green;
      color: white;
    }
  </style>
  <body>
    <h1>Ansible</h1>
  </body>
</html>

Giờ ta chạy câu lệnh init và apply nào.

terraform init && terraform apply

Bạn sẽ thấy local-exec sẽ thực thi ansbile.

...
aws_instance.ansible_server: Provisioning with 'local-exec'...
aws_instance.ansible_server (local-exec): Executing: ["/bin/sh" "-c" "ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -u ubuntu --key-file ansible.pem -T 300 -i '35.87.91.3,', playbook.yaml"]

aws_instance.ansible_server (local-exec): PLAY [Install Nginx] ***********************************************************

aws_instance.ansible_server (local-exec): TASK [Gathering Facts] *********************************************************
aws_instance.ansible_server: Still creating... [1m30s elapsed]
aws_instance.ansible_server (local-exec): ok: [35.87.91.3]

aws_instance.ansible_server (local-exec): TASK [Install Nginx] ***********************************************************
aws_instance.ansible_server: Still creating... [1m40s elapsed]
aws_instance.ansible_server: Still creating... [1m50s elapsed]
aws_instance.ansible_server (local-exec): changed: [35.87.91.3]

aws_instance.ansible_server (local-exec): TASK [Add index page] **********************************************************
aws_instance.ansible_server: Still creating... [2m0s elapsed]
aws_instance.ansible_server (local-exec): changed: [35.87.91.3]

aws_instance.ansible_server (local-exec): TASK [Start Nginx] *************************************************************
aws_instance.ansible_server (local-exec): ok: [35.87.91.3]

aws_instance.ansible_server (local-exec): PLAY RECAP *********************************************************************
aws_instance.ansible_server (local-exec): 35.87.91.3                 : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

aws_instance.ansible_server: Creation complete after 2m7s [id=i-0fd0e63361c597de1]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

ec2 = "35.87.91.3"

Sau khi nó chạy xong thì truy cập vào IP của EC2 ta vừa tạo, bạn sẽ thấy nginx server đang host file index.html của ta.

image.png

Oke, vậy là ta đã kết hợp được Terraform và Ansible 😁.

Github của toàn bộ series https://github.com/hoalongnatsu/terraform-series.

Deep into Provisioners

Ta sẽ nói rõ hơn về provisioners một chút, do bài này tiêu đề là về Terraform với Ansible mà nói lý thuyết nhiều quá chắc các bạn sẽ chán, nên mình làm ví dụ phần chính trước rồi sẽ nói kĩ phần lý thuyết sau, ai cần sẽ đọc để đi phỏng vấn thôi :)))).

Creation-Time and Destruction-time

Ở trên ta đã dùng provisioner để thực thi các câu CLI, và hầu hết nó đều chạy ở lúc terraform tạo resource, và provisioner cũng có thể cấu hình để chạy lúc terraform destroy resource để ta clear up server. Provisioner sẽ được thực thi ở hai lifecycle sau:

  • Creation-time provisioners.
  • Destruction-time provisioners.

Với creation-time provisioners thực thi lúc resource được tạo ra và destruction-time provisioners thực thi lúc resouce được xóa đi.

Ví dụ:

resource "google_project_service" "enabled_service" {
  for_each = toset(local.services)
  project  = var.project_id
  service  = each.key

  provisioner "local-exec" {
    command = "sleep 60"
  }

  provisioner "local-exec" {
    when    = destroy
    command = "sleep 15"
  }
}

Creation-time provisioners.

image.png

Destruction-time provisioners.

image.png

Với destruction-time provisioners thì ta sẽ thêm trường when = destroy vào, lúc này terraform sẽ hiểu ta cần chạy provisioner này ở lúc destroy resource.

Failure Behavior

Nếu provisioner của ta chạy thất bại thì sao? Mặc định nếu ta dùng provisioner thì khi nó chạy thất bại, resource của ta sẽ bị đánh là fail luôn. Ta có thể cấu hình nếu provisioner thất bại thì terraform sẽ xem resource đó là thất bại luôn, hay là resource đó vẫn được terraform đánh là tạo ra thành công và chạy tiếp.

Ta sẽ dùng thuộc tính on_failure của provisioner để cấu hình việc này, nó có hai giá trị là continuefail, mặc định là fail. Ví dụ, ta cấu hình như sau để khi provisioner có chạy thất bại thì terraform vẫn coi nó là thành công.

resource "aws_instance" "web" {
  ...

  provisioner "local-exec" {
    command    = "echo The server's IP address is ${self.private_ip}"
    on_failure = continue
  }
}

Kết luận

Vậy là ta đã tìm hiểu xong về provisioner và cách dùng nó đẻ kết hợp Terraform với Ansible 😁. Nếu có thắc mắc hoặc cần giải thích rõ thêm chỗ nào thì các bạn có thể hỏi dưới phần comment.

Mục tìm kiếm đồng đội

Hiện tại thì công ty bên mình, là Hoàng Phúc International, với hơn 30 năm kinh nghiệm trong lĩnh vực thời trang. Và sở hữu trang thương mại điện tử về thời trang lớn nhất Việt Nam. Team công nghệ của HPI đang tìm kiếm đồng đội cho các vị trí như:

Với mục tiêu trong vòng 5 năm tới về mảng công nghệ là:

  • Sẽ có trang web nằm trong top 10 trang web nhanh nhất VN với 20 triệu lượt truy cập mỗi tháng.
  • 5 triệu loyal customers và có hơn 10 triệu transactions mỗi năm.

Team đang xây dựng một hệ thống rất lớn với rất nhiều vấn đề cần giải quyết, và sẽ có rất nhiều bài toán thú vị cho các bạn. Nếu các bạn có hứng thú trong việc xây dựng một hệ thống lớn, linh hoạt, dễ dàng mở rộng, và performance cao với kiến trúc microservices thì hãy tham gia với tụi mình.

Nếu các bạn quan tâm hãy gửi CV ở trong trang tuyển dụng của Hoàng Phúc International hoặc qua email của mình nha [email protected]. Cảm ơn các bạn đã đọc.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.