複数AWSアカウントのインフラを同じコードで Terraform管理するプラクティス

f:id:kiyotaka_ginoza:20211119131753p:plain
こんにちは!
アンドパッドSREの 宜野座 です。

前回は AWSのアカウント運用改善の取り組みについて記事を書かせていただきました。

今回はアンドパッドでIacへの取り組みとして行っているものの一例として、複数アカウント・複数環境を同一コードでTerraform管理するプラクティスを紹介したいと思います。

少し長くなりますが、お読みいただけると幸いです。

前回ブログ記事
tech.andpad.co.jp

なぜIaC(Infrastructure as Code)に取り組んでいるのか

IaCに取り組むメリットは今までも色々な場所で論じられておりいくつもあると思いますが、主に以下が考えられました。

  • 作業の属人化・ブラックボックス化を防ぐ
  • インフラの一貫性を向上
  • 複数環境を手間少なく構築できる
  • DR対策として既存のインフラと同等の環境を別リージョンに立ち上げることができる
  • 過去の状態と現在の状態の差分がコードで確認でき、以前の構成に戻すこともできる

反対にデメリットとしては主に以下が考えられました。

  • 初期構築としては、慣れるまでは手作業より時間がかかる
  • 各ツールによって記述方法が異なるため、コードを見れる人が限られる

※ 他にもメリットやデメリットなどとして考えられる要素はあると思いますが、考えついたものを記載してあります。

上記などのメリット・デメリットを考慮した上で、既存のSREチームの体制で今後の拡張性やメリットとして享受できるものを考えると、インフラをコード管理することが必要だと考え取り組みを進めています。

Iacのメリット・デメリット参照記事
dev.classmethod.jp
www.redhat.com

Terraformを選んだ理由

AWSでIacに取り組むためのツールはいくつか考えられるかと思います。
・Terraform
・CloudFormation
・OpsWorks (Chef, Puppet などの経験がある方向け)
・SAM (CloudFormationの拡張。 Serverless系のリソース)
・Serverless Framework (Serverless系のリソース)
etc….

SAMやServerless Framework はDynamoDB、API Gateway、Lambdaを中心にしたサーバーレスシステムに向いてます。
Chefを既存のシステムで利用経験のある方は、Opsworksも良いかもしれません。

その中でより広いAWSのインフラの構築に使えるものとなると CloudFormation か Terraform が候補となり、デファクトスタンダードとなっていることからも良いだろうと考えました。

この2つに関しては非常に悩みどころだったのですが、

・GCP、datadogなど他のリソースをIaCしたい時にも同じ記法が使える
・社内ナレッジとしてTerraformを使える方が多い
・多くの企業で利用されており、updateも頻繁に行われている

などの理由で Terraform を選択しました。

同一コードでTerraformを複数アカウント・複数環境へplan, applyしたい

Iacのメリットの中にもありますが、複数環境をなるべく手間なく作成をしたいという目的を実現するためにはどの部分を共通化して、どの部分を分離するかを考える必要が出てくるかと思います。

今回紹介するケースは、本番環境 / ステージング環境 / 開発環境 などのそれぞれの環境・それぞれのアカウントへ同一のコードでplan, applyを実行したいというケースになります。

いくつか方法があるかと思いますが、今回は以下のような構造と方法にしました。

 |-- README.md
 |-- backends
 |     |--- development.tfbackend
 |     |--- staging.tfbackend
 |     `--- production.tfbackend
 |-- vars
 |     |--- development.tfvars
 |     |--- staging.tfvars
 |     `--- production.tfvars
 |--- main.tf
 |--- xxx.tf
 `-- variables.tf

各ファイルの中身の例は以下になります。

provider.tf

terraform {
  required_version = "= 1.0.xx"
  backend "s3" {
  }
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.xx"
    }
  }
}

provider "aws" {
  region  = "ap-northeast-1"
  profile = var.profile_name
}
  • backend の定義を別ファイルで読み込むために、空白にしています。
  • profile の情報を環境ごとに変えたいので、変数化しています。

backends/以下の各ファイル

bucket  = "<stateファイルを置くバケット名>"
region  = "<利用リージョン>"
key     = "<stateファイル名>"
encrypt = true or false
profile = "<profile名>"
  • 各項目の説明はこちらを参照

www.terraform.io

vars/以下の各ファイル

env_name            = "development"
profile_name        = "development"
subnet_az_1         = "ap-northeast-1a"
subnet_az_2         = "ap-northeast-1c"
subnet_az_3         = "ap-northeast-1d"
  • 各環境で利用する変数の値を定義しています。

variable.tf

variable "env_name" {}
variable "profile_name" {}
variable "subnet_az_1" {}
variable "subnet_az_2" {}
variable "subnet_az_3" {}
  • Terraformで var.xxx で呼び出して利用できるように共通の変数を定義しています。

リソースの作成例
その上で各リソースのサービス名は被らないように変数を利用して定義していきます。
以下はLambdaを利用したシンプルな例になります。

data "archive_file" "lambda_code_zip" {
  type          = "zip"
  source_dir    = "src/lambda_code/"
  output_path   = "src/lambda_code.zip"
}

resource "aws_lambda_function" "test_lambda" {
  function_name    = "test-lambda-${var.env_name}"
  handler          = "lambda_function.lambda_handler"
  filename         = data.archive_file.lambda_code_zip.output_path
  runtime          = "python3.9"
  role             = "<role_arn>" 
  source_code_hash = data.archive_file.lambda_code_zip.output_base64sha256  
}

同一アカウントに実行する場合は function_name などが各アカウントリージョン内で一意である必要があるため、すでにリソースが存在するというエラーを出力します。
他の手法でも同様になるかと思いますがこちらの問題に対応するために、各リソースに ${var.env_name} などをつけて一意の名前にしてあげる必要があります。

terraform init

ここまで準備が整ったら、terraform initを実行していきます。
実行の際は、以下のように実行します。

<初回実行>

$ terraform init -backend-config=backends/<環境名>.tfbackend

<手元のstateファイルを各環境のものに書き換えたい場合>

※ development環境に切り替える場合
$ terraform init -backend-config=backends/development.tfbackend -reconfigure
※ staging環境に切り替える場合
$ terraform init -backend-config=backends/staging.tfbackend -reconfigure
terraform plan

準備ができたら、terraform planを実行し差分を確認します。
この際に環境ごとの vars ファイルを読みとってあげる必要があります。

$ terrafrom plan -var-file=vars/<環境名>.tfvars
terraform apply

差分が問題ないことを確認し、terraform applyを実行します。
この際も環境ごとのvars ファイルを渡す必要があります。

$ terrafrom apply -var-file=vars/<環境名>.tfvars

参照リンク
github.com
qiita.com

環境を増やしたい場合

行うこととしては、

  • backends/ 以下でbackendを指定してファイルを作成する ※ staging2.tfbackend など
  • vars/ 以下の各環境の変数を設定しファイルを作成する ※ staging2.tfvars など

の2つを行います。

その後、上述した方法で terraform init, plan, apply を行うことで環境を1つ増やすことができます。

環境ごとにリソースを作成したり、作成しないようにしたい

複数環境を作成していると、

  • 本番環境では別のインスタンスタイプにしたい
  • 開発環境ではリソースポリシーで検証のため複数箇所からのアクセスや権限を許可しているが本番ではしっかり狭めたものに切り替えたい
  • 開発環境では必要だがステージング環境ではまだ必要ない

などのケースが発生するかと思います。

その際はこちらの記事にも非常によくまとまっていますが、 count や ? などを利用して環境ごとの切り替えを柔軟に行うことで実現できるかと思います。

qiita.com

他の方法に関して

複数環境をTerraformで管理する方法としてはいくつか他にも考えられます。

  • terraform workspaceを使う
  • モジュール化する

etc….

それぞれ以下のようなことを考え今回採用しませんでしたが、ケースによっては非常に有用に機能するかと思います。

terraform workspaceを使う
同一アカウントで複数環境を作成するのには非常に使えるかと思いました。
この方法だと検証したところ、stateファイルが同様のbackend S3の別の階層として作成されることがわかりました。
今回のように複数アカウントにapplyしたいケースでbackendを分けたい場合、workspaceの分離はbackendとstateファイルで環境を分離しているため必ずしも使う必要はないと考えました。
www.terraform.io
kenzo0107.hatenablog.com

モジュール化する
こちらは階層ごとにprofileを切り替えれば、実行は可能かと思われます。
採用しなかったのは多くのリソースをモジュール化しすぎるとレビューする側のコードを読む負荷が増すと思ったのと、各環境を足したい場合はTerraformの記述が一部必要となるので一貫性をより保つために更新頻度を減らしたかったため今回は採用しませんでした。
qiita.com

まとめ

今回は同じコードでAWSの複数アカウント・複数環境をTerraformで管理するプラクティスに関してご紹介させていただきました。

この他にも特定のブランチにマージした際にCodebuildなどで 実行するようにすることで、各環境へのデプロイを自動化することもできます。

Terraformの運用に関しては様々な会社や個人の方で各々の方法で行われているかと思います。

是非、このやり方だとこういう問題が起きるのでは?、会社ではこういうやり方をしています!、私はこういうやり方をしています!などいろいろなご意見や方法などがあれば教えていただきたいです!

終わりに

SREチームのIaCへの取り組みはまだ道半ばです。
今回取り上げたこと以外にもまだまだ多くの課題があり、チームで日々協力しながら取り組んでいます。

SREはもちろんですが、さまざまなポジションで仲間を大募集しています!
詳しくは下記サイトをご覧ください。
engineer.andpad.co.jp
hrmos.co


また開発部(SREチーム含む)では2023年度新卒採用を行っていますので、ご興味ある方はご覧ください。
hrmos.co