1. 概要

Infrastructure-as-Code(IaC)は、AWS、Google、Microsoftなどのパブリッククラウドプロバイダーの人気が高まるにつれ、主流になっている手法です。 一言で言えば、開発者がアプリケーションコードを管理するために使用するのと同じアプローチを使用して、一連のリソース(コンピューティング、ネットワーク、ストレージなど)を管理することで構成されます

このチュートリアルでは、インフラストラクチャタスクを自動化するためにDevOpsチームが使用する最も人気のあるツールの1つであるTerraformのクイックツアーを行います。 Terraformの主な魅力は、インフラストラクチャがどのように見えるかをどのように宣言するかであり、ツールはそのインフラストラクチャを「具体化」するために実行する必要のあるアクションを決定します。

2. 簡単な歴史

GitHubによると、Terraformの最初のコミットの日付は2014年5月21日でした。 著者は、Hashicorpの創設者の1人であるMitchell Hashimotoであり、その「ミッションステートメント」と呼ぶことができるものを説明するREADMEファイルのみが含まれています。

Terraformは、インフラストラクチャを安全かつ効率的に構築および変更するためのツールです。

このフレーズは、その意図をかなりよく説明しています。 それ以来、このツールは、サポートするインフラストラクチャプロバイダーの観点から機能を着実に向上させてきました。

この記事の執筆時点で、Terraformは約130のプロバイダーを公式にサポートしています。 そのコミュニティがサポートするプロバイダーのページには、さらに160がリストされています。 これらのプロバイダーの中には、ほんの数個のリソースを公開しているものもありますが、AWSやAzureなどの他のプロバイダーには数百ものリソースがあります。

この膨大な数のサポートされているリソースにより、Terraformは多くのDevOpsエンジニアに最適なツールになっています。 また、単一のツールを使用して複数のベンダーを管理することは大きな利点です。

3. こんにちは、テラフォーム

内部動作の詳細に入る前に、基本的なものから始めましょう。初期設定と簡単な「Hello、World」スタイルのプロジェクトです。

3.1. ダウンロードとインストール

Terraformディストリビューションは、Hashicorpのダウンロードページから無料でダウンロードできる単一のバイナリファイルで構成されています。 依存関係はなく、実行可能バイナリをオペレーティングシステムのPATH内のフォルダにコピーするだけで実行できます。

この手順を完了すると、次の簡単なコマンドで正しく機能していることを確認できます。

$ terraform -v
Terraform v0.12.24

それだけです—管理者権限は必要ありません! 引数なしでTerraformを実行することにより、使用可能なコマンドのクイックヘルプを取得できます。

$ terraform
Usage: terraform [-version] [-help] <command> [args]
... help content omitted

3.2. 最初のプロジェクトの作成

Terraformプロジェクトは、リソース定義を含むディレクトリ内のファイルのセットです。 慣例により.tfで終わるこれらのファイルは、 Terraformの構成言語を使用して、作成するリソースを定義します。

「Hello、Terraform」プロジェクトの場合、リソースは固定コンテンツのファイルになります。 コマンドシェルを開いていくつかのコマンドを入力することにより、これがどのように見えるかを見てみましょう。

$ cd $HOME
$ mkdir hello-terraform
$ cd hello-terraform
$ cat > main.tf <<EOF
provider "local" {
  version = "~> 1.4"
}
resource "local_file" "hello" {
  content = "Hello, Terraform"
  filename = "hello.txt"
}
EOF

main.tf ファイルには、プロバイダー宣言とリソース定義の2つのブロックが含まれています。 プロバイダー宣言は、バージョン1.4または互換性のあるローカルプロバイダーを使用することを示しています。

次に、タイプ local_fileこのresourceタイプのhelloという名前のresource定義があります。は、指定されたコンテンツを含むローカルファイルシステム上の単なるファイルであることを意味します。

3.3. init、 plan、および apply

それでは、このプロジェクトでTerraformを実行してみましょう。 このプロジェクトを実行するのはこれが初めてなので、initコマンドで初期化する必要があります。

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "local" (hashicorp/local) 1.4.0...

Terraform has been successfully initialized!
... more messages omitted

このステップでは、Terraformがプロジェクトファイルをスキャンし、必要なプロバイダー(この場合はローカルプロバイダー)をダウンロードします。

次に、 plan コマンドを使用して、Terraformがリソースを作成するために実行するアクションを確認します。 この手順は、GNUのmakeツールなどの他のビルドシステムで使用できる「ドライラン」機能とほぼ同じように機能します。

$ terraform plan
... messages omitted
Terraform will perform the following actions:

  # local_file.hello will be created
  + resource "local_file" "hello" {
      + content              = "Hello, Terraform"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "hello.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
... messages omitted

ここで、Terraformは、新しいリソースを作成する必要があることを示しています。これは、まだ存在していないため、予想されます。 設定した提供値と1組の権限属性も確認できます。 リソース定義でそれらを提供していないため、プロバイダーはデフォルト値を想定します。

これで、applyコマンドを使用して実際のリソースの作成に進むことができます。

$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.hello will be created
  + resource "local_file" "hello" {
      + content              = "Hello, Terraform"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "hello.txt"
      + id                   = (known after apply)
    }

Plan: 1 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.

  Enter a value: yes

local_file.hello: Creating...
local_file.hello: Creation complete after 0s [id=392b5481eae4ab2178340f62b752297f72695d57]

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

これで、ファイルが指定されたコンテンツで作成されたことを確認できます。

$ cat hello.txt
Hello, Terraform

よかった! ここで、 apply コマンドを再実行するとどうなるか見てみましょう。今回は、 -auto-approve フラグを使用して、Terraformが確認を求めずにすぐに実行できるようにします。

$ terraform apply -auto-approve
local_file.hello: Refreshing state... [id=392b5481eae4ab2178340f62b752297f72695d57]

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

今回は、ファイルがすでに存在していたため、Terraformは何もしませんでした。 しかし、それだけではありません。 リソースが存在する場合もありますが、誰かがその属性の1つを変更した可能性があります。これは、通常「構成ドリフト」と呼ばれるシナリオですこのシナリオでTerraformがどのように動作するかを見てみましょう。

$ echo foo > hello.txt
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

local_file.hello: Refreshing state... [id=392b5481eae4ab2178340f62b752297f72695d57]

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.hello will be created
  + resource "local_file" "hello" {
      + content              = "Hello, Terraform"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "hello.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
... more messages omitted

Terraformは、hello.txtファイルのコンテンツの変更を検出し、それを復元するための計画を生成しました。 local プロバイダーにはインプレース変更のサポートがないため、計画は単一のステップの—ファイルを再作成します。

これで、 apply を再度実行できるようになり、その結果、ファイルの内容が目的の内容に復元されます。

$ terraform apply -auto-approve
... messages omitted
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

$ cat hello.txt
Hello, Terraform

4. コアコンセプト

基本を説明したので、Terraformのコアコンセプトに取り掛かりましょう。

4.1. プロバイダー

p rovider は、オペレーティングシステムのデバイスドライバーとしてほとんど機能します。 共通の抽象化を使用して一連のリソースタイプを公開するため、ユーザーに対してほとんど透過的なリソースの作成、変更、および破棄の方法の詳細がマスクされます

Terraformは、特定のプロジェクトのリソースに基づいて、必要に応じてパブリックレジストリからプロバイダーを自動的にダウンロードします。 また、ユーザーが手動でインストールする必要があるカスタムプラグインを使用することもできます。 最後に、一部の組み込みプロバイダーはメインバイナリの一部であり、いつでも利用できます。

いくつかの例外を除いて、プロバイダーを使用するには、いくつかのパラメーターを使用してプロバイダーを構成する必要があります。 これらはプロバイダーによって大きく異なりますが、一般に、APIに到達してリクエストを送信できるように、資格情報を提供する必要があります。

厳密には必要ではありませんが、Terraformプロジェクトで使用するプロバイダーを明示的に宣言し、そのバージョンを通知することをお勧めします。 この目的のために、プロバイダー宣言で使用可能なversion属性を使用します。

provider "kubernetes" {
  version = "~> 1.10"
}

ここでは、追加のパラメーターを提供していないため、Terraformは他の場所で必要なパラメーターを探します。 この場合、プロバイダーの実装は、kubectlで使用されるのと同じ場所を使用して接続パラメーターを探します。 他の一般的な方法は、環境変数と変数ファイルの使用です。これらは、キーと値のペアを含む単なるファイルです。

4.2. 資力

Terraformでは、リソースとは、特定のプロバイダーのコンテキストでCRUD操作のターゲットになり得るものです。いくつかの例として、EC2インスタンス、Azure MariaDB、またはDNSエントリがあります。

簡単なリソース定義を見てみましょう。

resource "aws_instance" "web" {
  ami = "some-ami-id"
  instance_type = "t2.micro"
}

まず、定義を開始するresourceキーワードが常にあります。 次に、リソースタイプがあります。これは通常、Provider_type規則に従います。 上記の例では、 aws_instance はAWSプロバイダーによって定義されたリソースタイプであり、EC2インスタンスを定義するために使用されます。 その後、ユーザー定義のリソース名があります。これは、同じモジュール内のこのリソースタイプに対して一意である必要があります。モジュールについては後で詳しく説明します。

最後に、リソース仕様として使用される一連の引数を含むブロックがあります。 リソースに関する重要なポイントは、作成後、式を使用して属性をクエリできることです。また、同様に重要なこととして、これらの属性を他のリソースの引数として使用できます

これがどのように機能するかを説明するために、デフォルト以外のVPC(仮想プライベートクラウド)でEC2インスタンスを作成して、前の例を拡張してみましょう。

resource "aws_instance" "web" {
  ami = "some-ami-id"
  instance_type = "t2.micro"
  subnet_id = aws_subnet.frontend.id
}
resource "aws_subnet" "frontend" {
  vpc_id = aws_vpc.apps.id
  cidr_block = "10.0.1.0/24"
}
resource "aws_vpc" "apps" {
  cidr_block = "10.0.0.0/16"
}

ここでは、VPCリソースの id 属性を、フロントエンドのvpc_id引数の値として使用します。 次に、そのidパラメーターがEC2インスタンスの引数になります。この特定の構文にはTerraformバージョン0.12以降が必要であることに注意してください。 以前のバージョンでは、より面倒な“ $ {expression}” 構文が使用されていました。この構文は引き続き使用できますが、レガシーと見なされます。

この例は、Terraformの長所の1つも示しています。プロジェクトでリソースを宣言する順序に関係なく、リソースを解析するときに作成する依存関係グラフに基づいて、リソースを作成または更新する必要がある正しい順序を把握します。

4.3. countおよびfor_eachメタ引数

countおよびfor_each meta引数を使用すると、任意のリソースの複数のインスタンスを作成できます。 それらの主な違いは、 count は非負の数を期待するのに対し、for_eachは値のリストまたはマップを受け入れることです。

たとえば、 count を使用して、AWSでいくつかのEC2インスタンスを作成しましょう。

resource "aws_instance" "server" {
  count = var.server_count 
  ami = "ami-xxxxxxx"
  instance_type = "t2.micro"
  tags = {
    Name = "WebServer - ${count.index}"
  }
}

countを使用するリソース内で、expressionsでcountオブジェクトを使用できます。 このオブジェクトには、 index という1つのプロパティしかありません。これは、各インスタンスのインデックス(ゼロベース)を保持します。

同様に、 for_each メタ引数を使用して、マップに基づいてこれらのインスタンスを作成できます。

variable "instances" {
  type = map(string)
}
resource "aws_instance" "server" {
  for_each = var.instances 
  ami = each.value
  instance_type = "t2.micro"
  tags = {
    Name = each.key
  }
}

今回は、ラベルからAMI(Amazon Machine Image)名へのマップを使用してサーバーを作成しました。 リソース内で、 each オブジェクトを使用できます。これにより、特定のインスタンスの現在のキーおよびにアクセスできます。

countとfor_eachに関する重要なポイントは、式を割り当てることはできますが、Terraformはリソースアクションを実行する前にそれらの値を解決できる必要があるということです。 その結果、他のリソースからの出力属性に依存する式を使用することはできません。

4.4. データソース

データソースは「読み取り専用」リソースとほぼ同じように機能します。つまり、既存のリソースに関する情報を取得することはできますが、それらを作成または変更することはできません。 これらは通常、他のリソースを作成するために必要なパラメーターをフェッチするために使用されます。

典型的な例は、AWSプロバイダーで利用可能な aws_ami データソースです。これは、既存のAMIから属性を回復するために使用します。

data "aws_ami" "ubuntu" {
  most_recent = true
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*"]
  }
  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
  owners = ["099720109477"] # Canonical
}

この例では、AMIレジストリにクエリを実行し、検索された画像に関連するいくつかの属性を返す「ubuntu」と呼ばれるdataソースを定義します。 次に、これらの属性を他のリソース定義で使用して、属性名の前にdataプレフィックスを付けることができます。

resource "aws_instance" "web" {
  ami = data.aws_ami.ubuntu.id 
  instance_type = "t2.micro"
}

4.5. 州

Terraformプロジェクトの状態は、特定のプロジェクトのコンテキストで作成されたリソースに関するすべての詳細を格納するファイルです。 たとえば、プロジェクトで Azure_resourcegroup リソースを宣言し、Terraformを実行すると、状態ファイルにその識別子が格納されます。

状態ファイルの主な目的は、既存のリソースに関する情報を提供することです。そのため、リソース定義を変更すると、Terraformは何をする必要があるかを理解できます。

状態ファイルに関する重要なポイントは、機密情報が含まれている可能性があることです。 例には、データベースの作成に使用される初期パスワード、秘密鍵などが含まれます。

Terraformは、バックエンドの概念を使用して、状態ファイルを保存および取得します。 デフォルトのバックエンドはローカルバックエンドで、プロジェクトのルートフォルダー内のファイルを保存場所として使用します。 プロジェクトの.tfファイルの1つにあるterraformブロックで宣言することにより、代替のリモートバックエンドを構成することもできます。

terraform {
  backend "s3" {
    bucket = "some-bucket"
    key = "some-storage-key"
    region = "us-east-1"
  }
}

4.6. モジュール

Terraformモジュールは、複数のプロジェクト間でリソース定義を再利用したり、単一のプロジェクトでより良い組織を構築したりできるようにする主な機能です。 これは、標準のプログラミングで行うこととよく似ています。すべてのコードを含む単一のファイルではなく、複数のファイルとパッケージにコードを編成します。

モジュールは、1つ以上のリソース定義ファイルを含む単なるディレクトリです。 実際、すべてのコードを1つのファイル/ディレクトリに配置した場合でも、モジュールを使用しています。この場合は1つだけです。 重要な点は、サブディレクトリがモジュールの一部として含まれていないことです。 代わりに、親モジュールはmodule宣言を使用してそれらを明示的に含める必要があります。

module "networking" {
  source = "./networking"
  create_public_ip = true
}

ここでは、「networking」サブディレクトリにあるモジュールを参照し、それに単一のパラメータ(この場合は boolean 値)を渡します。

現在のバージョンでは、Terraformはcountおよびfor_eachを使用してモジュールの複数のインスタンスを作成することを許可していないことに注意することが重要です。

4.7. 入力変数

トップまたはメインモジュールを含むすべてのモジュールは、変数ブロック定義を使用して複数の入力変数を定義できます。

variable "myvar" {
  type = string
  default = "Some Value"
  description = "MyVar description"
}

変数にはtype があり、 string map setなどがあります。 また、mayにはデフォルト値と説明があります。 トップレベルモジュールで定義された変数の場合、Terraformはいくつかのソースを使用して変数に実際の値を割り当てます。

  • -varコマンドラインオプション
  • .tfvar ファイル、コマンドラインオプションを使用するか、既知のファイル/場所をスキャンする
  • TF_VAR_で始まる環境変数
  • 変数のデフォルトの値(存在する場合)

ネストされたモジュールまたは外部モジュールで定義された変数の場合、デフォルト値を持たない変数は、module参照の引数を使用して指定する必要があります。 入力変数の値を必要とするモジュールを使用しようとしたが、値を指定できなかった場合、Terraformはエラーを生成します。

定義したら、varプレフィックスを使用して式で変数を使用できます。

resource "xxx_type" "some_name" {
  arg = var.myvar
}

4.8. 出力値

設計上、モジュールのコンシューマーは、モジュール内で作成されたリソースにアクセスできません。 ただし、別のモジュールまたはリソースの入力として使用するために、これらの属性の一部が必要になる場合があります。 これらのケースに対処するために、モジュールは、作成されたリソースのサブセットを公開する出力ブロックを定義できます

output "web_addr" {
  value = aws_instance.web.private_ip
  description = "Web server's private IP address"
}

ここでは、モジュールが作成したEC2インスタンスのIPアドレスを含む「web_addr」という名前の出力値を定義しています。 これで、モジュールを参照するモジュールは、この値を module.module_name.web_addr として式で使用できます。ここで、module_nameは対応する名前で使用した名前です。 モジュール宣言。

4.9. ローカル変数

ローカル変数は標準変数のように機能しますが、そのスコープは宣言されているモジュールに限定されます。 ローカル変数を使用すると、特にモジュールからの出力値を処理するときに、コードの繰り返しが減る傾向があります。

locals {
  vpc_id = module.network.vpc_id
}
module "network" {
  source = "./network"
}
module "service1" {
  source = "./service1"
  vpc_id = local.vpc_id
}
module "service2" {
  source = "./service2"
  vpc_id = local.vpc_id
}

ここで、ローカル変数 vpc_id は、ネットワークモジュールから出力変数の値を受け取ります。 後で、この値を引数としてservice1モジュールとservice2モジュールの両方に渡します。

4.10. ワークスペース

Terraformワークスペースを使用すると、同じプロジェクトの複数の状態ファイルを保持できます。 プロジェクトで初めてTerraformを実行すると、生成された状態ファイルはdefaultワークスペースに入ります。 後で、 terraformworkspace new コマンドを使用して新しいワークスペースを作成できます。オプションで、既存の状態ファイルをパラメーターとして指定できます。

通常のVCSでブランチを使用するのとほぼ同じように、ワークスペースを使用できます。 たとえば、ターゲット環境(DEV、QA、PROD)ごとに1つのワークスペースを設定できます。また、ワークスペースを切り替えることで、新しいリソースを追加するときにテラフォーム適用の変更を行うことができます。

これが機能する方法を考えると、ワークスペースは、同じ構成セットの複数のバージョン(または必要に応じて「インカネーション」)を管理するための優れた選択肢です。 これは、悪名高い「私の環境で動作する」問題に対処しなければならなかったすべての人にとって素晴らしいニュースです。これにより、すべての環境が同じように見えるようになります。

シナリオによっては、対象とする特定のワークスペースに基づいて、一部のリソースの作成を無効にすると便利な場合があります。 そのような場合は、terraform.workspace定義済み変数を使用できます。 この変数には現在のワークスペースの名前が含まれており、式で他の変数と同じように使用できます。

5. 結論

Terraformは、プロジェクトでInfrastructure-as-Codeプラクティスを採用するのに役立つ非常に強力なツールです。 ただし、この力には課題が伴います。 この記事では、このツールの機能と基本的な概念をよりよく理解できるように、このツールの概要を簡単に説明しました。

いつものように、すべてのコードはGitHubを介して利用できます。