Terraform AWS Moduleのすすめ

AWS

2022.5.25

Topics

こんにちは、tkgです。

最近、Terraformをよく利用しているのですが、一からリソースを定義していくと思ったより手間がかかると感じています。

Terraformにはmoduleというテンプレートや関数のような概念があるのでそちらを定義して利用していくことも考えられますが、実運用で自作のmoduleを利用していくと逐次整備が必要になってくるなど、後々のコスト増が容易に想像できます。

今回はTerraform Registryで公開されているTerraform AWS modulesを使ってAWSリソースを作成してみることにしました。

想定読者

  • Terraformを利用して簡単なAWSリソース作成が出来る方
  • 「Terraform AWS modules」などコミュニティベースのmoduleを利用したことがない方

前提環境

今回は下記のようなterrafrom実行環境を準備しています。

$ terraform --version
Terraform v1.1.7
on linux_amd64
$ 

実際に書いていく

今回はサンプルとして、VPC、サブネット、EC2の作成を行います。
下記のような構成としています。

terraformファイルのディレクトリ構成は下記のような形とします。

.
├── ec2.tf
├── vpc.tf
├── providers.tf
└── securitygroup.tf
└── variables.tf

各モジュールの書き方については、Gitリポジトリ内のREADMEおよび、同梱されているexampleに例となるmain.tfがありますので、そちらを参考に書いていきます。

providers.tf

AWSプロバイダを設定しています。
バージョンは執筆時に適用していたバージョンとなります。

リージョンは別途変数で指定するため、変数名を代入しています。

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 4.0"
      }
    }
}

provider "aws" {
  region = local.region
}

variables.tf

コマンドベースでの値の上書きする運用を想定していないためlocal変数で定義しています。

そういった利用が想定される場合はvariableを利用してください。

locals {
  name_prefix = "tkg"
  region = "ap-northeast-1"
}

vpc.tf

ここから本格的にmodule機能を利用します。
moduleのソースにはローカルのフォルダやgitなど様々なものが利用可能ですが、今回はTerraform Registory上の利用したいモジュールのパスを指定しています。

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.14.0"

  name = "${local.name_prefix}-vpc"
  cidr = "10.1.0.0/16"

  azs             = ["${local.region}a","${local.region}c"]
  public_subnets  = ["10.1.1.0/24","10.1.2.0/24"]
  private_subnets = ["10.1.10.0/24","10.1.20.0/24"]

  enable_dns_hostnames = true 
  enable_nat_gateway = true
}

ec2.tf

今回はオートスケールではなく単体EC2をプライベートサブネット各AZごとに1台ずつ置く構成のため、パブリックサブネットのALBとプラベートサブネットのEC2の定義を記述しています。

モジュール上でもfor_each構文によって複数台の定義が可能ですので、そちらを利用して定義しました。

module "alb" {
  source  = "terraform-aws-modules/alb/aws"
  version = "6.10.0"

  name = "${local.name_prefix}-alb"

  vpc_id          = module.vpc.vpc_id
  subnets         = module.vpc.public_subnets
  security_groups = [module.sg-ext-http.security_group_id]

  http_tcp_listeners = [
    {
      port               = 80
      protocol           = "HTTP"
      target_group_index = 0
    }
  ]

  target_groups = [
    {
      name             = "${local.name_prefix}-tg"
      backend_protocol = "HTTP"
      backend_port     = 80
      target_type      = "instance"
      targets = [
        {
            target_id  = module.ec2.1.id
            port       = 80
        },
        {
            target_id  = module.ec2.2.id
            port       = 80
        }
      ]
    }
  ]
}

locals {
    multiple_instances = {
        01 = {
          availability_zone = element(module.vpc.azs, 0)
          subnet_id         = element(module.vpc.private_subnets, 0)
        }
        02 = {
          availability_zone = element(module.vpc.azs, 1)
          subnet_id         = element(module.vpc.private_subnets, 1)
        }
    }
}

module "ec2" {
    source  = "terraform-aws-modules/ec2-instance/aws"
    version = "4.0.0"
    for_each = local.multiple_instances

    name = "${local.name_prefix}-ec2-${each.key}"

    ami                    = "ami-0123c868871eaa6e5"
    instance_type          = "t3.micro"
    availability_zone      = each.value.availability_zone
    subnet_id              = each.value.subnet_id

    vpc_security_group_ids = [module.sg-local-http.security_group_id]

    root_block_device = [
        {
          encrypted   = true
          volume_type = "gp3"
          throughput  = 125
          volume_size = 10
        }
    ]
}

こちらで少々難しいのは参照先の指定で、特にターゲットグループの指定です。

理解すると簡単なのですが、上記のようなfor_each等のループ文でリソースを作成すると、"module.ec2"で参照した先に複数のリソースが存在する状態になります。

ec2のインスタンスIDは".id"で参照できますが、上記のとおり複数のリソースが存在するため"module.ec2.id"ではエラーとなります。そのため、".id"の前にリソースの順番をすることでIDを参照することが可能となります。

下記のような形で、参照先の中身をoutputするとわかりやすいです。

output "ec2" {
  value = module.ec2
}
  • terraform plan で出力される内容
Changes to Outputs:
  + ec2 = {
      + 1 = {
          + arn                                = (known after apply)
          + capacity_reservation_specification = (known after apply)
          + id                                 = (known after apply)
          + instance_state                     = (known after apply)
          + ipv6_addresses                     = (known after apply)
          + outpost_arn                        = (known after apply)
          + password_data                      = (known after apply)
          + primary_network_interface_id       = (known after apply)
          + private_dns                        = (known after apply)
          + private_ip                         = (known after apply)
          + public_dns                         = (known after apply)
          + public_ip                          = (known after apply)
          + spot_bid_status                    = ""
          + spot_instance_id                   = ""
          + spot_request_state                 = ""
          + tags_all                           = {
              + "Name"       = "tkg-ec2-1"
            }
        }
      + 2 = {
          + arn                                = (known after apply)
          + capacity_reservation_specification = (known after apply)
          + id                                 = (known after apply)
          + instance_state                     = (known after apply)
          + ipv6_addresses                     = (known after apply)
          + outpost_arn                        = (known after apply)
          + password_data                      = (known after apply)
          + primary_network_interface_id       = (known after apply)
          + private_dns                        = (known after apply)
          + private_ip                         = (known after apply)
          + public_dns                         = (known after apply)
          + public_ip                          = (known after apply)
          + spot_bid_status                    = ""
          + spot_instance_id                   = ""
          + spot_request_state                 = ""
          + tags_all                           = {
              + "Name"       = "tkg-ec2-2"
            }
        }
    }

securitygroup.tf

セキュリティグループの定義も少々難しいのですが、モジュールの"rule.tf"内部に一般的に利用される項目が定義されており、そちらを指定することで記載を簡略化しています。
今回はalbでhttpsおよびhttp、ALB-EC2間の通信用にhttpのセキュリティグループを設定しています。

module "sg-ext-http" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "4.9.0"

  name = "${local.name_prefix}-http-sec"
  vpc_id = module.vpc.vpc_id

  ingress_cidr_blocks = ["0.0.0.0/0"]
  ingress_rules = ["https-443-tcp", "http-80-tcp"]
  egress_rules = ["all-all"]
}

module "sg-local-http" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "4.9.0"

  name = "${local.name_prefix}-http-sec"
  vpc_id = module.vpc.vpc_id

  ingress_cidr_blocks = ["10.1.0.0/16"]
  ingress_rules = ["http-80-tcp"]
  egress_rules = ["all-all"]
}

terraform plan / apply をして動作を確認する

下記のような形で、planおよびapplyが成功したらリソースの展開は完了です。

$ terraform plan

~~~中略~~~

Plan: 34 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

~~~ 中略 ~~~

  Enter a value: yes ←入力してenter

~~~ 中略 ~~~

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

モジュールを使わず書いた場合と比較してみる

モジュールを使わず上記のvpc.tfを再現してみたところ、下記画像のような形になりました。
普段GUIの裏側で自動的に行われているような箇所が多いほどmodule内で完結するようにされているため、コードの圧縮が見込めそうです。

比較画像

所感

今回、Terraform Registoryで公開されているモジュールを利用してAWSリソースを定義してきましたが、Resourceを毎回定義していく方法と比べ書き方の違いはありますが、普段GUIで入力するような項目を変数として記載するだけで展開できるためコードの簡略化が可能です。
軽く目を通しただけでも環境を把握しやすいことは運用上かなり優秀で、特にVPCはかなりの圧縮効果が見られそうです。

今回利用したモジュール以外にも多数のリソース向けモジュールがありますので一度利用してみることをおすすめします。

tkg

2016年入社のインフラエンジニアです。 写真が趣味。防湿庫からはレンズが生え、押入れには機材が生えます。 2D/3DCG方面に触れていた時期もありました。

Recommends

こちらもおすすめ

Special Topics

注目記事はこちら