VisasQ Dev Blog

ビザスク開発ブログ

GCPで基本に戻って始める実践 Infrastructure as code再入門#1

こんにちは! 2020年2月からSREチームにJoinしました木村です!
仕事をする上での座右の銘は「明日交通事故にあってもシステムと仕事を回せるようにすること」です。

基本に戻って始める。と表題では書いていますが、私元々はAWS職人でGCPに本格的にコミットしてからまだ3ヶ月位です!

なのでヒィヒィ?言いながらGCPのキャッチアップに努めているわけですが今回は過去にAWSで得たInfrastructure as Codeの知識とビザスクに入社してキャッチアップで培ったGCPの知識を元に基本に戻って始めるGCPのInfrastructure as Code再入門ということで書かせていただきます。

尚実際に書き始めたら量が膨大になってしまったのでいくつかパートに分けて
書いていきたいと思っております。

今回やること

GCPCompute Engineをスコープとして

上記をやっていきたいと思います

その他のGCPで基本に戻って始める実践 Infrastructure as code再入門シリーズはこちら

Terraformとは?

等様々なクラウドインフラサービスがありますが、クラウドインフラの構成管理ができるツールです。HCL(hashicorp Languege)というhashicorp社が開発したマークアップ言語で簡潔にクラウドリソース設定を書くことができ、管理ができます。


AWSCloudFormationと仕組みはとても似ていています。

Ansibleとは?

Pythonで書かれていて一般的にはProvisioning Toolと呼ばれています。
サーバーのミドルウェアのインストールやconfigの設定や秘密鍵の配布等サーバー内部の設定をプログラマブルに書くことができます。Infrastructure as Codeですね。

Pythonなのですが、Ansibleを記述する際はほとんどの場合においてYAMLしか書きません。
(厳密に言うとYAMLをベースにJinja2のテンプレート記法を使います)

似たようなツールとしては

等があります。
Ansibleの特徴としてはサーバーにAgentを入れる必要がなくSSHで構成管理したいサーバーに接続してProvisioningをすることが特徴です。

Ansibleがリリースされた当初はこのエージェントレスというのが大量のサーバーを抱えているインフラエンジニアには都合がよく広まった一つの要因かな?と推測しております。

思い返せば私がAnsibleを知って使い始めたのが確か8年前位でWikiPediaで調べてみるとほぼ初版リリース時と一緒位のタイミングから使い初めていたのに驚きでした(確か1.4?位から使っていた記憶)。

Packerとは?

Terraformと同じくhashicorp社が開発をしているサーバーのImageを作成するツールです。
Provisioning Toolでサーバーを構築しているからPackerの必要性はあるのか?
と昔はよく質問されていたのですが、Provisioning Tool単体ですとサーバーを構成した時にはあったPackageがアップデート等でバージョンの喪失をしたり、とProviosioning Toos単体では冪等性が担保しづらく、それをPackerでImageを作成することによって最後にProvisioningされた状態を簡単に復元できる。というのがポイントです。

今回のGCPのInfrastructure as Code再入門でのゴールイメージ

順番は下記になります。

  • TerraformでCompute Instanceを作成 <= 今ココ
  • Compute Instanceに対してAnsibleでNginxをインストールする
  • AnsibleでProvisoningされたサーバーの状態をPackerでImage化


始める前に用意していただきたいもの

動作環境

  • Mac OSX
  • Terraform v0.12.23 ※0.12の機能を使っていますのでバージョンにご注意ください

Terraformのインストール

では早速Terraformをインストールしましょう

brew install terraform

GCloud SDKのインストール

後で必要になりますのでインストールします

brew cask install google-cloud-sdk

Terraformのディレクトリレイアウト

$ tree .
.
├── Makefile
├── backends
│   ├── dev.tfvars
│   ├── prod.tfvars
│   └── stg.tfvars
├── main.tf
├── modules
│   └── vm
│   │   ├── main.tf
│   │   └── variables.tf
│   └── iam
│       └── main.tf
├── variables.tf
└── vars
    ├── dev.tfvars
    ├── prod.tfvars
    └── stg.tfvars

main.tfにGCP Providerの設定を記述する

ProviderとはTerraformで管理したいインフラのことです。

プロバイダーは通常、IaaS(Alibaba Cloud、AWSGCPMicrosoft Azure、OpenStackなど)、PaaS(Herokuなど)、またはSaaSサービス(Terraform Cloud、DNSimple、Cloudflareなど)です。

terraform公式ドキュメントよりGoogle翻訳したもの
# main.tf

provider "google" {
  version = "3.5.0"
  project = var.project
  region  = var.region
  zone    = var.zone
}

main.tfにbackendsを指定する

Terraformはインフラの状態をjsonファイル(tfstateファイルと呼ばれます)で保持し、インフラリソースの追加・修正・削除時にtfstateファイルを見て、リソースの状態を記述した通りに再現します。

例えばTerraformでCompute Engineを作成後、手動でlabelを追加した後にTerraformを実行するとTerraformは手動で作成したlabelを削除しようとします。

これは初回にTerraformを実行した後にtfstateファイルに最後に実行したCompute Engineの状態が保存されていますが、手動で変更した内容は当然の事ながらTerraformのtfstateファイルに反映されていないからです。

Terraformは記述された内容に沿ってtfstateファイルの差分を見て状態を復元させようとします。Terraformにおいてこのtfstateファイルはとても重要なファイルです。

このtfstateファイルですが、デフォルトではローカルファイルに保存されますが

  • IPAddress
  • DBのパスワード

等機密情報も含めて保持をしていますのでgitにコミットすると脆弱性につながる可能性があり危険です。

またローカルファイルにtfstateファイルを保持するとtfstateファイル持っていない他の開発者がterraformを実行した場合にtfstateファイルがないので前回の状態がわからず、すでにあるCompute Engineを作成しようとします。

ではどうするか?と言いますとGCSにtfstateファイルを保存して複数の開発者とtfstateファイル共有します。このGCSの設定がbackendsになります。

話が少し長くなりましたがまずはGCSのバケットを作成します。

# main.tf

terraform {
  backend "gcs" {}
}

provider "google" {
  .
  .
  .
}

backends/dev.tfvarsにGCSの情報を追加していく

# backends/dev.tfvars

# GCSのバケット名
bucket = "terraform-sample"
# GCSのprefix
prefix  = "state"

backends内に上記記述をしてもTerraformは動作しますが、別ファイルにしています。
これは通常インフラリソースを作成する場合

  • staging環境
  • testing環境
  • production環境

とstageに応じたインフラの複製を作る事が多いのでこのように変数化をしておけばstageに応じてtfstateファイルを切り替える事が出来ます。

続いてバケットを作成します

gsutil mb terraform-sample -p {your GCP project id}

modules/vm/main.tfにCompute Instanceの設定を記述する

moduleについての詳しい説明はTerraform公式ドキュメントに譲ります

# modules/vm/main.tf
resource "google_compute_instance" "instance" {
  project      = var.project
  name         = "sample-instance"
  description  = ""
  machine_type = "n1-standard-1"
  zone         = var.zone

  boot_disk {
    initialize_params {
      image = "centos-7-v20200403"
    }
  }

  min_cpu_platform = "Intel Haswell"

  network_interface {
    network = "default"

    access_config {}
  }

  metadata = {
    // OSログインを有効にする
    enable-oslogin = true
  }

  labels = {
    stage         = var.stage
    service_type = "web"
  }
}

パラメーターの詳しい説明は公式ドキュメントに譲りますが、今回AnsibleでProvisionをする時にGCPOSログイン機能を用いてサービスアカウントユーザーで接続をします。
OSログイン機能を使うためにはInstanceのmetadataenable-loginのパラメーターを付与する必要があります

modules/vm/variables.tfを記述する

module内で記述されているvar.xxxxの部分は変数です。変数の中身は空なのですが、カレントのvariables.tfから変数の受け渡しをするので空のままで問題ありません。

variable "stage" {}
variable "project" {}
variable "zone" {}

moduels/iam/main.tfにサービスアカウントの記述をする

前述の通りサービスアカウントが必要なので追加します。
こちらも詳しいパラメーターの説明は公式ドキュメントに譲ります。

resource "google_service_account" "ansible_provision_account" {
  account_id   = "ansible-provision"
  display_name = "ansible-provision"
  description  = "ansibleのプロビジョニングで使用するためのサービスアカウント"
}

resource "google_project_iam_member" "ansible_gcp_provisioning_compute_admin" {
  role    = "roles/compute.admin"
  member  = "serviceAccount:${google_service_account.ansible_provision_account.email}"
}

# GCE OSログイン用に追加している
resource "google_project_iam_member" "ansible_gcp_provisioning_service_account_user" {
  role    = "roles/iam.serviceAccountUser"
  member  = "serviceAccount:${google_service_account.ansible_provision_account.email}"
}

member部分の記述ですが、同一モジュール内の場合resource名.変数名という記述で作成するリソース内容にアクセス出来ます。

main.tfに作成したモジュールを追加する

サービスアカウントとCompute Instanceのモジュールを追加します

# main.tf

terraform {
  backend "gcs" {}
}

provider "google" {
  version = "3.5.0"
  project = var.project
  region  = var.region
  zone    = var.zone
}

#
# ここから追加部分
#

# サービスアカウント
module "iam_service_account" {
  source = "./modules/iam"
}

# Compute Engine
module "vm" {
  source = "./modules/vm"

  stage   = var.stage
  project = var.project
  zone    = var.zone
}

variables.tfに変数を記述する

typeで変数の型を指定でき、defaultは変数のデフォルト値を指定できます。
defaultが存在するということはこの変数は上書きすることが出来ます。
(上書き方法については後述します)

# variables.tf

variable "project" {
  type        = string
  # ここは自分のGCPアカウントで作成したProject IDを指定してください
  default     = "{your GCP project id}"
  description = "GCP project id"
}

variable "stage" {
  type = string
  default     = "dev"
  description = "default stage"
}

variable "region" {
  type = string
  default     = "asia-northeast1"
  description = "default region"
}

variable "zone" {
  type = string
  # default = "us-central1-c"
  default = "asia-northeast1-c"
}

Terraformを使ってCompute Engineのインスタンスとサービスアカウントを作成する

ではお待ちかねterraformを実行してみましょう

# Terraformのbakcendsを切り替える
$ terraform init -reconfigure -backend-config=backends/dev.tfvars

# Terraformを実行する
$ terraform apply -var-file=vars/dev.tfvars
.
# git diffのような差分が出力されます
.
Plan: 3 to add, 0 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.
# 最後にこのようなstdinで確認されますので差分内容に問題がなければ'yes'を入力してください
 Enter a value: yes
.
.
.
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

GCP Consoleで確認してもらうとCompute Engine Instanceとサービスアカウントが作成されている事が確認出来ます。

まとめ

最初のTerraform部分だけでも大分ボリューミーでしたが、次回はTerraformで作成したCompute Engine Instanceに対してAnsibleを使ってProvisioningをする。というのをやっていきます。

TerraformのGCPに関する記述方法は公式が大変読みやすいのでそれほど迷うことはないと思います。

次回Ansibleを使って作成したインスタンスに対してProvisionを実行していきたいと思います。

エンジニア積極採用中

ビザスクでは一緒に働くエンジニアさん積極採用中です。
時期的にビデオ通話での面談になると思うので、ビザスクにちょっとでも興味のある方はいつもより気軽にお話を聞きに来ることができるかなと思います。