入門 Terraform Module
概要
個人の備忘録としてTerraformのModuleについてまとめていきます。
参考
Module導入前の課題
Terraformでインフラを管理していくにつれて、管理するインフラが増えてTerraform構成ファイルが複雑になっていきます。
固定の規則もなく一つの構成ファイルにて管理することで次のような課題につき当たります。
- 構成ファイルの理解とナビゲーションが難しくなる点
- 構成ファイルの更新がよりリスキーになる点。1つのセクションの更新が意図せず他の部分に影響を及ぼす恐れがあります
- 構成ファイル内に似たようなブロックの重複が増えていく点。例えば、dev/staging/productionの各環境を区分けする場合、これら環境の更新作業が負荷になります
- チームやプロジェクト間で構成ファイルの部分を共有しようと思い、構成ファイルをカッティングしたりペースティングした際にエラーが発生しメンテナンスが難しくなります
Module導入の利点
Moduleの導入により全セクションで紹介した課題が次のように解決することが見込めます
- 構成ファイルの組織化:関連のあるリソースを一つにまとめることで構成ファイルのナビゲーションや理解、そして更新作業が容易になります。モジュールを使うことで構成ファイルを論理的なコンポーネントに組織化して管理することができます。
- 構成ファイルの再利用:モジュールによる再利用化により時間の節約とエラーの発生を低減させることができます。
- 一貫性を提供しベストプラクティスを保証化してくれる点:一貫性が複雑な構成ファイルの理解を容易にしてくれるだけでなく、構成ファイル全体にわたってベストプラクティスが適用されることを高めてくれます。
Terraform Moduleについて
Terraform Moduleは1つのディレクトリにおけるTerraform構成ファイルの一連の集まりです。
Root Module
Terraformコマンドをあるディレクトリ内で実行した際にそのModuleが root moduleとみなされます。
例えば次のような最小構成の1つのディレクトリでTerraformコマンドを実行している場合、そのディレクトリがroot moduleになります。
.
├── LICENSE
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
Moduleの呼び出し
前節のRoot Moduleはただ1つのModuleだけですが、そのModuleから別のModuleを参照することもできます。この呼び出される側のModuleのことはchild module
と呼ばれたりします。
localとremote module
Moduleはローカルのファイルシステムや外部ソースから参照することができます。
Terrafromは多くのremote moduleを提供しており、その1つにTerraform Registryがあります。
Terraform Registryのmodule利用
ここでは下記のaws vpc moduleを利用します。
ページに記載の通り、構成ファイル例では二つの引数source
とargument
が記載されています。
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.14.0"
}
1: source
:この例ではTerraform Registry内で指定されたパスに合致するものをソースとします。そのほかにURLやローカルmoduleを指定できます。
2: version
: moduleのversionを指定します。未指定の場合は最新バージョンのmoduleを読み込みます。
構成ファイル例
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.4.0"
}
}
required_version = ">= 1.1.0"
}
provider "aws" {
region = "ap-northeast-1"
default_tags {
tags = {
hashicorp-learn = "module-use"
}
}
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.14.0"
name = var.vpc_name
cidr = var.vpc_cidr
azs = var.vpc_azs
private_subnets = var.vpc_private_subnets
public_subnets = var.vpc_public_subnets
enable_nat_gateway = var.vpc_enable_nat_gateway
tags = var.vpc_tags
}
module "ec2_instances" {
source = "terraform-aws-modules/ec2-instance/aws"
version = "3.5.0"
count = 2
name = "my-ec2-cluster"
ami = "ami-0ecb2a61303230c9d"
instance_type = "t2.micro"
vpc_security_group_ids = [module.vpc.default_security_group_id]
subnet_id = module.vpc.public_subnets[0]
tags = {
Terraform = "true"
Environment = "dev"
}
}
variable "vpc_name" {
description = "Name of VPC"
type = string
default = "example-vpc"
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "vpc_azs" {
description = "Availability zones for VPC"
type = list(string)
default = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
}
variable "vpc_private_subnets" {
description = "Private subnets for VPC"
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "vpc_public_subnets" {
description = "Public subnets for VPC"
type = list(string)
default = ["10.0.101.0/24", "10.0.102.0/24"]
}
variable "vpc_enable_nat_gateway" {
description = "Enable NAT gateway for VPC"
type = bool
default = true
}
variable "vpc_tags" {
description = "Tags to apply to resources created by VPC module"
type = map(string)
default = {
Terraform = "true"
Environment = "dev"
}
}
output "vpc_public_subnets" {
description = "IDs of the VPC's public subnets"
value = module.vpc.public_subnets
}
output "ec2_instance_public_ips" {
description = "Public IP addresses of EC2 instances"
value = module.ec2_instances[*].public_ip
}
次のコマンドでterraformリソースの初期化、反映を行っていきます。
# 初期化
terraform init
# 差分確認
terraform plan
# 環境反映
terraform apply
反映後、リソースの作成を確認できたら削除を実行します。
terraform destroy
local moduleの作成
全セクションの remote registryを使った例に加えて、
下記の構成ファイルを作成していきます。
resource "aws_s3_bucket" "s3_bucket" {
bucket = var.bucket_name
tags = var.tags
}
resource "aws_s3_bucket_website_configuration" "s3_bucket" {
bucket = aws_s3_bucket.s3_bucket.id
index_document {
suffix = "index.html"
}
error_document {
key = "error.html"
}
}
resource "aws_s3_bucket_acl" "s3_bucket" {
bucket = aws_s3_bucket.s3_bucket.id
acl = "public-read"
}
resource "aws_s3_bucket_policy" "s3_bucket" {
bucket = aws_s3_bucket.s3_bucket.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "PublicReadGetObject"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = [
aws_s3_bucket.s3_bucket.arn,
"${aws_s3_bucket.s3_bucket.arn}/*",
]
},
]
})
}
variable "bucket_name" {
description = "Name of the s3 bucket. Must be unique."
type = string
}
variable "tags" {
description = "Tags to set on the bucket."
type = map(string)
default = {}
}
output "arn" {
description = "ARN of the bucket"
value = aws_s3_bucket.s3_bucket.arn
}
output "name" {
description = "Name (id) of the bucket"
value = aws_s3_bucket.s3_bucket.id
}
output "domain" {
description = "Domain name of the bucket"
value = aws_s3_bucket_website_configuration.s3_bucket.website_domain
}
その後、ルートの resoruce.tf
で作成したmodules/s3
を次のように呼び出します(child module)
...
# 下記を追記
module "website_s3_bucket" {
source = "./modules/aws-s3-static-website-bucket"
bucket_name = "robin-test-dec-17-2019"
tags = {
Terraform = "true"
Environment = "dev"
}
}
その後、全セクションと同様にterraform plan
,terraform apply
を実行して環境に適用します。
resourceのmove
moved
ブロックを利用する構成ファイル内でのリソースの移動をトラックすることができます。
movde
ブロックによりリソースの移動をplan,プレビュー、そしてバリデートできるようになり、リファクタリングがより安全に行えます。
変更前構成ファイル
https://learn.hashicorp.com/tutorials/terraform/move-config?in=terraform/modules を参考に、下記リポジトリの内容を少し変更したものを利用します。
terraform {
required_version = ">= 1.1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.25.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
default_tags {
tags = {
hashicorp-learn = "move-config"
}
}
}
data "aws_availability_zones" "available" {
state = "available"
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.14.0"
name = var.vpc_name
cidr = var.vpc_cidr
azs = data.aws_availability_zones.available.names
private_subnets = var.vpc_private_subnets
public_subnets = var.vpc_public_subnets
enable_nat_gateway = var.vpc_enable_nat_gateway
map_public_ip_on_launch = true
tags = var.vpc_tags
}
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
resource "aws_instance" "example" {
ami = data.aws_ami.ubuntu.id
subnet_id = module.vpc.public_subnets[0]
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.sg_8080.id]
associate_public_ip_address = true
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y apache2
sed -i -e 's/80/8080/' /etc/apache2/ports.conf
echo "Hello World" > /var/www/html/index.html
systemctl restart apache2
EOF
tags = {
Name = "terraform-learn-move-ec2"
}
}
resource "aws_security_group" "sg_8080" {
vpc_id = module.vpc.vpc_id
name = "terraform-learn-move-sg"
ingress {
from_port = "8080"
to_port = "8080"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
// connectivity to ubuntu mirrors is required to run `apt-get update` and `apt-get install apache2`
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
variable "region" {
default = "us-east-2"
description = "The AWS region your resources will be deployed"
}
variable "vpc_name" {
description = "Name of VPC"
type = string
default = "learn-vpc"
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "vpc_private_subnets" {
description = "Private subnets for VPC"
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "vpc_public_subnets" {
description = "Public subnets for VPC"
type = list(string)
default = ["10.0.101.0/24", "10.0.102.0/24"]
}
variable "vpc_enable_nat_gateway" {
description = "Enable NAT gateway for VPC"
type = bool
default = true
}
variable "vpc_tags" {
description = "Tags to apply to resources created by VPC module"
type = map(string)
default = {
Terraform = "true"
Environment = "dev"
}
}
output "public_ip" {
description = "The Public IP address used to access the instance"
value = aws_instance.example.public_ip
}
# plan実行
% terraform plan
Plan: 22 to add, 0 to change, 0 to destroy.
# 環境への適用
% terraform apply
Apply complete! Resources: 22 added, 0 changed, 0 destroyed.
Outputs:
public_ip = "xxx.xxx.xxx.xx"
構成ファイルのリファクタリング
ここで 計算機リソース用のmoduleと、さらに後続で security group用のmoduleとに分割していきます。
まずは計算機リソースのmodule化で、次のようにmodule用のディレクトリを作成します。
mkdir -p modules/compute
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
resource "aws_instance" "example" {
ami = data.aws_ami.ubuntu.id
subnet_id = var.public_subnets[0]
instance_type = "t2.micro"
vpc_security_group_ids = [var.security_group]
associate_public_ip_address = true
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y apache2
sed -i -e 's/80/8080/' /etc/apache2/ports.conf
echo "Hello World" > /var/www/html/index.html
systemctl restart apache2
EOF
tags = {
Name = "terraform-learn-move-ec2"
}
}
variable "security_group" {
description = "The security groups assigned to this instance"
}
variable "public_subnets" {
description = "The public subnet IDs assigned to this instance for IP address assignment"
}
output "public_ip" {
description = "The Public IP address used to access the instance"
value = aws_instance.example.public_ip
}
続けてsecurity groupのmoduelも同様に作成していきます。
mkdir -p modules/security_group
resource "aws_security_group" "sg_8080" {
vpc_id = var.vpc_id
name = "terraform-learn-move-sg"
ingress {
from_port = "8080"
to_port = "8080"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
variable "vpc_id" {
description = "ID of the VPC where to create security group"
type = string
}
output "sg_id" {
description = "The security group ID passed from this module"
value = aws_security_group.sg_8080.id
}
module作成後、root moduleのresource.tf
のdata sourceaws_availability_zones
とmodulevpc
以外のcomputeとsecurity groupに関して作成したmoduleを呼び出すように次のように修正します。
module "ec2_instance" {
source = "./modules/compute"
security_group = module.security_group.sg_id
public_subnets = module.vpc.public_subnets
}
module "security_group" {
source = "./modules/security_group"
vpc_id = module.vpc.vpc_id
}
root moduleのoutputs.tfも次のように修正します
output "public_ip" {
description = "The Public IP address used to access the instance"
value = module.ec2_instance.public_ip
}
変更のreview・plan
ここで変更の差分をみていきます。
% terraform init
Initializing modules...
- ec2_instance in modules/compute
- security_group in modules/security_group
...
% terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
~ update in-place
- destroy
Terraform will perform the following actions:
# aws_instance.example will be destroyed
# (because aws_instance.example is not in configuration)
- resource "aws_instance" "example" {
...
# aws_security_group.sg_8080 will be destroyed
# (because aws_security_group.sg_8080 is not in configuration)
- resource "aws_security_group" "sg_8080" {
...
# module.ec2_instance.aws_instance.example will be created
+ resource "aws_instance" "example" {
...
# module.security_group.aws_security_group.sg_8080 will be created
+ resource "aws_security_group" "sg_8080" {
Plan: 2 to add, 13 to change, 2 to destroy.
Changes to Outputs:
~ public_ip = "xxx.xxx.xxx.xxx" -> (known after apply)
上記plan結果からもわかるように、今回の差分をそのまま適用すると一度既存のリソースを削除して再度作成されます。この間サービスが一時停止する影響が出てしまいます。
このような場合にterraformのmoved
ブロックを利用することでリソースの再作成なしに構成ファイルをリファクタリングできるようになります。
moved
ブロックを利用した構成ファイルのリファクタリング
moved
ブロックを利用することでTerraformに構成ファイルに変更があることを知らせることができます。またこれによりTerraformはplan実行時に安全に変更をレビューすることができます。
root moduleのresorce.tf
ファイルに次のようにmoved
ブロックを追加します。
この状態でplanを実行すると先ほどのリソースの削除と再作成がなくなっている事がわかります。
% terraform plan
...
Plan: 0 to add, 13 to change, 0 to destroy.
リソースのリネームとmove
既存リソースのリネームにもmoved
ブロックを利用できます。
root moduleのresource.tf
-module "vpc" {
+module "learn_vpc" {
source = "terraform-aws-modules/vpc/aws"
...
module "ec2_instance" {
source = "./modules/compute"
security_group = module.security_group.sg_id
- public_subnets = module.vpc.public_subnets
+ public_subnets = module.learn_vpc.public_subnets
}
module "security_group" {
source = "./modules/security_group"
- vpc_id = module.vpc.vpc_id
+ vpc_id = module.learn_vpc.vpc_id
}
続けて同ファイルの末尾にmoved
ブロックを次のように追記します。
moved {
from = module.vpc
to = module.learn_vpc
}
そして以上の変更をplan, そしてapplyしていきます。
% terraform init
Initializing modules...
Downloading registry.terraform.io/terraform-aws-modules/vpc/aws 3.14.0 for learn_vpc...
- learn_vpc in .terraform/modules/learn_vpc
動作確認が完了したらリソースを削除して終了です。
% terraform plan
Plan: 0 to add, 13 to change, 0 to destroy.
# 適用
% terrafrom apply
# resourceの削除
% terraform destroy
Discussion