概要
こんにちは、キュービックで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について
モジュールとは複数のインフラリソースをまとめたテンプレートみないものです。Terraformのレジストリ上に公式や個人が作成したモジュールが公開されていて自由に活用できます。
ですが、今回新規で作るというよりかは既存のAWSリソースをTerraformで管理する必要がありましたので、terraform import
でリソースをインポートし自作のモジュールの中に管理することにしました。
moduleディレクトリはその中にリソース別のディレクトリを作成し、Terraformの設定ファイルを作成しています。
VPCの例
例としてVPCのAWSリソースを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.tf
でVPCのモジュールを宣言し変数を代入します。
# 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による動的なリソース作成
先程のVPCのmain.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によるインフラ運用は始めたばかりですので、今後も最適なインフラ構成を目指せるように改善し続けていきます。