完全初心者向けTerraform入門(AWS)

はじめに

三菱総研DCS クラウドテクノロジー部の神(じん)です。
今回は、Infrastructure as Codeの代表的なツールであるTerraformについて、まだ触れたことがない方向けに特徴と使い方を簡単に説明したいと思います。

私が所属するチームでは約1年前からTerraformを使い始め、現在は複数のシステムで利用しています。当初は、非常に限られた期間で複数の環境を構築する必要があり、構築のリードタイム短縮に期待して導入したものでした。

しかし実際に利用してみると、構築スピードが向上しただけでなく、変更点の管理が容易になったほか、操作ミスの排除やコードレビューによるチェック体制の強化など、沢山のメリットが得られました。この経験を少しでも多くの方に共有したいと思い、本記事を執筆いたしました。

Terraformの入門記事は弊社ブログ以外にも既に世に存在しますが、本記事は弊社の知見を交えた入門記事となっています。Terraformの利用を考えている方はぜひご覧ください。

Terraformとは

Terraformとは、HashiCorp社により開発されているオープンソースのインフラ自動構築ツールです。TerraformではAWSなどのクラウドサービス上のインフラリソース(例えばサーバやネットワークなど)をコードで定義します。
Terraformは作成したコードに基づいて自動でリソースを構築します。Terraformを利用することでオペレーションミスが無くなるほか、複数システムでコードを再利用することで効率化を図ることが出来ます。また、一度に大量のリソースを定義することで手動よりも早く構築することが可能になります。

このようにインフラリソースをコードで定義することをInfrastructure as Code(IaC)と呼びます。他のIaCツールとしてはサーバ内の設定変更を得意とするAnsibleなどが有名ですが、Terraformはクラウド上のリソースを構築することに長けています。クラウドリソースはTerraformで構築し、サーバ内の設定変更はAnsible、といった使い分けが可能です。

Terraformと似た位置付けのツールにAWS CloudFormationが存在しますが、Terraformとは違いAWS専用です。TerraformはAWSだけでなくAzureやGCP、GitHub、Datadogなど、複数のクラウドプロバイダーに対応しているため、マルチクラウド環境においても構築手段を統一することが出来ます。

開発環境を構築する

本記事では、実際にTerraformを用いてAWS上にリソースを作成することを目指していきます。

まずはTerraformを実行するための開発環境を構築します。今回は簡単に準備可能なAWS Cloud9を利用します。
AWS Cloud9はブラウザ上で操作が出来る統合開発環境で、環境の作成が非常に簡単です。

早速Cloud9で環境を作成します。AdministratorAccess権限を持ったIAMユーザでAWSマネジメントコンソールにログインし、Cloud9のページに遷移します。

その後、画面右上のCreate environmentをクリックします。

Nameには任意の環境名を入力し、Next Stepをクリックします。
その次のConfigure settingsは特に何も変更せずにNext Stepをクリックし、設定内容を確認後、Create environmentをクリックします。

Cloud9が立ち上がるまで待機します。立ち上がると以下のような画面がブラウザ上に表示されます。

Cloud9には最初からTerraformがインストールされているため、いきなりコードを書き始めることが出来ます。
Cloud9上のターミナルで以下のコマンドを実行し、インストールされているTerraformのバージョンを確認してください。

$ terraform -v

本記事執筆時点では以下のバージョンがインストールされていました。

コードを作成する

本記事では、以下構成図のリソースをTerraformのみで構築することをゴールとしたいと思います。

Terraformでは、拡張子が.tfのファイルにリソース定義を記述していきます。
ファイル名は自由に付けることが可能です。また、tfファイルは好きなように分割することが出来ます。1つの巨大なtfファイルに全てのリソースを記載した場合も、細かく分割したtfファイルにリソースを分けて記載した場合も、実行結果に差異はありません。

ファイル構成としては、以下のようにサービス単位で分割すると管理しやすくなります。

  • aws_ec2.tf
  • aws_vpc.tf
  • aws_s3.tf

先ほど述べた通り、ファイル名は自由に決められるものなので、aws_ec2.tfの中にEC2ではないリソース定義を記載しても実行時に問題はありません。また、ファイルを分割せずにmain.tfのような1つのファイルの中に全てのリソースを記述しても何ら問題ありません。

ただし、リソースが増えれば増えるほど管理上の手間が増えるため、ある程度法則性を持たせて分割することをおすすめします。弊社ではいくつかのやり方を試した結果、AWSサービス毎に分割する方法が最も管理がしやすいという結論に至りました。

また、ファイル名の頭に「aws_」というプレフィックスを付けることで、マルチクラウド環境でファイルを管理する場合に、どのクラウドサービスのコードなのか瞬時に判断できるようになります。こちらも付与することを推奨します。

それでは早速コードを作成します。まずはCloud9上で作業用のディレクトリを作成し、そこに移動します。

$ mkdir terraform && cd terraform

次に以下のコマンドで3つのtfファイルを作成します。

$ touch provider.tf
$ touch aws_vpc.tf
$ touch aws_ec2.tf

作成したファイルを開き、以下の通りに記載します。

provider.tf

provider "aws" {
  region = "ap-northeast-1"
}

provider.tfには、構築するリソースのプロバイダー情報を記載します。今回はAWSのリソースを作成するためprovider "aws"を定義しています。
regionには、リソース構築先のAWSリージョンを指定します。今回は東京リージョン(ap-northeast-1)を指定しています。

もしMicrosoft Azureのリソースを記述したい場合は、provider "azurerm"の定義が必要になります。
その他Terraformで対応しているプロバイダーの一覧については、以下の公式ドキュメントをご覧ください。
https://registry.terraform.io/browse/providers

次に、VPCとEC2のコードを記述していきますが、その前にTerraformコードの構文について簡単に説明します。

Terraformのコードは、HCL(HashiCorp Configuration Language)というHashiCorp社製品で使われている独自の言語で記述する必要があります。
未経験の方は言語をゼロから習得する必要がありますが、HCLの構文は非常にシンプルで簡単です。
例えば、リソースを記述する場合は以下のようなコードになります。

resource "リソースの種類" "リソース名" {
    設定項目1 = 設定値
    設定項目2 = 設定値
    設定項目3 = 設定値
}

「リソースの種類」に記述する内容は、Terraformで事前定義されているものを指定する必要があります。例えばEC2インスタンスを定義したい場合は「aws_instance」、VPCを定義したい場合は「aws_vpc」を記述します。
リソースの種類の全量については、以下の公式ドキュメントで確認が可能です。
https://registry.terraform.io/providers/hashicorp/aws/latest/docs

「リソース名」は、任意の文字列を記載することが可能であるため、そのリソースを判別しやすい名前を付けることをおすすめします。ただし、リソースの種類が同一のものに関しては、異なるリソース名にする必要があります。

例えば、以下のような定義は不可能です。(Terraform実行時に構文エラーで失敗します。)

  • resource "aws_instance" "sample"
  • resource "aws_instance" "sample"

以下のような場合はリソース名は同じでもリソースの種類が異なるため定義が可能です。

  • resource "aws_instance" "sample"
  • resource "aws_vpc" "sample"

中括弧 { } の中には、リソースの設定値を定義していきます。
各設定項目はTerraform側で事前に定義されており、公式ドキュメント内に記載があります。
設定項目は必須のものと任意のものがあり、任意のものを省略すると省略値が適用される場合があります。

その他、#から始まる行はコメント行となります。行の途中で#を使用し、コメントアウトすることも可能です。

上記を踏まえ、VPCとEC2のコードを実際に記述していきます。前述した構成図と一緒に見て頂けると理解が深まると思います。

aws_vpc.tf

#----------------------------------------
# VPCの作成
#----------------------------------------
resource "aws_vpc" "sample_vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
}

#----------------------------------------
# パブリックサブネットの作成
#----------------------------------------
resource "aws_subnet" "sample_subnet" {
  vpc_id                  = aws_vpc.sample_vpc.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true
}

#----------------------------------------
# インターネットゲートウェイの作成
#----------------------------------------
resource "aws_internet_gateway" "sample_igw" {
  vpc_id = aws_vpc.sample_vpc.id
}

#----------------------------------------
# ルートテーブルの作成
#----------------------------------------
resource "aws_route_table" "sample_rtb" {
  vpc_id = aws_vpc.sample_vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.sample_igw.id
  }
}

#----------------------------------------
# サブネットにルートテーブルを紐づけ
#----------------------------------------
resource "aws_route_table_association" "sample_rt_assoc" {
  subnet_id      = aws_subnet.sample_subnet.id
  route_table_id = aws_route_table.sample_rtb.id
}

#----------------------------------------
# セキュリティグループの作成
#----------------------------------------
resource "aws_security_group" "sample_sg" {
  name   = "sample-sg"
  vpc_id = aws_vpc.sample_vpc.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

}

上記コードでは、VPC、サブネット、インターネットゲートウェイ、ルートテーブル、セキュリティグループを作成しています。
これらのリソースはそれぞれ別の種類のリソースですが、AWSマネジメントコンソール上ではVPCサービスに含まれるリソース群であるため、aws_vpc.tfにまとめて記述しました。

上記のコードでは、設定値がハードコーディングされている部分と、他のリソースを参照している部分に分かれています。
例えば、サブネットのcidr_blockは10.0.1.0/24という値がハードコーディングされています。cidr_blockは事前に判明している設定値なので、このようにハードコーディングしても問題ありません。しかし、サブネットのvpc_idは、VPC作成後でないと値がわかりません。
このような場合は、サンプルコードのようにaws_vpc.sample_vpc.idといった値を設定します。Terraform内で定義された別のリソースのパラメータは、「リソースの種類.リソース名.属性」で参照することが可能です。

リソースの種類とリソース名は既に説明した通りですが、ここで初めて属性という概念が出てきます。上記の場合はidが属性にあたります。idは、aws_vpcリソースのVPC ID(vpc-xxxxxxxx)を返します。
このようにリソースの属性を参照することで、設定値をハードコーディングせずに動的に参照することが可能になります。

属性の種類はリソースの種類毎に異なりますが、aws_vpcの場合はidの他にarnやcidr_blockなど10種類以上の属性が存在します。
例えばcidr_blockを参照したい場合は、aws_vpc.sample.cidr_blockのように指定します。

次にEC2のコードを見ていきましょう。

aws_ec2.tf

#----------------------------------------
# EC2インスタンスの作成
#----------------------------------------
resource "aws_instance" "sample_web_server" {
  ami                    = "ami-09d28faae2e9e7138" # Amazon Linux 2
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.sample_subnet.id
  vpc_security_group_ids = [aws_security_group.sample_sg.id]

  user_data = <<EOF
#! /bin/bash
sudo yum install -y httpd
sudo systemctl start httpd
sudo systemctl enable httpd
EOF

}

amiは、AWSマネジメントコンソール上で事前にAMI IDを確認し、記述します。

サブネットIDやセキュリティグループIDは、aws_vpc.tf上で定義しているリソースの属性を参照します。このようにファイルを跨った参照でも何ら問題ありません。

user_dataには、Webサーバ(Apache)をインストールおよび起動させるためのユーザデータを記述します。ユーザデータとは、EC2インスタンスを起動する際に実行することが出来るシェルスクリプトです。ユーザデータの指定方法は色々存在しますが、このサンプルコードではヒアドキュメント形式でシェルスクリプトを記述しています。

リソースを作成する

コードの作成が完了したため、いよいよAWS環境上にリソースを構築します。
リソース作成の実行はterraformコマンドを利用します。

terraformコマンドを実行するためには、コマンドを実行する環境からインターネットへのアクセスが可能である必要があります。また、リソースを作成するためのAWSの権限設定も必要です。
しかし、Cloud9上で実行する場合は上記の前提が満たされているため、特に設定は不要です。
※Cloud9のデフォルト設定で実行可能なAWSの権限には一部制限がありますが、本記事で構築するリソースであれば問題なく実行可能です。

最初に以下のコマンドを実行し、Terraformの実行に必要なプラグインをインターネットから取得します。

$ terraform init

実行結果にTerraform has been successfully initialized!と表示されていれば成功です。
次に以下のコマンドを実行し、Terraformコードの構文に問題がないことを確認します。

$ terraform validate

構文に問題がある場合は、どのファイルの何行目に問題があるか表示されます。エラー内容を確認し、コードを適宜修正してください。
何も問題が無ければSuccess! The configuration is valid.と出力されます。

以下のコマンドを実行すると、コードのインデントを自動で整形することが可能です。複数人開発の場合は必ず実行するようにしましょう。

$ terraform fmt

最後に以下のコマンドを実行し、実環境上にリソースを構築します。

$ terraform apply

上記コマンド実行時に、まず設定変更の差分が一覧表示されます。

今回は既存リソースの設定変更ではなく、全て新規リソースの作成となるため、表示されている内容が全て+ createとなっています。既存リソースを設定変更する場合は~ update in-place、リソース削除の場合は- destroyのように表示されます。既存リソースの場合でも、設定値の変更が不可能で、再度リソースを作り直す必要があるものは-/+ destroy and then create replacementと表示されます。


構築内容に問題が無いことを確認後、Enter a value:にyesと入力します。(yes以外を入力するとapplyがキャンセルされます。)

Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
と出力されていればapplyは成功しています。

構築後、AWSマネジメントコンソールのEC2の画面に遷移し、問題なく構築されていることを確認します。
また、構築されたEC2インスタンスのパブリックIPアドレスにブラウザからアクセスし、ApacheのTestページが表示されることも確認します。(パブリックIPアドレスはAWSマネジメントコンソール上で確認可能です。)

無事、構築が成功したようです。
VPCやサブネットなど、その他のリソースについても問題なく構築できていることを確認してみてください。

基本的な構築の流れは以上です。

では、構築後にリソースの追加や設定変更が生じた場合はどうすればいいのでしょうか。
そのような場合、まずtfファイルを編集し、リソース定義の追加やパラメータの修正などを行います。
その後、terraform initからterraform applyまでの一連の流れを再度実行します。そうすることで、コードの修正箇所のみ設定変更することが可能です。

リソースを削除する

今回は検証目的でリソースを作成したので、最後にリソースを削除します。
以下のコマンドを実行し、リソースを全て破棄します。

$ terraform destroy

apply時と同様に、削除するリソースの一覧を確認し、問題が無ければyesを入力しリソースの破棄を実行します。
Destroy complete! Resources: 7 destroyed.と出力されていれば削除成功です。

なお、上記コマンドは基本的にリソースを全て破棄する際に使用するコマンドであることに注意してください。リソースの一部のみを削除したい場合は上記のコマンドは使用しません。

一部のリソースを削除する場合は、tfファイル上から該当箇所の記述を削除し、設定変更する際と同じようにterraform applyを実行します。
そうすることで、削除箇所のみが変更差分として表示され、その部分のみを削除することが出来ます。

おわりに

いかがだったでしょうか。
今回はあくまで入門記事のため、設定値は最小限に留めており、Terraformの機能もごく一部にしか触れていませんが、どういったツールなのかはご理解頂けたと思います。

Terraformを上手く使いこなすことで、チームのパフォーマンスや生産性は大きく向上します。本記事を足掛かりにその第一歩を踏み出して頂けたら嬉しく思います。

また、これは私個人の感想ですが、Terraformで思い通りにリソースが構築できると、とても気持ちが良いです。パズルのピースが上手くハマったような感覚になります。まだ触ったことがない皆様には、ぜひこの感覚を体験頂けたら幸いです。

本記事は以上となりますが、次回以降はより実践的なTerraformのプラクティスをご紹介できればと思います。