Setup CEPH cluster trên GCP trong 5' với Terraform

Gần đây tôi hay có nhu cầu setup CEPH cluster để test tích hợp với một số hệ thống khác. Việc phải setup cả cụm CEPH khá tốn công sức và thời gian vì vậy tôi nghĩ cách để đơn giản hóa và automate chuyện này.

Thứ nhất, cụm CEPH chủ yếu để test tích hợp với các hệ thống khác nên chỉ cần test tính năng chứ không cần performance cao cũng như không cần nguyên cả một cluster multi-node làm gì --> vì vậy nếu build "cluster" 1-node được là tốt nhất, vừa nhanh vừa giảm thiểu chi phí (vì tôi setup trên Public Cloud).

Thứ hai, việc dùng gcloud-cli để tạo instance chưa phải phương án tối ưu do vẫn phải gõ lệnh manual, đợi instance khởi tạo xong, boot lên, thử ssh vài lần cho tới khi vào được, cài ansible lên, download ceph playbook, sửa tham số, run ansible, đợi OSD up/in rồi check CEPH status... - một chuỗi chờ đợi và gõ lệnh manual thật mất công.
Hơn nữa khi test tích hợp nhiều khi phải setup lại nhiều lần để tạo môi trường clean, dẫn đến lượng công sức bỏ ra để lặp lại nhiều lần manual như trên khá lớn và nhàm chán.

Vậy giải pháp ở đây là gì? Terraform!

Terraform là một công cụ được dùng để provisioning các instance, disk, network,... hay nói chung là các cloud-resources trên các hạ tầng Cloud phổ biến như AWS, GCE, Azure, OpenStack, DO, v.v.. (Các cloud providers này không chỉ giới hạn trong các IaaS Cloud mà còn có thể các service/provider bất kỳ "dạng cloud"). Terraform còn hỗ trợ việc setup các instances sau khi khởi tạo bằng cơ chế provisioner. Với cơ chế provisioner này, ta có thể setup các instances sau khi khởi tạo bằng các Configuration Management tool phổ biến như Ansible, Chef, Puppet, hoặc đơn giản chỉ là chạy một shell script định sẵn.

Để bắt đầu hãy download Terraform về và cài đặt https://www.terraform.io/downloads.html Quy trình thông thường khi setup một hệ thống với Terraform là viết các file config -> kiểm tra bằng "$ terraform plan" -> thực thi bằng "$ terraform apply"

Các file config Terraform mà chúng ta viết cần có đuôi .tf và được đặt trong cùng một thư mục, khi chạy lệnh terraform sẽ đọc các file config này trong thư mục hiện tại. Với yêu cầu như đầu bài tôi đã viết một số file Terraform config như sau:

 ~/repo/ceph-soledad-terraform
$ tree              
.
├── main.tf
├── output.tf
├── provider.tf
├── scripts
│   └── ceph-one-node-install.sh
├── terraform.tfvars
└── variables.tf

1 directory, 6 files  

Đây là một cấu trúc và naming được dùng phổ biến khi viết Terraform config mặc dù về nguyên tắc thì bạn có thể viết tất cả config vào chỉ một file .tf và đặt tên file bất kỳ. Tiếp theo tôi sẽ giải thích nội dung của từng file.

Trong file provider.tf ta khai báo các nội dung liên quan đến cloud-provider được sử dụng để Terraform biết phải tạo các resource trên cloud nào, ở đâu, làm thế nào để tạo (authN? authZ?), v.v...:

$ cat provider.tf 
provider "google" {  
  region      = "${var.region}"
  project     = "${var.project_name}"
  credentials = "${file("${var.credentials_file_path}")}"
}

Ở đây các thông số region/project/credentials không được config trực tiếp mà được đọc từ các biến; các biến này được khai báo tập trung trong file variables.tf:

$ cat variables.tf 
variable "region" {  
  default = "us-central1"
}

variable "region_zone" {  
  default = "us-central1-c"
}

variable "project_name" {  
  description = "The ID of the Google Cloud project"
}

variable "credentials_file_path" {  
  description = "Path to the JSON file used to describe your account credentials"
  default     = "~/.gcloud/service-account-Terraform.json"
}

variable "private_key_path" {  
  description = "Path to file containing private key"
  default     = "~/.ssh/id_rsa"
}

File variables.tf này định nghĩa các biến cần khai báo, ý nghĩa, giá trị mặc định nếu có; mục đích là để nếu ta share config thì người khác cũng biết phải thay đổi những biến gì và giá trị recommend ra sao.
Để set giá trị cho các biến này ta dùng file terraform.tfvars - lưu ý rằng đây là một file đặc biệt đối với Terraform nên bạn sẽ không thể dùng tên tùy ý như với các file *.tf khác:

$ cat terraform.tfvars 
region = "asia-east1"  
region_zone = "asia-east1-b"  
project_name = "<my-project-name>"  
credentials_file_path = "</path/to/my/service-account.json>"  
private_key_path = "~/.ssh/id_rsa.passwordless"  

Ở đây SSH private-key của tôi đã được password-protected nên để tiện cho Terraform có thể dùng provisioner để setup trong instance sau khi tạo xong thì tôi cần convert SSH private-key sang dạng passwordless:
$ openssl rsa -in ~/.ssh/id_rsa -out ~/.ssh/id_rsa.passwordless

Tiếp theo là file config cho các resources sẽ được khởi tạo: (có diễn giải bên dưới)

$ cat main.tf
variable "osd_count" {  
  default = "3"
}

resource "google_compute_disk" "osd" {  
  count = "${var.osd_count}"
  name  = "osd-disk-${count.index}"
  type  = "pd-ssd"
  zone  = "${var.region_zone}"
  size  = 10
}

resource "google_compute_instance" "ceph" {  
  name         = "ceph-soledad"
  machine_type = "g1-small"
  zone         = "${var.region_zone}"

  tags = ["ceph", "testing"]

  disk {
    image = "ubuntu-os-cloud/ubuntu-1604-xenial-v20170619a"
  }

  disk {
    disk = "${google_compute_disk.osd.0.name}"
    // a unique device_name that will be reflected into the /dev/disk/by-id/google-* tree
    // of a Linux operating system running within the instance
    // I will use it for predictable/determined result. Thanks Google for this wise!
    device_name = "osd-0"
    auto_delete = false
  }
  disk {
    disk = "${google_compute_disk.osd.1.name}"
    device_name = "osd-1"
    auto_delete = false
  }
  disk {
    disk = "${google_compute_disk.osd.2.name}"
    device_name = "osd-2"
    auto_delete = false
  }

  network_interface {
    network = "default"

    access_config {
      // Empty block will generate ephemeral IP for floatingIP.
      // If this block is omit, instance will not accessible from internet.
    }
  }

  scheduling {
    // Using it for reduced-price because i just lab.
    // DON'T USE IT ON PRODUCTION SYSTEM !!
    preemptible = true
  }

  provisioner "remote-exec" {
    script = "scripts/ceph-one-node-install.sh"

    connection {
      type     = "ssh"
      user     = "ubuntu"
      private_key = "${file("${var.private_key_path}")}"
      agent    = false
     }

  }

}

Ở đây tôi tạo 1 instance tên là ceph-soledad với OS@rootdisk là Ubuntu 16.04 và gắn thêm 3 persistent-disk loại SSD, dung lượng mỗi disk là 10GB; các disk này được đặt tên là osd-1, osd-2, osd-3 - GCP có cơ chế để các disk này có thể được tham chiếu trong instance qua /dev/disk/by-id/google-<device_name> (bản chất chỉ là các symlink tới /dev/sdX), điều này cung cấp một cơ chế tất định và dễ dàng hơn khi chúng ta (CEPH) thao tác trên các disk được attach thêm vào instance. Instance ceph-soledad được connect vào network có name default và ta request GCP cấp ephemeral/dynamic IP WAN cho nó (IP sẽ bị xóa khi ta xóa instance).
Instance được scheduling @GCP với preemptible mode để tiết kiệm chi phí (instance có thể bị terminate bất kỳ lúc nào nếu GCP cần thu hồi tài nguyên cho instance khác - nhưng không vấn đề gì vì đây là hệ thống Lab)
Như hình dưới ta thấy giá sẽ rẻ hơn khoảng 60% nếu dùng preemptible mode
Provisioner ở đây ta dùng kiểu "remote-exec" - chỉ đơn thuần copy và chạy một file script để setup CEPH; nội dung script này như sau:

$ cat scripts/ceph-one-node-install.sh
#!/bin/bash
# For easy debugging on console output
set -x

RELEASE=${1:debian-kraken}  
# Creating a directory based on timestamp... not unique enough
mkdir -p ~/ceph-deploy/install-$(date +%Y%m%d%H%M%S) && cd $_

#Install ceph key
wget -q -O- 'https://download.ceph.com/keys/release.asc' | sudo apt-key add -a

#install ceph by pointing release repo to your Ubuntu sources list.
echo deb http://download.ceph.com/debian-kraken/ "$(lsb_release --codename --short)" main | sudo tee /etc/apt/sources.list.d/ceph.list  
#Check & remove existing ceph setup
ceph-remove () {  
ceph-deploy purge $HOST  
ceph-deploy purgedata $HOST  
ceph-deploy forgetkeys  
}

#Ready to update & install ceph-deploy
sudo apt-get update && sudo apt-get install -y ceph-deploy

#Deploy ceph
HOST=$(hostname -s)  
#ceph-remove
ceph-deploy new $HOST

#Add below lines into ceph.conf, pool size for number of replicas of data
#Chooseleaf is required to tell ceph we are only a single node and that it’s OK to store the same copy of data on the same physical node
cat <<EOF >> ceph.conf  
osd pool default size=2  
osd crush chooseleaf type = 0  
EOF  
#Time to install ceph
ceph-deploy install $HOST

#Create Monitor
ceph-deploy mon create-initial

#Create OSD & OSD with mounted drives /dev/sdb /dev/sdc /dev/sdd or /dev/disk/by-id/google-*
ceph-deploy osd prepare $HOST:/dev/disk/by-id/google-osd-0 $HOST:/dev/disk/by-id/google-osd-1 $HOST:/dev/disk/by-id/google-osd-2  
ceph-deploy osd activate $HOST:/dev/disk/by-id/google-osd-0-part1 $HOST:/dev/disk/by-id/google-osd-1-part1 $HOST:/dev/disk/by-id/google-osd-2-part1

#Redistribute config and keys
ceph-deploy admin $HOST

#Read permission to read keyring
sudo chmod +r /etc/ceph/ceph.client.admin.keyring

#Wait up to 45 seconds for ceph HEALTH_OK
sleep 45

#Here we go, check ceph health
ceph -s  

File config cuối cùng là output.tf, Terraform sẽ dựa vào nội dung file này để hiện thị các thông tin mà ta muốn biết về cloud-resources mới được tạo ra (sau khi cả quá trình provisioning đã diễn ra xong):

$ cat output.tf 
output "wan_ip" {  
  value = "${google_compute_instance.ceph.network_interface.0.access_config.0.assigned_nat_ip}"
}

Ở đây tôi config Terraform chỉ output ra 1 info duy nhất là IP WAN của instance.

Ok, các bước config đã xong, tiếp theo ta sẽ chạy terraform plan:

$ terraform plan
+ google_compute_disk.osd.0
[...]
+ google_compute_instance.ceph
    can_ip_forward:                                      "" => "false"
    disk.#:                                              "" => "4"
    disk.0.auto_delete:                                  "" => "true"
    disk.0.image:                                        "" => "ubuntu-os-cloud/ubuntu-1604-xenial-v20170619a"
    disk.1.auto_delete:                                  "" => "false"
    disk.1.device_name:                                  "" => "osd-0"
    disk.1.disk:                                         "" => "osd-disk-0"
    disk.2.auto_delete:                                  "" => "false"
    disk.2.device_name:                                  "" => "osd-1"
    disk.2.disk:                                         "" => "osd-disk-1"
    disk.3.auto_delete:                                  "" => "false"
    disk.3.device_name:                                  "" => "osd-2"
    disk.3.disk:                                         "" => "osd-disk-2"
    machine_type:                                        "" => "g1-small"
    metadata_fingerprint:                                "" => "<computed>"
    name:                                                "" => "ceph-soledad"
    network_interface.#:                                 "" => "1"
    network_interface.0.access_config.#:                 "" => "1"
    network_interface.0.access_config.0.assigned_nat_ip: "" => "<computed>"
    network_interface.0.address:                         "" => "<computed>"
    network_interface.0.name:                            "" => "<computed>"
    network_interface.0.network:                         "" => "default"
    scheduling.#:                                        "" => "1"
    scheduling.0.preemptible:                            "" => "true"
    self_link:                                           "" => "<computed>"
    tags.#:                                              "" => "2"
    tags.3908262406:                                     "" => "testing"
    tags.714940070:                                      "" => "ceph"
    tags_fingerprint:                                    "" => "<computed>"
    zone:                                                "" => "asia-east1-b"

Plan: 4 to add, 0 to change, 0 to destroy.  

Bước validate ok không báo lỗi gì, các planned-actions đã như mong muốn. Tiếp theo ta chạy terraform apply để thực sự apply các actions này:

$ terraform apply
[...]
google_compute_instance.ceph: Still creating... (6m0s elapsed)  
google_compute_instance.ceph (remote-exec): + ceph -s  
google_compute_instance.ceph (remote-exec):     cluster 3a927134-b9e2-41f2-8f6b-bb4ef838fbaa  
google_compute_instance.ceph (remote-exec):      health HEALTH_OK  
google_compute_instance.ceph (remote-exec):      monmap e1: 1 mons at {ceph-soledad=10.140.0.2:6789/0}  
google_compute_instance.ceph (remote-exec):             election epoch 3, quorum 0 ceph-soledad  
google_compute_instance.ceph (remote-exec):      osdmap e12: 3 osds: 2 up, 2 in  
google_compute_instance.ceph (remote-exec):             flags sortbitwise,require_jewel_osds  
google_compute_instance.ceph (remote-exec):       pgmap v18: 64 pgs, 1 pools, 0 bytes data, 0 objects  
google_compute_instance.ceph (remote-exec):             69564 kB used, 10150 MB / 10217 MB avail  
google_compute_instance.ceph (remote-exec):                   64 active+clean  
google_compute_instance.ceph: Creation complete

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

The state of your infrastructure has been saved to the path  
below. This state is required to modify and destroy your  
infrastructure, so keep it safe. To inspect the complete state  
use the `terraform show` command.

State path: terraform.tfstate

Outputs:

  wan_ip = 35.187.152.151

Đến đây quá trình setup CEPH cluster đã hoàn tất. Khi không có nhu cầu sử dụng các cloud-resources này nữa, ta có thể clean đi bằng lệnh terraform destroy.


Câu hỏi: Tại sao lại dùng Terraform để provisioning các cloud-resources trong khi các tool như Ansible/Chef/Puppet/... cũng có thể thực hiện được?

Trả lời ngắn gọn hơn thì Terraform có độ tin cậy cao hơn do nó sử dụng file terraform.tfstate* để lưu trữ trạng thái của infra đồng thời có cú pháp vừa đơn giản, sáng sủa, vừa linh hoạt, do được optimize cho việc quản lý các cloud-resources. Terraform cũng tối ưu hóa thực thi và quản lý dependency các tác vụ CRUD trên cloud-resources bằng graph theory giúp cho việc thực hiện các tác vụ nhanh và tin cậy hơn. Bằng việc lưu trữ trạng thái hiện tại của infra vào file terraform.tfstate (file này được tạo ra sau khi chạy terraform apply), nó có thể được đẩy vào Git repo hoặc vào một State-storage backend như Consul/Etcd/... - giúp [team-]collaboration/[processes-]interoperate tốt và tin cậy hơn.

Nếu bạn muốn một câu trả lời sâu sắc và đầy đủ hơn, hãy đọc thêm ở đây.