この1月に普通自動二輪の免許を取得したので、早く暖かくなってツーリングに行きたいなとそわそわしている笹原です。
みなさんはAWSを使った開発をするときに、どうやってIAMユーザを管理してますか?
開発者に権限を与えすぎれば セキュリティリスクは高まる ので、与える権限を少なくしたいですが、少なくしすぎても 開発・運用時に作業や権限の申請が頻繁に必要 になります。
また、入社時や退職時にIAMユーザの作成・削除をしたり、定期的な棚卸しを実施したりといった運用も管理が煩雑になっていると意外と工数がとられたりするものです。
今回はAWSユーザ管理を管理者からの一方通行ではなく、TerraformとCircle CIを使って開発者からのPull Requestベースでやってみた事例を公開したいと思います!
AWSのマルチアカウント構成
AWS Organizationsが入ってからこのような構成が一般的になってきてると思います。
- AWS Organizationsによるマルチアカウント戦略とその実装 - クラウドワークス エンジニアブログ
- AWS Multi-Account Architecture with Terraform, Yeoman, and Jenkins
AWSアカウントはLogin用のアカウントを一つ作成して、実際に利用するServiceアカウントにはAssumeRoleを行うことで入るようにしています。
- Login
- 開発者用のIAMユーザを作成する
- Service
- サービス毎、本番・開発等の環境毎にAWSアカウントを作成
- 開発者自身が利用するIAMユーザは基本的に作成せずに、loginアカウントからAssumeRoleを行う
Terraform
AWSのマルチアカウント構成を運用する上で、AWSコンソール上から手作業でIAMユーザを管理するのは、管理者の負担になるだけでなく属人化も招くので、Terraformで管理します。
下準備
各AWSアカウントの管理をするにあたって、それぞれのアカウントでTerraformを実行することになるので、各アカウントにAssumeRoleできるIAMユーザをLoginアカウント上に作成します。
まず、各AWSアカウント上で、LoginアカウントからAssumeRoleするためのIAMロールを作成します。 このとき、いずれCI上でTerraformを実行することを見据えて、オプションのMFAはチェックを外したままにしておきます。
次に、Loginアカウント上でIAMユーザを作成します。
このIAMユーザには、IAMFullAccessとS3FullAccessに加えて、カスタムポリシーで各アカウントへのAssumeRoleを許可しておきましょう。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "arn:aws:iam::*:role/Terraform" } ] }
ディレクトリ構成
Terraformのディレクトリ構成はこんな感じです。
Terraformコマンドの実行はルートディレクトリから行います。
この構成のポイントは
- 外部変数と内部変数と出力については別途専用のファイル( variables.tf と local.tf と outputs.tf )を用意
- IAMロールやIAMグループの作成だったり、各アカウントに対する処理は大部分が共通化できるのでmodule化
├── README.md ├── locals.tf ├── outputs.tf ├── main.tf ├── login-account.tf ├── service-1-account.tf ├── service-2-account.tf ├── ... ├── modules/ │ ├── account/ │ │ ├── variables.tf │ │ ├── outputs.tf │ │ ├── main.tf │ ├── iam/ │ │ ├── group/ │ │ │ ├── variables.tf │ │ │ ├── outputs.tf │ │ │ ├── main.tf │ │ ├── role/
Serviceアカウント
各Serviceアカウントで行うことは以下の2つです。
- LoginアカウントからAssumeRoleすることができるIAMロールを作成する
- IAMロールに対して、各アカウント内で必要な権限を付与する
各ServiceアカウントのTerraform実行は、modules/accountを呼び出して実行します。
以下のように、呼び出し元から対象のServiceアカウントのAWSアカウントIDを渡してやり、下準備で作成したロールにAssumeRoleします。
service-1-account.tf
locals { aws_region = "ap-northeast-1" aws_accounts = { service_1_account = "000000000001" service_2_account = "000000000002" } } module "service_1_account" { source = "./modules/account" aws_region = "${local.aws_region}" aws_account_id = "${local.aws_accounts["service_1_account"]}" }
modules/account/main.tf
provider "aws" { region = "${var.aws_region}" assume_role { role_arn = "arn:aws:iam::${var.aws_account_id}:role/Terraform" } }
CircleCI
さて、Terraformを使うことで、AWSのIAMユーザ管理の属人化は薄れましたが、Terraformをどのように実行するかには依然として問題はあります。
開発者もTerraformプロジェクトを書き換えて、ローカルで実行できてしまうのでは元も子もありません。
そこで、Terraformの実行をローカルではなくCI上で行うことにします。
弊社ではCircleCI上でTerraformを実行しています。
Job概要
Jobの流れは図のとおりです。
masterブランチでのみ、terraform apply
が走るようになっています。
これでAWSアカウント権限に関する承認・付与を、Pull Requestベースで実行することが可能になりました。
補足が必要そうな点として、Planの前にLintが入っています。これは、terraform fmt
を実行したときに変更されるファイルがあるかどうかを見ています。
設定
使用している設定ファイルも晒します。
工夫したのは
- versionが2.1から利用できる機能を利用している
- orbsを使った簡単なSlack通知
- executorsによる共通化
- terraform planを実行した結果をpersist_to_workspaceを使ってterraform apply時に渡すことでplanで確認したとおりに実行している
といったところです。
version: 2.1 orbs: slack: circleci/slack@volatile executors: my-executor: working_directory: ~/work docker: - image: hashicorp/terraform:0.11.10 jobs: checkout_code: executor: my-executor steps: - restore_cache: keys: - source-v1-{{ .Revision }} - source-v1- - checkout - save_cache: key: source-v1-{{ .Revision }} paths: - .git - run: terraform init -input=false - persist_to_workspace: root: ~/ paths: work lint: executor: my-executor steps: - attach_workspace: at: ~/ - run: name: terraform fmt command: | if [ $(terraform fmt | grep -v .terraform | tee fmt_result.txt | wc -l) -gt 0 ]; then echo "Format of this terraform files is not appropriate:" echo cat fmt_result.txt echo echo "Please run terraform fmt" exit 1 fi plan: executor: my-executor steps: - attach_workspace: at: ~/ - run: terraform plan -input=false -out=terraform.plan - persist_to_workspace: root: ~/ paths: work send-approval-link: docker: - image: buildpack-deps:trusty steps: - slack/notify: message: | Please check and approve Job to deploy. https://circleci.com/workflow-run/${CIRCLE_WORKFLOW_ID} apply: executor: my-executor steps: - attach_workspace: at: ~/ - run: terraform apply -input=false --auto-approve terraform.plan workflows: version: 2 plan_and_apply: jobs: - checkout_code - lint: requires: - checkout_code - plan: requires: - lint - send-approval-link: requires: - plan filters: branches: only: master - waiting-for-approval: type: approval requires: - plan filters: branches: only: - master - apply: requires: - hold
運用フロー
実際の運用フローとしてはこういった形になります。
ただ、Terraform 0.11の仕様ではこのフローでカバーできない範囲があり、そこでは手作業が発生しています。
運用上の問題点: IAMユーザの削除時に意図しない変更が行われる
IAMユーザはTerraform上に以下のように設定を記載しています。
locals { user_1 = "user_1" user_2 = "user_2" user_3 = "user_3" aws_iam_users = [ "${local.user_1}", "${local.user_2}", "${local.user_3}", ] } resource "aws_iam_user" "iam_users" { count = "${length(local.aws_iam_users)}" name = "${element(local.aws_iam_users, count.index)}" path = "/" force_destroy = true }
ここからuser_2をそのままコードからのみ削除を行って、terraform planを行うと
~ aws_iam_user.iam_users[1] name: "user_2" => "user_3" - aws_iam_user.iam_users[2]
として、意図した変更と異なる差分が出力されます。
これは、tfstateファイル内のリソースアドレスに、リストの場合はindexが使用されていることが原因です。
Terraform 0.12ではfor_eachが追加されており、リソースアドレスにindexを使わない方法が提供されるので、解決予定ではあります。
HashiCorp Terraform 0.12 Preview: For and For-Each
Terraform 0.12は2019 1Qでリリース予定ではありますが、それまでは手動でリソースアドレスの書き換えを行う必要があります。
terraform state mvを使って、削除したいリソースをリストの最後に持っていくことで削除のみが行われるようになります。
$ terraform state mv aws_iam_user.iam_users[1] aws_iam_user.iam_users[3] $ terraform state mv aws_iam_user.iam_users[2] aws_iam_user.iam_users[1] $ terraform state mv aws_iam_user.iam_users[3] aws_iam_user.iam_users[2]
今後について
今回はAWSのIAMユーザ管理をTerraformとCircleCIを使って行いましたが、利用している開発者向けサービスはAWSだけではありません。
TerraformはAWSやGCPのようなPaaSのみならず、GitHubやNew RelicのようなSaaSまでカバーしているので、そういったところもコード化していく予定です。