💡

ログラスのTerraform構成とリファクタリングツールの紹介

2024/07/08に公開

はじめに

ログラスのクラウド基盤でエンジニアをやっているゲイン🐰です。

ログラスではAWS上でアプリケーションを動かすためにIaCとしてTerraformを採用しています。

我々のTerraformの構成を紹介するとともに、現状の課題とリファクタリングの事例を共有できれば幸いです。

ログラスのTerraform構成

ざっくりログラスのアプリケーションにまつわるTerraform構成は以下のようになっています。

基本的にはterraform/usecaseディレクトリ配下にmoduleとして定義されています。

中身は比較的にベタでリソースが書かれており、それらをterraform/envディレクトリの各ディレクトリ内で呼び出す形です。

env配下はほぼAWSアカウントと紐づく形となっており、AWSアカウントの数だけenvディレクトリが記載してないものも含め複数あります。

一部特定の環境だけで使う用途のusecaseの部分はenvディレクトリの呼び出す側で制御を行っております。

また安定度が高く確実に共通化できる部分のみterraform/moduleディレクトリ配下に切り出していますが、比較的usecaseにベタで書かれていることのほうが多いというのは特徴でしょうか。

またアプリケーションコード以外の各種自動化を行うツールのコードも同居しており、dockerディレクトリ配下やlambdaディレクトリ配下にそれぞれコードを配置しています。

これらはRustで書かれていることが多いのですが場合によってはPythonやGoなども併用しています。

.
├── README.md
├── docker
│   ├── container_tools1
│   │   └── Dockerfile
│   └── container_tools2
│       └── Dockerfile
├── lambda
│   ├── lambda_tools1
│   └── lambda_tools2
├── terraform
│   ├── env
│   │   ├── dev
│   │   │   ├── usecase1
│   │   │   ├── usecase2
│   │   │   └── usecase3
│   │   ├── stg
│   │   │   ├── usecase1
│   │   │   ├── usecase2
│   │   │   └── usecase3
│   │   ├── ...
│   │   │   ├── usecase1
│   │   │   ├── usecase2
│   │   │   └── usecase3
│   │   └── prd
│   │   │   ├── usecase1
│   │   │   ├── usecase2
│   │   │   ├── usecase3
│   │   │   └── prd_only_usecase1
│   ├── module
│   │   ├── module1
│   │   │   ├── main.tf
│   │   │   ├── outputs.tf
│   │   │   └── variables.tf
│   │   ├── ...
│   │   │   ├── main.tf
│   │   │   ├── outputs.tf
│   │   │   └── variables.tf
│   │   └── module2
│   │       ├── main.tf
│   │       ├── outputs.tf
│   │       └── variables.tf
│   └── usecase
│       ├── usecase1
│       │   ├── main.tf
│       │   └── variables.tf
│       ├── ...
│       │   ├── main.tf
│       │   └── variables.tf
│       ├── usecase3
│       │   ├── main.tf
│       │   └── variables.tf
│       └── prd_only_usecase1
│           ├── main.tf
│           └── variables.tf
└── tools
    ├── tools1
    │   └── main.sh
    └── tools2
        └── main.sh

課題

とはいえ我々は急成長し続けているベンチャーでもありますし、初期に構築したコードはここまでの成長を見越してないことも多々あります。

例えばあるログラスの根幹を司るusecase配下のmoduleはネットワークからLB,アプリケーションまでが一つにまとまっている用な比較的巨大なmoduleでした。

今まではこの状態で問題ありませんでしたが、だんだんと新規でやりたいことが増えてきたりするとmoduleが提供したい価値と柔軟性のギャップが大きくなってきました。

variablesでフラグのようなものを追加して、そのtrue/falseによってリソース作成を判別するようなコードを書かないとやりたいことができないような状況など皆さんも経験があるのではないでしょうか。

改めてですがこれは急成長し続けているベンチャーがゆえのある意味いい悩みでもありますし、とはいえ今後伸び続けるためには対処しなければならない課題でもあると感じました。

しかしTerraformの大量のリソースを環境の数だけリファクタリングするというのは中々骨の折れる作業です。

過去に大量の terraform importterraform state rmを行ったことがありますが、そこまでの時間は確保できませんしせっかくTerraformのコードもきちんとある中でなんとかコードベースでリファクタリングが出来ないかを考えました。

そこで自作のリファクタリングツールを作成して、部分的に切り出した事例をご紹介します。

自作ツールとリファクタリング

tfirmgというツールをGoで作成しました。

こちらで公開しております。

これはコードベースの移動とtfstateの状態を紐づけて、import / removed / moved blockを生成するツールです。

例えば以下のような状況を考えてみます。

## terraform/usecase/loglass_super_hyper_ultra_good_app/main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.47.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

variable "env" {
  type = string
}

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

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = false
  enable_vpn_gateway = false

  tags = {
    Terraform   = "true"
    Environment = var.env
  }
}

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]
}

resource "aws_network_interface" "foo" {
  subnet_id   = module.vpc.private_subnet_arns[0]
  private_ips = ["172.16.10.100"]

  tags = {
    Name = "primary_network_interface"
  }
}

resource "aws_instance" "my_super_hyper_ultra_good_app" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"

  network_interface {
    network_interface_id = aws_network_interface.foo.id
    device_index         = 0
  }

  tags = {
    Terraform = "true"
    Name      = "HelloWorld"
  }
}

このusecase moduleを各envで呼ぶコードはこの用になります。

これが環境の数だけあると思ってください。

## terraform/env/dev/loglass_super_hyper_ultra_good_app/main.tf

module "loglass_super_hyper_ultra_good_app" {
  source = "../../../usecase/loglass_super_hyper_ultra_good_app"
  env    = "dev"
}

このようなmoduleから module.vpc だけをnetworks usecaseに切り出して以下のような状況を実現したいです。

.
└── terraform
    ├── env
    │   ├── dev
    │   │   ├── loglass_super_hyper_ultra_good_app
    │   │   └── networks
    │   ├── prd
    │   │   ├── loglass_super_hyper_ultra_good_app
    │   │   └── networks
    │   └── stg
    │       ├── loglass_super_hyper_ultra_good_app
    │       └── networks
    └── usecase
        ├── loglass_super_hyper_ultra_good_app
        └── network

そのためにまず terraform/usecase/networks というディレクトリを作成し同じ名前でごそっと module.vpc のコードを移動します。

## terraform/usecase/networks/main.tf
terraform {
 required_providers {
   aws = {
     source  = "hashicorp/aws"
     version = "5.47.0"
   }
 }
}

provider "aws" {
 region = "ap-northeast-1"
}

variable "env" {
 type = string
}

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

 name = "my-vpc"
 cidr = "10.0.0.0/16"

 azs             = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
 private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
 public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

 enable_nat_gateway = false
 enable_vpn_gateway = false

 tags = {
   Terraform   = "true"
   Environment = var.env
 }
}

この状況で以下のようなコマンドを実行すると自動で removed blockとimport blockを生成します。

tfirmg module --src-tfstate-path file://my-tfstate/tfstate --src-module loglass_super_hyper_ultra_good_app --dst-module networks --src-dir terraform/env/dev/loglass_super_hyper_ultra_good_app --dst-dir terraform/env/dev/networks

## terraform/env/dev/networks/import.tf

import {
 to = module.vpc.aws_default_network_acl.this[0]
 id = "acl-123456789abcdef"
}

import {
 to = module.vpc.aws_default_security_group.this[0]
 id = "sg-123456789abcdef"
}

import {
 to = module.vpc.aws_internet_gateway.this[0]
 id = "igw-123456789abcdef"
}
...

 ## terraform/env/dev/loglass_super_hyper_ultra_good_appp/removed.tf
 removed {
  from = module.vpc.aws_default_network_acl.this
  lifecycle {
    destroy = false
  }
}

removed {
  from = module.vpc.aws_default_route_table.default
  lifecycle {
    destroy = false
  }
}

removed {
  from = module.vpc.aws_default_security_group.this
  lifecycle {
    destroy = false
  }
}
...

このmoduleコード的に移行したことで、 aws_network_interface.fooの subnet_idとして指定していた module.vpc.private_subnet_arns[0] が使えなくなったためこれは通常通りリファクタリングが必要です。

ここはdata resourceを使ったり、remote stateによる参照などが考えられるでしょうか。

Planが通るまで修正を行った後はこれを環境の分だけ terraform apply を実行すれば無事にリソースの移動が完了します。

実際にこれを用いてnetwork関連のmoduleを分割しましたが、作業自体は10分ぐらいで終わりました。

今回はmoduleの移行例でしたが、リソースの移動にも対応しています。

また今回はtfstateを跨ぐ事例ですが、module to moduleのtfstateを跨がない場合にはmoved blockを生成することでリファクタリングを可能としています。

内部としては2つのディレクトリとtfstateにある状態の差分を比較し、変更があったリソースのidを使用して各種import / removed / moved blockを生成している形となります。

そのためimportの形式とtfstateに記録されているidの形式が異なるリソースがあり、そこは泥臭く個別でルールを書いていたりもします。

まだまだバグが多いものも認知していますし、上述の個別のルールなど対応できてないリソースは無限にあるためPRやIssueなどお待ちしております!

まとめ

ログラスのTerraform構成とリファクタリングの事例を紹介しました。

これからどんどん新しい機能が追加される中で今この瞬間の構成が合わなくなることもあると思いますし、現状に満足せずその時々のベターなTerraform構成を考えアップデートしていきたいです。

またこれからどんどん新しい機能が追加されるログラスを支えるエンジニアも大募集しております!

特に偶有的複雑性を支える横串の組織に興味がある方がいらっしゃいましたら、カジュアル面談からいかがでしょうか。

皆様のご応募お待ちしております。

https://hrmos.co/pages/loglass/jobs/1813462408235663388

https://hrmos.co/pages/loglass/jobs/1813462408235663410

株式会社ログラス テックブログ

Discussion