CUEBiC TEC BLOG

キュービックTECチームの技術ネタを投稿しております。

マルチアカウントでTerraform運用を実践してみた

概要

こんにちは、キュービックでSREをやっているYuhta28です。キュービック内のテック技術について発信します。

以前の記事にて、弊社のAWSマルチアカウント運用について紹介しました。今回はマルチアカウント配下にて活用しているTerraformの運用について紹介いたします。

Terraform

www.terraform.io Terraformを使ったインフラリソースのコード化事例は数多く世の中に出回っています。

ですが、Terraformを使ったIaC管理は各社ごとに方法は様々で数多くの運用方法が紹介されています。というのもTerraformのバージョンがGA(Generally Available)リリースされたのが2021年6月以降で、バージョンアップのたびに機能の非互換性で動かなくなるという問題もありました。

そのため当時のTerraformのバージョンでは問題なくても、今では非推奨といった事例もあります。そのうえキュービックではコンソール画面から作成された既存のAWSリソースが数多くあります。なので、既存インフラリソースをまずはIaCで管理できるようにするところから始めました。

ディレクトリ構成

弊社でのTerraformのディレクトリ構成以下の通りです。

$ tree
.
├── README.md
├── env
│   ├── prd
│   │   ├── README.md
│   │   ├── ec2.tf
│   │   ├── ecs.tf
│   │   ├── efs.tf
│   │   ├── ga.tf
│   │   ├── iam.tf
│   │   ├── main.tf
│   │   ├── rds.tf
│   │   └── vpc.tf
│   └ ── sandbox
│      ├── README.md
│      ├── ec2.tf
│      ├── ecs.tf
│      ├── efs.tf
│      ├── iam.tf
│      ├── main.tf
│      ├── rds.tf
│      └── vpc.tf
|
└── modules
    ├── ec2
    │   ├── alb.tf
    │   ├── asg.tf
    │   ├── ec2.tf
    │   ├── eip.tf
    │   ├── output.tf
    │   ├── sg.tf
    │   └── variables.tf
    ├── ecs
    │   ├── main.tf
    │   └── variables.tf
    ├── efs
    │   ├── main.tf
    │   └── variables.tf
    ├── ga
    │   ├── main.tf
    │   └── variables.tf
    ├── iam
    │   ├── main.tf
    │   ├── output.tf
    │   └── variables.tf
    ├── rds
    │   ├── main.tf
    │   └── variables.tf
    └── vpc
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

いわゆるModuleを使った環境ごとのリソース管理になります。Terraformのリソースはmodulesディレクトリで共通化し、差異のある部分を変数として外だします。

envディレクトリ以下の環境ごとのディレクトリで各種リソースのモジュールを宣言し、そこに変数をあてはめることで、Terraformで管理していきます。

Terraformの実行環境ですが、sandbox(検証)環境のEC2インスタンス内で実行することにしました。これにより各種リソースの権限をIAMロールとしてEC2にアタッチさせるだけで実現できますので、認証情報の管理が楽になります。本番環境のAWSリソース作成に関しましては、以前の記事でも説明したAssume Role1を使ってEC2インスタンスに本番環境へのAWSリソース作成権限を渡しています。

provider "aws" {
  region = "ap-northeast-1"
  assume_role {
    role_arn  =  "arn:aws:iam::XXXXXXXXXXXXXXXXX:role/Terraform-Prd-Switch"
  }
}

moduleについて

www.terraform.io

モジュールとは複数のインフラリソースをまとめたテンプレートみないものです。Terraformのレジストリ上に公式や個人が作成したモジュールが公開されていて自由に活用できます。

ですが、今回新規で作るというよりかは既存のAWSリソースをTerraformで管理する必要がありましたので、terraform importでリソースをインポートし自作のモジュールの中に管理することにしました。

moduleディレクトリはその中にリソース別のディレクトリを作成し、Terraformの設定ファイルを作成しています。

VPCの例

例としてVPCAWSリソースをTerraformで管理したい場合について說明します。 まずvpcディレクトリ配下のmain.tfの中身はこのようになっています。

# main.tf
resource "aws_vpc" "terraform-vpc" {
  cidr_block = var.cidr_block
  tags = {
    Name           = "terraform-${var.Tag_Name}-vpc"
    Terraform      = "True"
    CmBillingGroup = "terraform:${var.Tag_Name}"
  }
}

resource "aws_internet_gateway" "terraform-igw" {
  vpc_id = aws_vpc.terraform-vpc.id
  tags = {
    Name           = "terraform-${var.Tag_Name}-igw"
    CmBillingGroup = "terraform:${var.Tag_Name}"
    Terraform      = "True"
  }
}


resource "aws_subnet" "terraform-public-subnet" {
  for_each          = var.public-AZ
  vpc_id            = aws_vpc.terraform-vpc.id
  cidr_block        = each.value
  availability_zone = "ap-northeast-1${each.key}"
  tags = {
    Name           = "terraform-${var.Tag_Name}-public-subnet-${each.key}"
    CmBillingGroup = "terraform:${var.Tag_Name}"
    Terraform      = "True"
  }
}

resource "aws_subnet" "terraform-private-subnet" {
  for_each          = var.private-AZ
  vpc_id            = aws_vpc.terraform-vpc.id
  cidr_block        = each.value
  availability_zone = "ap-northeast-1${each.key}"
  tags = {
    Name           = "terraform-${var.Tag_Name}-private-subnet-${each.key}"
    CmBillingGroup = "terraform:${var.Tag_Name}"
    Terraform      = "True"
  }
}

resource "aws_nat_gateway" "terraform-nat" {
  for_each      = toset(var.eip-NAT-AZ)
  allocation_id = aws_eip.terraform-nat-eip[each.key].id
  depends_on    = [aws_internet_gateway.terraform-igw]
  subnet_id     = aws_subnet.terraform-public-subnet[each.key].id
  tags = {
    Name           = "terraform-${var.Tag_Name}-nat-${each.key}"
    CmBillingGroup = "terraform:${var.Tag_Name}"
    Terraform      = "True"
  }
}

resource "aws_eip" "terraform-nat-eip" {
  for_each = toset(var.eip-NAT-AZ)
  tags = {
    Name      = "terraform-${var.Tag_Name}-nat-${each.key}"
    Terraform = "True"
  }
}

resource "aws_route_table" "terraform-public-rt" {
  vpc_id = aws_vpc.terraform-vpc.id
  tags = {
    Name           = "terraform-${var.Tag_Name}-public-rt"
    CmBillingGroup = "terraform:${var.Tag_Name}"
    Terraform      = "True"
  }
}

resource "aws_route_table" "terraform-private-rt" {
  for_each = toset(var.private-rt)
  vpc_id   = aws_vpc.terraform-vpc.id
  tags = {
    Name           = "terraform-${var.Tag_Name}-private-rt-${each.key}"
    CmBillingGroup = "terraform:${var.Tag_Name}"
    Terraform      = "True"
  }
}

CIDRブロックやネーミングタグ部分など環境ごとに差異が生じる部分を変数として外だしています。Terraformで変数を使うときはvariables.tfなど別のTerraform設定ファイルで宣言しておけばファイルの可読性が低下せずに済みます。

# variables.tf
variable "Tag_Name" {
  type        = string
  description = "Tag"
}

variable "cidr_block" {
  type        = string
  description = "vpcのサブネットです"
}

~~~~~~~~~省略~~~~~~~~~

そしてenvディレクトリの環境別に存在するvpc.tfVPCのモジュールを宣言し変数を代入します。

# env.tf
module "terraform-vpc" {
  source     = "../../modules/vpc"
  Tag_Name   = "Dev"
  cidr_block = "172.26.0.0/16"
  public-AZ  = { a = "172.26.10.0/24", c = "172.26.11.0/24" }
  private-AZ = { a = "172.26.20.0/24", c = "172.26.21.0/24" }
  eip-NAT-AZ = ["a"]
  private-rt = ["a"]
}

リソースとモジュールの設定ファイルが作成できましたら既存リソースをインポートします。

terraform import module.terraform-vpc.aws_vpc.terraform-vpc <vpc-id>
terraform import module.terraform-vpc.aws_internet_gateway.terraform-igw <igw-id>
~~~~~~~~~~~以下同様にimport~~~~~~~~~~~

# リソースがTerraform配下に置かれているか確認
$ terraform state list
module.terraform-vpc.aws_eip.terraform-nat-eip["a"]
module.terraform-vpc.aws_internet_gateway.terraform-igw
module.terraform-vpc.aws_nat_gateway.terraform-nat["a"]
module.terraform-vpc.aws_route_table.terraform-private-rt["a"]
module.terraform-vpc.aws_route_table.terraform-public-rt
module.terraform-vpc.aws_subnet.terraform-private-subnet["a"]
module.terraform-vpc.aws_subnet.terraform-private-subnet["c"]
module.terraform-vpc.aws_subnet.terraform-public-subnet["a"]
module.terraform-vpc.aws_subnet.terraform-public-subnet["c"]
module.terraform-vpc.aws_vpc.terraform-vpc

この状態でterraform planを実行し、既存インフラとの差異がないことを確認します。

$ terraform plan

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

No changes. Your infrastructure matches the configuration.

環境毎にリソース数が異なる場合

環境によってはコスト最適化のためだったり、冗長性の目的などでリソース数が異なるケースがあります。 弊社ではNATゲートウェイは冗長性確保のために本番環境は3台運用していますが、検証環境ではコスト最適化のために1台で運用しています。

このように環境ごとに作成するリソース数が異なりますとmoduleディレクトリ配下のリソース作成の際、動的にリソースを変更できるように工夫する必要があります。そのようなときに活用できるのhがfor_eachを使います。

for_eachによる動的なリソース作成

www.terraform.io

先程のVPCmain.tfをもう一度見てみます。

resource "aws_nat_gateway" "terraform-nat" {
  for_each      = toset(var.eip-NAT-AZ)
  allocation_id = aws_eip.terraform-nat-eip[each.key].id
  depends_on    = [aws_internet_gateway.terraform-igw]
  subnet_id     = aws_subnet.terraform-public-subnet[each.key].id
  tags = {
    Name           = "terraform-${var.Tag_Name}-nat-${each.key}"
    CmBillingGroup = "terraform:${var.Tag_Name}"
    Terraform      = "True"
  }
}

for_eachではeip-NAT-AZという変数をセットしています。この変数はリスト型の変数となっており、AZの識別子をリスト形式で格納できます。each.keyはリストのキーを参照しています。

# variables.tf
variable "eip-NAT-AZ" {
  type        = list(string)
  description = "EIPを割り当てているNATのAZ識別子"
}

検証環境と本番環境のそれぞれのvpc.tfを確認すると以下のように設定されています。

# 検証環境
module "terraform-vpc" {
  source     = "../../modules/vpc"
  eip-NAT-AZ = ["a"]

# 本番環境
module "terraform-vpc" {
  source     = "../../modules/vpc"
  eip-NAT-AZ = ["a", "c", "d"]
}

実際にNATをどのようにインポートしたのか画像を用いて說明します。

NATゲートウェイの例

VPCをインポートする時、terraform import ADDRESS IDとユーザーが設定するアドレス(module.terraform-vpc.aws_vpc.terraform-vpc)とAWS固有のリソースID(VPC-ID)でどのリソースをTerraformリソースにインポートするか指定しました。for_eachで動的に作成したTerraformリソースをインポート先に指定する場合は以下の書き方になります。

# 検証環境
$ terraform import 'module.terraform-vpc.aws_nat_gateway.terraform-nat["a"] <nat-id>' 

# 本番環境
$ terraform import 'module.terraform-vpc.aws_nat_gateway.terraform-nat["a"]' <nat-id> 
$ terraform import 'module.terraform-vpc.aws_nat_gateway.terraform-nat["c"]' <nat-id> 
$ terraform import 'module.terraform-vpc.aws_nat_gateway.terraform-nat["d"]' <nat-id> 

アドレス末尾にAZの識別子を添字として挿入し、対応したAZに存在しているNATのIDをインポート元にすることで数が異なるリソースを共通モジュールで管理することができます。

# 検証環境
module.terraform-vpc.aws_nat_gateway.terraform-nat["a"]

# 本番環境
$ terraform state list
module.terraform-vpc.aws_nat_gateway.terraform-nat["a"]
module.terraform-vpc.aws_nat_gateway.terraform-nat["c"]
module.terraform-vpc.aws_nat_gateway.terraform-nat["d"]

所感

既存AWSリソースをインポートするTerraform運用について紹介しました。すでに稼働しているAWSリソースをインポートする作業はけっこう大変なため、新規作成して置き換えられる部分はTerraformで置き換えたほうが運用が楽になると思います。弊社でもこうしてインポート作業をしていくなかで不要なリソースや新規作成しても問題無い部分についてはどんどん置き換えていきました。

まだまだIaCによるインフラ運用は始めたばかりですので、今後も最適なインフラ構成を目指せるように改善し続けていきます。

参考文献

beyondjapan.com

www.terraform.io