From c259bc89e46b52033d13232c6865fd5c44dddca8 Mon Sep 17 00:00:00 2001 From: Stewart Wallace Date: Fri, 15 Oct 2021 10:25:40 +0100 Subject: [PATCH] Refactoring to use a step function Linting, tests and reducing IAM role scope Enabling tracing Refactoring some more --- .gitignore | 1 + Makefile | 3 +- src/account_processing.yml | 602 ++++++++++++++++++ .../account_processing/__init__.py | 0 .../configure_account_alias.py | 41 ++ .../configure_account_ou.py | 20 + .../configure_account_tags.py | 34 + .../account_processing/create_account.py | 39 ++ .../account_processing/delete_default_vpc.py | 51 ++ .../account_processing/get_account_regions.py | 43 ++ .../process_account_files.py | 72 +++ .../account_processing/pytest.ini | 2 + .../register_account_for_support.py | 177 +++++ .../account_processing/requirements.txt | 3 + .../account_processing/tests/__init__.py | 0 .../tests/test_account_alias.py | 21 + .../tests/test_account_creation.py | 114 ++++ .../adf-build/provisioner/main.py | 130 ---- src/template.yml | 13 +- 19 files changed, 1234 insertions(+), 132 deletions(-) create mode 100644 src/account_processing.yml create mode 100644 src/lambda_codebase/account_processing/__init__.py create mode 100644 src/lambda_codebase/account_processing/configure_account_alias.py create mode 100644 src/lambda_codebase/account_processing/configure_account_ou.py create mode 100644 src/lambda_codebase/account_processing/configure_account_tags.py create mode 100644 src/lambda_codebase/account_processing/create_account.py create mode 100644 src/lambda_codebase/account_processing/delete_default_vpc.py create mode 100644 src/lambda_codebase/account_processing/get_account_regions.py create mode 100644 src/lambda_codebase/account_processing/process_account_files.py create mode 100644 src/lambda_codebase/account_processing/pytest.ini create mode 100644 src/lambda_codebase/account_processing/register_account_for_support.py create mode 100644 src/lambda_codebase/account_processing/requirements.txt create mode 100644 src/lambda_codebase/account_processing/tests/__init__.py create mode 100644 src/lambda_codebase/account_processing/tests/test_account_alias.py create mode 100644 src/lambda_codebase/account_processing/tests/test_account_creation.py delete mode 100755 src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/main.py diff --git a/.gitignore b/.gitignore index 9fca97731..16165b2e2 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ master-deploy.yml .pytest_cache shared_layer.zip .aws-sam +samconfig.toml pipeline.json template-sam.yml deploy.sh diff --git a/Makefile b/Makefile index f1f09700e..fdf4d728e 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ init: test: # Run unit tests pytest src/lambda_codebase/account -vvv -s -c src/lambda_codebase/account/pytest.ini + pytest src/lambda_codebase/account_processing -vvv -s -c src/lambda_codebase/account_processing/pytest.ini pytest src/lambda_codebase/initial_commit/bootstrap_repository -vvv -s -c src/lambda_codebase/initial_commit/bootstrap_repository/pytest.ini pytest src/lambda_codebase/initial_commit/bootstrap_repository/adf-bootstrap/deployment/lambda_codebase -vvv -s -c src/lambda_codebase/initial_commit/bootstrap_repository/adf-bootstrap/deployment/lambda_codebase/pytest.ini pytest src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python -vvv -s -c src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/pytest.ini @@ -11,4 +12,4 @@ test: pytest src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared -vvv -s -c src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/pytest.ini lint: # Linter performs static analysis to catch latent bugs - find src/ -iname "*.py" | xargs pylint --rcfile .pylintrc + find src/ -iname "*.py" -not -path ".aws-sam/*" | xargs pylint --rcfile .pylintrc diff --git a/src/account_processing.yml b/src/account_processing.yml new file mode 100644 index 000000000..de31c6cca --- /dev/null +++ b/src/account_processing.yml @@ -0,0 +1,602 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Description: ADF CloudFormation Stack pertaing to account processing / OU management. +Parameters: + OrganizationID: + Type: String + ADFVersion: + Type: String + LambdaLayer: + Type: String + CrossAccountAccessRoleName: + Type: String + +Resources: + AccountProcessingLambdaRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - "lambda.amazonaws.com" + Action: + - "sts:AssumeRole" + AccountProcessingLambdaRolePolicy: + Type: "AWS::IAM::ManagedPolicy" + Properties: + Description: "Policy to allow the account file processing Lambda to perform actions" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Action: + - "sts:AssumeRole" + - "lambda:GetLayerVersion" + - "logs:CreateLogGroup" + - "logs:CreateLogStream" + - "logs:PutLogEvents" + - "organizations:DescribeOrganizationalUnit" + - "organizations:ListParents" + - "organizations:ListAccounts" + - "organizations:DescribeOrganization" + - "organizations:DescribeAccount" + - "states:StartExecution" + - "xray:Put*" + Resource: "*" + - Effect: "Allow" + Action: "s3:ListBucket" + Resource: !GetAtt ADFAccountBucket.Arn + - Effect: "Allow" + Action: "s3:GetObject" + Resource: + !Join + - '' + - - !GetAtt ADFAccountBucket.Arn + - '/*' + Roles: + - !Ref AccountProcessingLambdaRole + ADFAccountAccessRolePolicy: + Type: "AWS::IAM::ManagedPolicy" + Properties: + Description: "Additional policy that allows a lambda to assume the cross account access role" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "sts:AssumeRole" + Resource: !Sub "arn:aws:iam::*:role/${CrossAccountAccessRoleName}" + Roles: + - !Ref AccountProcessingLambdaRole + - !Ref GetAccountRegionsFunctionRole + - !Ref DeleteDefaultVPCFunctionRole + ADFAccountProcessingLambdaBasePolicy: + Type: "AWS::IAM::ManagedPolicy" + Properties: + Description: "Base policy for all ADF account processing lambdas" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "logs:CreateLogGroup" + - "logs:CreateLogStream" + - "logs:PutLogEvents" + - "xray:PutTelemetryRecords" + - "xray:PutTraceSegments" + Resource: "*" + Roles: + - !Ref AccountProcessingLambdaRole + - !Ref GetAccountRegionsFunctionRole + - !Ref DeleteDefaultVPCFunctionRole + - !Ref AccountAliasConfigFunctionRole + - !Ref AccountTagConfigFunctionRole + - !Ref AccountOUConfigFunctionRole + - !Ref CreateAccountFunctionRole + - !Ref RegisterAccountForSupportFunctionRole + + StateMachineExecutionRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - states.amazonaws.com + Action: "sts:AssumeRole" + Path: "/" + Policies: + - PolicyName: "adf-state-machine-role-policy" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "lambda:InvokeFunction" + - "xray:PutTelemetryRecords" + - "xray:PutTraceSegments" + Resource: "*" + AccountFileProcessingFunction: + Type: 'AWS::Serverless::Function' + Properties: + Architectures: + - arm64 + Handler: process_account_files.lambda_handler + CodeUri: lambda_codebase/account_processing + Layers: + - !Ref LambdaLayer + Description: "ADF Lambda Function - Account File Processing" + Environment: + Variables: + MASTER_ACCOUNT_ID: !Ref AWS::AccountId + ORGANIZATION_ID: !Ref OrganizationID + ADF_VERSION: !Ref ADFVersion + ADF_LOG_LEVEL: INFO + ACCOUNT_MANAGEMENT_STATEMACHINE_ARN: !Ref AccountManagementStateMachine + ADF_ROLE_NAME: !Ref CrossAccountAccessRoleName + FunctionName: AccountFileProcessorFunction + Role: !GetAtt AccountProcessingLambdaRole.Arn + Runtime: python3.8 + Timeout: 300 + Events: + S3Event: + Type: S3 + Properties: + Bucket: + Ref: ADFAccountBucket + Events: s3:ObjectCreated:* + Tracing: Active + AccountAliasConfigFunctionRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - lambda.amazonaws.com + Action: "sts:AssumeRole" + Path: "/" + Policies: + - PolicyName: "adf-lambda-policy" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "iam:CreateAcountAlias" + Resource: "*" + + AccountAliasConfigFunction: + Type: 'AWS::Serverless::Function' + Properties: + Architectures: + - arm64 + Handler: configure_account_alias.lambda_handler + CodeUri: lambda_codebase/account_processing + Layers: + - !Ref LambdaLayer + Description: "ADF Lambda Function - Account Alias Configuration" + Environment: + Variables: + MASTER_ACCOUNT_ID: !Ref AWS::AccountId + ORGANIZATION_ID: !Ref OrganizationID + ADF_VERSION: !Ref ADFVersion + ADF_LOG_LEVEL: INFO + ADF_ROLE_NAME: !Ref CrossAccountAccessRoleName + FunctionName: AccountAliasConfigurationFunction + Role: !GetAtt AccountAliasConfigFunctionRole.Arn + Runtime: python3.8 + Timeout: 300 + Tracing: Active + AccountTagConfigFunctionRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - lambda.amazonaws.com + Action: "sts:AssumeRole" + Path: "/" + Policies: + - PolicyName: "adf-lambda-policy" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "organizations:TagResource" + Resource: "*" + AccountTagConfigFunction: + Type: 'AWS::Serverless::Function' + Properties: + Architectures: + - arm64 + Handler: configure_account_tags.lambda_handler + CodeUri: lambda_codebase/account_processing + Layers: + - !Ref LambdaLayer + Description: "ADF Lambda Function - Account OU Configuration" + Environment: + Variables: + MASTER_ACCOUNT_ID: !Ref AWS::AccountId + ORGANIZATION_ID: !Ref OrganizationID + ADF_VERSION: !Ref ADFVersion + ADF_LOG_LEVEL: INFO + FunctionName: AccountTagConfigurationFunction + Role: !GetAtt AccountTagConfigFunctionRole.Arn + Runtime: python3.8 + Timeout: 300 + Tracing: Active + AccountOUConfigFunction: + Type: 'AWS::Serverless::Function' + Properties: + Architectures: + - arm64 + Handler: configure_account_ou.lambda_handler + CodeUri: lambda_codebase/account_processing + Layers: + - !Ref LambdaLayer + Description: "ADF Lambda Function - Account OU Configuration" + Environment: + Variables: + MASTER_ACCOUNT_ID: !Ref AWS::AccountId + ORGANIZATION_ID: !Ref OrganizationID + ADF_VERSION: !Ref ADFVersion + ADF_LOG_LEVEL: INFO + FunctionName: AccountOUConfigurationFunction + Role: !GetAtt AccountOUConfigFunctionRole.Arn + Runtime: python3.8 + Timeout: 300 + Tracing: Active + AccountOUConfigFunctionRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - lambda.amazonaws.com + Action: "sts:AssumeRole" + Path: "/" + Policies: + - PolicyName: "adf-lambda-policy" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "organizations:*" + Resource: "*" + GetAccountRegionsFunction: + Type: 'AWS::Serverless::Function' + Properties: + Architectures: + - arm64 + Handler: get_account_regions.lambda_handler + CodeUri: lambda_codebase/account_processing + Layers: + - !Ref LambdaLayer + Description: "ADF Lambda Function - Get Default Regions for an account" + Environment: + Variables: + MASTER_ACCOUNT_ID: !Ref AWS::AccountId + ORGANIZATION_ID: !Ref OrganizationID + ADF_VERSION: !Ref ADFVersion + ADF_LOG_LEVEL: INFO + ADF_ROLE_NAME: !Ref CrossAccountAccessRoleName + FunctionName: GetAccountRegionsFunction + Role: !GetAtt GetAccountRegionsFunctionRole.Arn + Runtime: python3.8 + Timeout: 300 + Tracing: Active + GetAccountRegionsFunctionRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - lambda.amazonaws.com + Action: "sts:AssumeRole" + Path: "/" + DeleteDefaultVPCFunction: + Type: 'AWS::Serverless::Function' + Properties: + Architectures: + - arm64 + Handler: delete_default_vpc.lambda_handler + CodeUri: lambda_codebase/account_processing + Layers: + - !Ref LambdaLayer + Description: "ADF Lambda Function - Delete the default vpc for a region" + Environment: + Variables: + MASTER_ACCOUNT_ID: !Ref AWS::AccountId + ORGANIZATION_ID: !Ref OrganizationID + ADF_VERSION: !Ref ADFVersion + ADF_LOG_LEVEL: INFO + ADF_ROLE_NAME: !Ref CrossAccountAccessRoleName + FunctionName: DeleteDefaultVPCFunction + Role: !GetAtt DeleteDefaultVPCFunctionRole.Arn + Runtime: python3.8 + Timeout: 300 + Tracing: Active + DeleteDefaultVPCFunctionRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - lambda.amazonaws.com + Action: "sts:AssumeRole" + Path: "/" + CreateAccountFunction: + Type: 'AWS::Serverless::Function' + Properties: + Architectures: + - arm64 + Handler: create_account.lambda_handler + CodeUri: lambda_codebase/account_processing + Layers: + - !Ref LambdaLayer + Description: "ADF Lambda Function - Create an account" + Environment: + Variables: + MASTER_ACCOUNT_ID: !Ref AWS::AccountId + ORGANIZATION_ID: !Ref OrganizationID + ADF_VERSION: !Ref ADFVersion + ADF_LOG_LEVEL: INFO + ADF_ROLE_NAME: !Ref CrossAccountAccessRoleName + FunctionName: CreateAccountFunction + Role: !GetAtt CreateAccountFunctionRole.Arn + Runtime: python3.8 + Timeout: 300 + Tracing: Active + CreateAccountFunctionRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - lambda.amazonaws.com + Action: "sts:AssumeRole" + Path: "/" + Policies: + - PolicyName: "adf-lambda-policy" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "organizations:CreateAccount" + - "organizations:DescribeCreateAccountStatus" + Resource: "*" + RegisterAccountForSupportFunction: + Type: 'AWS::Serverless::Function' + Properties: + Architectures: + - arm64 + Handler: register_account_for_support.lambda_handler + CodeUri: lambda_codebase/account_processing + Layers: + - !Ref LambdaLayer + Description: "ADF Lambda Function - Registers an account for enterprise support" + Environment: + Variables: + MASTER_ACCOUNT_ID: !Ref AWS::AccountId + ORGANIZATION_ID: !Ref OrganizationID + ADF_VERSION: !Ref ADFVersion + ADF_LOG_LEVEL: INFO + FunctionName: RegisterAccountForSupportFunction + Role: !GetAtt RegisterAccountForSupportFunctionRole.Arn + Runtime: python3.8 + Timeout: 300 + Tracing: Active + RegisterAccountForSupportFunctionRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - lambda.amazonaws.com + Action: "sts:AssumeRole" + Path: "/" + Policies: + - PolicyName: "adf-lambda-policy" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "support:DescribeSeverityLevels" + - "support:CreateCase" + Resource: "*" + ADFAccountBucket: + Type: "AWS::S3::Bucket" + DeletionPolicy: Retain + Properties: + AccessControl: BucketOwnerFullControl + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 + VersioningConfiguration: + Status: Enabled + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + AccountManagementStateMachine: + Type: "AWS::StepFunctions::StateMachine" + Properties: + DefinitionString: !Sub |- + { + "Comment": "Create account?", + "StartAt": "CreateAccountChoice", + "States": { + "CreateAccountChoice": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.needs_created", + "BooleanEquals": true, + "Comment": "Create Account", + "Next": "CreateAccount" + } + ], + "Default": "ConfigureAccountAlias" + }, + "ConfigureAccountAlias": { + "Type": "Task", + "Resource": "${AccountAliasConfigFunction.Arn}", + "Retry": [{ + "ErrorEquals": ["RetryError"], + "IntervalSeconds": 10, + "BackoffRate": 1.0, + "MaxAttempts": 20 + }], + "Next": "ConfigureAccountTags" + }, + "CreateAccount": { + "Type": "Task", + "Resource": "${CreateAccountFunction.Arn}", + "Retry": [{ + "ErrorEquals": ["RetryError"], + "IntervalSeconds": 10, + "BackoffRate": 1.0, + "MaxAttempts": 20 + }], + "Next": "WaitFor10Seconds" + }, + "WaitFor10Seconds": { + "Type": "Wait", + "Seconds": 10, + "Next": "ConfigureAccountSupport" + }, + "ConfigureAccountSupport": { + "Type": "Task", + "Resource": "${RegisterAccountForSupportFunction.Arn}", + "Retry": [{ + "ErrorEquals": ["RetryError"], + "IntervalSeconds": 10, + "BackoffRate": 1.0, + "MaxAttempts": 20 + }], + "Next": "ConfigureAccountAlias" + }, + "ConfigureAccountTags": { + "Type": "Task", + "Resource": "${AccountTagConfigFunction.Arn}", + "Retry": [{ + "ErrorEquals": ["RetryError"], + "IntervalSeconds": 10, + "BackoffRate": 1.0, + "MaxAttempts": 20 + }], + "Next": "ConfigureAccountOU" + }, + "ConfigureAccountOU": { + "Type": "Task", + "Resource": "${AccountOUConfigFunction.Arn}", + "Retry": [{ + "ErrorEquals": ["RetryError"], + "IntervalSeconds": 10, + "BackoffRate": 1.0, + "MaxAttempts": 20 + }], + "Next": "DeleteDefaultVPCChoice" + }, + "DeleteDefaultVPCChoice": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.delete_default_vpc", + "BooleanEquals": true, + "Next": "GetAccountDefaultRegionsFunction" + } + ], + "Default": "Success" + }, + "GetAccountDefaultRegionsFunction": { + "Type": "Task", + "Resource": "${GetAccountRegionsFunction.Arn}", + "Retry": [{ + "ErrorEquals": ["RetryError"], + "IntervalSeconds": 10, + "BackoffRate": 1.0, + "MaxAttempts": 20 + }], + "Next": "DeleteDefaultVPCMap" + }, + "DeleteDefaultVPCMap": { + "Type": "Map", + "Next": "Success", + "Iterator": { + "StartAt": "DeleteDefaultVPC", + "States": { + "DeleteDefaultVPC": { + "Type": "Task", + "Resource": "${DeleteDefaultVPCFunction.Arn}", + "OutputPath": "$.Payload", + "Parameters": { + "Payload.$": "$" + }, + "Retry": [ + { + "ErrorEquals": [ + "Lambda.ServiceException", + "Lambda.AWSLambdaException", + "Lambda.SdkClientException" + ], + "IntervalSeconds": 2, + "MaxAttempts": 6, + "BackoffRate": 2 + } + ], + "End": true + } + } + }, + "ItemsPath": "$.default_regions", + "MaxConcurrency": 20, + "Parameters": { + "region.$": "$$.Map.Item.Value", + "account.$": "$.Id" + }, + "ResultPath": null + }, + "Success": { + "Type": "Succeed" + } + } + } + RoleArn: !GetAtt StateMachineExecutionRole.Arn + TracingConfiguration: + Enabled: True +Outputs: + Bucket: + Value: !Ref ADFAccountBucket \ No newline at end of file diff --git a/src/lambda_codebase/account_processing/__init__.py b/src/lambda_codebase/account_processing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/lambda_codebase/account_processing/configure_account_alias.py b/src/lambda_codebase/account_processing/configure_account_alias.py new file mode 100644 index 000000000..3e5093a87 --- /dev/null +++ b/src/lambda_codebase/account_processing/configure_account_alias.py @@ -0,0 +1,41 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +""" +Creates or updates an ALIAS for an account +""" + +import os +from sts import STS +from aws_xray_sdk.core import patch_all +patch_all() + +ADF_ROLE_NAME = os.getenv("ADF_ROLE_NAME") + + +def create_account_alias(account, iam_client): + print( + f"Ensuring Account: {account.get('account_full_name')} has alias {account.get('alias')}" + ) + iam_client.create_account_alias(AccountAlias=account.get("alias")) + return account + + +def lambda_handler(event, _): + + if event.get("alias"): + print( + f"Ensuring Account: {event.get('account_full_name')} has alias {event.get('alias')}" + ) + sts = STS() + account_id = event.get("Id") + role = sts.assume_cross_account_role( + f"arn:aws:iam::{account_id}:role/{ADF_ROLE_NAME}", + "adf_account_alias_config", + ) + create_account_alias(event.get("alias"), role.client("iam")) + else: + print( + f"Ensuring Account: {event.get('account_full_name')} does not need an alias" + ) + return event diff --git a/src/lambda_codebase/account_processing/configure_account_ou.py b/src/lambda_codebase/account_processing/configure_account_ou.py new file mode 100644 index 000000000..4e6b7821d --- /dev/null +++ b/src/lambda_codebase/account_processing/configure_account_ou.py @@ -0,0 +1,20 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +""" +Moves an account to the specified OU. +""" +from organizations import Organizations +import boto3 +from aws_xray_sdk.core import patch_all + +patch_all() + + +def lambda_handler(event, _): + print( + f"Ensuring Account: {event.get('account_full_name')} is in OU {event.get('organizational_unit_path')}" + ) + organizations = Organizations(boto3) + organizations.move_account(event.get("Id"), event.get("organizational_unit_path")) + return event diff --git a/src/lambda_codebase/account_processing/configure_account_tags.py b/src/lambda_codebase/account_processing/configure_account_tags.py new file mode 100644 index 000000000..4db4022f1 --- /dev/null +++ b/src/lambda_codebase/account_processing/configure_account_tags.py @@ -0,0 +1,34 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +""" +Creates or adds tags to an account. +Currently only appends new tags. +Will not delete tags that aren't +in the config file. +""" + +from organizations import Organizations + +import boto3 +from aws_xray_sdk.core import patch_all + +patch_all() + + +def create_account_tags(account_id, tags, org_session: Organizations): + org_session.create_account_tags(account_id, tags) + + +def lambda_handler(event, _): + if event.get("tags"): + print( + f"Ensuring Account: {event.get('account_full_name')} has tags {event.get('tags')}" + ) + organizations = Organizations(boto3) + create_account_tags(event.get("Id"), event.get("tags"), organizations) + else: + print( + f"Account: {event.get('account_full_name')} does not need tags configured" + ) + return event diff --git a/src/lambda_codebase/account_processing/create_account.py b/src/lambda_codebase/account_processing/create_account.py new file mode 100644 index 000000000..1bc021e3c --- /dev/null +++ b/src/lambda_codebase/account_processing/create_account.py @@ -0,0 +1,39 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +""" +Creates an account within your organisation. +""" + +import os +from aws_xray_sdk.core import patch_all +patch_all() +ADF_ROLE_NAME = os.getenv("ADF_ROLE_NAME") +import boto3 + + +def create_account(account, adf_role_name, org_client): + allow_billing = "ALLOW" if account.get("allow_billing", False) else "DENY" + response = org_client.create_account( + Email=account.get("email"), + AccountName=account.get("account_full_name"), + RoleName=adf_role_name, # defaults to OrganizationAccountAccessRole + IamUserAccessToBilling=allow_billing, + )["CreateAccountStatus"] + while response["State"] == "IN_PROGRESS": + response = org_client.describe_create_account_status( + CreateAccountRequestId=response["Id"] + )["CreateAccountStatus"] + if response.get("FailureReason"): + raise IOError( + f"Failed to create account {account.get('account_full_name')}: {response['FailureReason']}" + ) + account_id = response["AccountId"] + account["Id"] = account_id + return account + + +def lambda_handler(event, _): + print(f"Creating account {event.get('account_full_name')}") + org_client = boto3.client("organizations") + return create_account(event, ADF_ROLE_NAME, org_client) diff --git a/src/lambda_codebase/account_processing/delete_default_vpc.py b/src/lambda_codebase/account_processing/delete_default_vpc.py new file mode 100644 index 000000000..c11fff8e8 --- /dev/null +++ b/src/lambda_codebase/account_processing/delete_default_vpc.py @@ -0,0 +1,51 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +""" +Deletes the default VPC in a particular region +""" +import os +from sts import STS +from aws_xray_sdk.core import patch_all +patch_all() + +ADF_ROLE_NAME = os.getenv("ADF_ROLE_NAME") + + +def lambda_handler(event, _): + event = event.get("Payload") + print(f"Deleting Default vpc: {event.get('account_full_name')}") + sts = STS() + account_id = event.get("account") + + role = sts.assume_cross_account_role( + f"arn:aws:iam::{account_id}:role/{ADF_ROLE_NAME}", + "adf_delete_default_vpc", + ) + ec2_client = role.client("ec2", region_name=event.get("region")) + vpc_response = ec2_client.describe_vpcs() + default_vpc_id = None + for vpc in vpc_response["Vpcs"]: + if vpc["IsDefault"] is True: + default_vpc_id = vpc["VpcId"] + if default_vpc_id: + ec2 = role.resource("ec2", region_name=event.get("region")) + vpc = ec2.Vpc(default_vpc_id) + for gw in vpc.internet_gateways.all(): + vpc.detach_internet_gateway(InternetGatewayId=gw.id) + gw.delete() + # Route table associations + for rt in vpc.route_tables.all(): + for rta in rt.associations: + if not rta.main: + rta.delete() + # Security Group + for sg in vpc.security_groups.all(): + if sg.group_name != "default": + sg.delete() + for subnet in vpc.subnets.all(): + for interface in subnet.network_interfaces.all(): + interface.delete() + subnet.delete() + ec2_client.delete_vpc(VpcId=default_vpc_id) + return {"Payload": event} diff --git a/src/lambda_codebase/account_processing/get_account_regions.py b/src/lambda_codebase/account_processing/get_account_regions.py new file mode 100644 index 000000000..fe2aeffff --- /dev/null +++ b/src/lambda_codebase/account_processing/get_account_regions.py @@ -0,0 +1,43 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +""" +Gets all the default regions for an accounts. +""" + +import os +from sts import STS +from aws_xray_sdk.core import patch_all +patch_all() + +ADF_ROLE_NAME = os.getenv("ADF_ROLE_NAME") + + +def lambda_handler(event, _): + print(f"Fetching Default regions {event.get('account_full_name')}") + sts = STS() + account_id = event.get("Id") + role = sts.assume_cross_account_role( + f"arn:aws:iam::{account_id}:role/{ADF_ROLE_NAME}", + "adf_account_alias_config", + ) + + ec2_client = role.client("ec2") + default_regions = [ + region["RegionName"] + for region in ec2_client.describe_regions( + AllRegions=False, + Filters=[ + { + "Name": "opt-in-status", + "Values": [ + "opt-in-not-required", + ], + } + ], + )["Regions"] + ] + print(default_regions) + event["default_regions"] = default_regions + print(event) + return event diff --git a/src/lambda_codebase/account_processing/process_account_files.py b/src/lambda_codebase/account_processing/process_account_files.py new file mode 100644 index 000000000..94544616a --- /dev/null +++ b/src/lambda_codebase/account_processing/process_account_files.py @@ -0,0 +1,72 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +""" +Listens to notifications from the adf-account-bucket +Identifies accounts that need to be created and then +invokes the account processing step function per account. +""" + +import json +import os +from typing import Tuple +from aws_xray_sdk.core.patcher import patch +import yaml + +from aws_xray_sdk.core import xray_recorder, patch_all + +patch_all() + +from organizations import Organizations + +import boto3 + +ACCOUNT_MANAGEMENT_STATEMACHINE = os.getenv("ACCOUNT_MANAGEMENT_STATEMACHINE_ARN") + + +def get_details_from_event(event: dict): + s3_details = event.get("Records", [{}])[0].get("s3") + bucket_name = s3_details.get("bucket", {}).get("name") + object_key = s3_details.get("object", {}).get("key") + return bucket_name, object_key + + +def get_file_from_s3(s3_object: Tuple, s3_client: boto3.resource): + bucket_name, object_key = s3_object + s3_object = s3_client.Object(bucket_name, object_key) + s3_object.download_file(f"/tmp/{object_key}") + with open(f"/tmp/{object_key}", encoding="utf-8") as data_stream: + data = yaml.safe_load(data_stream) + + return data + + +def get_all_accounts(): + org_client = Organizations(boto3) + return org_client.get_accounts() + + +def lambda_handler(event, _): + """Main Lambda Entry point""" + all_accounts = get_all_accounts() + account_file = get_file_from_s3(get_details_from_event(event), boto3.resource("s3")) + accounts = account_file.get("accounts") + for account in accounts: + print(account["account_full_name"]) + try: + account_id = next( + acc["Id"] + for acc in all_accounts + if acc["Name"] == account["account_full_name"] + ) + account["Id"] = account_id + account["needs_created"] = False + except StopIteration: # If the account does not exist yet.. + account["needs_created"] = True + sfn = boto3.client("stepfunctions") + for account in accounts: + sfn.start_execution( + stateMachineArn=ACCOUNT_MANAGEMENT_STATEMACHINE, + input=f"{json.dumps(account)}", + ) + return event diff --git a/src/lambda_codebase/account_processing/pytest.ini b/src/lambda_codebase/account_processing/pytest.ini new file mode 100644 index 000000000..5ee647716 --- /dev/null +++ b/src/lambda_codebase/account_processing/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests diff --git a/src/lambda_codebase/account_processing/register_account_for_support.py b/src/lambda_codebase/account_processing/register_account_for_support.py new file mode 100644 index 000000000..ffcdf2ce5 --- /dev/null +++ b/src/lambda_codebase/account_processing/register_account_for_support.py @@ -0,0 +1,177 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +""" +If enterprise support is enabled. +Will automatically create a ticket in your OU root account +to register the newly created account for support. +""" + +from enum import Enum +from logger import configure_logger +from aws_xray_sdk.core import patch_all +patch_all() + +import boto3 +from botocore.exceptions import ClientError, BotoCoreError +from botocore.config import Config + + +LOGGER = configure_logger(__name__) + + +class SupportLevel(Enum): + BASIC = "basic" + DEVELOPER = "developer" + BUSINESS = "business" + ENTERPRISE = "enterprise" + + +class Support: # pylint: disable=R0904 + """Class used for accessing AWS Support API""" + + _config = Config(retries=dict(max_attempts=30)) + + def __init__(self, role): + self.client = role.client( + "support", region_name="us-east-1", config=Support._config + ) + + def get_support_level(self) -> SupportLevel: + """ + Gets the AWS Support Level of the current Account + based on the Role passed in during the init of the Support class. + + :returns: + SupportLevels Enum defining the level of AWS support. + + :raises: + ClientError + BotoCoreError + + """ + try: + severity_levels = self.client.get_severity_levels()["severityLevels"] + available_support_codes = [level["code"] for level in severity_levels] + + # See: https://aws.amazon.com/premiumsupport/plans/ for insights into the interpretation of + # the available support codes. + + if ( + "critical" in available_support_codes + ): # Business Critical System Down Severity + return SupportLevel.ENTERPRISE + if "urgent" in available_support_codes: # Production System Down Severity + return SupportLevel.BUSINESS + if "low" in available_support_codes: # System Impaired Severity + return SupportLevel.DEVELOPER + + return SupportLevel.BASIC + + except (ClientError, BotoCoreError) as e: + if e.response["Error"]["Code"] == "SubscriptionRequiredException": + LOGGER.info("Enterprise Support is not enabled") + return SupportLevel.BASIC + raise + + def set_support_level_for_account( + self, + account: dict, + account_id: str, + current_level: SupportLevel = SupportLevel.BASIC, + ): + """ + Sets the support level for the account. If the current_value is the same as the value in the instance + of the account Class it will not create a new ticket. + + Currently only supports "basic|enterprise" tiers. + + :param account: Instance of Account class + :param account_id: AWS Account ID of the account that will have support configured for it. + :param current_level: SupportLevel value that represents the current support tier of the account (Default: Basic) + :return: Void + :raises: ValueError if account.support_level is not a valid/supported SupportLevel. + """ + desired_level = SupportLevel(account.get("support_level", "basic")) + + if desired_level is current_level: + LOGGER.info( + f'Account {account.get("account_full_name")} ({account_id}) already has {desired_level.value} support enabled.' + ) + + elif desired_level is SupportLevel.ENTERPRISE: + LOGGER.info( + f'Enabling {desired_level.value} for Account {account.get("account_full_name")} ({account_id})' + ) + self._enable_support_for_account(account, account_id, desired_level) + + else: + LOGGER.error( + f"Invalid support tier configured: {desired_level.value}. " + f'Currently only "{SupportLevel.BASIC.value}" or "{SupportLevel.ENTERPRISE.value}" ' + "are accepted.", + exc_info=True, + ) + raise ValueError(f"Invalid Support Tier Value: {desired_level.value}") + + def _enable_support_for_account( + self, account: dict, account_id, desired_level: SupportLevel + ): + """ + Raises a support ticket in the organization root account, enabling support for the account specified + by account_id. + + :param account: Instance of Account class + :param account_id: AWS Account ID, of the account that will have support configured + :param desired_level: Desired Support Level + :return: Void + :raises: ClientError, BotoCoreError. + """ + try: + cc_email = account.get("email") + subject = ( + f"[ADF] Enable {desired_level.value} Support for account: {account_id}" + ) + body = ( + f"Hello, \n" + f'Can {desired_level.value} support be enabled on Account: {account_id} ({account.get("email")}) \n' + "Thank you!\n" + "(This ticket was raised automatically via ADF)" + ) + LOGGER.info( + f"Creating AWS Support ticket. {desired_level.value} Support for Account " + f'{account.get("account_full_name")}({account_id})' + ) + + response = self.client.create_case( + subject=subject, + serviceCode="account-management", + severityCode="low", + categoryCode="billing", + communicationBody=body, + ccEmailAddresses=[ + cc_email, + ], + language="en", + ) + + LOGGER.info( + f'AWS Support ticket: {response["caseId"]} ' + f"has been created. {desired_level.value} Support has " + f'been requested on Account {account.get("account_full_name")} ({account_id}). ' + f'{account.get("email")} has been CCd' + ) + + except (ClientError, BotoCoreError): + LOGGER.error( + f"Failed to enable {desired_level.value} support for account: " + f'{account.get("account_full_name")} ({account.get("alias", "")}): {account_id}', + exc_info=True, + ) + raise + + +def lambda_handler(event, _): + support = Support(boto3) + support.set_support_level_for_account(event, event.get("Id")) + return event diff --git a/src/lambda_codebase/account_processing/requirements.txt b/src/lambda_codebase/account_processing/requirements.txt new file mode 100644 index 000000000..97f04e731 --- /dev/null +++ b/src/lambda_codebase/account_processing/requirements.txt @@ -0,0 +1,3 @@ +pyyaml +wrapt==1.12 +aws-xray-sdk \ No newline at end of file diff --git a/src/lambda_codebase/account_processing/tests/__init__.py b/src/lambda_codebase/account_processing/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/lambda_codebase/account_processing/tests/test_account_alias.py b/src/lambda_codebase/account_processing/tests/test_account_alias.py new file mode 100644 index 000000000..e8ec67737 --- /dev/null +++ b/src/lambda_codebase/account_processing/tests/test_account_alias.py @@ -0,0 +1,21 @@ +""" +Tests the account alias configuration lambda +""" + +import boto3 +from botocore.stub import Stubber + +from ..configure_account_alias import create_account_alias + +# pylint: disable=W0106 +def test_account_alias(): + test_account = {"Id": 1234567890, "alias": "MyCoolAlias"} + iam_client = boto3.client("iam") + stubber = Stubber(iam_client) + create_alias_response = {} + stubber.add_response( + "create_account_alias", create_alias_response, {"AccountAlias": "MyCoolAlias"} + ), + stubber.activate() + response = create_account_alias(test_account, iam_client) + assert response == test_account diff --git a/src/lambda_codebase/account_processing/tests/test_account_creation.py b/src/lambda_codebase/account_processing/tests/test_account_creation.py new file mode 100644 index 000000000..3261d7bb4 --- /dev/null +++ b/src/lambda_codebase/account_processing/tests/test_account_creation.py @@ -0,0 +1,114 @@ +""" +Tests the account creation lambda +""" + +import unittest +import boto3 +from botocore.stub import Stubber +from ..create_account import create_account + + +# pylint: disable=W0106 +class SuccessTestCase(unittest.TestCase): + def test_account_creation(self): + test_account = { + "account_full_name": "ADF Test Creation Account", + "email": "test+account@domain.com", + } + iam_client = boto3.client("organizations") + stubber = Stubber(iam_client) + create_account_response = { + "CreateAccountStatus": {"State": "IN_PROGRESS", "Id": "1234567890"} + } + describe_account_response = { + "CreateAccountStatus": { + "State": "IN_PROGRESS", + "AccountId": "9087564231", + "Id": "1234567890", + } + } + describe_account_response_complete = { + "CreateAccountStatus": { + "State": "SUCCEEDED", + "AccountId": "9087564231", + "Id": "1234567890", + } + } + stubber.add_response( + "create_account", + create_account_response, + { + "Email": test_account.get("email"), + "AccountName": test_account.get("account_full_name"), + "RoleName": "OrganizationAccountAccessRole", + "IamUserAccessToBilling": "DENY", + }, + ), + stubber.add_response( + "describe_create_account_status", + describe_account_response, + {"CreateAccountRequestId": "1234567890"}, + ) + stubber.add_response( + "describe_create_account_status", + describe_account_response_complete, + {"CreateAccountRequestId": "1234567890"}, + ) + + stubber.activate() + response = create_account( + test_account, "OrganizationAccountAccessRole", iam_client + ) + self.assertDictEqual(response, test_account) + + +class FailuteTestCase(unittest.TestCase): + def test_account_creation_failure(self): + test_account = { + "account_full_name": "ADF Test Creation Account", + "email": "test+account@domain.com", + } + iam_client = boto3.client("organizations") + stubber = Stubber(iam_client) + create_account_response = { + "CreateAccountStatus": {"State": "IN_PROGRESS", "Id": "1234567890"} + } + describe_account_response = { + "CreateAccountStatus": { + "State": "IN_PROGRESS", + "AccountId": "9087564231", + "Id": "1234567890", + } + } + describe_account_response_complete = { + "CreateAccountStatus": { + "State": "FAILED", + "AccountId": "9087564231", + "Id": "1234567890", + "FailureReason": "ACCOUNT_LIMIT_EXCEEDED", + } + } + stubber.add_response( + "create_account", + create_account_response, + { + "Email": test_account.get("email"), + "AccountName": test_account.get("account_full_name"), + "RoleName": "OrganizationAccountAccessRole", + "IamUserAccessToBilling": "DENY", + }, + ), + stubber.add_response( + "describe_create_account_status", + describe_account_response, + {"CreateAccountRequestId": "1234567890"}, + ) + stubber.add_response( + "describe_create_account_status", + describe_account_response_complete, + {"CreateAccountRequestId": "1234567890"}, + ) + + stubber.activate() + with self.assertRaises(Exception): + create_account(test_account, "OrganizationAccountAccessRole", iam_client) diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/main.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/main.py deleted file mode 100755 index 415d4f996..000000000 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/main.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 - -"""Main -""" - -#!/usr/bin/env python3 - -import os -from concurrent.futures import ThreadPoolExecutor -import boto3 -import tenacity -from organizations import Organizations -from logger import configure_logger -from parameter_store import ParameterStore -from sts import STS -from src import read_config_files, delete_default_vpc, Support - - -LOGGER = configure_logger(__name__) -ACCOUNTS_FOLDER = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..', '..', 'adf-accounts')) - - -def main(): - accounts = read_config_files(ACCOUNTS_FOLDER) - if not bool(accounts): - LOGGER.info( - f"Found {len(accounts)} account(s) in configuration file(s). Account provisioning will not continue.") - return - LOGGER.info(f"Found {len(accounts)} account(s) in configuration file(s).") - organizations = Organizations(boto3) - support = Support(boto3) - all_accounts = organizations.get_accounts() - parameter_store = ParameterStore( - os.environ.get('AWS_REGION', 'us-east-1'), boto3) - adf_role_name = parameter_store.fetch_parameter( - 'cross_account_access_role') - for account in accounts: - try: - account_id = next( - acc["Id"] for acc in all_accounts if acc["Name"] == account.full_name) - except StopIteration: # If the account does not exist yet.. - account_id = None - create_or_update_account( - organizations, support, account, adf_role_name, account_id) - - -def create_or_update_account(org_session, support_session, account, adf_role_name, account_id=None): - """Creates or updates a single AWS account. - :param org_session: Instance of Organization class - :param account: Instance of Account class - """ - if not account_id: - LOGGER.info(f'Creating new account {account.full_name}') - account_id = org_session.create_account(account, adf_role_name) - # This only runs on account creation at the moment. - support_session.set_support_level_for_account(account, account_id) - - sts = STS() - role = sts.assume_cross_account_role( - 'arn:aws:iam::{0}:role/{1}'.format( - account_id, - adf_role_name - ), 'adf_account_provisioning' - ) - - LOGGER.info( - f'Ensuring account {account_id} (alias {account.alias}) is in OU {account.ou_path}') - org_session.move_account(account_id, account.ou_path) - if account.delete_default_vpc: - ec2_client = role.client('ec2') - all_regions = get_all_regions(ec2_client) - args = ( - (account_id, region, role) - for region in all_regions - ) - with ThreadPoolExecutor(max_workers=10) as executor: - for _ in executor.map(lambda f: schedule_delete_default_vpc(*f), args): - pass - - if account.alias: - LOGGER.info(f'Ensuring account alias for {account_id} of {account.alias}') - org_session.create_account_alias(account.alias, role) - - if account.tags: - LOGGER.info( - f'Ensuring tags exist for account {account_id}: {account.tags}') - org_session.create_account_tags(account_id, account.tags) - - -@tenacity.retry( - stop=tenacity.stop_after_attempt(9), - wait=tenacity.wait_random_exponential(), -) -def get_all_regions(ec2_client): - try: - all_regions = [ - region['RegionName'] - for region in ec2_client.describe_regions( - AllRegions=False, - Filters=[ - { - 'Name': 'opt-in-status', - 'Values': [ - 'opt-in-not-required', - ] - } - ] - )['Regions'] - ] - LOGGER.info(f'Regions are: {all_regions}') - return all_regions - except Exception as ce: - LOGGER.info('Failed to describe regions: %s, retrying...', ce) - raise - - -def schedule_delete_default_vpc(account_id, region, role): - """Schedule a delete_default_vpc on a thread - :param account_id: The account ID to remove the VPC from - :param org_session: The Organization class instance - :param region: The name of the region the VPC is resided - """ - ec2_client = role.client('ec2', region_name=region) - delete_default_vpc(ec2_client, account_id, region, role) - - -if __name__ == '__main__': - main() diff --git a/src/template.yml b/src/template.yml index dfe735ca3..45970d594 100644 --- a/src/template.yml +++ b/src/template.yml @@ -116,6 +116,15 @@ Resources: BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true + AccountProcessingApplication: + Type: AWS::Serverless::Application + Properties: + Location: account_processing.yml + Parameters: + LambdaLayer: !Ref LambdaLayerVersion + ADFVersion: !FindInMap ['Metadata', 'ADF', 'Version'] + OrganizationID: !GetAtt Organization.OrganizationId + CrossAccountAccessRoleName: !Ref CrossAccountAccessRoleName LambdaLayerVersion: Type: "AWS::Serverless::LayerVersion" Properties: @@ -483,6 +492,8 @@ Resources: Value: './adf-build/shared/python' - Name: S3_BUCKET Value: !Ref BootstrapTemplatesBucket + - Name: ACCOUNT_BUCKET + Value: !GetAtt AccountProcessingApplication.Outputs.Bucket - Name: MASTER_ACCOUNT_ID Value: !Ref AWS::AccountId - Name: DEPLOYMENT_ACCOUNT_BUCKET @@ -511,8 +522,8 @@ Resources: - sam package --output-template-file adf-bootstrap/deployment/global.yml --s3-prefix adf-bootstrap/deployment --s3-bucket $DEPLOYMENT_ACCOUNT_BUCKET - aws s3 sync ./adf-build/shared s3://$DEPLOYMENT_ACCOUNT_BUCKET/adf-build --quiet # Shared Modules to be used with AWS CodeBuild - aws s3 sync . s3://$S3_BUCKET --quiet --delete # Base Templates + - aws s3 sync ./adf-accounts s3://$ACCOUNT_BUCKET --quiet - python adf-build/main.py # Updates config, updates (or creates) base stacks. - - python adf-build/provisioner/main.py # Ensures/Creates AWS Accounts based on accounts definitions files. Type: CODEPIPELINE Tags: - Key: "Name"