第 11 章:設定伺服器環境與程式
佈建伺服器方式
- 使用客製化 AMI
- 使用 packer 服務,或是在雲端服務直接建立客製化 AMI
 
- 使用標準 AMI 後透過指令安裝相關程式
- file upload
- exec
- Chef, Puppet, Ansible
- user data in cloud service
 
簡單的網頁伺服器
一台簡單的網頁伺服器需要的工作:
- 建立一個虛擬網路
- 切出一個網段
- 開啟一個虛擬機
- 放到 SSH 公鑰
- 設定防火牆
- 安裝 nginx 做為網頁服務引擎
要安裝 nginx,的方法有很多,可以登入伺服器,下指令安裝。但是這樣不符合,IaC 的精神。
我們來試試 Terraform 的佈建器 (provisioner)
佈建器 provisioner
用佈建器來完成在伺服器上安裝 nginx 這件工作。我們會用到兩個區塊,connction 跟 provisioner 區塊,兩個都要放在 aws_instance 裡面。
首先是 connection 區塊,常用的引數 (Arguments) 有:
- type: 支援 ssh跟winrm
- user: 建立連線所使用的使用者
- host: 伺服器的 IP
- private_key: ssh 私鑰
再來是 provisioner 區塊,有分 file, local-exec 跟 remote-exec 三種。
provisioner example
resource "aws_key_pair" "edward-key"{
  ami = "$(lookup(var.AMIS,var.AWS_REGION))"
  instance_type = "t2.micro"
  key_name = "$(aws_keypair.mykey.key_name)"
  provisioner "file"{
    source = "script.sh"
    destination = "/opt/script.sh"
    connection{
      user = "${var.instance_username}"
      private_key = "${file({var.path_to_private_key})}"
    }
  }
  provisioner "remote-exec"{
    inline = [
      "chmod +x /opt/script.sh",
      "/opt/script.sh arguments"
    ]
  }
}
我們要在伺服器執行一些指令,使用的是 remote-exec,主要的引數:
- inline: 指令清單
完整範例
把輸入變數 (Input Variables) 跟輸出值 (Output Values) 加入組態檔,並使用佈建器 (provisioner) 來安裝服務。
完成的檔案清單:
- variables.tf
- main.tf
- outputs.tf
- terraform.tfvars
variables.tf
variable "aws_region" {
  type        = string
  description = "AWS region to launch servers."
  default     = "ap-northeast-1"
}
variable "cidr" {
  type        = string
  description = "vpc cidr block"
}
variable "public_subnet" {
  type        = string
  description = "public subnet cidr block"
}
variable "public_key_path" {
  type        = string
  description = "Path to SSH public key"
  default     = "~/.ssh/id_rsa.pub"
}
variable "private_key_path" {
  type        = string
  description = "Path to SSH private key"
  default     = "~/.ssh/id_rsa"
}
variable "ami" {
  type        = string
  description = "ami id"
}
variable "my_ip" {
  type        = string
  description = "my ip to allow ssh connection"
}
main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.5.0"
    }
  }
}
provider "aws" {
  profile = "default"
  region  = "ap-northeast-1"
}
resource "aws_vpc" "this" {
  cidr_block = var.cidr
}
resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id
}
resource "aws_route" "internet_access" {
  route_table_id         = aws_vpc.this.main_route_table_id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.this.id
}
resource "aws_subnet" "this" {
  vpc_id                  = aws_vpc.this.id
  cidr_block              = var.public_subnet
  map_public_ip_on_launch = true
}
resource "aws_security_group" "ssh" {
  name        = "ssh"
  description = "sg for ssh incoming"
  vpc_id      = aws_vpc.this.id
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [format("%s/32", var.my_ip)]
  }
}
resource "aws_security_group" "web" {
  name        = "web"
  description = "sg for web incoming"
  vpc_id      = aws_vpc.this.id
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  # outbound internet access
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
resource "aws_key_pair" "practice" {
  key_name   = "practice"
  public_key = file(var.public_key_path)
}
resource "aws_instance" "web" {
  ami           = var.ami
  instance_type = "t2.micro"
  key_name = aws_key_pair.practice.id
  vpc_security_group_ids = [
    aws_security_group.ssh.id,
    aws_security_group.web.id,
  ]
  subnet_id = aws_subnet.this.id
  connection {
    type        = "ssh"
    user        = "ubuntu"
    host        = self.public_ip
    private_key = file(var.private_key_path)
  }
  provisioner "remote-exec" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get install -y nginx",
    ]
  }
}
output.tf
output "vpc_id" {
  description = "ID of VPC"
  value       = aws_vpc.this.id
}
output "web_instance_id" {
  description = "ID of web instance"
  value       = aws_instance.web.id
}
output "web_public_ip" {
  description = "Public IP of web server"
  value       = aws_instance.web.public_ip
}
terraform.tfvars
cidr            = "10.0.0.0/16"
public_subnet   = "10.0.1.0/24"
public_key_path = "~/.ssh/id_rsa.pub"
# Ubuntu Server 20.04 LTS (HVM), SSD Volume Type, 64-bit x86
ami = "ami-0461b11e2fad8c14a"
執行
先查詢你目前的 public ip,接著執行 apply 指令並帶入變數
terraform apply -var="my_ip=xxx.xxx.xxx.xxx"
...
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
vpc_id = vpc-03fa104e06a386d5d
web_instance_id = i-02726dce6cb801181
web_public_ip = 123.123.123.123
完成了就可以開啟 http://123.123.123.123 驗收一下網頁服務。
佈建器 (provisioner) 這個方法並不是 Terraform 推薦的方法,在測試的過程中我有遇到幾次的失敗。
後面我們再找其他的辦法來試試
前面我們所使用佈建器 (Provisioner) 的 remote-exec 不怎麼好用,而且 Terraform 無法檢查設定有沒有變動。
aws 有一個叫做使用者資料 (User Data) 的功能,可以輔助我們設定虛擬機。只要在建立機器時傳送使用者資料 (User Data),在執行個體 (instance) 啟動之後就會執行指令。
使用者資料 (User Data)
使用者資料是基於 cloud-init 這個工具在運作的。cloud-init 是由 Canonical 所發佈的工具
目的就是要讓雲端虛擬機初始化可以更容易的自動化。目前常見的雲端平台跟多數的 Linux 作業系統都有支援 cloud-init 功能。
使用者資料可以使用 gzip 壓縮,大小通常會有 16384 位元組的上限。支援兩種格式:
- shell 腳本: 檔案以 #!開始
- cloud-int 組態: 檔案以 #cloud-config開始
以下提供幾個簡單的範例,完整的設定方式請詳閱 Modules - cloud-init
建立使用者
使用 users 建立使用者,並可以設定 sudo 權限,放入公鑰等等。
#cloud-configusers:
  - default
  - name: barfoo
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: users, admin
    ssh_import_id: None
    lock_passwd: true
    ssh_authorized_keys:
      - <ssh pub key 1>
建立檔案
使用 write_files 把檔案放到目的地。
#cloud-configwrite_files:
  - path: /var/www/html/terraform.html
    content: |
      <h1>Provisioning via Terraform</h1>
安裝套件
要更新 apt 套件資料庫的話,要把 package_update 設定為 true。
要安裝的套件全部列在 packages 下面 。
#cloud-configpackage_update: true
packages:
 - nginx
 - git
Reference
我們來實際用使用者資料設定虛擬機,以下是預計要讓使用者資料處理的工作:
- 建立一個叫做 terraform的使用者
- 安裝 nginx
- 放入一個 HTML 檔案
建立使用者資料
建立使用者資料的檔案 user_data.yaml
user_data.yaml
#cloud-config# create usersusers:
  - default
  - name: terraform
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
# install nginxpackage_update: true
packages:
  - nginx
# put html filewrite_files:
  - path: /var/www/html/terraform.html
    content: |
      <h1>Provisioning via Terraform</h1>
設定組態檔
使用 template_file 這個 data 區塊來載入 user_data.yaml,再把載入的資料放到 aws_instance 的 user_data 引數中。
data template_file 的引數:
- template: 放模版資料,可以用 file()函數來載入檔案。
上面的設定都放在  main.tf 裡面,完整檔案可以參考: https://github.com/nyogjtrc/practice-terraform/tree/master/web-server-user-data
main.tf
data "template_file" "user_data" {
  template = file("user_data.yaml")
}
resource "aws_instance" "web" {
  ami           = var.ami
  instance_type = "t2.micro"
  key_name = aws_key_pair.practice.id
  vpc_security_group_ids = [
    aws_security_group.ssh.id,
    aws_security_group.web.id,
  ]
  subnet_id = aws_subnet.this.id
  user_data = data.template_file.user_data.rendered
  tags = {
    Name  = "Web-Terraform"
    topic = "web-server-user-data"
  }
}
執行 terraform
terraform apply
...
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
vpc_id = vpc-0db96967262b14fcf
web_instance_id = i-008d3a7371fba33b2
web_public_ip = 18.177.119.161
使用者資料是在虛擬機建立之後才執行的,所以需要稍微等一下
aws 自動分配了 18.177.119.161 給虛擬機,在瀏覽器開啟 http://18.177.119.161/terraform.html 就可以看到我們剛剛放上去的檔案