イメージの展開とVMインスタンスのスケーリング運用



## はじめに
今回は、GCPでTerraformとPackerを用いた運用の一例を紹介させていただきます。

- Packerで取ったイメージを展開
 Ansibleでプロビジョニングしています。


- 定期的にスケーリング
弊社が運用しているサービスは、昼と夜の決まった時間にトラフィックが上がるのでサーバの負荷が上がるタイミングをある程度予測することができるという特徴を持っています。そのため定刻にスケーリングさせたいという要望がありました。


---
本記事で説明に使用するサンプルです→https://github.com/Takashi-Kawano/scaling_sample
## インスタンステンプレート作成
```terraform/providers/dev/terraform.tfvars
project_id      = "meta-iterator-225901"
region          = "us-west1"
project_name    = "scaling-sample"
dev_target_size = "1"
zone            = "us-west1-a"
network         = "default"

machine_dev     = {
    "name"              = "dev"
    "machine_type"      = "f1-micro"
    "env_name"          = "dev"
    "group_name"        = "dev"

    "disk_type"         = "pd-standard"
    "disk_size"         = "30"

    "update_strategy"   = "NONE"
}

"instance_group_zones"  = [
    "us-west1-a",
]
```

```scaling_sample/terraform/providers/dev/managed_group.tf
resource "google_compute_instance_template" "instance_template" {
  name_prefix  = "${var.project_name}-${lookup(var.machine_dev, "name")}-"
  machine_type = "${lookup(var.machine_dev, "machine_type", "f1-micro")}"
  region       = "${var.region}"

  disk {
    source_image = "${coalesce(var.dev_image, data.terraform_remote_state.output.dev_image)}"
    disk_type    = "${lookup(var.machine_dev, "disk_type", "pd-standard")}"
    disk_size_gb = "${lookup(var.machine_dev, "disk_size", "30")}"
    device_name  = "${lookup(var.machine_dev, "device_name", "persistent-disk-0")}"
  }

  network_interface {
    network = "${var.network}"
    access_config {}
  }

  lifecycle {
    create_before_destroy = true
  }
}
```
source_imageの値を変数(`dev_image`)にすることで、Terraformコマンドを実行する際にイメージを指定することができます。

```
実行例
$ terraform apply -var "dev_image=scaling-sample-dev-instance-1545206547"
```

## マネージドインスタンスグループ作成
マネージドインスタンスグループに割り当てられているインスタンステンプレートに割り当てられているビルドイメージを元にVMインスタンスが作成されるようになります。

```scaling_sample/terraform/providers/dev/managed_group.tf
resource "google_compute_instance_group_manager" "instance_group_manager" {
  name = "instance-group-manager"
  instance_template = "${google_compute_instance_template.instance_template.self_link}"
  base_instance_name = "dev-instance"
  zone = "${var.zone}"
  target_size = "${var.dev_target_size}"
  update_strategy = "ROLLING_UPDATE"
  rolling_update_policy {
    minimal_action = "REPLACE"
    type = "PROACTIVE"
    max_unavailable_fixed = 0
  }
}
```
target_sizeの値を変数(`dev_target_size`)にすることで、Terraformコマンドを実行する際に起動するVMインスタンスの数を指定することができます。


```
実行例
$ terraform apply -var "dev_target_size=1"
```

また、`update_strategy`を使用して、`rolling_update_policy`にローリングアップデートの条件を設定しています。
既に起動しているVMインスタンスに対しては、再生成をして新しいVMインスタンスに置き換えることでイメージを適用させます。Terraformコマンド実行時にローリングアップデート処理を開始し、再生成中に停止しているVMインスタンスの数を０と設定しています。こうすることで、運用中のサービスでも止めることなくローリングアップデートができます。
こちらはbeta版ということらしいです。https://www.terraform.io/docs/providers/google/r/compute_instance_group_manager.html

今回サンプルでは使用してみたのですが、実際の運用ではまだ導入はしておらず、イメージの割り当て後にGCPコンソールから手動でローリングアップデートを実行しています。


## Packerでイメージ作成
Packerを使ってインスタンスイメージをビルドします。プロビジョニングにAnsibleを使用しています。

```scaling_sample/ansible/playbook/build_image.yml
- name: Execute Base Setting...
  hosts: "{{ packer_build_target }}"
  become: yes
  gather_facts: yes

  tasks:
    - name: Create sample file
      copy:
        content: helloworld
        dest: /message
```
```ansible/packer/packer.json
{
  "builders": [{
    "type": "googlecompute",
    "account_file": "{{user `credential_file`}}",
    "project_id": "{{user `project_id`}}",
    "source_image_family": "centos-7",
    "zone": "{{user `zone`}}",
    "instance_name": "{{user `project`}}-{{user `app_stage`}}-instance-base",
    "machine_type": "{{user `machine_type`}}",
    "disk_size": "{{user `disk_size`}}",
    "image_family": "{{user `project`}}-{{user `app_stage`}}-instance",
    "image_name": "{{user `project`}}-{{user `app_stage`}}-instance-{{timestamp}}",
    "network": "default",
    "subnetwork": "default",
    "omit_external_ip": false,
    "use_internal_ip": false,
    "tags": [
        "packer-build"
    ],
    "ssh_username": "ansible",
    "ssh_private_key_file": "../credentials/ansible_key"
  }],
  "provisioners": [
    {
      "type": "ansible",
      "playbook_file": "{{user `playbook_file`}}",
      "extra_arguments": [
          "-i", "./inventory",
          "-e", "packer_build_target={{user `project`}}-{{user `app_stage`}}-instance-base",
          "-e", "packer_build=yes"
      ]
    }
  ]
}
```

```ansible/packer/vars/scaling-sample-dev.json
{
    "credential_file": "../credentials/scaling-sample-427451a6e5ec.json",
    "project_id": "meta-iterator-225901",
    "zone": "us-west1-a",
    "project": "scaling-sample",
    "machine_type": "f1-micro",
    "disk_size": "30",
    "app_stage": "dev",
    "group": "dev",
    "network": "default",
    "playbook_file": "playbook/build_image.yml"
}
```

以下のコマンドを実行してイメージビルドします。

```
$ packer build -var-file=packer/vars/scaling-sample-dev.json packer/packer.json
```
処理が正常に終了すると、以下のようにビルドされたイメージファイルの名前が表示されます。

```
(略)
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

dev_image = scaling-sample-dev-instance-1545206547
dev_target_size = 1
```


## イメージ割り当て(ローリングアップデート)+スケーリング

上で説明したイメージの割り当てとスケーリングをひとまとめに実行します。

```
$ terraform apply -var "dev_target_size=1" -var "dev_image=scaling-sample-dev-instance-1545206547"
```

GCPコンソール上で確認すると、新しいVMインスタンスが起動し、既存のVMインスタンスが停止→削除される様子が確認できます。
![スクリーンショット 2018-12-19 22.24.41.png](https://qiita-image-store.s3.amazonaws.com/0/317054/871361a5-da3f-b9eb-0b19-15e1a1aeacac.png)




VMインスタンスにsshログインして以下のようになれば、正常にイメージが割り当てられていることを確認できます。

```
[dev-instance-b2rv ~]$ cat /message
helloworld
```



## Jenkinsでスケーリングを定期実行
上記のTerraformコマンドでスケーリングとイメージの割り当て（とローリングアップデート）ができるので、これをシェルスクリプトで実行し、定期的にジョブを走らせるようにcronを設定しています。
JenkinsでTerraformコマンドを実行させるためのサンプル用の設定は気が向いたらやっておきます:bow:

##なぜAutoScalingを使っていないのか
GCPのマネージドインスタンスグループにはAutoScalingという機能が提供されています。これは平均CPU使用率や秒間リクエスト数、StackdriverMonitoringの指標に閾値を設定して、設定した閾値に到達した際に自動でVMインスタンスを増やすという機能です。

現在のアーキテクチャーの仕組みだと新規にVMインスタンスを追加したときに、デプロイ処理を行ってロードバランサーのヘルスチェックが通るまでの間に時間が掛かかります。なので、ヘルスチェックが通るまでの時間差と負荷に対する閾値の考慮が必要でそれを検証するための時間がまず割けていません。。尚且つ、最初に説明した通りある程度トラフィックが上がるタイミングを予測できるので、Jenkinsを使って定期的にスケーリングをするという方針をとっています。

## プリエンプティブインスタンスを利用

トラフィックが上がってくるタイミングでVMインスタンスを増やす際には[プリエンプティブインスタンス](https://cloud.google.com/compute/docs/instances/preemptible?hl=ja)を利用しています。これを利用することでコストを大幅に削減することができています。ですが、東京リージョンを使用している方々が多いせいか、一時プリエンプティブリソースが充足していない時があって必要な数のプリエンプティブインスタンスが起動せずに焦ることがあったりなど、コスト削減と引き換えに運用面で苦労する場面があるので利用する際には注意が必要です。弊社での運用例の詳細は[こちら](https://tech.griphone.co.jp/2017/12/24/web-preemptible/)の記事を参照してください。



## 終わりに
定刻でスケーリングするだけでなく、トラフィックが多くなると予想されるイベントが始まる前に事前にスケールアウトして備えておくといったことも簡単にできるのでとても便利です。


今後の展望としては、今回のサンプルで使用したローリングアップデートの自動化を適用したり、デプロイ処理をもっと早くできるようにして、よりスムーズにイメージの適用とスケーリングを行えるようにしていきたいと考えています。



以上となります。
最後まで読んでいただきありがとうございました！