こんにちは。 SRE の @suzuki-shunsuke です。 AWS IAM の管理を miam から Terraform に移行した話を紹介します。
なお、 AWS や miam に限らず「Terraform で管理されていない大量のリソースを Terraform で管理する」ことを検討している方には参考になる内容かと思います。
背景
本ブログでも何度か紹介したとおり、弊社では AWS のリソースを Terraform で管理しています。 しかし、実は IAM に関しては miam という別のツールで主に管理されていました。 miam は AWS IAM を管理することに特化したツールです。 miam には以下のような特徴があります。
弊社で miam が採用された理由は、自分が入社以前のことなので分かりませんが、 社内で Ruby が広く採用されており、 Ruby の DSL でかけることの親和性が高かったのかなと思います。社内に miam のコントリビューターが複数人いたというのも大きいでしょう。
しかし最近では Terraform の CI が整備され、以前に比べてだいぶ快適になったこともあり、 IAM Role を中心に Terraform で作られることも多くなってきました。 例えば CodeBuild の Service Role なんかは CodeBuild Project と一緒に Terraform で管理したほうが楽です。 Terraform で管理するようにした場合、 miam の方で削除されないように exclude するルールを追加しないといけないのですが、これが面倒でした。 また、 miam によるリソース管理の強制は、 IaC を強制するという意味では重要ではありますが、 これがブロッカーになることもままありました。 Pull Request (以下 PR) を作成し dry run を実行したら、関係ないリソースが削除されそうになっていて、それをなんとかするまで他の変更ができないといったケースもままありました。 Terraform の場合 State を細かく分割しているため、ブロッカーが発生するリスクはだいぶ低く抑えられています。 他にも色々理由はありますが、そういった理由のもと、 IAM 管理を miam から Terraform に移行することにしました。
どうやって移行するか
- Terraformer によってすべての IAM リソースを一つの State に import
- tfmigrator を使って Terraform Configuration と State を分割・整形
- 分割した State 及び Terraform Configuration の Terraform のバージョンを upgrade
- 分割された State 及び Terraform Configuration を Terraform の Monorepo に追加
Terraformer を使うと実際のリソースをもとに Terraform Configuration と State を自動生成することができます。 それで終わりであれば話は簡単なのですが、そうもいきません。 弊社では State をマイクロサービスや環境(staging, production, etc)といった単位で細かく分割しているため、 生成された State や Terraform Configuration を分割する必要があります。 Terraformer の import 対象のリソースをフィルタリングする機能を使って 分割して import する方法もあるかもしれませんが、 そうすると同じリソースを重複して import したり、はたまた import されないリソースが出たりする懸念があったので、 フィルタリングは使わずすべての IAM リソースを一つの state にまとめて import したあとに分割するという戦略を取ることにしました。
$ terraformer import aws -r iam -v --profile=sapuri --regions ap-northeast-1
また、 Terraformer で生成された Terraform Configuration のリソース名は割と綺麗ではなかったりします。
例えばこんな感じのファイルが生成されます。
resource "aws_iam_group" "tfer--foo-002D-production" { name = "foo-production" path = "/" }
これを次のようにリネームしたいと思いました。
resource "aws_iam_group" "foo-production" { name = "foo-production" path = "/" }
もちろん HCL だけでなく State も更新する必要があります。
tfmigrator
これらを実現するために、 tfmigrator という Go のライブラリを使い、簡単なプログラムを実装しました。 次のようなコードを書くことで Terraform Configuration と State をマイグレーションできます(下記のコードは tfmigrator v0.5.1 を使っているので、将来的に API が変わる可能性があります)。
package main import ( "context" "log" "os" "os/signal" "github.com/tfmigrator/tfmigrator/tfmigrator" ) func main() { if err := core(); err != nil { log.Fatal(err) } } func core() error { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() return tfmigrator.QuickRun(ctx, tfmigrator.CombinedPlanners( tfmigrator.NewPlanner(removeTerraformTag), tfmigrator.NewPlanner(policy), )) } // ManagedBy: Terraform という Tag がついてたら、既に Terraform で管理されているはずなので除外する func removeTerraformTag(src *tfmigrator.Source) (*tfmigrator.MigratedResource, error) { tags, ok := src.Resource.AttributeValues["tags"] if !ok { return nil, nil } if tags == nil { return nil, nil } managedBy, ok := tags.(map[string]interface{})["ManagedBy"] if !ok { return nil, nil } if managedBy.(string) != "Terraform" { return nil, nil } return &tfmigrator.MigratedResource{ Removed: true, }, nil } func policy(src *tfmigrator.Source) (*tfmigrator.MigratedResource, error) { if src.Resource.Type != "aws_iam_policy" { return nil, nil } name := src.Resource.AttributeValues["name"].(string) // foo-production という IAM Policy を foo/production に分割し、リソース名を IAM Policy name と同じにする if name == "foo-production" { return &tfmigrator.MigratedResource{ Address: src.Resource.Type + "." + name, Dirname: "foo/production", }, nil } return nil, nil }
上記のように tfmigrator.QuickRun という API を使うと次のようなコマンドが実装できます。
$ go run main.go -help $ go run main.go -dry-run -log-level=debug *.tf
上のサンプルだとちょっとわかりにくいかもしれませんが、 次のような引数と戻り値を取る関数を実装すればマイグレーションができます。
tfmigrator CLI
tfmigrator には Go のライブラリだけでなく、ライブラリを使って実装された CLI もあります。 https://github.com/tfmigrator/cli 上記の例だと次のような設定ファイルを書けば良いです。
tfmigrator.yaml
rules: - if: | "tags" in Resource.AttributeValues and "ManagedBy" in Resource.AttributeValues.tags and Resource.AttributeValues.tags.ManagedBy == "Terraform" removed: true - if: Resource.Type == "aws_iam_policy" and Resource.AttributeValues.name == "foo-production" address: "{{.Resource.Type}}.{{.Resource.AttributeValues.name}}" dirname: foo/production
$ tfmigrator run *.tf
柔軟性という意味ではライブラリより劣りますが、基本的なユースケースならこれで十分だと思います。
tfmigrator を使う上での注意点
tfmigrator を使う際は、一気にすべてのリソースを分割しようとするのではなく、 一個ずつシンプルなルールを記述してはマイグレーションを実行し結果をチェックするのが良いと思います。
- 一個ずつルールを追加してはマイグレーションの実行を繰り返す
- HCL と State を Git で管理しておくと間違えても戻せる
- マイグレーション前に DRY RUN は実行する
- ルールは曖昧にせず、対象の attribute を明確にする
Terraform を upgrade
残念ながら Terraformer は version 0.8.14 の時点で Terraform v0.13 までしかサポートしておらず、生成されたコードを v0.15 まで upgrade する必要がありました
(特に既存の State に組み込む場合にはバージョンを揃える必要があります)。
幸い HCL の修正はほぼ不要でしたが、 terraform apply
を実行して State を更新する必要がありました。
なお、 Terraform v0.15 に対応する PR が出ているので、いずれ対応するかもしれません。
https://github.com/GoogleCloudPlatform/terraformer/pull/845
分割された State 及び Terraform Configuration を Terraform の Monorepo に追加
分割された State を既存の Terraform リポジトリに追加していくわけですが、 State によって以下の場合があります。
- 新規の場合
- 既に State が存在する場合(関連するリソースがTerraform で既に管理されている)
新規の場合は簡単で Backend の設定をしたあと terraform init
を実行して State を S3 に upload したあとに PR を投げれば終わりです。
既に State が存在する場合は State をマージする必要があるのですが、 terraform state mv
とかしていると時間がかかるので
直接 State を手で修正しました。
$ terraform state pull > terraform.tfstate $ vi terraform.tfstate # serial を increment しつつ、新しく生成した State の内容を追加 $ terraform state push terraform.tfstate
Terraformer で一度 import したあとに miam で IAM リソースが変更されると面倒なので、リポジトリ自体を archive しておくと無難です。 ただ archive するにしても長期間 IAM が更新できない状態なのは困るので、移行中はこの作業に専念して数日で終わらせました。
さいごに
以上、 Terraformer と tfmigrator を使い、 AWS IAM の管理を miam から Terraform に移行した話を紹介しました。 ちなみに、 IAM を移行したあとに AWS Route53 の管理も同様に Roadworker から Terraform に移行しました。 リソースが Terraform 以外のツールで管理されていたり、はたまた全くコード管理されていない状態から Terraform で管理するようにしたいというケースはままあったりすると思うので、そういった場合に参考になれば幸いです。
イベントのお知らせ
Quipper ではスタディサプリの開発に関わる SRE を積極的に募集しています。
また、2021-08-25 に SRE を対象として スタディサプリ/Quipper オンラインミートアップ #3 を開催します。 Quipper やスタディサプリの製品や技術にご興味がある方はぜひお気軽にご応募いただけると嬉しいです!