一覧に戻る
Governance

マルチアカウント構造(アカウントファクトリ)

AWS Control Tower や AWS Organizations、CloudFormation を活用し、安全なセキュリティガードレールと初期設定を施した AWS アカウントを自動発行します。

構成要素 (AWS Services):

OrganizationsControl TowerCloudFormationIAM

構築される主要リソース (17種):

IAM Role2
Lambda Function2
S3 Bucket1
S3 BucketPolicy1
DynamoDB Table1
SNS Topic1
Logs LogGroup1
KMS Key1
KMS Alias1
Events Rule1
Lambda Permission1
ServiceCatalog Portfolio1
ServiceCatalog CloudFormationProduct1
ServiceCatalog PortfolioProductAssociation1
ServiceCatalog LaunchRoleConstraint1
CloudWatch Dashboard1
CloudWatch Alarm1

アーキテクチャ図 (Architecture Diagram)

クリックで拡大表示
マルチアカウント構造(アカウントファクトリ) アーキテクチャ図

AWS CLI でのデプロイ例

AWS CLIを使用してCloudFormationスタックをデプロイする場合は、以下のコマンドを実行します。

aws cloudformation deploy \
  --template-file account-factory.yaml \
  --stack-name account-factory-stack \
  --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM
account-factory.yaml
DL
AWSTemplateFormatVersion: '2010-09-09'
Description: 'AWS Account Factory Architecture - Automated account provisioning with Service Catalog, Organizations, and StackSets'

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Portfolio Configuration
        Parameters:
          - PortfolioName
          - PortfolioDescription
      - Label:
          default: Account Baseline Configuration
        Parameters:
          - BaselineStackSetName
          - OrganizationId
      - Label:
          default: Tagging Configuration
        Parameters:
          - Environment
          - CostCenter

Parameters:
  PortfolioName:
    Type: String
    Default: 'Account Factory Portfolio'
    Description: Name of the Service Catalog portfolio

  PortfolioDescription:
    Type: String
    Default: 'Automated AWS account provisioning with baseline configurations'
    Description: Description of the Service Catalog portfolio

  BaselineStackSetName:
    Type: String
    Default: 'AccountBaseline'
    Description: Name of the StackSet for account baseline

  OrganizationId:
    Type: String
    Description: AWS Organization ID (e.g., o-xxxxxxxxxx)
    AllowedPattern: '^o-[a-z0-9]{10,32}$'

  Environment:
    Type: String
    Default: production
    AllowedValues:
      - development
      - staging
      - production
    Description: Environment type

  CostCenter:
    Type: String
    Default: 'IT'
    Description: Cost center for billing

Resources:
  # S3 Bucket for CloudFormation templates
  TemplateBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'account-factory-templates-${AWS::AccountId}'
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      VersioningConfiguration:
        Status: Enabled
      LifecycleConfiguration:
        Rules:
          - Id: DeleteOldVersions
            Status: Enabled
            NoncurrentVersionExpirationInDays: 90
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-templates'
        - Key: Environment
          Value: !Ref Environment

  TemplateBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref TemplateBucket
      PolicyDocument:
        Statement:
          - Sid: AllowServiceCatalogRead
            Effect: Allow
            Principal:
              Service: servicecatalog.amazonaws.com
            Action:
              - s3:GetObject
            Resource: !Sub '${TemplateBucket.Arn}/*'

  # DynamoDB table for account metadata
  AccountMetadataTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub '${AWS::StackName}-accounts'
      BillingMode: PAY_PER_REQUEST
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true
      SSESpecification:
        SSEEnabled: true
      AttributeDefinitions:
        - AttributeName: AccountId
          AttributeType: S
        - AttributeName: AccountEmail
          AttributeType: S
      KeySchema:
        - AttributeName: AccountId
          KeyType: HASH
      GlobalSecondaryIndexes:
        - IndexName: EmailIndex
          KeySchema:
            - AttributeName: AccountEmail
              KeyType: HASH
          Projection:
            ProjectionType: ALL
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-metadata'
        - Key: Environment
          Value: !Ref Environment

  # SNS Topic for notifications
  AccountProvisioningTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub '${AWS::StackName}-notifications'
      DisplayName: Account Factory Notifications
      KmsMasterKeyId: alias/aws/sns
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-topic'
        - Key: Environment
          Value: !Ref Environment

  # CloudWatch Log Group
  AccountFactoryLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/account-factory/${AWS::StackName}'
      RetentionInDays: 30
      KmsKeyId: !GetAtt LogEncryptionKey.Arn

  # KMS Key for CloudWatch Logs
  LogEncryptionKey:
    Type: AWS::KMS::Key
    Properties:
      Description: KMS key for Account Factory logs
      EnableKeyRotation: true
      KeyPolicy:
        Version: '2012-10-17'
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            Action: 'kms:*'
            Resource: '*'
          - Sid: Allow CloudWatch Logs
            Effect: Allow
            Principal:
              Service: !Sub 'logs.${AWS::Region}.amazonaws.com'
            Action:
              - 'kms:Encrypt'
              - 'kms:Decrypt'
              - 'kms:ReEncrypt*'
              - 'kms:GenerateDataKey*'
              - 'kms:CreateGrant'
              - 'kms:DescribeKey'
            Resource: '*'
            Condition:
              ArnLike:
                'kms:EncryptionContext:aws:logs:arn': !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*'

  LogEncryptionKeyAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub 'alias/${AWS::StackName}-logs'
      TargetKeyId: !Ref LogEncryptionKey

  # IAM Role for Lambda
  AccountProvisioningLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${AWS::StackName}-lambda-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: AccountFactoryPermissions
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - organizations:CreateAccount
                  - organizations:DescribeCreateAccountStatus
                  - organizations:DescribeAccount
                  - organizations:ListAccounts
                  - organizations:TagResource
                  - organizations:MoveAccount
                Resource: '*'
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:GetItem
                  - dynamodb:UpdateItem
                  - dynamodb:Query
                Resource:
                  - !GetAtt AccountMetadataTable.Arn
                  - !Sub '${AccountMetadataTable.Arn}/index/*'
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource: !Ref AccountProvisioningTopic
              - Effect: Allow
                Action:
                  - cloudformation:CreateStackSet
                  - cloudformation:CreateStackInstances
                  - cloudformation:DescribeStackSet
                  - cloudformation:DescribeStackInstance
                Resource: !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stackset/${BaselineStackSetName}:*'
              - Effect: Allow
                Action:
                  - sts:AssumeRole
                Resource: !Sub 'arn:aws:iam::*:role/OrganizationAccountAccessRole'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-lambda-role'

  # Lambda Function for account provisioning
  AccountProvisioningFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${AWS::StackName}-provisioning'
      Runtime: python3.12
      Handler: index.handler
      Role: !GetAtt AccountProvisioningLambdaRole.Arn
      Timeout: 900
      MemorySize: 512
      Environment:
        Variables:
          METADATA_TABLE: !Ref AccountMetadataTable
          SNS_TOPIC_ARN: !Ref AccountProvisioningTopic
          ORGANIZATION_ID: !Ref OrganizationId
          STACKSET_NAME: !Ref BaselineStackSetName
      Code:
        ZipFile: |
          import json
          import boto3
          import os
          from datetime import datetime
          
          orgs = boto3.client('organizations')
          dynamodb = boto3.resource('dynamodb')
          sns = boto3.client('sns')
          
          def handler(event, context):
              print(f"Event: {json.dumps(event)}")
              
              table = dynamodb.Table(os.environ['METADATA_TABLE'])
              
              # Extract account details from event
              account_name = event.get('AccountName')
              account_email = event.get('AccountEmail')
              ou_id = event.get('OrganizationalUnitId', None)
              
              try:
                  # Create account
                  response = orgs.create_account(
                      Email=account_email,
                      AccountName=account_name
                  )
                  
                  request_id = response['CreateAccountStatus']['Id']
                  
                  # Store metadata
                  table.put_item(
                      Item={
                          'AccountId': request_id,
                          'AccountEmail': account_email,
                          'AccountName': account_name,
                          'Status': 'IN_PROGRESS',
                          'CreatedAt': datetime.utcnow().isoformat(),
                          'RequestId': request_id
                      }
                  )
                  
                  # Send notification
                  sns.publish(
                      TopicArn=os.environ['SNS_TOPIC_ARN'],
                      Subject='Account Provisioning Started',
                      Message=f'Account provisioning started for {account_name} ({account_email})'
                  )
                  
                  return {
                      'statusCode': 200,
                      'body': json.dumps({
                          'RequestId': request_id,
                          'Status': 'IN_PROGRESS'
                      })
                  }
                  
              except Exception as e:
                  print(f"Error: {str(e)}")
                  return {
                      'statusCode': 500,
                      'body': json.dumps({'error': str(e)})
                  }
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-provisioning'

  # Lambda Function for account status check
  AccountStatusCheckFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${AWS::StackName}-status-check'
      Runtime: python3.12
      Handler: index.handler
      Role: !GetAtt AccountProvisioningLambdaRole.Arn
      Timeout: 300
      MemorySize: 256
      Environment:
        Variables:
          METADATA_TABLE: !Ref AccountMetadataTable
          SNS_TOPIC_ARN: !Ref AccountProvisioningTopic
      Code:
        ZipFile: |
          import json
          import boto3
          import os
          
          orgs = boto3.client('organizations')
          dynamodb = boto3.resource('dynamodb')
          sns = boto3.client('sns')
          
          def handler(event, context):
              table = dynamodb.Table(os.environ['METADATA_TABLE'])
              
              for record in event['Records']:
                  request_id = record['dynamodb']['Keys']['AccountId']['S']
                  
                  try:
                      response = orgs.describe_create_account_status(
                          CreateAccountRequestId=request_id
                      )
                      
                      status = response['CreateAccountStatus']['State']
                      
                      if status == 'SUCCEEDED':
                          account_id = response['CreateAccountStatus']['AccountId']
                          
                          table.update_item(
                              Key={'AccountId': request_id},
                              UpdateExpression='SET #status = :status, ActualAccountId = :account_id',
                              ExpressionAttributeNames={'#status': 'Status'},
                              ExpressionAttributeValues={
                                  ':status': 'SUCCEEDED',
                                  ':account_id': account_id
                              }
                          )
                          
                          sns.publish(
                              TopicArn=os.environ['SNS_TOPIC_ARN'],
                              Subject='Account Provisioning Completed',
                              Message=f'Account {account_id} provisioned successfully'
                          )
                      
                  except Exception as e:
                      print(f"Error checking status: {str(e)}")
              
              return {'statusCode': 200}
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-status-check'

  # EventBridge Rule for periodic status checks
  AccountStatusCheckRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub '${AWS::StackName}-status-check'
      Description: Periodic check for account provisioning status
      ScheduleExpression: 'rate(5 minutes)'
      State: ENABLED
      Targets:
        - Arn: !GetAtt AccountStatusCheckFunction.Arn
          Id: StatusCheckTarget

  AccountStatusCheckPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref AccountStatusCheckFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt AccountStatusCheckRule.Arn

  # Service Catalog Portfolio
  AccountFactoryPortfolio:
    Type: AWS::ServiceCatalog::Portfolio
    Properties:
      DisplayName: !Ref PortfolioName
      Description: !Ref PortfolioDescription
      ProviderName: 'Platform Engineering Team'
      Tags:
        - Key: Name
          Value: !Ref PortfolioName
        - Key: Environment
          Value: !Ref Environment

  # IAM Role for Service Catalog Launch
  ServiceCatalogLaunchRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${AWS::StackName}-sc-launch-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: servicecatalog.amazonaws.com
            Action: sts:AssumeRole
      # NOTE: AdministratorAccess is used here for simplicity in this sample template.
      # In production, replace with a least-privilege policy scoped to only the
      # resources and actions required by your account vending product.
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AdministratorAccess
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-sc-launch-role'

  # Service Catalog Product - Account Vending Machine
  AccountVendingProduct:
    Type: AWS::ServiceCatalog::CloudFormationProduct
    Properties:
      Name: 'AWS Account Vending Machine'
      Description: 'Provision a new AWS account with baseline configurations'
      Owner: 'Platform Engineering Team'
      Distributor: 'Cloud Center of Excellence'
      SupportDescription: 'Contact platform-engineering@example.com for support'
      SupportEmail: 'platform-engineering@example.com'
      ProvisioningArtifactParameters:
        - Name: 'v1.0'
          Description: 'Initial version with baseline configurations'
          Info:
            LoadTemplateFromURL: !Sub 'https://${TemplateBucket.RegionalDomainName}/account-template.yaml'
          Type: CLOUD_FORMATION_TEMPLATE
      Tags:
        - Key: Name
          Value: 'Account Vending Machine'

  # Associate Product with Portfolio
  PortfolioProductAssociation:
    Type: AWS::ServiceCatalog::PortfolioProductAssociation
    Properties:
      PortfolioId: !Ref AccountFactoryPortfolio
      ProductId: !Ref AccountVendingProduct

  # Launch Constraint
  LaunchConstraint:
    Type: AWS::ServiceCatalog::LaunchRoleConstraint
    Properties:
      PortfolioId: !Ref AccountFactoryPortfolio
      ProductId: !Ref AccountVendingProduct
      RoleArn: !GetAtt ServiceCatalogLaunchRole.Arn

  # CloudWatch Dashboard
  AccountFactoryDashboard:
    Type: AWS::CloudWatch::Dashboard
    Properties:
      DashboardName: !Sub '${AWS::StackName}-dashboard'
      DashboardBody: !Sub |
        {
          "widgets": [
            {
              "type": "metric",
              "properties": {
                "metrics": [
                  ["AWS/Lambda", "Invocations", {"stat": "Sum", "label": "Provisioning Invocations"}],
                  [".", "Errors", {"stat": "Sum", "label": "Provisioning Errors"}],
                  [".", "Duration", {"stat": "Average", "label": "Avg Duration"}]
                ],
                "period": 300,
                "stat": "Average",
                "region": "${AWS::Region}",
                "title": "Account Provisioning Metrics",
                "yAxis": {
                  "left": {
                    "min": 0
                  }
                }
              }
            },
            {
              "type": "log",
              "properties": {
                "query": "SOURCE '${AccountFactoryLogGroup}'\n| fields @timestamp, @message\n| sort @timestamp desc\n| limit 20",
                "region": "${AWS::Region}",
                "title": "Recent Provisioning Logs"
              }
            }
          ]
        }

  # CloudWatch Alarms
  ProvisioningErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub '${AWS::StackName}-provisioning-errors'
      AlarmDescription: Alert when account provisioning errors occur
      MetricName: Errors
      Namespace: AWS/Lambda
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 1
      Threshold: 1
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref AccountProvisioningFunction
      AlarmActions:
        - !Ref AccountProvisioningTopic

Outputs:
  PortfolioId:
    Description: Service Catalog Portfolio ID
    Value: !Ref AccountFactoryPortfolio
    Export:
      Name: !Sub '${AWS::StackName}-PortfolioId'

  ProductId:
    Description: Service Catalog Product ID
    Value: !Ref AccountVendingProduct
    Export:
      Name: !Sub '${AWS::StackName}-ProductId'

  TemplateBucketName:
    Description: S3 Bucket for CloudFormation templates
    Value: !Ref TemplateBucket
    Export:
      Name: !Sub '${AWS::StackName}-TemplateBucket'

  MetadataTableName:
    Description: DynamoDB table for account metadata
    Value: !Ref AccountMetadataTable
    Export:
      Name: !Sub '${AWS::StackName}-MetadataTable'

  ProvisioningFunctionArn:
    Description: Lambda function ARN for account provisioning
    Value: !GetAtt AccountProvisioningFunction.Arn
    Export:
      Name: !Sub '${AWS::StackName}-ProvisioningFunction'

  NotificationTopicArn:
    Description: SNS topic ARN for notifications
    Value: !Ref AccountProvisioningTopic
    Export:
      Name: !Sub '${AWS::StackName}-NotificationTopic'

  DashboardURL:
    Description: CloudWatch Dashboard URL
    Value: !Sub 'https://console.aws.amazon.com/cloudwatch/home?region=${AWS::Region}#dashboards:name=${AWS::StackName}-dashboard'