tjinjin's blog

インフラ要素多めの個人メモ

2018年版Terraformで複数AWSアカウントのIAMロールを管理する

About

2018年になったので見直してみました。

以前

以前はこんな感じでした。

cross-black777.hatenablog.com

cross-black777.hatenablog.com

環境ごとに設定ファイルをばらける作りにしていたため、環境追加とかが非常に面倒でした。

最新版

先に最終的な構成のサンプルを貼っておきます。解説不要な方はこちらご覧になっておかしな点あればこっそり教えて下さい。

github.com

以前からのTerraformの大きな変更点は下記です。

  • 環境グルーピングごとの適用するようにした
  • assumeRole対応したのでkick用のスクリプトが不要になった

他にもいろいろあるのでコードを見ながら解説していきます。

全体構成

.
├── README.md
├── environments
│   ├── development
│   │   ├── aws_development.tf
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   └── varibles.tf -> ../../variables.tf
│   ├── production
│   │   ├── aws_production.tf
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   └── varibles.tf -> ../../variables.tf
│   └── staging
├── module
│   ├── controller
│   │   └── group
│   │       ├── main.tf
│   │       └── variable.tf
│   ├── group
│   │   ├── admin
│   │   │   ├── hoge.tf
│   │   │   └── variables.tf
│   │   ├── developer
│   │   │   ├── hoge.tf
│   │   │   └── variables.tf
│   │   └── operator
│   │       ├── hoge.tf
│   │       └── variables.tf
│   └── iam_role
│       ├── main.tf
│       └── variable.tf
└── variables.tf

12 directories, 20 files

前提

  • 複数のAWSアカウントを横断的に管理したい
  • ユーザグループに応じて権限を振りたい。ユーザグループはadmin/developer/operatorの3つ
  • AWSアカウント毎にstageがわかれており、development/staging/productionのどれかを1つ選択する
  • 実行時の権限は各アカウントにassumeRoleして行う。Terraform実行用のユーザ・ロールはすでに別手段で作成済み

実行方法

実行はenvironments毎に実行します

$ cd environments/development
$ terraform init && terraform apply

ディレクトリごとの解説

environments/

variables.tfは各stage毎で共通な設定となっています。そのためsymboliclinkを使っています。

variable "admin_policy_arns" {
  default = {
    default.policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
  }
}

variable "developer_policy_arns" {
  type = "map"

  default = {
    production.policy_arn = "arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess"
    staging.policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
    development.policy_arn    = "arn:aws:iam::aws:policy/PowerUserAccess"
    default.policy_arn     = ""
  }
}

policyはサンプル用に適用に拾ってきていますが、各ユーザグループ * stage毎の設定を記載しています。上記の変数はmain.tfでlookupを使うことで適当な値をmoduleに連携するような仕組みにしました。

module "dev-1" {
  providers = {
    # 適用する環境を指定
    aws = "aws.dev-1"
  }

  source               = "../../module/controller/group"
  # lookup関数でvariables.tfから値を取得している 
  admin_policy_arn     = "${lookup(var.admin_policy_arns,"development.policy_arn",var.admin_policy_arns["default.policy_arn"])}"
  developer_policy_arn = "${lookup(var.developer_policy_arns,"development.policy_arn",var.developer_policy_arns["default.policy_arn"])}"
  operator_policy_arn  = "${lookup(var.operator_policy_arns,"development.policy_arn",var.operator_policy_arns["default.policy_arn"])}"
}

またproviderのalias機能を使って、各環境ごとにmoduleを分けました。分けておくことで仮に特定の環境だけ権限を変えたいというケースでも対応できそうです。というか複数providerに一括適用がうまくできなかったというのもあります。環境が増えた場合はこれをコピーして最小2行変更すれば(assumeRoleの設定は別途必要)いけるので、一応許容範囲ということで。

module/controller/group

ここでは各ユーザグループに対してどの権限で実行するかの情報の連携のみ行っています。若干冗長な気もしますが、ユーザグループというレイヤーでのハンドリングをするために1層を挟んでいます。

module "admin" {
  source = "../../group/admin"

  policy_arn = "${var.admin_policy_arn}"
}

module "developer" {
  source = "../../group/developer"

  policy_arn = "${var.developer_policy_arn}"
}

module "operator" {
  source = "../../group/operator"

  policy_arn = "${var.operator_policy_arn}"
}

module/group/<user_group>

ここは実際のユーザ設定を置いておくところになります。今回各ユーザは自信のAWSアカウントを持っているという想定のため、様々な情報を持たせる必要があります。

module "hoge" {
  source = "../../iam_role"

  role_arn      = "arn:aws:iam::bbbbbbbbbbbb:root"
  iam_role_name = "hoge_admin"
  policy_arn    = "${var.policy_arn}"
}

ここで先程のmoduleで受けたpolicy設定を使ってユーザを作る設定を最終的に流しています。この書き方では各ユーザ毎にファイルを分ける想定をしています。運用を考えた際にユーザの削除・ユーザグループの移動をファイルの削除・移動で対応できるためです。variableを使ってユーザ定義もできそうでしたが、こっちの方がよさそうかなという判断になりました。

module/iam_role

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "AWS"
      identifiers = ["${var.role_arn}"]
    }
  }
}

resource "aws_iam_role" "role" {
  name               = "${var.iam_role_name}"
  assume_role_policy = "${data.aws_iam_policy_document.assume_role.json}"
}

resource "aws_iam_role_policy_attachment" "attach_policy" {
  role       = "${aws_iam_role.role.name}"
  policy_arn = "${var.policy_arn}"

  # skip if var.policy_arn is ""
  count = "${var.policy_arn != "" ? 1 : 0}"
}

最後は実際に作成するところです。一応工夫した点としてはcountを使ってpolicy_arnが空文字だった場合、policyのアタッチをさせないことにしています。この方法でやっておけば、仮に緊急対応で一時的に誰かに権限を発行した際にも定期的にTerraformを実行しておけば初期の状態に保つことができるので便利です。

というように環境グループ->ユーザグループ->ユーザというようなかたちでレイヤを分けてtfファイルを作ってみました。

FAQ

各個別のユーザ毎に権限を割り振りたいケースはどうするの?

基本的に考えていません。管理するアカウントがどんどん増えていくと個というものははかない存在となっていきます。そういう場合はユーザーグループ(チーム)で対応するのが大切かと思います。

マネージドポリシー以外の権限を付与したい

policyを作成すれば可能です。

複数の権限を付与したい

まだ対応できていません。ここはどうしような悩み中です(まだ試してないです)

まとめ

Terraformはmodule機能ができてより柔軟に書けるようになってきたかなと思います。どんどん事例出していってコードレベルを上げていけたらいいですねー!

余談

  • module内でのcountが利用できるようになるともっとよさそうですね。。。頼む!!!それがあればworkspace毎に実行とかができそうな気がしている
  • providerの指定を複数かけばいい感じに複数環境対応できるのかな?と思ってたやりかたを下記に書いておきますね。やりかた知ってるかたいらっしゃったら教えて欲しいです。
module "dev" {
  providers = {
    aws = "aws.dev-1"
    aws = "aws.dev-2"
  }

  source               = "../../module/controller/group"
  admin_policy_arn     = "${lookup(var.admin_policy_arns,"development.policy_arn",var.admin_policy_arns["default.policy_arn"])}"
  developer_policy_arn = "${lookup(var.developer_policy_arns,"development.policy_arn",var.developer_policy_arns["default.policy_arn"])}"
  operator_policy_arn  = "${lookup(var.operator_policy_arns,"development.policy_arn",var.operator_policy_arns["default.policy_arn"])}"
}