はじめに
みなさんIaCしてますか?
IaCツールといえばTerraformとAnsibleの二大巨頭なイメージがあり、どっちを使うか?みたいな話題をよく耳にしますが、個人的にはAWS/Azure/GCPといったパブリッククラウドのプロビジョニングはTerraform、オンプレミスのネットワーク設定や仮想サーバ内の設定はAnsibleがそれぞれ強いイメージです。
ただ昨今ではやはりパブリッククラウドの利用とそれに伴うIaC化が活発な印象があるので、ここらで一度Terraformの利用方法をチュートリアル形式で学ぶコンテンツを作ってみようと思います。
チュートリアルのセクション
Tutorial-1. AWSリソースを作成する(初級編) ←本記事
Tutorial-2. Local Valuesとリソース参照の活用
Tutorial-3. countを使って複数のリソースを一気に作る
Tutorial-4. TerraformでAWSリソースを作成する(中級編)
Tutorial-5. for_eachを使ってよりフレキシブルな繰り返し処理を実現する
Tutorial-6. moduleを使ってresourceを共通化する
Tutorial-7. TerraformでAWSリソースを作成する(上級編)
Extra-1. Secret情報を暗号化してGitにアップする
Extra-2. tfstateファイルに外部のストレージを利用する
Extra-3. 他のTerraformで管理しているリソースを参照する
チュートリアルで目指すゴール
- Terraformの基本的な利用方法を理解する
- tfファイルの記述方法やディレクトリ構成を理解する
- 現場である程度のレベルでTerraform導入を実施することができる
注意事項
このチュートリアルはTerraformを初めて触る人が順番にセクションを実施することで実践的なノウハウを習得することを目標としています。そのため、チュートリアルの中ではあえて非推奨な記述を行うことがありますが、Terraformの理解を深めるためのプロセスとしてご容赦ください。
前提条件
チュートリアルは全体的に以下を前提として進行します。
- ある程度AWSのサービスとその関連性を理解している(AWS-SAAレベル)
- terraformを実行できる環境がある
- Terraformのインストール方法はこちらなどを参照ください。
- 今回は Terraform v1.4.4 を利用します。
- AWSアカウントを持っており、AdministratorAccess権限を持つIAMユーザーを利用できる
本編
では、ここからチュートリアルを始めていきます。
このセクションでは、AWSの簡単な構成のデプロイを通じて、Terraformの基本的な記述方法を理解します。
本セクションのゴール
以下のような構成をTerraformでデプロイできること
事前準備
作業用フォルダを作成します。
terraformはディレクトリでソースを管理するので、ローカルに作業用のディレクトリを作成しておきます。
mkdir -p ~/terraform-tutorial/tutorial-1
cd ~/terraform-tutorial/tutorial-1
環境設定用の.tfファイルを作成→初期化を実行する
tfファイルについて
Terraformでは、デプロイするリソース情報や認証情報などの情報を .tfファイル で記載します。
Terraformコマンドの実行時、カレントディレクトリにある.tfファイルをすべて読み込みます。
そのため、全ての情報をひとつの.tfファイルに書いてしまっても問題ありませんが、管理面が煩雑になるので、いくつかの要素に分けて.tfファイルを作成します。
環境設定用の.tfファイル
terraformを実行するために、いくつか環境設定のようなファイルを作成する必要があります。
今回は以下の2つのファイルを作成します。
- provider.tf
- versions.tf
provider.tf
Terraformにおいて、どのプロバイダー(AWS,Azure,GCP,K8sなど)のリソースをデプロイするかをProviderと呼ばれるコンポーネントで定義します。
AWS Providerではアクセスキーとシークレットキー、デフォルトのリージョン設定が必要になります。(aws-cliにおけるaws configureを入力した際に必要な値と一緒)
ここではAdministratorAccess
の権限をもつIAMユーザーの情報を入力ください。
provider aws {
access_key = "XXXXXXXXXXXXXXXXXXXXXXXXX"
secret_key = "YYYYYYYYYYYYYYYYYYYYYYYYY"
region = "ap-northeast-1"
}
versions.tf
ProviderやTerraform自体のバージョンの整合性を取っておくと、今後バージョンアップがあったときになぜかエラーになる、といった事象を防げます。
ここではAWSのProviderとTerraformのバージョンを指定して、これ以外のバージョンのものを使おうとした時には実行前にエラーになるようにしています。
AWS Providerの最新バージョンはこちらで確認できます。
terraform {
required_version = "= 1.4.4"
required_providers {
aws = "= 4.61.0"
}
}
terraform init
Terraformのワークスペースの初期化や、プラグインをダウンロードするために terraform init
を実行します。
これによってprovider.tfで指定したProviderのプラグインを取得できます。
terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "4.61.0"...
- Installing hashicorp/aws v4.61.0...
- Installed hashicorp/aws v4.61.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
取得したプラグインは .terraform
ディレクトリに格納されます。
tree -a
.
├── .terraform
│ └── providers
│ └── registry.terraform.io
│ └── hashicorp
│ └── aws
│ └── 4.61.0
│ └── darwin_arm64
│ └── terraform-provider-aws_v4.61.0_x5
├── .terraform.lock.hcl
├── provider.tf
└── versions.tf
7 directories, 4 files
デプロイ用のtfファイル作成→実行
Terraformからソースをデプロイする際は、 Resource
というコンポーネントを使います。
Resourceが記載された.tfファイルを格納したフォルダでデプロイ実行コマンドを入力すると、自動でデプロイが実行されます。
各種Resourceの記述方法については、作成したいリソースをGoogleで検索して、検索結果のTemplateを基本コピペして使うだけで殆ど問題ありません。
例えばRDSをTerraformコードにしたいときは「terraform AWS RDS resource」のように検索するとトップにTerraform公式サイトが出てきます。
VPCの作成
まずはじめにVPCを作成します。
.tfファイルの作成
VPCをデプロイするためのResourceの.tfファイルを作成します。
このResourceの公式ドキュメントはこちら
-
enable_dns_hostnames
をtrueにしておくことで、VPC内からでもエンドポイントのドメイン名を解決できます。
resource "aws_vpc" "vpc" {
cidr_block = "192.168.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "tutorial-vpc"
}
}
今のフォルダ構成は以下の通り
.
├── .terraform
├── .terraform.lock.hcl
├── provider.tf
├── versions.tf
└── vpc.tf
デプロイの実行
実際にコマンドを実行してデプロイしてみます。
terraform plan
を実行すると、カレントディレクトリ内の.tfファイルを読み取って、どんなAWSリソースがデプロイされるのか確認できます。
構文エラーがあればこのplan時に弾かれるので、実行前は必ずplanを実行するようにしましょう。
terraform plan
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_vpc.vpc will be created
+ resource "aws_vpc" "tutorial_vpc" {
+ arn = (known after apply)
+ cidr_block = "192.168.0.0/16"
+ default_network_acl_id = (known after apply)
+ default_route_table_id = (known after apply)
+ default_security_group_id = (known after apply)
+ dhcp_options_id = (known after apply)
+ enable_classiclink = (known after apply)
+ enable_classiclink_dns_support = (known after apply)
+ enable_dns_hostnames = true
+ enable_dns_support = true
+ enable_network_address_usage_metrics = (known after apply)
+ id = (known after apply)
+ instance_tenancy = "default"
+ ipv6_association_id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_network_border_group = (known after apply)
+ main_route_table_id = (known after apply)
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "tutorial-vpc"
}
+ tags_all = {
+ "Name" = "tutorial-vpc"
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
───────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to
take exactly these actions if you run "terraform apply" now.
デプロイは terraform apply
コマンドを実行します。
plan時と同様の内容が出力されるので、問題なければ Enter a value:
に yes
と入力してデプロイ実行します。
するとデプロイが走り、しばらくすると完了した旨のメッセージが出力されます。
terraform apply
---omit(planと同様)---
Enter a value: yes ←yesを入力
aws_vpc.vpc: Creating...
aws_vpc.vpc: Still creating... [10s elapsed]
aws_vpc.vpc: Creation complete after 12s [id=vpc-04fb5154d22486542]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
AWSコンソールを見ると、Subnetが作成されていることがわかります。
tfstateファイルについて
最初のapplyが完了した時、カレントディレクトリに terraform.tfstate
というファイルが生成されます。
このファイルは実際にデプロイしたリソースの情報をjson形式で保存しているものです。
今後 terraform apply
で差分実行したときは、terraform.tfstate
ファイル、実機、tfファイルの3つの情報を元に差分を検出し、実行されます。
「tfstateなんて見ずに実機とtfファイルの差分だけ見りゃよくね?」と思う人もいるかもしれませんが、リソースの依存関係など、実機にない情報も必要らしく、このファイルも状態管理には必要です。詳しくは公式ページで説明されています。
ちなみにVPCを作成した段階のtfstateファイルは以下のような形になっています。
{
"version": 4,
"terraform_version": "1.4.4",
"serial": 1,
"lineage": "a2216e14-10e4-bfb0-99e2-d69cfa87452d",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "aws_vpc",
"name": "vpc",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"arn": "arn:aws:ec2:ap-northeast-1:XXXXXXXXXXXX:vpc/vpc-04fb5154d22486542",
"assign_generated_ipv6_cidr_block": false,
"cidr_block": "192.168.0.0/16",
"default_network_acl_id": "acl-07759005723d928fc",
"default_route_table_id": "rtb-0ab93bd6a0c2cf95e",
"default_security_group_id": "sg-0f7cc93c60b68fbb6",
"dhcp_options_id": "dopt-01152b10208f13dd5",
"enable_classiclink": false,
"enable_classiclink_dns_support": false,
"enable_dns_hostnames": true,
"enable_dns_support": true,
"enable_network_address_usage_metrics": false,
"id": "vpc-04fb5154d22486542",
"instance_tenancy": "default",
"ipv4_ipam_pool_id": null,
"ipv4_netmask_length": null,
"ipv6_association_id": "",
"ipv6_cidr_block": "",
"ipv6_cidr_block_network_border_group": "",
"ipv6_ipam_pool_id": "",
"ipv6_netmask_length": 0,
"main_route_table_id": "rtb-0ab93bd6a0c2cf95e",
"owner_id": "878728386472",
"tags": {
"Name": "tutorial-vpc"
},
"tags_all": {
"Name": "tutorial-vpc"
}
},
"sensitive_attributes": [],
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
}
],
"check_results": null
}
Internet Gatewayの作成
では次にInternet Gatewayを作成していきます。
.tfファイルの作成
Internet GatewayをデプロイするためのResourceの.tfファイルを作成します。
このResourceの公式ドキュメントはこちら
-
vpc_id
には、先ほど作成したVPCのIDを入力しておきます。これによってこのInternet Gatewayはtutorial-vpc
内に作成されます。
resource "aws_internet_gateway" "igw" {
vpc_id = "vpc-04fb5154d22486542"
tags = {
Name = "tutorial-igw"
}
}
デプロイの実行
terraform apply
でInternet Gatewayを作成します。
terraform plan
terraform apply
コンソールで確認すると、たしかにInternet Gatewayが作成されています。
Subnetの作成
次にEC2インスタンスを立てるSubnetを作成します。
.tfファイルの作成
SubnetをデプロイするためのResourceの.tfファイルを作成します。
このリソースの公式ドキュメントはこちら
-
vpc_id
はtutorial_vpc
のIDを入力します。 -
cidr_block
のCIDRはVPC CIDRの範囲内に指定します。
resource aws_subnet subnet {
vpc_id = "vpc-04fb5154d22486542"
cidr_block = "192.168.1.0/24"
availability_zone = "ap-northeast-1a"
tags = {
Name = "tutorial-public-subnet"
}
}
デプロイの実行
terraform apply
でSubnetを作成します。
terraform plan
terraform apply
コンソールで確認すると、たしかにSubnetが作成されています。
Route Tableの作成
作成したSubnetとInternet GatewayをRoute Tableで結び付けないと、VPC内のEC2のインターネットに通信できません。そのため、Internet Gatewayへの通信用のRoute Tableを作成します。
.tfファイルの作成
Route Tableをデプロイするためのresourceの.tfファイルを作成します。
このリソースの公式ドキュメントはこちら
-
vpc_id
はtutorial-vpc
のIDを入力します。 -
gateway_id
はtutorial-igw
のIDを入力します。
resource aws_route_table route_table {
vpc_id = "vpc-04fb5154d22486542"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "igw-0e204fdaad34cae29"
}
tags = {
Name = "tutorial-route-table"
}
}
デプロイの実行
terraform apply
でRoute Tableを作成します。
terraform plan
terraform apply
コンソールで確認すると、たしかにRoute Tableが作成されています。
Route TableとSubnetの関連付け
作成したRoute TableとSubnetを関連付けます。
.tfファイルの作成
Route TableとSubnetを関連づけるためのResourceの.tfファイルを作成します。
このリソースの公式ドキュメントはこちら
-
subnet_id
は先ほど作成したSubnetのIDを指定します。 -
route_table_id
は先ほど作成したRoute TableのIDを指定します。
resource aws_route_table_association rta {
subnet_id = "subnet-0bf5661c57db0d425"
route_table_id = "rtb-072ca46273b6a88e6"
}
デプロイの実行
terraform apply
でRoute TableとSubnetを関連づけます。
terraform plan
terraform apply
コンソールで確認すると、たしかにRoute TableとSubnetが関連づけられています。
秘密鍵・公開鍵の作成
EC2インスタンスを作成する際、GUIで行った場合はAWS側でSSHキーの作成&公開鍵の設定までやってくれますが、aws-cliやTerraformで実行する場合は事前に作成したSSHキーのインポートしか対応していません。そのため、事前にSSHのキーペアを作成しておく必要があります。
ssh-keygen -t rsa -b 4096
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/XXX/.ssh/id_rsa): /Users/XXX/.ssh/aws-ec2
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/XXX/.ssh/aws-ec2.
Your public key has been saved in /Users/XXX/.ssh/aws-ec2.pub
The key fingerprint is:
-------omit-------------
ll ~/.ssh/
-rw------- 1 XXX XXX 3326 11月 26 01:10 aws-ec2
-rw-r--r-- 1 XXX XXX 781 11月 26 01:10 aws-ec2.pub
キーペアのアップロード
作成したSSH公開鍵をAWS上にアップロードします。
.tfファイルの作成
キーのアップロード用のresourceの.tfファイルを作成します。
このリソースの公式ドキュメントはこちら
- public_keyには公開鍵(
aws-ec2.pub
)の中身を記載します。
resource "aws_key_pair" "tutorial_key" {
key_name = "tutorial-key"
public_key = "ssh-rsa AAAAB3NzaC1yc2---omit---XXX"
}
デプロイの実行
terraform apply
で公開鍵をアップロードします。
terraform plan
terraform apply
コンソールで確認すると、キーがアップロードされていることがわかります。
Security Groupの作成
EC2インスタンスの作成時、Security Groupの指定が必要ですが、デフォルトのSecurity Groupを使うと0.0.0.0/0で22番ポートを公開しかねません。キーペアでアクセス制御できているので問題はないですが、念の為特定のアクセス元のみを許可するSecurity Groupを事前に作成しておきます。
.tfファイルの作成
Security Group用のResourceの.tfファイルを作成します。
このリソースの公式ドキュメントはこちら
- インバウンドルール(ingress)に自分の作業端末のIPからのSSHアクセスを許可します。
- アウトバウンドルール(egress)は全ての通信を許可します。
resource "aws_security_group" "sg" {
name = "tutorial-security-group"
description = "Allow SSH inbound traffic from XXX"
vpc_id = "vpc-04fb5154d22486542"
ingress {
description = "SSH from XXX"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["XXX.XXX.XXX.XXX/32"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "tutorial-security-group"
}
}
デプロイの実行
terraform apply
でSecurity Groupを作成します。
terraform plan
terraform apply
画面で確認すると、たしかにSecurity Groupが作成されています。
EC2インスタンスの作成
インスタンス作成時に必要なKeyとSecurity Groupの作成が完了したので、実際にインスタンスをデプロイしていきます。
.tfファイルの作成
EC2インスタンス用のresourceの.tfファイルを作成します。
このリソースの公式ドキュメントはこちら
-
ami
はap-northeast-1
のAmazon Linuxのイメージを指定します。 -
subnet_id
は作成したSubnetのIDを指定します。 -
key_name
は作成したキーの名前を指定します。 -
vpc_security_group_ids
は作成したSecurity GroupのIDを指定します。
resource aws_instance instance {
ami = "ami-02a2700d37baeef8b"
instance_type = "t2.micro"
subnet_id = "subnet-0bf5661c57db0d425"
associate_public_ip_address = true
key_name = "tutorial-key"
vpc_security_group_ids = ["sg-09e5b29077cb81637"]
tags = {
Name = "tutorial-instance"
}
}
デプロイの実行
terraform apply
でEC2インスタンスをデプロイします。
terraform plan
terraform apply
コンソールで確認すると、たしかにEC2インスタンスが作成されています。
EC2へSSHアクセス
これで目的の構成をデプロイできたので、EC2へアクセスしてみます。
ssh -i ./aws-ec2 ec2-user@43.207.107.19
The authenticity of host '43.207.107.19 (43.207.107.19)' can't be established.
ED25519 key fingerprint is SHA256:D+N8jZVoLknkReqOBEr8zJS1HHfkyLt1Ks+DCuxUmhs.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '43.207.107.19' (ED25519) to the list of known hosts.
Enter passphrase for key './aws-ec2':
, #_
~\_ ####_ Amazon Linux 2023
~~ \_#####\
~~ \###|
~~ \#/ ___ https://aws.amazon.com/linux/amazon-linux-2023
~~ V~' '->
~~~ /
~~._. _/
_/ _/
_/m/'
[ec2-user@ip-192-168-1-197 ~]$
ということで、無事EC2インスタンスへSSHアクセスすることができました。
デプロイしたリソースの削除
このチュートリアルで構築した環境を削除する方法は3つあります。
- .tfファイル内のresourceの記述を削除 or コメントアウトして
terraform apply
を実行する - ファイルの拡張子を変更(ex : aaa.tf→aaa.tf_old)して
terraform apply
を実行する -
terraform destroy
を実行する
1. .tfファイル内のresourceの記述を削除 or コメントアウトする
試しにRoute TableとSubnetを紐付けている"route_table_association.tf"ファイルをコメントアウトしてみます。
#resource aws_route_table_association rta {
# subnet_id = "subnet-0bf5661c57db0d425"
# route_table_id = "rtb-072ca46273b6a88e6"
#}
この状態でplanを実行してみる見てもらうとわかる通り、紐付けのリソースを削除する旨の内容が出力されます。
terraform plan
---omit---
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# aws_route_table_association.rta will be destroyed
# (because aws_route_table_association.rta is not in configuration)
- resource "aws_route_table_association" "rta" {
- id = "rtbassoc-089a58f9f877fe6d8" -> null
- route_table_id = "rtb-072ca46273b6a88e6" -> null
- subnet_id = "subnet-0bf5661c57db0d425" -> null
}
Plan: 0 to add, 0 to change, 1 to destroy.
───────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to
take exactly these actions if you run "terraform apply" now.
問題なければ terraform apply
します。
terraform apply
---omit(Planと同様)---
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
aws_route_table_association.rta: Destroying... [id=rtbassoc-089a58f9f877fe6d8]
aws_route_table_association.rta: Destruction complete after 0s
Apply complete! Resources: 0 added, 0 changed, 1 destroyed.
この状態で実際にSSHをすると、アクセスできないことがわかります。
ssh -i ./aws-ec2 ec2-user@43.207.107.19 -vvv
---omit---
debug3: ssh_connect_direct: entering
debug1: Connecting to 43.207.107.19 [43.207.107.19] port 22.
debug3: set_sock_tos: set socket 3 IP_TOS 0x48
<以降、返答なし>
2. ファイルの拡張子を変更する(ex : aaa.tf→aaa.tf_old)
削除したいリソースに紐づく.tfファイルの拡張子を変更します。
mv route_table_association.tf route_table_association.tf_old
その後の挙動はコメントアウト時と同様です。
3. terraform destroy
terraformでは特定のリソースもしくは全てのリソースを削除するためのコマンド terraform destroy
が存在します。
全てのリソースを削除する場合は以下のコマンドを実行します。
terraform plan -destroy
terraform destroy
特定のリソースを削除したいときは-targetオプションでリソースを指定します。
terraform plan -destroy -target aws_route_table_association.rta
terraform destroy -target aws_route_table_association.rta
ターゲットの指定は <resource種別>.<resource名>
で行います。例えば aws_route_table_association
Resourceで作成した rta
のコンポーネントを削除する場合は aws_route_table_association.rta
と指定します。
環境のクリーンアップ
最後にこのセクションで作成した一連のリソースを削除します。
terraform plan -destroy
terraform destroy
おわりに
このセクションではTerraformのplan/apply/destroyと基本的な操作と、AWSの簡易的な構成をデプロイするためのtfファイルの書き方について学びました。次回のセクション「AWSで学ぶTerraform実践②〜Local Valuesとリソース参照の活用」では、tfファイルのより効率的な記述方法について学んでいきます。
参考
今回使用したコードは以下のリポジトリにアップしています。
https://github.com/skitamura7446/terraform-tutorial/tree/master/tutorial-1
※各種tfファイル内のIDやAWSアカウントの情報、ec2の公開鍵などは個人で利用したものを記載しているため、このリポジトリをそのままapplyすることはできないこと、ご注意ください。