SREチームの @tmknom です。ジョジョ5部のアニメ化に興奮を隠せない今日このごろです。
みなさん、AWS Organizationsは使ってますか?
クラウドワークスでも最近使い始めました。AWS Organizations、超絶便利です。こんなに便利なのに、意外と公開されてる事例が少なくて、ぐぬぬってなります。というわけで、使い始めたばかりですが、サクッと公開してみます。他の会社さんも、公開してくれ!!
- AWS Organizations
- マルチアカウント戦略
- Masterアカウントによるアカウント管理
- Auditアカウントによる監査ログの集約管理
- CustodianアカウントによるIAMユーザの集約管理
- 各種AWSアカウントの初期セットアップ
- 今後の展望
AWS Organizations
AWS Organizationsとは、複数の AWS アカウントを統合して管理するサービスです。
- 新規アカウント作成
- アカウントの階層的なグループ化
- 各アカウントのAPIへのアクセス制御(root含む)
- 一括請求
特にスゴいのが、新規アカウント作成で、なんとAWSアカウントをAPIで作成することが可能です。フツーは、連絡先情報を入力して、カード番号を入力して、電話認証して、というクソダルい手間のかかる手順が必要でした。それがこんな感じのコマンドで、一瞬にしてAWSアカウントが作成できます。
aws organizations create-account \ --iam-user-access-to-billing ALLOW \ --account-name "Giorno Giovanna" \ --email "gold-experience@example.com"
もうこの時点で、ワクワクが止まりませんね。そんな、ステキすぎるAWS Organizationsの詳細は、公式のAWS Organizations の用語と概念を参照いただくとして、概念図だけ引用します。
なんとなく理解できると思いますが、OU(Organizational Unit)という単位でAWSアカウントをグループ化することができます。そして、OUもしくはAWSアカウントに対して、ポリシーを定義し、APIへのアクセス制御をすることができます。
マルチアカウント戦略
AWS OrganizationsでAWSアカウントを作りまくる前に、全体設計をします。
この中で、取り返しがつかないのが、「VPCのIPアドレス空間」だったりします。
先行事例の調査
AWS Organizationsで、AWSアカウントを作るのは簡単になりましたが、ガバナンスと利便性のバランスをどう取るか、というのは結構悩ましいです。そこで先駆者の知見を全力で取り入れます。
- Architecting Security and Governance Across a Multi-Account Strategy (日本語レポ)
- AWSにおけるマルチアカウント管理の手法とベストプラクティス
- ぼくらのアカウント戦略〜マルチアカウントでのガバナンスと権限管理の全て〜
特に、トムソン・ロイター社の事例は必見なので、ぜひ参考にしましょう。
コンセプト策定
マルチアカウント管理におけるコンセプトは「Trust, but verify」です。
エンジニアには自由な環境を提供したい!一方で、社会的責任がそれなりにある会社でノーケアというわけにもいかない…!!
というわけで、AWSアカウントについては、すべての行動を記録して、マズそうなことが起きたら即座に検知する環境を整備していきます。ありがちな方針ではありますが、明文化しておかないと、方針がぶれてしまっておかしなことになりやすいので、意識して明文化します。
最近だと、サイバーエージェントさんの事例が大変素晴らしく、全力で見習いたいトコロです。
Terraform戦略
たくさんのAWSアカウントを運用するのに、手作業はありえません。当然コード化します。
AWSの場合、CloudFormationという選択肢もありますが、Terraformのほうがリーダブルでモジュラリティの高いコードが書けるのでTerraformで管理します。悩んだのはTerraformのコードをどの程度共有するかです。
Terraformモジュールによる共通化
Terraformにはモジュール化の機能があり、同じようなリソースをDRYに表現できます。しかし、経験則的に、AWSのインフラストラクチャのような複雑なものは、大体同じだけど、ちょっとだけ違う設定にしたい!という状態にすぐになります。
もちろん、モジュールを修正して、多様なニーズに応えられる汎用的なモジュールを実装することも可能ですが、いきすぎるとTerraform上に独自DSLを実装するハメになり、圧倒的コレジャナイ感を味わうことになります。
インフラテンプレート
というわけで、Terraformの単一モジュールを複数のAWSアカウントで共有しないことにしました。そのかわり、Terraformによる「インフラテンプレート」を用意し、各AWSアカウントでは、それをforkして、個別にコードを管理する方針にしました。もちろん、fork後は独自に進化させてOKという前提です。
ベストプラクティスをインフラテンプレートに集約し、社内で知見をシェアできるようにして、「強くてニューゲーム」を実現します。全然DRYじゃないですが、共通化しすぎて身動きが取れなくなるよりはいいや、と割り切っています。これは、マイクロサービスアーキテクチャで紹介されているプラクティス「サービステンプレート」と同じ発想です。
VPCのIPアドレス空間
AWSアカウントが別なら、VPCのIPアドレス空間なんて好きに設計すればいい。そんなふうに考えていた時期が俺にもありました。
ダメなんです。VPCピアリングできなくなる可能性があります。公式の無効な VPC ピアリング接続設定には、下記のように明記されています。
例えば、クラウドワークスの場合、Redshiftを中心とした、データ基盤の整備を進めているのですが、そこにVPCピアリングできないと、NATサーバ1かVPNサーバを自前で立てて運用するハメになります。
仕方ないので、IPアドレス台帳を作って、中央集権的に管理することにしました。クラウドとは一体…という気持ちしかありませんが、最低でも、社内の共通サービスが置かれるIPアドレス空間とは重複させないように配慮します2。
メールアドレスの管理ポリシー
AWSアカウント作成時には、必ずメールアドレスを紐づけます。このメールアドレスは、AWSからの各種通知や、ルートユーザのパスワード復旧プロセスで使用する重要なモノなので、適切にコントロールしなければいけません。
そこでクラウドワークスでは、SREチームの管理するメールアドレスをAWSアカウントに紐づけています。また、aws+account-name@example.com
のように、AWSアカウントごとにエイリアスを分け、どのAWSアカウント宛のメールであるかを識別できるようしています。
それに加え、SREチームのメールアドレスとは別に、AWSアカウントを所有する各チームのメールアドレスを、代替の連絡先に登録しています。
すると、AWSアカウントを所有するチームと、全体を統括するSREチームの双方に、重要な情報をもれなく通知できます。同時に、事故が起きた場合に、ダメージが甚大なルートユーザの管理もSREチームに集約させることが可能です。
OU(Organizational Unit)の責務
トムソン・ロイター社の事例をパクって参考にして、3つのOUを定義しました。
- service
- システム毎・ビジネスユニット毎にAWSアカウントを作成
- 本番用、開発用など、環境ごとに別アカウントを払い出し
- 各システムの担当チームに、管理権限と管理責任を委譲
- admin
- 管理用のAWSアカウントを作成
- SREチームで中央集権的に管理
- sandbox
- 検証用のAWSアカウントを作成
- エンジニア個人用のアカウントを配って、自由に実験できる環境を提供
ココで重要なのはservice OUのAWSアカウントの管理権限をまるっと、担当チームに渡してしまうことです。各チームに自由と責任を渡すことで、より自律性の高いエンジニアリング文化の醸成を図ります。
また、構想段階ではありますが、安全に使えるsandbox環境をエンジニア一人ひとりに提供することで、個人のスキルアップや実験に活用してもらう予定です。
管理用AWSアカウントの責務
今後、もう少し増える予定ですが、現在は管理用の特殊なアカウントを3つ運用しています。
- Masterアカウント
- Auditアカウント
- Custodianアカウント
なお、Custodianという単語は、管理人とか守衛さん的なニュアンスの単語で、トムソン・ロイター社の事例から拝借しました。Bastion(踏み台)にするか迷ったんですが、Bastionサーバと区別するときにややこしくなりそうだったので、他で使われなさそうな単語を採用してます。
Masterアカウントによるアカウント管理
Masterアカウントは、AWSアカウントの管理のみを担います。AWSにおけるマルチアカウント管理の手法とベストプラクティスに、AWS Organizationsのベストプラクティスが紹介されているので、引用します。Masterアカウントはこれを前提に設計します。
組織
Masterアカウントでは、まず最初に「組織」を作成します。公式の組織の作成を参考に、AWSマネジメントコンソールから作成するのが早いです。ちなみに、AWS CLIやTerraformでも作成可能です。
組織作成時に、「すべての機能」を有効化するか、「一括請求機能のみ」有効化するか選択できますが、問答無用で「すべての機能」を有効にします。
OU(Organizational Unit)
AWSマネジメントコンソールから作る場合は、公式の組織単位 (OU) の管理を参考にします。AWS CLIでも作成可能です。ただし、2018年7月現在、Terraformにはリソースが存在しません。3
あとで簡単に変更可能なので、サクッと作成します。
AWSアカウント
AWSアカウントはわりと頻繁に作ることになるので、AWS CLIを使います。
aws organizations create-account \ --iam-user-access-to-billing ALLOW \ --account-name "Giorno Giovanna" \ --email "gold-experience@example.com"
なお、Terraformでもaws_organizations_accountリソースが提供されています。しかし、Terraformのドキュメントでも警告されているとおり、Terraform単体ではAWS アカウントの閉鎖ができません。
aws_organizations_accountのリソース削除は単純に組織から削除して、スタンドアロン化するだけです。しかも、スタンドアロン化ためには事前に、カスタマーアグリーメントに同意し、連絡先情報と支払方法を入力する必要があり、めちゃくちゃメンドウです。
そのため、今のところ、AWSアカウントはTerraform管理外にしてます。
サービスコントロールポリシー(SCP)
サービスコントロールポリシーでは、「ブラックリスト方式」と「ホワイトリスト方式」のどちらでいくか、最初に決めます。我々は「ブラックリスト方式」を採用し、ガバナンス上、重要なリソースの操作のみを禁止しています。
ここでは、CloudTrailの更新・削除を禁止する、Terraform定義を示します。これで、ルートユーザを含め、CloudTrailの設定を弄れなくなります。
resource "aws_organizations_policy" "cloudtrail" { name = "deny_cloudtrail_deletion" content = <<CONTENT { "Version": "2012-10-17", "Statement": [ { "Effect": "Deny", "Action": [ "cloudtrail:DeleteTrail", "cloudtrail:StopLogging", "cloudtrail:UpdateTrail" ], "Resource": "*" } ] } CONTENT } resource "aws_organizations_policy_attachment" "service" { policy_id = "${aws_organizations_policy.cloudtrail.id}" target_id = "ou-abcd-12345678" }
Auditアカウントによる監査ログの集約管理
Auditアカウントは監査ログの管理を担います。監査ログを集約するためのS3バケットを作成し、ログ検索の仕組みを提供します。現状は、CloudTrailログの集約のみしています。
CloudTrail集約用バケット
S3バケット
CloudTrail用のバケットは下記の設定を行います。
- バージョニングの有効化
- S3アクセスログ配信の有効化
- デフォルト暗号化
特に重要なのは、バージョニング設定で、万が一、ログが改ざんされても戻せるようにしておきます。各AWSアカウントはココで定義したS3バケットを、CloudTrailの保存先に指定します。
# S3アクセスログ用バケット resource "aws_s3_bucket" "s3_log" { bucket = "audit-s3-log" acl = "log-delivery-write" versioning { enabled = true } server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } lifecycle { prevent_destroy = true } } # CloudTrail用バケット resource "aws_s3_bucket" "cloudtrail" { bucket = "audit-cloudtrail" acl = "private" logging { target_bucket = "${aws_s3_bucket.s3_log.id}" target_prefix = "logs/audit-cloudtrail/" } versioning { enabled = true } server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } lifecycle { prevent_destroy = true } }
バケットポリシー
CloudTrailログを集約するため、他のAWSアカウントから書き込めるよう設定します。ポイントは s3:PutObject
アクションの定義です。公式ドキュメントの複数のアカウントのバケットポリシーの設定では、AWSアカウントごとに個別にリソースを定義する記述が紹介されています。
"Resource": [ "arn:aws:s3:::myBucketName/AWSLogs/111111111111/*", "arn:aws:s3:::myBucketName/AWSLogs/222222222222/*" ]
しかし、AWS Organizationsの環境下では、多数のAWSアカウントが作成されるため、AWSアカウントが増えるたびにこの定義を修正するのはメンドウです。また、修正が漏れて、ログを保存しそこねる事故が起きるとかなりツライです。そこで、どのアカウントのCloudTrailからでも書き込めるようにします4。
"Resource": "arn:aws:s3:::myBucketName/*"
この設定だと、赤の他人のAWSアカウントのログが書き込まれる可能性もありますが、参照権限は渡してないので、許容可能と判断しています。
# CloudTrail用バケットのバケットポリシー resource "aws_s3_bucket_policy" "cloudtrail" { bucket = "${aws_s3_bucket.cloudtrail.id}" policy = <<POLICY { "Version": "2012-10-17", "Statement": [ { "Sid": "AWSCloudTrailAclCheck", "Effect": "Allow", "Principal": { "Service": "cloudtrail.amazonaws.com" }, "Action": "s3:GetBucketAcl", "Resource": "arn:aws:s3:::${aws_s3_bucket.cloudtrail.id}" }, { "Sid": "AWSCloudTrailWrite", "Effect": "Allow", "Principal": { "Service": "cloudtrail.amazonaws.com" }, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::${aws_s3_bucket.cloudtrail.id}/*", "Condition": { "StringEquals": { "s3:x-amz-acl": "bucket-owner-full-control" } } } ] } POLICY }
Athena
CloudTrailのログはAthenaで検索できるようにするのが手軽です。
なお、CloudTrailの画面からワンクリックでAthena検索できる環境を構築する機能が提供されていますが、これはマルチアカウント前提ではないため、今回のユースケースではこの仕組みには頼れません。
Athenaのデータベース定義
Athenaのデータベース定義はTerraformで行います。データベース名に特に縛りはありませんが、分かりやすい名前にします。
# Athenaのクエリ結果保存バケット resource "aws_s3_bucket" "athena" { bucket = "audit-athena-query-result" acl = "private" } # CloudTrail用データベース resource "aws_athena_database" "cloudtrail" { name = "cloudtrail" bucket = "${aws_s3_bucket.athena.bucket}" }
Athenaのテーブル定義
テーブル定義は、AWSマネジメントコンソールのAthenaのQuery Editorから、DDLを入力して作成するのが簡単です。
LOCATION定義で、AWSLogs
ディレクトリ直下を指定することで、全AWSアカウントのログの横断検索が可能です。しかし、こうすると、ログ件数が多くなりすぎるので、実際の運用では、AWSアカウントごとにテーブルを作成したり、期間でパーティションを切ることになるでしょう5。
CREATE EXTERNAL TABLE all_cloudtrail ( eventVersion STRING, userIdentity STRUCT< type: STRING, principalId: STRING, arn: STRING, accountId: STRING, invokedBy: STRING, accessKeyId: STRING, userName: STRING, sessionContext: STRUCT< attributes: STRUCT< mfaAuthenticated: STRING, creationDate: STRING>, sessionIssuer: STRUCT< type: STRING, principalId: STRING, arn: STRING, accountId: STRING, userName: STRING>>>, eventTime STRING, eventSource STRING, eventName STRING, awsRegion STRING, sourceIpAddress STRING, userAgent STRING, errorCode STRING, errorMessage STRING, requestParameters STRING, responseElements STRING, additionalEventData STRING, requestId STRING, eventId STRING, resources ARRAY<STRUCT< arn: STRING, accountId: STRING, type: STRING>>, eventType STRING, apiVersion STRING, readOnly STRING, recipientAccountId STRING, serviceEventDetails STRING, sharedEventID STRING, vpcEndpointId STRING ) COMMENT 'CloudTrail table for audit-cloudtrail bucket' ROW FORMAT SERDE 'com.amazon.emr.hive.serde.CloudTrailSerde' STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat' LOCATION 's3://audit-cloudtrail/AWSLogs/' TBLPROPERTIES ('classification'='cloudtrail');
CustodianアカウントによるIAMユーザの集約管理
AWSマネジメントコンソールへのログインは、このCustodianアカウントに集約します。各AWSアカウントには、CustodianアカウントからAssumeRoleすることで切り替えます。
こうすることで、各エンジニアが管理するIAMユーザのパスワード・多要素認証が一つだけでよくなり、AWSアカウントの数が増えても、煩雑になりません。
IAMポリシーとIAMユーザ
どのIAMユーザが、どのAWSアカウントにAssumeRoleできるようにするかは、Custodianアカウントのポリシー定義で一元管理します。
例えば、アカウントID111111111111
にのみ、AssumeRoleできるようにするには、IAMポリシーのsts:AssumeRole
アクションのリソースに、対象アカウントのIAMロールARNを指定します。この設定で、アカウントID111111111111
のadmin_role
に切り替わることが可能です。
statement { effect = "Allow" actions = ["sts:AssumeRole"] resources = ["arn:aws:iam::111111111111:role/admin_role"] }
admin_role
については、後述するとして、ひとまず、CustodianアカウントのTerraform定義の全体像を示します。なお、IAM関連のリソースは、IAM リソースの管理に関するポリシーの例を参考に、適当に絞ってます。
# IAM ポリシー resource "aws_iam_policy" "group" { name = "${aws_iam_group.group.name}" policy = "${data.aws_iam_policy_document.group.json}" } data "aws_iam_policy_document" "group" { statement { effect = "Allow" actions = [ "sts:DecodeAuthorizationMessage", "sts:GetCallerIdentity", "sts:GetSessionToken", ] resources = ["*"] } statement { effect = "Allow" actions = ["sts:AssumeRole"] resources = [ "arn:aws:iam::111111111111:role/admin_role", ] } statement { effect = "Allow" actions = [ "iam:Get*", "iam:List*", ] resources = ["*"] } statement { effect = "Allow" actions = [ "iam:ChangePassword", "iam:*LoginProfile", "iam:*AccessKey*", ] resources = [ "arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/&{aws:username}", ] } statement { effect = "Allow" actions = [ "iam:CreateVirtualMFADevice", "iam:EnableMFADevice", "iam:ResyncMFADevice", "iam:DeleteVirtualMFADevice", ] resources = [ "arn:aws:iam::${data.aws_caller_identity.current.account_id}:mfa/&{aws:username}", "arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/&{aws:username}", ] } statement { effect = "Allow" actions = [ "iam:DeactivateMFADevice", ] resources = [ "arn:aws:iam::${data.aws_caller_identity.current.account_id}:mfa/&{aws:username}", "arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/&{aws:username}", ] condition { test = "Bool" variable = "aws:MultiFactorAuthPresent" values = ["true"] } } } data "aws_caller_identity" "current" {} # IAMグループ resource "aws_iam_group" "group" { name = "passione" } resource "aws_iam_group_policy_attachment" "group" { group = "${aws_iam_group.group.name}" policy_arn = "${aws_iam_policy.group.arn}" } # IAMユーザ resource "aws_iam_user" "bruno_bucciarati" { name = "BrunoBucciarati" } resource "aws_iam_group_membership" "group" { name = "${aws_iam_group.group.name}" group = "${aws_iam_group.group.name}" users = [ "${aws_iam_user.bruno_bucciarati.name}", ] }
各種AWSアカウントの初期セットアップ
service OUなどに作成する、各種AWSアカウントには最初に、2つのリソースを作成します。
- AssumeRole対象のIAMロール
- CloudTrail
メンドウですが、各AWSアカウントに一つ一つ設定する必要があります。
AssumeRole対象のIAMロール
CustodianアカウントからAssumeRoleするためのIAMロールを定義します。IAMロールでは、AssumeRolePolicyDocumentがポイントになります。
まず、AssumeRolePolicyDocumentのプリンシパルには、CustodianアカウントのアカウントIDを指定します。例えば、CustodianアカウントのIDが999999999999
の場合はこんな感じです。
principals { type = "AWS" identifiers = ["arn:aws:iam::999999999999:root"] }
また、Condition定義でaws:MultiFactorAuthPresent
を設定します。これを定義しておくことで、AssumeRoleするIAMユーザに、多要素認証を強制することが可能です。多要素認証を設定してないと、Custodianアカウント以外にログインできなくなるので、セキュリティレベルが大幅に向上します。
なお、この設定を追加すると、Terraform実行時にも、多要素認証が求められます。しかし、Terraform単体では、多要素認証に対応していません。そのため、coinbase/assume-roleなどで、事前にAssumeRoleしたうえで、Terraformを実行する必要があります。
condition { test = "Bool" variable = "aws:MultiFactorAuthPresent" values = ["true"] }
AssumeRolePolicyDocument以外は、普通のIAMロールの定義です。
# IAMロール resource "aws_iam_role" "admin" { name = "admin_role" assume_role_policy = "${data.aws_iam_policy_document.assume_role_policy.json}" } data "aws_iam_policy_document" "assume_role_policy" { statement { effect = "Allow" actions = ["sts:AssumeRole"] principals { type = "AWS" identifiers = ["arn:aws:iam::999999999999:root"] } condition { test = "Bool" variable = "aws:MultiFactorAuthPresent" values = ["true"] } } } # IAMポリシー resource "aws_iam_policy" "admin" { name = "admin_policy" policy = "${data.aws_iam_policy_document.admin.json}" } data "aws_iam_policy_document" "admin" { statement { effect = "Allow" actions = ["*"] resources = ["*"] } } resource "aws_iam_role_policy_attachment" "admin" { role = "${aws_iam_role.admin.name}" policy_arn = "${aws_iam_policy.admin.arn}" }
CloudTrail
Auditアカウントに作成したS3バケットをCloudTrailのログ保存先に指定します。また、各種オプションを有効にしておきます。
- 全リージョンで有効化
- IAMなどのグローバルサービスのログを取得
- ログファイルのバリデーションの有効化
# CloudTrail resource "aws_cloudtrail" "cloudtrail" { name = "default-trail" enable_logging = true s3_bucket_name = "audit-cloudtrail" is_multi_region_trail = true include_global_service_events = true enable_log_file_validation = true }
今後の展望
CloudTrailのログの保存はしてるので、最低限のトレースは可能ですが、そもそも問題発生を検知して通知する仕組みがありません。そこで、次は検知の仕組みを整える予定です。
また、CloudWatch Event BusやAWS Config Aggregatorなど、マルチアカウント管理を円滑に行うためのサービスも提供されているので、うまく活用していきたいところです。この手の活動は、分かりやすく売上に貢献するものではありませんが、持続的にビジネスをしていくためには重要なので、今後もカイゼンしていく予定です。
そんなわけで、クラウドワークスでは、マルチアカウント管理をゴリゴリやっていきたいマニアックなエンジニアを募集してますので、ご興味ありましたらぜひ。
-
AWSのフルマネージドサービスであるNAT ゲートウェイは、残念ながらVPCピアリングに対応していません。↩
-
余談ですが、AWS Solutions Architect ブログでは、『一般にあまり選ばれにくいレンジを共通サービス向けVPCに割り当てるようにすると、少しでも重複が起きる可能性を下げられる』というTipsがマジメに紹介されています。↩
-
プリンシパルを
cloudtrail.amazonaws.com
に絞ってあるので、誰でも書き込めるわけではありません。↩ -
海外の事例ですが、毎日Lambdaでパーティション作成するスクリプトを実装している人もいます。↩