ちょっと話題の記事

【個人的には神ツール】AwsOrganizationFormation(OSS)でAWS Organizationsをコードで管理する

2020.03.09

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

中山(順)です

「AWS Organizationsをコードで管理したい・・・」

そんなことを思ったことはありませんか?

今日はAwsOrganizationFormationというOSSのご紹介です。

READMEには以下のように記載されています。

AWS Organization Formation is an Infrastructure as Code (IaC) tool for AWS Organizations. OlafConijn/AwsOrganizationFormation

AWS Organizationをコードで管理するツールのようです。 これは俺得。

AwsOrganizationFormationの機能

主要な機能として、以下の3つが挙げられています。

  • Infrastructure as Code for AWS Organizations(AWS Organizations自体をコード化)
  • CloudFormation annotations to provision resources cross account(アカウントを横断してリソースをプロビジョニングするためのアノテーション)
  • Automation of account creation and resource provisioning(アカウントの作成とリソースのプロビジョニングを自動化)

なんと、Organizationsのコード化だけでなくアカウント横断のリソースのプロビジョニングまでできるようです。 ますます俺得!

やってみた

  • 事前準備
  • 既存のOrganizationをコード化
  • 子アカウントの作成
  • OU/SCP/Password Policyの管理
  • 子アカウントにプロビジョニングするための準備(テンプレートの作成)
  • 各アカウントでプロビジョニングされるスタックのテンプレートを確認
  • 子アカウントへのリソースのプロビジョニング

事前準備

まず、AwsOrganizationFormationをインストールします。

sudo npm install -g aws-organization-formation

提供されているコマンドは以下の通りです。

$ org-formation --help
Usage: org-formation [options] [command]

aws organization formation

Options:
  -v, --version                                   output the version number
  -h, --help                                      output usage information

Commands:
  create-change-set [options] <templateFile>      create change set that can be reviewed and executed later
  delete-stacks [options]                         removes all stacks deployed to accounts using org-formation
  describe-stacks [options]                       list all stacks deployed to accounts using org-formation
  execute-change-set [options] <change-set-name>  execute previously created change set
  init-pipeline [options]                         initializes organization and created codecommit repo, codebuild and codepipeline
  init [options] <file>                           generate template & initialize organization
  perform-tasks [options] <tasks-file>            performs all tasks from either a file or directory structure
  print-stacks [options] <templateFile>           outputs cloudformation templates generated by org-formation to the console
  update [options] <templateFile>                 update organization resources
  update-stacks [options] <templateFile>          update cloudformation resources in accounts
  validate-stacks [options] <templateFile>        validates the cloudformation templates that will be generated
  validate-tasks [options] <templateFile>         Will validate the tasks file, including configured tasks

AwsOrganizationFormationを介してAWSにアクセスするため、マスターアカウントのアクセスキーを設定しておく必要があります。 これ以降、AwsOrganizationFormationのコマンドを実行する際には以下のプロファイルを指定します。

aws configure --profile organizations

既存のOrganizationをコード化

検証を開始する時点で、親アカウントとAWS Organizationsを通して作成した子アカウント1つが存在する状態です。

まず、initコマンドで現在の状態をコードとして出力します。

org-formation init organization.yml --region us-east-1 --profile organizations
INFO: Your organization template is written to organization.yml
INFO: Hope this will get you started!
INFO:
INFO: You can keep the organization.yml file on disk or even better, under source control.
INFO: If you work with code pipeline you might find init-pipeline an interesting command too.
INFO:
INFO: Dont worry about losing the organization.yml file, at any point you can recreate it.
INFO: Have fun!
INFO:
INFO: --OC

init-pipelineコマンドでパイプラインごと作成することもできるようですが、それはまたの機会に・・・

出力されたコードを確認してみます。

cat organization.yml
AWSTemplateFormatVersion: '2010-09-09-OC'
Description: default template generated for organization with master account XXXXXXXXXXXX

Organization:
  MasterAccount:
    Type: OC::ORG::MasterAccount
    Properties:
      AccountName: Master
      AccountId: 'XXXXXXXXXXXX'

  OrganizationRoot:
    Type: OC::ORG::OrganizationRoot
    Properties:

  NobuhiroNakayamaAccount:
    Type: OC::ORG::Account
    Properties:
      AccountName: Nobuhiro Nakayama
      AccountId: 'YYYYYYYYYYYY'
      RootEmail: ********+001@gmail.com

なお、initコマンドを実行すると自動で状態ファイルの生成とS3バケットへの保存が行われます。 S3バケットがない場合にはバケットも自動で作成されます。 このS3バケットは"--region"で指定したリージョンに作成されます。

{
  "masterAccountId": "XXXXXXXXXXXX",
  "bindings": {
    "OC::ORG::MasterAccount": {
      "MasterAccount": {
        "type": "OC::ORG::MasterAccount",
        "logicalId": "MasterAccount",
        "physicalId": "XXXXXXXXXXXX",
        "lastCommittedHash": "d84c19fee925ab85000bb5c4012b4b85"
      }
    },
    "OC::ORG::OrganizationRoot": {
      "OrganizationRoot": {
        "type": "OC::ORG::OrganizationRoot",
        "logicalId": "OrganizationRoot",
        "physicalId": "r-i18d",
        "lastCommittedHash": "6be66ccf6b2a5417439fec93c294f165"
      }
    },
    "OC::ORG::Account": {
      "NobuhiroNakayamaAccount": {
        "type": "OC::ORG::Account",
        "logicalId": "NobuhiroNakayamaAccount",
        "physicalId": "YYYYYYYYYYYY",
        "lastCommittedHash": "dab47fd962b4aade7fc51a7b5afbb711"
      }
    }
  },
  "stacks": {},
  "values": {},
  "previousTemplate": "{\"AWSTemplateFormatVersion\":\"2010-09-09-OC\",\"Description\":\"default template generated for organization with master account XXXXXXXXXXXX\",\"Organization\":{\"MasterAccount\":{\"Type\":\"OC::ORG::MasterAccount\",\"Properties\":{\"AccountName\":\"Master\",\"AccountId\":\"XXXXXXXXXXXX\"}},\"OrganizationRoot\":{\"Type\":\"OC::ORG::OrganizationRoot\",\"Properties\":null},\"NobuhiroNakayamaAccount\":{\"Type\":\"OC::ORG::Account\",\"Properties\":{\"AccountName\":\"Nobuhiro Nakayama\",\"AccountId\":\"YYYYYYYYYYYY\",\"RootEmail\":\"********+001@gmail.com\"}}}}"
}

子アカウントの作成

テンプレートを修正します。

AWSTemplateFormatVersion: '2010-09-09-OC'
Description: default template generated for organization with master account XXXXXXXXXXXX

Organization:
  MasterAccount:
    Type: OC::ORG::MasterAccount
    Properties:
      AccountName: Master
      AccountId: 'XXXXXXXXXXXX'

  OrganizationRoot:
    Type: OC::ORG::OrganizationRoot
    Properties:

  NobuhiroNakayamaAccount:
    Type: OC::ORG::Account
    Properties:
      AccountName: Nobuhiro Nakayama
      AccountId: 'YYYYYYYYYYYY'
      RootEmail: ********+001@gmail.com

  MemberAccount:
    Type: OC::ORG::Account
    Properties:
      AccountName: OrgFormationTest
      RootEmail: ********+orgformation@gmail.com

Change Setを作成します。

$ org-formation create-change-set organization.yml --profile organizations
{
  "changeSetName": "5f0417cb-9fe4-4255-98ea-d30ae89992f5",
  "changes": [
    {
      "logicalId": "MemberAccount",
      "type": "OC::ORG::Account",
      "action": "Create"
    }
  ]
}

execute-change-setコマンドで変更を適用します。

org-formation execute-change-set 5f0417cb-9fe4-4255-98ea-d30ae89992f5 --profile organizations
OC::ORG::Account              | MemberAccount                 | Create (ZZZZZZZZZZZZ)
OC::ORG::Account              | MemberAccount                 | CommitHash
INFO: done

AWS CLIでアカウントが作成されたことを確認します。

$ aws organizations list-accounts --profile organizations --query "sort_by(Accounts, &JoinedTimestamp)[-1]"
$ aws organizations list-accounts --profile organizations --query "sort_by(Accounts, &JoinedTimestamp)[-1]"
{
    "Status": "ACTIVE",
    "Name": "OrgFormationTest",
    "Email": "********+orgformation@gmail.com",
    "JoinedMethod": "CREATED",
    "JoinedTimestamp": 1583569462.768,
    "Id": "ZZZZZZZZZZZZ",
    "Arn": "arn:aws:organizations::XXXXXXXXXXXX:account/o-xxxxxxxxxx/ZZZZZZZZZZZZ"
}

OU/SCP/Password Policyの管理

そもそも何を管理できるかドキュメントで確認してみましょう。

https://github.com/OlafConijn/AwsOrganizationFormation/blob/master/docs/organization-resources.md

  • MasterAccount
  • Account
  • OrganizationRoot
  • OrganizationalUnit
  • ServiceControlPolicy
  • PasswordPolicy

最初に生成されたリソース以外に、"OrganizationalUnit", "ServiceControlPolicy", "PasswordPolicy" の3つのリソースをサポートしているようです。 以下のようにテンプレートを修正してみます。 ドキュメントの例をほぼそのまま拝借しました。 ちなみに、いろいろやり直したりしたため、アカウントのリソースIDやプロパティが少し変わっていますのでご了承くださいw

AWSTemplateFormatVersion: '2010-09-09-OC'
Description: default template generated for organization with master account XXXXXXXXXXXX

Organization:
  MasterAccount:
    Type: OC::ORG::MasterAccount
    Properties:
      AccountName: Master
      AccountId: 'XXXXXXXXXXXX'
      PasswordPolicy: !Ref PasswordPolicy

  OrganizationRoot:
    Type: OC::ORG::OrganizationRoot
    Properties:

  OrgFormationTestAccount:
    Type: OC::ORG::Account
    Properties:
      AccountName: OrgFormationTest
      AccountId: 'ZZZZZZZZZZZZ'
      RootEmail: ********+orgformation@gmail.com
      PasswordPolicy: !Ref PasswordPolicy

  NobuhiroNakayamaAccount:
    Type: OC::ORG::Account
    Properties:
      AccountName: Nobuhiro Nakayama
      AccountId: 'YYYYYYYYYYYY'
      RootEmail: ********+001@gmail.com
      PasswordPolicy: !Ref PasswordPolicy

  ProductionOU:
    Type: OC::ORG::OrganizationalUnit
    Properties:
      OrganizationalUnitName: production
      ServiceControlPolicies: !Ref RestrictUnusedRegionsSCP
      Accounts: !Ref OrgFormationTestAccount

  DevelopmentOU:
    Type: OC::ORG::OrganizationalUnit
    Properties:
      OrganizationalUnitName: development
      ServiceControlPolicies: !Ref RestrictUnusedRegionsSCP
      Accounts: !Ref NobuhiroNakayamaAccount

  RestrictUnusedRegionsSCP:
    Type: OC::ORG::ServiceControlPolicy
    Properties:
      PolicyName: RestrictUnusedRegions
      Description: Restrict Unused regions
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: DenyUnsupportedRegions
            Effect: Deny
            NotAction:
              - 'cloudfront:*'
              - 'iam:*'
              - 'route53:*'
              - 'support:*'
            Resource: '*'
            Condition:
              StringNotEquals:
                'aws:RequestedRegion':
                  - us-east-1
                  - us-weat-2
                  - ap-northeast-1

  PasswordPolicy:
    Type: OC::ORG::PasswordPolicy
    Properties:
      MaxPasswordAge: 30
      MinimumPasswordLength: 12
      RequireLowercaseCharacters: true
      RequireNumbers: true
      RequireSymbols: true
      RequireUppercaseCharacters: true
      PasswordReusePrevention: 5
      AllowUsersToChangePassword: true

変更を適用します。 今度はChange Setを作成せずにorg-formation updateコマンドを利用しています。

org-formation update organization.yml --profile organizations
OC::ORG::ServiceControlPolicy | RestrictUnusedRegionsSCP      | Create (p-8w6roxp1)
OC::ORG::Account              | OrgFormationTestAccount       | Update
OC::ORG::Account              | OrgFormationTestAccount       | CommitHash
OC::ORG::Account              | NobuhiroNakayamaAccount       | Update
OC::ORG::Account              | NobuhiroNakayamaAccount       | CommitHash
OC::ORG::OrganizationalUnit   | ProductionOU                  | Create (ou-i18d-cmtwak1n)
OC::ORG::OrganizationalUnit   | ProductionOU                  | Attach Policy (RestrictUnusedRegionsSCP)
OC::ORG::OrganizationalUnit   | ProductionOU                  | Attach Account (OrgFormationTestAccount)
OC::ORG::OrganizationalUnit   | ProductionOU                  | CommitHash
OC::ORG::OrganizationalUnit   | DevelopmentOU                 | Create (ou-i18d-md5cpw1c)
OC::ORG::OrganizationalUnit   | DevelopmentOU                 | Attach Policy (RestrictUnusedRegionsSCP)
OC::ORG::OrganizationalUnit   | DevelopmentOU                 | Attach Account (NobuhiroNakayamaAccount)
OC::ORG::OrganizationalUnit   | DevelopmentOU                 | CommitHash
OC::ORG::MasterAccount        | MasterAccount                 | Update
OC::ORG::MasterAccount        | MasterAccount                 | CommitHash
INFO: done

詳細は割愛しますが、設定が反映されていることを確認しました。

子アカウントにプロビジョニングするための準備(テンプレートの作成)

org-formationでは、組織内のアカウントに対してリソースをまとめて展開することができます。 例えば、CloudTrailの設定を全てのアカウントに設定したり、ログを特定のバケットに集約することが可能です。

以下のサンプルファイルを使って使い方を確認していきます。

https://github.com/OlafConijn/AwsOrganizationFormation/blob/master/examples/templates/cloudtrail.yml

サンプルテンプレートを現行のOrganizationに併せて修正

まず、修正したものは以下の通りです。

AWSTemplateFormatVersion: '2010-09-09-OC'

# Include file that contains Organization Section.
# The Organization Section describes Accounts, Organizational Units, etc.
Organization: !Include ../organization.yml

# Any Binding that does not explicitly specify a region will default to this.
# Value can be either string or list
DefaultOrganizationBindingRegion: ap-northeast-1

# Section that contains a named list of Bindings.
# Bindings determine what resources are deployed where
# These bindings can be !Ref'd from the Resources in the resource section
OrganizationBindings:

  # Binding for: S3Bucket, S3BucketPolicy
  CloudTrailBucketBinding:
    Account: !Ref MasterAccount

  # Binding for: CloudTrail, CloudTrailLogGroup, CloudTrailLogGroupRole
  CloudTrailBinding:
    Account: '*'
    IncludeMasterAccount: true

Resources:

  CloudTrailS3Bucket:
    OrganizationBinding: !Ref CloudTrailBucketBinding
    DeletionPolicy: Retain
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'cloudtrail-${MasterAccount}'

  CloudTrailS3BucketPolicy:
    OrganizationBinding: !Ref CloudTrailBucketBinding
    Type: AWS::S3::BucketPolicy
    DependsOn: CloudTrailS3Bucket
    Properties:
      Bucket: !Ref CloudTrailS3Bucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: 'AWSCloudTrailAclCheck'
            Effect: 'Allow'
            Principal: { Service: 'cloudtrail.amazonaws.com' }
            Action: 's3:GetBucketAcl'
            Resource: !Sub 'arn:aws:s3:::${CloudTrailS3Bucket}'
          - Sid: 'AWSCloudTrailWrite'
            Effect: 'Allow'
            Principal: { Service: 'cloudtrail.amazonaws.com' }
            Action: 's3:PutObject'
            Resource: !Sub 'arn:aws:s3:::${CloudTrailS3Bucket}/AWSLogs/*/*'
            Condition:
              StringEquals:
                s3:x-amz-acl: 'bucket-owner-full-control'

  CloudTrailLogGroup:
    OrganizationBinding: !Ref CloudTrailBinding
    Type: 'AWS::Logs::LogGroup'
    Properties:
      RetentionInDays: 14
      LogGroupName: CloudTrail/audit-log

  CloudTrailLogGroupRole:
    OrganizationBinding: !Ref CloudTrailBinding
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: AWSCloudTrailLogGroupRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Sid: AssumeRole1
          Effect: Allow
          Principal:
            Service: 'cloudtrail.amazonaws.com'
          Action: 'sts:AssumeRole'
      Policies:
      - PolicyName: 'cloudtrail-policy'
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Sid: AWSCloudTrailCreateLogStream
            Effect: Allow
            Action:
              - 'logs:CreateLogStream'
              - 'logs:PutLogEvents'
            Resource: !GetAtt 'CloudTrailLogGroup.Arn'

  CloudTrail:
    OrganizationBinding: !Ref CloudTrailBinding
    Type: AWS::CloudTrail::Trail
    DependsOn:
      - CloudTrailS3BucketPolicy
      - CloudTrailLogGroup
      - CloudTrailLogGroupRole
    Properties:
      S3BucketName: !Ref CloudTrailS3Bucket
      IsLogging: false
      IncludeGlobalServiceEvents: true
      IsMultiRegionTrail: true
      CloudWatchLogsLogGroupArn: !GetAtt 'CloudTrailLogGroup.Arn'
      CloudWatchLogsRoleArn: !GetAtt 'CloudTrailLogGroupRole.Arn'

ここでポイントなのは、各リソースの属性として設定してある"OrganizationBinding"です。

CloudTrailをマルチアカウント環境で利用する場合、「あるアカウントにログを集約するためのS3バケットを作成」「全てのアカウントでCloudTrailを有効にしてログ集約用のS3バケットにログを保存」といった設計をよくやります。 上述のテンプレートでは、"AWS::S3::Bucket", "AWS::S3::BucketPolicy", "AWS::Logs::LogGroup", "AWS::IAM::Role", "AWS::CloudTrail::Trail"のリソースが1つのテンプレート上に記述されています。 このうち、"AWS::S3::Bucket", "AWS::S3::BucketPolicy"は、どこか一つのアカウントに作成したいのですが、これをOrganizationBindingで制御しています。

前半のOrganizationBindings > CloudTrailBucketBindingでログ集約用のS3バケットを作成するアカウントを定義しています。 また、OrganizationBindings > CloudTrailBindingでCloudTrailを有効にするアカウントを定義しており、ここではマスターアカウントを含むOrganization内の全てのアカウントが指定されています。

(OrganizationおよびDefaultOrganizationBindingRegionの説明は割愛します)

テンプレートを管理するディレクトリを作成し、上記のテンプレートを配置します。

mkdir templates
cd templates/

各アカウントでプロビジョニングされるスタックのテンプレートを確認

print-stacksコマンドで各アカウントにプロビジョニングされるスタックのテンプレートを確認することができます。 意図した内容のテンプレートが生成されていることが確認できます。

org-formation print-stacks cloudtrail.yml --profile organizations
template for account XXXXXXXXXXXX and region ap-northeast-1
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters": {},
  "Resources": {
    "CloudTrailS3Bucket": {
      "DeletionPolicy": "Retain",
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "BucketName": "cloudtrail-XXXXXXXXXXXX"
      }
    },
    "CloudTrailS3BucketPolicy": {
      "Type": "AWS::S3::BucketPolicy",
      "DependsOn": "CloudTrailS3Bucket",
      "Properties": {
        "Bucket": {
          "Ref": "CloudTrailS3Bucket"
        },
        "PolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "AWSCloudTrailAclCheck",
              "Effect": "Allow",
              "Principal": {
                "Service": "cloudtrail.amazonaws.com"
              },
              "Action": "s3:GetBucketAcl",
              "Resource": {
                "Fn::Sub": "arn:aws:s3:::${CloudTrailS3Bucket}"
              }
            },
            {
              "Sid": "AWSCloudTrailWrite",
              "Effect": "Allow",
              "Principal": {
                "Service": "cloudtrail.amazonaws.com"
              },
              "Action": "s3:PutObject",
              "Resource": {
                "Fn::Sub": "arn:aws:s3:::${CloudTrailS3Bucket}/AWSLogs/*/*"
              },
              "Condition": {
                "StringEquals": {
                  "s3:x-amz-acl": "bucket-owner-full-control"
                }
              }
            }
          ]
        }
      }
    },
    "CloudTrailLogGroup": {
      "Type": "AWS::Logs::LogGroup",
      "Properties": {
        "RetentionInDays": 14,
        "LogGroupName": "CloudTrail/audit-log"
      }
    },
    "CloudTrailLogGroupRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "RoleName": "AWSCloudTrailLogGroupRole",
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "AssumeRole1",
              "Effect": "Allow",
              "Principal": {
                "Service": "cloudtrail.amazonaws.com"
              },
              "Action": "sts:AssumeRole"
            }
          ]
        },
        "Policies": [
          {
            "PolicyName": "cloudtrail-policy",
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Sid": "AWSCloudTrailCreateLogStream",
                  "Effect": "Allow",
                  "Action": [
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                  ],
                  "Resource": {
                    "Fn::GetAtt": [
                      "CloudTrailLogGroup",
                      "Arn"
                    ]
                  }
                }
              ]
            }
          }
        ]
      }
    },
    "CloudTrail": {
      "Type": "AWS::CloudTrail::Trail",
      "DependsOn": [
        "CloudTrailS3BucketPolicy",
        "CloudTrailLogGroup",
        "CloudTrailLogGroupRole"
      ],
      "Properties": {
        "S3BucketName": {
          "Ref": "CloudTrailS3Bucket"
        },
        "IsLogging": false,
        "IncludeGlobalServiceEvents": true,
        "IsMultiRegionTrail": true,
        "CloudWatchLogsLogGroupArn": {
          "Fn::GetAtt": [
            "CloudTrailLogGroup",
            "Arn"
          ]
        },
        "CloudWatchLogsRoleArn": {
          "Fn::GetAtt": [
            "CloudTrailLogGroupRole",
            "Arn"
          ]
        }
      }
    }
  },
  "Outputs": {
    "printDashCloudTrailS3Bucket": {
      "Value": {
        "Ref": "CloudTrailS3Bucket"
      },
      "Description": "Cross Account dependency",
      "Export": {
        "Name": "print-CloudTrailS3Bucket"
      }
    }
  }
}
template for account YYYYYYYYYYYY and region ap-northeast-1
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters": {
    "CloudTrailS3Bucket": {
      "Description": "Cross Account dependency",
      "Type": "String",
      "ExportAccountId": "XXXXXXXXXXXX",
      "ExportRegion": "ap-northeast-1",
      "ExportName": "print-CloudTrailS3Bucket"
    }
  },
  "Resources": {
    "CloudTrailLogGroup": {
      "Type": "AWS::Logs::LogGroup",
      "Properties": {
        "RetentionInDays": 14,
        "LogGroupName": "CloudTrail/audit-log"
      }
    },
    "CloudTrailLogGroupRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "RoleName": "AWSCloudTrailLogGroupRole",
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "AssumeRole1",
              "Effect": "Allow",
              "Principal": {
                "Service": "cloudtrail.amazonaws.com"
              },
              "Action": "sts:AssumeRole"
            }
          ]
        },
        "Policies": [
          {
            "PolicyName": "cloudtrail-policy",
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Sid": "AWSCloudTrailCreateLogStream",
                  "Effect": "Allow",
                  "Action": [
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                  ],
                  "Resource": {
                    "Fn::GetAtt": [
                      "CloudTrailLogGroup",
                      "Arn"
                    ]
                  }
                }
              ]
            }
          }
        ]
      }
    },
    "CloudTrail": {
      "Type": "AWS::CloudTrail::Trail",
      "DependsOn": [
        "CloudTrailLogGroup",
        "CloudTrailLogGroupRole"
      ],
      "Properties": {
        "S3BucketName": {
          "Ref": "CloudTrailS3Bucket"
        },
        "IsLogging": false,
        "IncludeGlobalServiceEvents": true,
        "IsMultiRegionTrail": true,
        "CloudWatchLogsLogGroupArn": {
          "Fn::GetAtt": [
            "CloudTrailLogGroup",
            "Arn"
          ]
        },
        "CloudWatchLogsRoleArn": {
          "Fn::GetAtt": [
            "CloudTrailLogGroupRole",
            "Arn"
          ]
        }
      }
    }
  },
  "Outputs": {}
}
template for account ZZZZZZZZZZZZ and region ap-northeast-1
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters": {
    "CloudTrailS3Bucket": {
      "Description": "Cross Account dependency",
      "Type": "String",
      "ExportAccountId": "XXXXXXXXXXXX",
      "ExportRegion": "ap-northeast-1",
      "ExportName": "print-CloudTrailS3Bucket"
    }
  },
  "Resources": {
    "CloudTrailLogGroup": {
      "Type": "AWS::Logs::LogGroup",
      "Properties": {
        "RetentionInDays": 14,
        "LogGroupName": "CloudTrail/audit-log"
      }
    },
    "CloudTrailLogGroupRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "RoleName": "AWSCloudTrailLogGroupRole",
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "AssumeRole1",
              "Effect": "Allow",
              "Principal": {
                "Service": "cloudtrail.amazonaws.com"
              },
              "Action": "sts:AssumeRole"
            }
          ]
        },
        "Policies": [
          {
            "PolicyName": "cloudtrail-policy",
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Sid": "AWSCloudTrailCreateLogStream",
                  "Effect": "Allow",
                  "Action": [
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                  ],
                  "Resource": {
                    "Fn::GetAtt": [
                      "CloudTrailLogGroup",
                      "Arn"
                    ]
                  }
                }
              ]
            }
          }
        ]
      }
    },
    "CloudTrail": {
      "Type": "AWS::CloudTrail::Trail",
      "DependsOn": [
        "CloudTrailLogGroup",
        "CloudTrailLogGroupRole"
      ],
      "Properties": {
        "S3BucketName": {
          "Ref": "CloudTrailS3Bucket"
        },
        "IsLogging": false,
        "IncludeGlobalServiceEvents": true,
        "IsMultiRegionTrail": true,
        "CloudWatchLogsLogGroupArn": {
          "Fn::GetAtt": [
            "CloudTrailLogGroup",
            "Arn"
          ]
        },
        "CloudWatchLogsRoleArn": {
          "Fn::GetAtt": [
            "CloudTrailLogGroupRole",
            "Arn"
          ]
        }
      }
    }
  },
  "Outputs": {}
}

子アカウントへのリソースのプロビジョニング

validate-stacksコマンドでテンプレートを検証します。

org-formation validate-stacks cloudtrail.yml --profile organizations
INFO: template for stack validation account XXXXXXXXXXXX/ap-northeast-1 valid.
INFO: template for stack validation account YYYYYYYYYYYY/ap-northeast-1 valid.
INFO: template for stack validation account ZZZZZZZZZZZZ/ap-northeast-1 valid.
INFO: done

update-stacksコマンドでテンプレートを利用してスタックをデプロイします。 初回作成時もこのコマンドです。

org-formation update-stacks cloudtrail.yml --stack-name cloudtrail --profile organizations
INFO: stack cloudtrail successfully updated in XXXXXXXXXXXX/ap-northeast-1.
INFO: stack cloudtrail successfully updated in YYYYYYYYYYYY/ap-northeast-1.
INFO: stack cloudtrail successfully updated in ZZZZZZZZZZZZ/ap-northeast-1.
INFO: done

describe-stacksコマンドで作成されたスタックを確認します。

org-formation describe-stacks --profile organizations
{
  "cloudtrail": [
    {
      "accountId": "XXXXXXXXXXXX",
      "region": "ap-northeast-1",
      "stackName": "cloudtrail",
      "lastCommittedHash": "99ca4f9d5e7c3c14b5b93625b3d4f838",
      "logicalAccountId": "MasterAccount",
      "terminationProtection": false
    },
    {
      "accountId": "ZZZZZZZZZZZZ",
      "region": "ap-northeast-1",
      "stackName": "cloudtrail",
      "lastCommittedHash": "99ca4f9d5e7c3c14b5b93625b3d4f838",
      "logicalAccountId": "OrgFormationTestAccount",
      "terminationProtection": false
    },
    {
      "accountId": "YYYYYYYYYYYY",
      "region": "ap-northeast-1",
      "stackName": "cloudtrail",
      "lastCommittedHash": "99ca4f9d5e7c3c14b5b93625b3d4f838",
      "logicalAccountId": "NobuhiroNakayamaAccount",
      "terminationProtection": false
    }
  ]
}

AWS CLIで各アカウントのスタックに含まれるリソースを確認してみます。

まず、集約用のS3バケットを作成するとしたアカウントのスタックです。

aws cloudformation describe-stack-resources \
    --stack-name cloudtrail
{
    "StackResources": [
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/cloudtrail/bfad7db0-6144-11ea-9ad5-0e7389934c04",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            },
            "ResourceType": "AWS::CloudTrail::Trail",
            "Timestamp": "2020-03-08T13:58:03.042Z",
            "StackName": "cloudtrail",
            "PhysicalResourceId": "cloudtrail-CloudTrail-3NCR3OF874M7",
            "LogicalResourceId": "CloudTrail"
        },
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/cloudtrail/bfad7db0-6144-11ea-9ad5-0e7389934c04",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            },
            "ResourceType": "AWS::Logs::LogGroup",
            "Timestamp": "2020-03-08T13:57:33.866Z",
            "StackName": "cloudtrail",
            "PhysicalResourceId": "CloudTrail/audit-log",
            "LogicalResourceId": "CloudTrailLogGroup"
        },
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/cloudtrail/bfad7db0-6144-11ea-9ad5-0e7389934c04",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            },
            "ResourceType": "AWS::IAM::Role",
            "Timestamp": "2020-03-08T13:57:52.160Z",
            "StackName": "cloudtrail",
            "PhysicalResourceId": "AWSCloudTrailLogGroupRole",
            "LogicalResourceId": "CloudTrailLogGroupRole"
        },
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/cloudtrail/bfad7db0-6144-11ea-9ad5-0e7389934c04",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            },
            "ResourceType": "AWS::S3::Bucket",
            "Timestamp": "2020-03-08T13:57:55.497Z",
            "StackName": "cloudtrail",
            "PhysicalResourceId": "cloudtrail-XXXXXXXXXXXX",
            "LogicalResourceId": "CloudTrailS3Bucket"
        },
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/cloudtrail/bfad7db0-6144-11ea-9ad5-0e7389934c04",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            },
            "ResourceType": "AWS::S3::BucketPolicy",
            "Timestamp": "2020-03-08T13:57:58.925Z",
            "StackName": "cloudtrail",
            "PhysicalResourceId": "cloudtrail-CloudTrailS3BucketPolicy-BK6YXBTJQJ8B",
            "LogicalResourceId": "CloudTrailS3BucketPolicy"
        }
    ]
}

他のアカウントのスタックは以下のようになっております。 S3バケットおよびバケットポリシーが含まれていないことを確認できます。

aws cloudformation describe-stack-resources \
    --stack-name cloudtrail \
    --profile organizations-member
{
    "StackResources": [
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:YYYYYYYYYYYY:stack/cloudtrail/e8cd9900-6144-11ea-800c-0e8a9cd56576",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            },
            "ResourceType": "AWS::CloudTrail::Trail",
            "Timestamp": "2020-03-08T13:59:05.432Z",
            "StackName": "cloudtrail",
            "PhysicalResourceId": "cloudtrail-CloudTrail-IATKD7YI5H6Y",
            "LogicalResourceId": "CloudTrail"
        },
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:YYYYYYYYYYYY:stack/cloudtrail/e8cd9900-6144-11ea-800c-0e8a9cd56576",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            },
            "ResourceType": "AWS::Logs::LogGroup",
            "Timestamp": "2020-03-08T13:58:42.563Z",
            "StackName": "cloudtrail",
            "PhysicalResourceId": "CloudTrail/audit-log",
            "LogicalResourceId": "CloudTrailLogGroup"
        },
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:YYYYYYYYYYYY:stack/cloudtrail/e8cd9900-6144-11ea-800c-0e8a9cd56576",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            },
            "ResourceType": "AWS::IAM::Role",
            "Timestamp": "2020-03-08T13:59:01.096Z",
            "StackName": "cloudtrail",
            "PhysicalResourceId": "AWSCloudTrailLogGroupRole",
            "LogicalResourceId": "CloudTrailLogGroupRole"
        }
    ]
}

CloudTrail以外を展開するためのサンプルテンプレートも用意されています。

https://github.com/OlafConijn/AwsOrganizationFormation/tree/master/examples

まとめ

これは神ツールですね。

アカウント数が2桁を超えるとOrganizationの管理に難儀する印象ですが、ここまでできるといろいろ捗りそうです。 いろいろ使ってガンガンフィードバックしていきたいと思った次第です。

早速、怪しい挙動を見つけたのでフィードバックしました。

Errors occur in the init command when member account names are duplicated? #41

現場からは以上です。