AWS Control Tower や AWS Organizations、CloudFormation を活用し、安全なセキュリティガードレールと初期設定を施した AWS アカウントを自動発行します。
AWS CLIを使用してCloudFormationスタックをデプロイする場合は、以下のコマンドを実行します。
aws cloudformation create-stack \ --stack-name account-factory-stack \ --template-body file://account-factory.yaml \ --capabilities CAPABILITY_IAM
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
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'