├── .gitignore ├── ChildAccountCloudformation ├── account-iam-user.yaml └── cross-account-admin.yaml ├── LICENSE ├── README.md ├── TestEvents ├── create-account.json └── created-account-data.json ├── account-vending-machine.yaml ├── images ├── AccountVendingMachine.png ├── AccountVendingMachine_AccountStructure.png └── launch-stack.svg └── src ├── accountCreationTrigger.py ├── botoHelper.py ├── createAccount.py ├── createOU.py ├── debug.py ├── deployCloudFormation.py ├── getAccountCreateStatus.py ├── moveAccount.py ├── notifyAdmins.py ├── notifyOwner.py ├── sendErrorNotification.py └── storeAccountData.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /ChildAccountCloudformation/account-iam-user.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Creates an owner user in the new account 3 | 4 | Parameters: 5 | Username: 6 | Type: String 7 | Description: Username for the IAM user 8 | Password: 9 | Type: String 10 | NoEcho: true 11 | MinLength: 8 12 | Description: Password for the IAM user 13 | 14 | Resources: 15 | AccountOwnerIAMUser: 16 | Type: "AWS::IAM::User" 17 | Properties: 18 | UserName: !Ref Username 19 | Path: / 20 | LoginProfile: 21 | Password: !Ref Password 22 | PasswordResetRequired: true 23 | 24 | IAMAdminGroup: 25 | Type: "AWS::IAM::Group" 26 | Properties: 27 | ManagedPolicyArns: 28 | - "arn:aws:iam::aws:policy/AdministratorAccess" 29 | GroupName: Admins 30 | Path: / 31 | 32 | AssociateUserWithGroup: 33 | Type: "AWS::IAM::UserToGroupAddition" 34 | Properties: 35 | GroupName: !Ref IAMAdminGroup 36 | Users: 37 | - !Ref AccountOwnerIAMUser 38 | -------------------------------------------------------------------------------- /ChildAccountCloudformation/cross-account-admin.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Creates a cross account role with admin access. 3 | Parameters: 4 | RoleName: 5 | Type: String 6 | MaxLength: 64 7 | MinLength: 1 8 | AllowedPattern: "[\\w+=,.@-]+" 9 | Description: The name of the role to create 10 | ExternalID: 11 | Type: String 12 | Default: NO_VALUE 13 | Description: The External ID that will be required to assume the role. 14 | RequireMFA: 15 | Type: String 16 | Default: false 17 | AllowedValues: 18 | - true 19 | - false 20 | Description: Indicate if MFA need to be present to assume the role. 21 | OtherAccountNumber: 22 | Type: String 23 | MaxLength: 12 24 | MinLength: 12 25 | AllowedPattern: "[0-9]+" 26 | Description: The 12 digit AWS account number to grant access to. 27 | 28 | Conditions: 29 | ExternalIDSet: !And 30 | - !Not [!Equals [!Ref ExternalID, NO_VALUE]] 31 | - !Not [!Equals [!Ref ExternalID, ""]] 32 | RequireMFASet: !Equals [!Ref RequireMFA, "true"] 33 | 34 | Resources: 35 | CrossAccountRole: 36 | Type: "AWS::IAM::Role" 37 | Properties: 38 | RoleName: !Ref RoleName 39 | AssumeRolePolicyDocument: 40 | Version: 2012-10-17 41 | 42 | Statement: 43 | - Effect: Allow 44 | Condition: 45 | StringEquals: 46 | sts:ExternalId: !If 47 | - ExternalIDSet 48 | - !Ref ExternalID 49 | - !Ref AWS::NoValue 50 | Bool: 51 | aws:MultiFactorAuthPresent: !If 52 | - RequireMFASet 53 | - !Ref RequireMFA 54 | - !Ref AWS::NoValue 55 | 56 | Principal: 57 | AWS: !Sub "arn:aws:iam::${OtherAccountNumber}:root" 58 | Action: 59 | - sts:AssumeRole 60 | AdminPolicy: 61 | Type: "AWS::IAM::Policy" 62 | Properties: 63 | PolicyName: master-cross-account-admin 64 | PolicyDocument: 65 | Version: 2012-10-17 66 | Statement: 67 | - Effect: Allow 68 | Action: 69 | - "*" 70 | Resource: "*" 71 | Roles: 72 | - !Ref CrossAccountRole 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jimmy Dahlqvist 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Account Vending Machine 2 | 3 | This is a project to setup a serverless and dynamic Account Vending Machine for AWS accounts. 4 | 5 | ## Overview 6 | 7 | The solution interacts with AWS Organizations to create accounts and Organizational Units (OU). 8 | It uses Lambda and Step Functions to coordinate the different steps in the process. 9 | Amazon SNS and SES is used for notifications and Amazon S3 is used both for triggering the creation and store information about created accounts. 10 | 11 |  12 | 13 | ### Creation Steps 14 | 15 | 1. Creates a new Organization Unit, if it doesn't already exists, with the specified name. 16 | 2. Creates a new AWS account with the specified name. 17 | 3. Moves the new AWS account in under the OU. 18 | 4. Assumes the Organization Admin Role in the new account and deploys specified CloudFormation templates. In default setup and events that would be: 19 | 1. Create a cross account admin role with trust to the specified admin account number and with the specified name. 20 | 2. Create an IAM User with specified username and password, this user is considered the account owner. 21 | 5. Notifies the Organization admins and the account owner that the create process is completed. 22 | 23 | ## Setup 24 | 25 | The solution uses Simple Email Service (SES) to send e-mail to the IAM user that is considered the owner of the account. 26 | Therefor you must make sure both the e-mail address used as sender and all of the receiving addresses has been whitelisted in SES. 27 | Either by leaving the sandbox or verifying the entire domain. 28 | 29 | Deploy the CloudFormation template, account-vending-machine.yaml into the Organization master account. 30 | Specify the two parameters needed; the e-mail address that will be used as sender in SES and a unique identifier (Name) that will be appended to resources that need to be globally unique, like S3 buckets. 31 | Click on the `Launch Stack` button to start deployment. 32 | [](https://console.aws.amazon.com/cloudformation/home#/stacks/new?stackName=amv-infrastructure&templateURL=https://jimmyd-public-cfn-eu-west-1.s3-eu-west-1.amazonaws.com/account-vending-machine.yaml) 33 | 34 | Upload the two CloudFormation templates in the solution to the created S3 bucket for child account templates, bucket name can be found in outputs of the account-vending-machine.yaml deployment. 35 | 36 | ## Account creation process 37 | 38 | To create an account an json event file describing the account should be uploaded to the created S3 bucket. This will kickstart the entire process. 39 | The entire process is fully automatic and both the Organization Admin team and the account owner (IAM user) will be notified when the account is created. 40 | 41 | The solution will deploy CloudFormation templates into the created account to do basic setup. 42 | The solution provide two templates that will create an IAM user for the account owner (Admin) and setup cross account access for an Administrator role that can be used be the Organization Admin team. 43 | 44 |  45 | 46 | However the solution can deploy any number of CloudFormation templates, this is specified in the account creation json file. 47 | To deploy additional templates make sure they are present in the CloudFormation S3 bucket and add item to the _cfnTemplates_ array in the event json file. 48 | 49 | ## Account Json file 50 | 51 | Below is an example of an event file to create a new account. More data will be added to the event during create process. For CloudFormation template parameters use {{key-name}} to dynamically replace with values from the event, 52 | 53 | ```json 54 | { 55 | "accountName": "Test Account", 56 | "accountEmail": "awsaccount@example.com", 57 | "ouName": "TestAccounts", 58 | "iamUser": "iamuser@example.com", 59 | "iamPassword": "InitialPassword", 60 | "accountRole": "organization-account-role", 61 | "adminAccount": "123408753636", 62 | "adminAccountRole": "cross-account-admin-role", 63 | "cfnTemplates": [ 64 | { 65 | "templateName": "account-iam-user.yaml", 66 | "stackName": "avm-owning-iam-user", 67 | "parameters": [ 68 | { 69 | "key": "Username", 70 | "value": "{{iamUser}}" 71 | }, 72 | { 73 | "key": "Password", 74 | "value": "{{iamPassword}}" 75 | } 76 | ] 77 | }, 78 | { 79 | "templateName": "cross-account-admin.yaml", 80 | "stackName": "avm-cross-account-admin", 81 | "parameters": [ 82 | { 83 | "key": "RoleName", 84 | "value": "{{adminAccountRole}}" 85 | }, 86 | { 87 | "key": "RequireMFA", 88 | "value": "true" 89 | }, 90 | { 91 | "key": "OtherAccountNumber", 92 | "value": "{{adminAccount}}" 93 | } 94 | ] 95 | } 96 | ] 97 | } 98 | ``` 99 | 100 | The following data will be added to the event during the create process, prior to CloudFormation deployment. 101 | 102 | ```json 103 | { 104 | "rootOuId": "r-xxxx", 105 | "ouId": "ou-xxxx-yyyyyyyy", 106 | "accountRequestId": "car-hgdjagdgjdgsdh", 107 | "createAccountStatus": "STATUS", 108 | "accountId": "4963463846832" 109 | } 110 | ``` 111 | 112 | 113 | ## Credits 114 | Inspired by the [AWS Blog Post](https://aws.amazon.com/blogs/mt/automate-account-creation-and-resource-provisioning-using-aws-service-catalog-aws-organizations-and-aws-lambda/) -------------------------------------------------------------------------------- /TestEvents/create-account.json: -------------------------------------------------------------------------------- 1 | { 2 | "accountName": "Test Account", 3 | "accountEmail": "awsaccount@example.com", 4 | "ouName": "TestAccounts", 5 | "iamUser": "iamuser@example.com", 6 | "iamPassword": "InitialPassword", 7 | "accountRole": "organization-account-role", 8 | "adminAccount": "123408753636", 9 | "adminAccountRole": "cross-account-admin-role", 10 | "cfnTemplates": [ 11 | { 12 | "templateName": "account-iam-user.yaml", 13 | "stackName": "avm-owning-iam-user", 14 | "parameters": [ 15 | { 16 | "key": "Username", 17 | "value": "{{iamUser}}" 18 | }, 19 | { 20 | "key": "Password", 21 | "value": "{{iamPassword}}" 22 | } 23 | ] 24 | }, 25 | { 26 | "templateName": "cross-account-admin.yaml", 27 | "stackName": "avm-cross-account-admin", 28 | "parameters": [ 29 | { 30 | "key": "RoleName", 31 | "value": "{{adminAccountRole}}" 32 | }, 33 | { 34 | "key": "RequireMFA", 35 | "value": "true" 36 | }, 37 | { 38 | "key": "OtherAccountNumber", 39 | "value": "{{adminAccount}}" 40 | } 41 | ] 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /TestEvents/created-account-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "accountName": "Test Account", 3 | "accountEmail": "awsaccount@example.com", 4 | "ouName": "TestAccounts", 5 | "iamUser": "iamuser@example.com", 6 | "iamPassword": "InitialPassword", 7 | "accountRole": "organization-account-role", 8 | "adminAccount": "123408753636", 9 | "adminAccountRole": "cross-account-admin-role", 10 | "rootOuId": "r-xxxx", 11 | "ouId": "ou-xxxx-yyyyyyyy", 12 | "accountRequestId": "car-hgdjagdgjdgsdh", 13 | "createAccountStatus": "STATUS", 14 | "accountId": "4963463846832", 15 | "cfnTemplates": [ 16 | { 17 | "templateName": "account-iam-user.yaml", 18 | "stackName": "avm-owning-iam-user", 19 | "parameters": [ 20 | { 21 | "key": "Username", 22 | "value": "{{iamUser}}" 23 | }, 24 | { 25 | "key": "Password", 26 | "value": "{{iamPassword}}" 27 | } 28 | ] 29 | }, 30 | { 31 | "templateName": "cross-account-admin.yaml", 32 | "stackName": "avm-cross-account-admin", 33 | "parameters": [ 34 | { 35 | "key": "RoleName", 36 | "value": "{{adminAccountRole}}" 37 | }, 38 | { 39 | "key": "RequireMFA", 40 | "value": "true" 41 | }, 42 | { 43 | "key": "OtherAccountNumber", 44 | "value": "{{adminAccount}}" 45 | } 46 | ] 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /account-vending-machine.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Account Vending Machine Account Creation Infrastructure 3 | Transform: "AWS::Serverless-2016-10-31" 4 | Parameters: 5 | EmailSender: 6 | Type: String 7 | Description: The e-mail address to set as sender when sending information about IAM user via e-mail. This address must be verified by SES. 8 | Name: 9 | Type: String 10 | Description: A unique name that will be appended to resources that have a global unique name, e.g. S3, to avoid collisions. 11 | 12 | Resources: 13 | # Common IAM Policies 14 | CloudWatchLogsPolicy: 15 | Type: "AWS::IAM::Policy" 16 | Properties: 17 | PolicyName: amvCloudWatchPolicy 18 | PolicyDocument: 19 | Version: 2012-10-17 20 | Statement: 21 | - Effect: Allow 22 | Action: 23 | - logs:* 24 | Resource: "arn:aws:logs:*:*:*" 25 | Roles: 26 | - !Ref AVMCreateOuFunctionRole 27 | - !Ref AVMCreateAccountFunctionRole 28 | - !Ref AVMGetAccountStatusFunctionRole 29 | - !Ref AVMMoveAccountFunctionRole 30 | - !Ref AMVSendErrorNotificationFunctionRole 31 | - !Ref AMVDeployCloudFormationFunctionRole 32 | - !Ref AVMUnsubscribeMarketingFunctionRole 33 | - !Ref AVMNotifyAdminsFunctionRole 34 | - !Ref AVMNotifyOwnerFunctionRole 35 | - !Ref AMVStoreAccountDataFunctionRole 36 | - !Ref AVMAccountCreationTriggerFunctionRole 37 | 38 | # Lambda function for creating a new OU. 39 | AVMCreateOuFunctionRole: 40 | Type: "AWS::IAM::Role" 41 | Properties: 42 | AssumeRolePolicyDocument: 43 | Version: 2012-10-17 44 | Statement: 45 | - Effect: Allow 46 | Principal: 47 | Service: 48 | - lambda.amazonaws.com 49 | Action: 50 | - sts:AssumeRole 51 | AVMCreateOuFunctionPolicy: 52 | Type: "AWS::IAM::Policy" 53 | Properties: 54 | PolicyName: amvCreateOu 55 | PolicyDocument: 56 | Version: 2012-10-17 57 | Statement: 58 | - Effect: Allow 59 | Action: 60 | - sts:AssumeRole 61 | Resource: "*" 62 | - Effect: Allow 63 | Action: 64 | - organizations:* 65 | Resource: "*" 66 | Roles: 67 | - !Ref AVMCreateOuFunctionRole 68 | AVMCreateOuFunction: 69 | Type: AWS::Serverless::Function 70 | Properties: 71 | FunctionName: amv-create-new-organization-unit 72 | Runtime: python3.6 73 | MemorySize: 256 74 | Timeout: 120 75 | CodeUri: ./src 76 | Handler: createOU.handler 77 | Role: !GetAtt AVMCreateOuFunctionRole.Arn 78 | 79 | # Lambda function fo creating a new account. 80 | AVMCreateAccountFunctionRole: 81 | Type: "AWS::IAM::Role" 82 | Properties: 83 | AssumeRolePolicyDocument: 84 | Version: 2012-10-17 85 | Statement: 86 | - Effect: Allow 87 | Principal: 88 | Service: 89 | - lambda.amazonaws.com 90 | Action: 91 | - sts:AssumeRole 92 | AVMCreateAccountFunctionPolicy: 93 | Type: "AWS::IAM::Policy" 94 | Properties: 95 | PolicyName: amvCreateAccount 96 | PolicyDocument: 97 | Version: 2012-10-17 98 | Statement: 99 | - Effect: Allow 100 | Action: 101 | - organizations:* 102 | Resource: "*" 103 | Roles: 104 | - !Ref AVMCreateAccountFunctionRole 105 | AVMCreateAccountFunction: 106 | Type: AWS::Serverless::Function 107 | Properties: 108 | FunctionName: amv-create-new-account 109 | Runtime: python3.6 110 | MemorySize: 256 111 | Timeout: 240 112 | CodeUri: ./src 113 | Handler: createAccount.handler 114 | Role: !GetAtt AVMCreateAccountFunctionRole.Arn 115 | 116 | # Lambda function for getting the account creation status 117 | AVMGetAccountStatusFunctionRole: 118 | Type: "AWS::IAM::Role" 119 | Properties: 120 | AssumeRolePolicyDocument: 121 | Version: 2012-10-17 122 | Statement: 123 | - Effect: Allow 124 | Principal: 125 | Service: 126 | - lambda.amazonaws.com 127 | Action: 128 | - sts:AssumeRole 129 | AVMGetAccountStatusFunctionPolicy: 130 | Type: "AWS::IAM::Policy" 131 | Properties: 132 | PolicyName: amvGetAccountStatus 133 | PolicyDocument: 134 | Version: 2012-10-17 135 | Statement: 136 | - Effect: Allow 137 | Action: 138 | - organizations:* 139 | Resource: "*" 140 | Roles: 141 | - !Ref AVMGetAccountStatusFunctionRole 142 | AVMGetAccountStatusFunction: 143 | Type: AWS::Serverless::Function 144 | Properties: 145 | FunctionName: amv-get-account-creation-status 146 | Runtime: python3.6 147 | MemorySize: 256 148 | Timeout: 30 149 | CodeUri: ./src 150 | Handler: getAccountCreateStatus.handler 151 | Role: !GetAtt AVMGetAccountStatusFunctionRole.Arn 152 | 153 | # Lambda function for moving account to correct OU 154 | AVMMoveAccountFunctionRole: 155 | Type: "AWS::IAM::Role" 156 | Properties: 157 | AssumeRolePolicyDocument: 158 | Version: 2012-10-17 159 | Statement: 160 | - Effect: Allow 161 | Principal: 162 | Service: 163 | - lambda.amazonaws.com 164 | Action: 165 | - sts:AssumeRole 166 | AVMMoveAccountFunctionPolicy: 167 | Type: "AWS::IAM::Policy" 168 | Properties: 169 | PolicyName: amvMoveAccount 170 | PolicyDocument: 171 | Version: 2012-10-17 172 | Statement: 173 | - Effect: Allow 174 | Action: 175 | - organizations:* 176 | Resource: "*" 177 | Roles: 178 | - !Ref AVMMoveAccountFunctionRole 179 | AVMMoveAccountFunction: 180 | Type: AWS::Serverless::Function 181 | Properties: 182 | FunctionName: amv-move-account 183 | Runtime: python3.6 184 | MemorySize: 256 185 | Timeout: 30 186 | CodeUri: ./src 187 | Handler: moveAccount.handler 188 | Role: !GetAtt AVMMoveAccountFunctionRole.Arn 189 | 190 | # Lambda function for for deploying cloudformation. 191 | AMVDeployCloudFormationFunctionRole: 192 | Type: "AWS::IAM::Role" 193 | Properties: 194 | AssumeRolePolicyDocument: 195 | Version: 2012-10-17 196 | Statement: 197 | - Effect: Allow 198 | Principal: 199 | Service: 200 | - lambda.amazonaws.com 201 | Action: 202 | - sts:AssumeRole 203 | AMVDeployCloudFormationFunctionPolicy: 204 | Type: "AWS::IAM::Policy" 205 | Properties: 206 | PolicyName: amvDeployCloudFormation 207 | PolicyDocument: 208 | Version: 2012-10-17 209 | Statement: 210 | - Effect: Allow 211 | Action: 212 | - sts:AssumeRole 213 | Resource: "*" 214 | - Effect: Allow 215 | Action: 216 | - s3:* 217 | Resource: !Sub "arn:aws:s3:::${AMVChildAccountCloudFormationBucket}/*" 218 | Roles: 219 | - !Ref AMVDeployCloudFormationFunctionRole 220 | AMVDeployCloudFormationFunction: 221 | Type: AWS::Serverless::Function 222 | Properties: 223 | FunctionName: amv-deploy-cloudformation 224 | Runtime: python3.6 225 | MemorySize: 256 226 | Timeout: 600 227 | CodeUri: ./src 228 | Handler: deployCloudFormation.handler 229 | Role: !GetAtt AMVDeployCloudFormationFunctionRole.Arn 230 | Environment: 231 | Variables: 232 | CLOUDFORMATION_TEMPLATE_BUCKET: !Ref AMVChildAccountCloudFormationBucket 233 | 234 | # Infrastructure for notifying errors 235 | # Create SNS topic that Admins Can subscribe to 236 | ErrorNotificationTopic: 237 | Type: AWS::SNS::Topic 238 | Properties: 239 | TopicName: amv-error-notifications 240 | # Lambda function for sending error notifications. 241 | AMVSendErrorNotificationFunctionRole: 242 | Type: "AWS::IAM::Role" 243 | Properties: 244 | AssumeRolePolicyDocument: 245 | Version: 2012-10-17 246 | Statement: 247 | - Effect: Allow 248 | Principal: 249 | Service: 250 | - lambda.amazonaws.com 251 | Action: 252 | - sts:AssumeRole 253 | AMVSendErrorNotificationFunctionPolicy: 254 | Type: "AWS::IAM::Policy" 255 | Properties: 256 | PolicyName: amvSendErrorNotification 257 | PolicyDocument: 258 | Version: 2012-10-17 259 | Statement: 260 | - Effect: Allow 261 | Action: 262 | - sns:publish 263 | Resource: !Ref ErrorNotificationTopic 264 | Roles: 265 | - !Ref AMVSendErrorNotificationFunctionRole 266 | AMVSendErrorNotificationFunction: 267 | Type: AWS::Serverless::Function 268 | Properties: 269 | FunctionName: amv-send-error-notification 270 | Runtime: python3.6 271 | MemorySize: 256 272 | Timeout: 30 273 | CodeUri: ./src 274 | Handler: sendErrorNotification.handler 275 | Role: !GetAtt AMVSendErrorNotificationFunctionRole.Arn 276 | Environment: 277 | Variables: 278 | ERROR_SNS_TOPIC: !Ref ErrorNotificationTopic 279 | 280 | # Lambda function for unsubscribe from marketing. 281 | AVMUnsubscribeMarketingFunctionRole: 282 | Type: "AWS::IAM::Role" 283 | Properties: 284 | AssumeRolePolicyDocument: 285 | Version: 2012-10-17 286 | Statement: 287 | - Effect: Allow 288 | Principal: 289 | Service: 290 | - lambda.amazonaws.com 291 | Action: 292 | - sts:AssumeRole 293 | AVMUnsubscribeMarketingFunctionPolicy: 294 | Type: "AWS::IAM::Policy" 295 | Properties: 296 | PolicyName: amvUnsubscribeMarketing 297 | PolicyDocument: 298 | Version: 2012-10-17 299 | Statement: 300 | - Effect: Allow 301 | Action: 302 | - sns:* 303 | Resource: "*" 304 | Roles: 305 | - !Ref AMVSendErrorNotificationFunctionRole 306 | AVMUnsubscribeMarketingFunction: 307 | Type: AWS::Serverless::Function 308 | Properties: 309 | FunctionName: amv-unsubscribe-marketing-emails 310 | Runtime: python3.6 311 | MemorySize: 256 312 | Timeout: 30 313 | CodeUri: ./src 314 | Handler: unsubscribeMarketing.handler 315 | Role: !GetAtt AVMUnsubscribeMarketingFunctionRole.Arn 316 | 317 | # Infrastructure for notifying Admins 318 | # Create SNS topic that Admins Can subscribe to 319 | AdminNotificationTopic: 320 | Type: AWS::SNS::Topic 321 | Properties: 322 | TopicName: amv-admin-notifications 323 | 324 | # Lambda function for notify admins about creation. 325 | AVMNotifyAdminsFunctionRole: 326 | Type: "AWS::IAM::Role" 327 | Properties: 328 | AssumeRolePolicyDocument: 329 | Version: 2012-10-17 330 | Statement: 331 | - Effect: Allow 332 | Principal: 333 | Service: 334 | - lambda.amazonaws.com 335 | Action: 336 | - sts:AssumeRole 337 | AVMNotifyAdminsFunctionPolicy: 338 | Type: "AWS::IAM::Policy" 339 | Properties: 340 | PolicyName: amvNotifyAdmins 341 | PolicyDocument: 342 | Version: 2012-10-17 343 | Statement: 344 | - Effect: Allow 345 | Action: 346 | - sns:Publish 347 | Resource: !Ref AdminNotificationTopic 348 | Roles: 349 | - !Ref AVMNotifyAdminsFunctionRole 350 | AVMNotifyAdminsFunction: 351 | Type: AWS::Serverless::Function 352 | Properties: 353 | FunctionName: amv-notify-admins 354 | Runtime: python3.6 355 | MemorySize: 256 356 | Timeout: 30 357 | CodeUri: ./src 358 | Handler: notifyAdmins.handler 359 | Role: !GetAtt AVMNotifyAdminsFunctionRole.Arn 360 | Environment: 361 | Variables: 362 | ADMIN_SNS_TOPIC: !Ref AdminNotificationTopic 363 | 364 | # Lambda function for notify the new account Owner 365 | AVMNotifyOwnerFunctionRole: 366 | Type: "AWS::IAM::Role" 367 | Properties: 368 | AssumeRolePolicyDocument: 369 | Version: 2012-10-17 370 | Statement: 371 | - Effect: Allow 372 | Principal: 373 | Service: 374 | - lambda.amazonaws.com 375 | Action: 376 | - sts:AssumeRole 377 | AVMNotifyOwnerFunctionPolicy: 378 | Type: "AWS::IAM::Policy" 379 | Properties: 380 | PolicyName: amvNotifyOwner 381 | PolicyDocument: 382 | Version: 2012-10-17 383 | Statement: 384 | - Effect: Allow 385 | Action: 386 | - ses:* 387 | Resource: "*" 388 | Roles: 389 | - !Ref AVMNotifyOwnerFunctionRole 390 | AVMNotifyOwnerFunction: 391 | Type: AWS::Serverless::Function 392 | Properties: 393 | FunctionName: amv-notify-owner 394 | Runtime: python3.6 395 | MemorySize: 256 396 | Timeout: 30 397 | CodeUri: ./src 398 | Handler: notifyOwner.handler 399 | Role: !GetAtt AVMNotifyOwnerFunctionRole.Arn 400 | Environment: 401 | Variables: 402 | SENDER: !Ref EmailSender 403 | 404 | # Lambda function for storing the account data 405 | AMVStoreAccountDataFunctionRole: 406 | Type: "AWS::IAM::Role" 407 | Properties: 408 | AssumeRolePolicyDocument: 409 | Version: 2012-10-17 410 | Statement: 411 | - Effect: Allow 412 | Principal: 413 | Service: 414 | - lambda.amazonaws.com 415 | Action: 416 | - sts:AssumeRole 417 | AMVStoreAccountDataFunctionPolicy: 418 | Type: "AWS::IAM::Policy" 419 | Properties: 420 | PolicyName: amvStoreAccountData 421 | PolicyDocument: 422 | Version: 2012-10-17 423 | Statement: 424 | - Effect: Allow 425 | Action: 426 | - s3:* 427 | Resource: !Sub "arn:aws:s3:::${AMVAccountOutputBucket}/*" 428 | Roles: 429 | - !Ref AMVStoreAccountDataFunctionRole 430 | AMVStoreAccountDataFunction: 431 | Type: AWS::Serverless::Function 432 | Properties: 433 | FunctionName: amv-store-account-data 434 | Runtime: python3.6 435 | MemorySize: 256 436 | Timeout: 30 437 | CodeUri: ./src 438 | Handler: storeAccountData.handler 439 | Role: !GetAtt AMVStoreAccountDataFunctionRole.Arn 440 | Environment: 441 | Variables: 442 | ACCOUNT_DATA_BUCKET: !Ref AMVAccountOutputBucket 443 | 444 | # Lambda function for triggering the account creation process. 445 | AVMAccountCreationTriggerFunction: 446 | Type: AWS::Serverless::Function 447 | Properties: 448 | FunctionName: amv-s3-create-account-trigger 449 | Runtime: python3.6 450 | MemorySize: 256 451 | Timeout: 30 452 | CodeUri: ./src 453 | Handler: accountCreationTrigger.handler 454 | Role: !GetAtt AVMAccountCreationTriggerFunctionRole.Arn 455 | Events: 456 | S3Trigger: 457 | Type: S3 458 | Properties: 459 | Bucket: !Ref AMVAccountTriggerBucket 460 | Events: s3:ObjectCreated:* 461 | Environment: 462 | Variables: 463 | ACCOUNT_CREATOR_STEPFUNCTION: !Ref AMVStateMachine 464 | AVMAccountCreationTriggerFunctionRole: 465 | Type: "AWS::IAM::Role" 466 | Properties: 467 | AssumeRolePolicyDocument: 468 | Version: 2012-10-17 469 | Statement: 470 | - Effect: Allow 471 | Principal: 472 | Service: 473 | - lambda.amazonaws.com 474 | Action: 475 | - sts:AssumeRole 476 | AVMAccountCreationTriggerFunctionPolicy: 477 | Type: "AWS::IAM::Policy" 478 | Properties: 479 | PolicyName: amvTriggerAccountCreationLambda 480 | PolicyDocument: 481 | Version: 2012-10-17 482 | Statement: 483 | - Effect: Allow 484 | Action: 485 | - states:StartExecution 486 | Resource: !Ref AMVStateMachine 487 | - Effect: Allow 488 | Action: 489 | - s3:* 490 | Resource: !Sub arn:aws:s3:::${AMVAccountTriggerBucket}/* 491 | Roles: 492 | - !Ref AVMAccountCreationTriggerFunctionRole 493 | 494 | # Account Vending Machine Stepfunction 495 | AMVStateMachine: 496 | Type: "AWS::StepFunctions::StateMachine" 497 | Properties: 498 | StateMachineName: amv-provision-account 499 | RoleArn: !GetAtt AMVStateMachineRole.Arn 500 | DefinitionString: !Sub |- 501 | { 502 | "StartAt": "Create", 503 | "States": { 504 | "Create": { 505 | "Type": "Parallel", 506 | "Branches": [ 507 | { 508 | "StartAt": "CreateOrganizationUnit", 509 | "States": { 510 | "CreateOrganizationUnit": { 511 | "Type": "Task", 512 | "Resource": "${AVMCreateOuFunction.Arn}", 513 | "Next": "CreateAccount" 514 | }, 515 | "CreateAccount": { 516 | "Type": "Task", 517 | "Resource": "${AVMCreateAccountFunction.Arn}", 518 | "Next": "WaitForAccountCreation" 519 | }, 520 | "WaitForAccountCreation": { 521 | "Type": "Wait", 522 | "Seconds": 180, 523 | "Next": "GetAccountCreationStatus" 524 | }, 525 | "GetAccountCreationStatus": { 526 | "Type": "Task", 527 | "Resource": "${AVMGetAccountStatusFunction.Arn}", 528 | "Next": "CheckAccountStatus" 529 | }, 530 | "CheckAccountStatus": { 531 | "Type": "Choice", 532 | "Choices": [ 533 | { 534 | "Variable": "$.createAccountStatus", 535 | "StringEquals": "IN_PROGRESS", 536 | "Next": "WaitForAccountCreation" 537 | }, 538 | { 539 | "Variable": "$.createAccountStatus", 540 | "StringEquals": "FAILED", 541 | "Next": "AccountCreateFailedState" 542 | }, 543 | { 544 | "Variable": "$.createAccountStatus", 545 | "StringEquals": "SUCCEEDED", 546 | "Next": "MoveAccount" 547 | } 548 | ], 549 | "Default": "MoveAccount" 550 | }, 551 | "MoveAccount": { 552 | "Type": "Task", 553 | "Resource": "${AVMMoveAccountFunction.Arn}", 554 | "Retry": [ { 555 | "ErrorEquals": [ "States.ALL" ], 556 | "IntervalSeconds": 5, 557 | "MaxAttempts": 3, 558 | "BackoffRate": 1.5 559 | } ], 560 | "Next": "DeployCloudFormation" 561 | }, 562 | "DeployCloudFormation": { 563 | "Type": "Task", 564 | "Resource": "${AMVDeployCloudFormationFunction.Arn}", 565 | "Retry": [ { 566 | "ErrorEquals": [ "States.ALL" ], 567 | "IntervalSeconds": 10, 568 | "MaxAttempts": 3, 569 | "BackoffRate": 1.5 570 | } ], 571 | "Next": "StoreAccountInformation" 572 | }, 573 | "StoreAccountInformation": { 574 | "Type": "Task", 575 | "Resource": "${AMVStoreAccountDataFunction.Arn}", 576 | "End": true 577 | }, 578 | "AccountCreateFailedState": { 579 | "Type": "Fail", 580 | "Cause": "Account creation failed." 581 | } 582 | } 583 | } 584 | ], 585 | "Catch": [ { 586 | "ErrorEquals": ["States.ALL"], 587 | "Next": "NotifyError" 588 | } ], 589 | "Next": "HandleEmails" 590 | }, 591 | "HandleEmails": { 592 | "Type": "Parallel", 593 | "Branches": [ 594 | { 595 | "StartAt": "NotifyAdmins", 596 | "States": { 597 | "NotifyAdmins": { 598 | "Type": "Task", 599 | "Resource": "${AVMNotifyAdminsFunction.Arn}", 600 | "End": true 601 | } 602 | } 603 | }, 604 | { 605 | "StartAt": "NotifyOwner", 606 | "States": { 607 | "NotifyOwner": { 608 | "Type": "Task", 609 | "Resource": "${AVMNotifyOwnerFunction.Arn}", 610 | "End": true 611 | } 612 | } 613 | } 614 | ], 615 | "Catch": [ { 616 | "ErrorEquals": ["States.ALL"], 617 | "Next": "NotifyError" 618 | } ], 619 | "End": true 620 | }, 621 | "NotifyError": { 622 | "Type": "Task", 623 | "Resource": "${AMVSendErrorNotificationFunction.Arn}", 624 | "End": true 625 | } 626 | } 627 | } 628 | AMVStateMachineRole: 629 | Type: "AWS::IAM::Role" 630 | Properties: 631 | AssumeRolePolicyDocument: 632 | Version: "2012-10-17" 633 | Statement: 634 | - Effect: "Allow" 635 | Principal: 636 | Service: 637 | - "states.amazonaws.com" 638 | Action: 639 | - "sts:AssumeRole" 640 | Policies: 641 | - PolicyName: amvCreateAccountStateMachine 642 | PolicyDocument: 643 | Version: "2012-10-17" 644 | Statement: 645 | - Effect: "Allow" 646 | Action: "lambda:*" 647 | Resource: 648 | - !GetAtt AVMCreateOuFunction.Arn 649 | - !GetAtt AVMCreateAccountFunction.Arn 650 | - !GetAtt AVMGetAccountStatusFunction.Arn 651 | - !GetAtt AVMMoveAccountFunction.Arn 652 | - !GetAtt AMVDeployCloudFormationFunction.Arn 653 | - !GetAtt AMVSendErrorNotificationFunction.Arn 654 | - !GetAtt AVMUnsubscribeMarketingFunction.Arn 655 | - !GetAtt AVMNotifyAdminsFunction.Arn 656 | - !GetAtt AVMNotifyOwnerFunction.Arn 657 | - !GetAtt AMVStoreAccountDataFunction.Arn 658 | 659 | AMVChildAccountCloudFormationBucket: 660 | Type: AWS::S3::Bucket 661 | Properties: 662 | BucketName: !Sub avm-child-accounts-cloudformation-${Name}-${AWS::Region} 663 | VersioningConfiguration: 664 | Status: Enabled 665 | BucketEncryption: 666 | ServerSideEncryptionConfiguration: 667 | - ServerSideEncryptionByDefault: 668 | SSEAlgorithm: AES256 669 | 670 | AMVAccountTriggerBucket: 671 | Type: AWS::S3::Bucket 672 | Properties: 673 | BucketName: !Sub avm-trigger-new-account-${Name}-${AWS::Region} 674 | BucketEncryption: 675 | ServerSideEncryptionConfiguration: 676 | - ServerSideEncryptionByDefault: 677 | SSEAlgorithm: AES256 678 | 679 | AMVAccountOutputBucket: 680 | Type: AWS::S3::Bucket 681 | Properties: 682 | BucketName: !Sub avm-created-accounts-${Name}-${AWS::Region} 683 | VersioningConfiguration: 684 | Status: Enabled 685 | BucketEncryption: 686 | ServerSideEncryptionConfiguration: 687 | - ServerSideEncryptionByDefault: 688 | SSEAlgorithm: AES256 689 | 690 | Outputs: 691 | AMVChildAccountCloudFormationBucket: 692 | Description: The S3 bucket that will hold the CloudFormation templates to deploy to child accounts. 693 | Export: 694 | Name: !Sub ${AWS::StackName}:child-cloudformation-bucket 695 | Value: !Ref AMVChildAccountCloudFormationBucket 696 | -------------------------------------------------------------------------------- /images/AccountVendingMachine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JimmyDqv/AWS-Account-Vending-Machine/713c353773d11eca412c420ebdbd317fdcf016d9/images/AccountVendingMachine.png -------------------------------------------------------------------------------- /images/AccountVendingMachine_AccountStructure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JimmyDqv/AWS-Account-Vending-Machine/713c353773d11eca412c420ebdbd317fdcf016d9/images/AccountVendingMachine_AccountStructure.png -------------------------------------------------------------------------------- /images/launch-stack.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/accountCreationTrigger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import boto3 4 | import botocore 5 | import json 6 | import time 7 | from debug import debug_print 8 | from debug import error_print 9 | from botoHelper import get_boto_client 10 | 11 | 12 | def handler(event, context): 13 | debug_print(json.dumps(event, indent=2)) 14 | s3_event = event["Records"][0]["s3"] 15 | s3_bucket = s3_event["bucket"]["name"] 16 | s3_object = s3_event["object"]["key"] 17 | 18 | client = get_boto_client("s3") 19 | response = client.get_object(Bucket=s3_bucket, Key=s3_object) 20 | content = json.loads(response['Body'].read().decode('utf-8')) 21 | debug_print(json.dumps(content, indent=2)) 22 | 23 | step_function_arn = os.environ["ACCOUNT_CREATOR_STEPFUNCTION"] 24 | invoke_statemachine(step_function_arn, content) 25 | 26 | 27 | def invoke_statemachine(arn, input): 28 | client = get_boto_client("stepfunctions") 29 | account_name = input.get("accountName") 30 | response = client.start_execution( 31 | stateMachineArn=arn, 32 | name="{}-creation-{}".format(account_name, time.time()), 33 | input=json.dumps(input) 34 | ) 35 | debug_print(response) 36 | return(response) 37 | -------------------------------------------------------------------------------- /src/botoHelper.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | 4 | def get_boto_client(service, access_key=None, secret_access_key=None, session_token=None): 5 | client = boto3.client( 6 | service, 7 | aws_access_key_id=access_key, 8 | aws_secret_access_key=secret_access_key, 9 | aws_session_token=session_token 10 | ) 11 | return client 12 | -------------------------------------------------------------------------------- /src/createAccount.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import boto3 4 | import botocore 5 | import json 6 | import time 7 | from debug import debug_print 8 | from debug import error_print 9 | from botoHelper import get_boto_client 10 | 11 | 12 | def handler(event, context): 13 | debug_print(json.dumps(event, indent=2)) 14 | return main(event) 15 | 16 | 17 | def main(event): 18 | account_name = event.get("accountName") 19 | account_email = event.get("accountEmail") 20 | account_role = event.get("accountRole") 21 | 22 | account_request_id = create_account( 23 | account_name, account_email, account_role) 24 | 25 | event["accountRequestId"] = account_request_id 26 | return event 27 | 28 | 29 | def create_account(name, email, role): 30 | account_request_id = None 31 | client = get_boto_client('organizations') 32 | 33 | debug_print( 34 | "Creating account with {} name and e-mail {}".format(name, email)) 35 | response = client.create_account(Email=email, AccountName=name, 36 | RoleName=role, 37 | IamUserAccessToBilling="ALLOW") 38 | account_request_id = response['CreateAccountStatus']['Id'] 39 | 40 | return account_request_id 41 | -------------------------------------------------------------------------------- /src/createOU.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import boto3 4 | import botocore 5 | import json 6 | import time 7 | from debug import debug_print 8 | from botoHelper import get_boto_client 9 | 10 | 11 | def handler(event, context): 12 | debug_print(json.dumps(event, indent=2)) 13 | return main(event) 14 | 15 | 16 | def main(event): 17 | ou_name = event.get("ouName") 18 | debug_print("OU Name: {}".format(ou_name)) 19 | root_ou_id = get_organization_root_id() 20 | debug_print("Root OU ID: {}".format(root_ou_id)) 21 | new_ou_id = create_organizational_unit(root_ou_id, ou_name) 22 | debug_print("New OU ID: {}".format(new_ou_id)) 23 | 24 | event["rootOuId"] = root_ou_id 25 | event["ouId"] = new_ou_id 26 | 27 | return event 28 | 29 | 30 | def get_organization_root_id(): 31 | client = get_boto_client("organizations") 32 | response = client.list_roots() 33 | # debug_print(response) 34 | root_id = response['Roots'][0]['Id'] 35 | 36 | return root_id 37 | 38 | 39 | def create_organizational_unit(root_ou_id, ou_name): 40 | debug_print("Creating new OU if needed with name {}".format(ou_name)) 41 | 42 | ou_id = get_ou_id_for_name(root_ou_id, ou_name) 43 | if ou_id == None: 44 | client = get_boto_client("organizations") 45 | response = client.create_organizational_unit( 46 | ParentId=root_ou_id, 47 | Name=ou_name 48 | ) 49 | new_ou_id = response["OrganizationalUnit"]["Id"] 50 | debug_print("Created OU with ID: {}".format(new_ou_id)) 51 | return new_ou_id 52 | 53 | debug_print("OU already existed. ID: {}".format(ou_id)) 54 | return ou_id 55 | 56 | 57 | def get_ou_id_for_name(root_id, ou_name): 58 | debug_print("get id for {} in {}".format(ou_name, root_id)) 59 | client = get_boto_client("organizations") 60 | response = client.list_organizational_units_for_parent( 61 | ParentId=root_id, 62 | MaxResults=10) 63 | ous = response["OrganizationalUnits"] 64 | for ou in ous: 65 | if ou["Name"] == ou_name: 66 | return ou["Id"] 67 | 68 | while('NextToken' in response): 69 | response = client.list_organizational_units_for_parent( 70 | ParentId=root_id, 71 | MaxResults=10, 72 | NextToken=response['NextToken'] 73 | ) 74 | ous = response["OrganizationalUnits"] 75 | for ou in ous: 76 | if ou["Name"] == ou_name: 77 | return True 78 | 79 | return None 80 | -------------------------------------------------------------------------------- /src/debug.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | log = logging.getLogger('amv') 5 | log.setLevel(logging.DEBUG) 6 | 7 | ch = logging.StreamHandler(sys.stdout) 8 | log.addHandler(ch) 9 | 10 | 11 | def debug_print(message): 12 | log.debug(message) 13 | 14 | 15 | def error_print(message): 16 | log.error(message) 17 | -------------------------------------------------------------------------------- /src/deployCloudFormation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import boto3 4 | import json 5 | import time 6 | from debug import debug_print 7 | from debug import error_print 8 | from botoHelper import get_boto_client 9 | 10 | 11 | def handler(event, context): 12 | debug_print(json.dumps(event, indent=2)) 13 | return main(event) 14 | 15 | 16 | def main(event): 17 | account_id = event.get("accountId") 18 | account_role = event.get("accountRole") 19 | 20 | credentials = assume_role(account_id, account_role) 21 | 22 | access_key = credentials['AccessKeyId'] 23 | secret_access_key = credentials['SecretAccessKey'] 24 | session_token = credentials['SessionToken'] 25 | cfn_client = get_boto_client("cloudformation", access_key, 26 | secret_access_key, session_token) 27 | 28 | templates = event["cfnTemplates"] 29 | for template in templates: 30 | deploy_cloudformation_template(cfn_client, template, event) 31 | 32 | return event 33 | 34 | 35 | def deploy_cloudformation_template(cfn_client, template, event): 36 | bucket = os.environ['CLOUDFORMATION_TEMPLATE_BUCKET'] 37 | templateName = template["templateName"] 38 | stackName = template["stackName"] 39 | debug_print("Deploying CFN Template: {}".format(templateName)) 40 | parameters = create_cloudformation_parameters( 41 | template["parameters"], event) 42 | debug_print(json.dumps(parameters, indent=2)) 43 | presigned_url = create_s3_presigned_url(bucket, templateName) 44 | debug_print(presigned_url) 45 | if not cloudformation_stack_exists(cfn_client, stackName, "eu-west-1"): 46 | create_cloudformation_stack( 47 | cfn_client, presigned_url, stackName, "eu-west-1", parameters) 48 | else: 49 | update_cloudformation_stack( 50 | cfn_client, presigned_url, stackName, "eu-west-1", parameters) 51 | 52 | 53 | def create_cloudformation_parameters(parameters, event): 54 | cfnParams = [] 55 | for parameter in parameters: 56 | key = parameter["key"] 57 | value = parameter["value"] 58 | if value.startswith("{{") and value.endswith("}}"): 59 | value = value[2:len(value)-2] 60 | value = event.get(value) 61 | 62 | cfnParams.append({ 63 | 'ParameterKey': key, 64 | 'ParameterValue': value 65 | }) 66 | return cfnParams 67 | 68 | 69 | def assume_role(account_id, role_name): 70 | debug_print("Assuming role.....") 71 | role_arn = "arn:aws:iam::{0}:role/{1}".format(account_id, role_name) 72 | client = get_boto_client('sts') 73 | assumed_role = client.assume_role( 74 | RoleArn=role_arn, 75 | RoleSessionName="account_vending_machine_lambda" 76 | ) 77 | 78 | return assumed_role['Credentials'] 79 | 80 | 81 | def create_s3_presigned_url(bucket, object): 82 | client = get_boto_client('s3') 83 | response = client.generate_presigned_url('get_object', 84 | Params={ 85 | 'Bucket': bucket, 86 | 'Key': object 87 | }, 88 | ExpiresIn=3600) 89 | return response 90 | 91 | 92 | def create_cloudformation_stack(client, template_url, stackname, stackregion, parameters): 93 | create_date = time.strftime("%d/%m/%Y") 94 | 95 | response = client.create_stack( 96 | StackName=stackname, 97 | TemplateURL=template_url, 98 | Parameters=parameters, 99 | NotificationARNs=[], 100 | Capabilities=[ 101 | 'CAPABILITY_NAMED_IAM', 102 | ], 103 | OnFailure='ROLLBACK', 104 | Tags=[ 105 | { 106 | 'Key': 'CreatedBy', 107 | 'Value': 'Account-Vending-Machine' 108 | }, 109 | { 110 | 'Key': 'CreatedAt', 111 | 'Value': create_date 112 | } 113 | ] 114 | ) 115 | debug_print("Stack creation in process...") 116 | debug_print(response) 117 | stack_creating = True 118 | while stack_creating is True: 119 | event_list = client.describe_stack_events( 120 | StackName=stackname).get("StackEvents") 121 | stack_event = event_list[0] 122 | 123 | if (stack_event.get('ResourceType') == 'AWS::CloudFormation::Stack' and 124 | stack_event.get('ResourceStatus') == 'CREATE_COMPLETE'): 125 | stack_creating = False 126 | debug_print("Stack creation completed!") 127 | elif (stack_event.get('ResourceType') == 'AWS::CloudFormation::Stack' and 128 | stack_event.get('ResourceStatus') == 'ROLLBACK_COMPLETE'): 129 | stack_creating = False 130 | debug_print("Stack construction failed!!") 131 | else: 132 | debug_print("Stack creating...") 133 | time.sleep(5) 134 | 135 | 136 | def update_cloudformation_stack(client, template_url, stackname, stackregion, parameters): 137 | debug_print("Updating stack: {}".format(stackname)) 138 | try: 139 | update_date = time.strftime("%d/%m/%Y") 140 | 141 | response = client.update_stack( 142 | StackName=stackname, 143 | TemplateURL=template_url, 144 | Parameters=parameters, 145 | NotificationARNs=[], 146 | Capabilities=[ 147 | 'CAPABILITY_NAMED_IAM', 148 | ], 149 | Tags=[ 150 | { 151 | 'Key': 'CreatedBy', 152 | 'Value': 'Account-Vending-Machine' 153 | }, 154 | { 155 | 'Key': 'UpdatedAt', 156 | 'Value': update_date 157 | } 158 | ] 159 | ) 160 | debug_print("Stack update in process...") 161 | debug_print(response) 162 | stack_updating = True 163 | while stack_updating is True: 164 | event_list = client.describe_stack_events( 165 | StackName=stackname).get("StackEvents") 166 | stack_event = event_list[0] 167 | 168 | if (stack_event.get('ResourceType') == 'AWS::CloudFormation::Stack' and 169 | stack_event.get('ResourceStatus') == 'UPDATE_COMPLETE'): 170 | stack_updating = False 171 | debug_print("Stack update completed!") 172 | elif (stack_event.get('ResourceType') == 'AWS::CloudFormation::Stack' and 173 | stack_event.get('ResourceStatus') == 'UPDATE_ROLLBACK_COMPLETE'): 174 | stack_updating = False 175 | debug_print("Stack update failed!!") 176 | else: 177 | debug_print("Stack updating...") 178 | time.sleep(5) 179 | except Exception as e: 180 | message = getattr(e, 'message', str(e)) 181 | # debug_print("------------------------------------") 182 | # debug_print(message) 183 | # debug_print("------------------------------------") 184 | if "No updates are to be performed" not in message: 185 | raise e 186 | else: 187 | debug_print("Stack already up to date!") 188 | 189 | 190 | def cloudformation_stack_exists(client, stackname, stackregion): 191 | try: 192 | client.describe_stacks( 193 | StackName=stackname 194 | ) 195 | return True 196 | except: 197 | return False 198 | -------------------------------------------------------------------------------- /src/getAccountCreateStatus.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import boto3 4 | import json 5 | import time 6 | from debug import debug_print 7 | from debug import error_print 8 | from botoHelper import get_boto_client 9 | 10 | 11 | def handler(event, context): 12 | debug_print(json.dumps(event, indent=2)) 13 | return main(event) 14 | 15 | 16 | def main(event): 17 | account_request_id = event.get("accountRequestId") 18 | account_status = get_account_creation_status(account_request_id) 19 | state = account_status["CreateAccountStatus"]["State"] 20 | event["createAccountStatus"] = state 21 | if "AccountId" in account_status["CreateAccountStatus"]: 22 | account_id = account_status["CreateAccountStatus"]["AccountId"] 23 | event["accountId"] = account_id 24 | 25 | return event 26 | 27 | 28 | def get_account_creation_status(account_request_id): 29 | client = get_boto_client('organizations') 30 | response = client.describe_create_account_status( 31 | CreateAccountRequestId=account_request_id 32 | ) 33 | return response 34 | -------------------------------------------------------------------------------- /src/moveAccount.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import boto3 4 | import json 5 | import time 6 | from debug import debug_print 7 | from debug import error_print 8 | from botoHelper import get_boto_client 9 | 10 | 11 | def handler(event, context): 12 | debug_print(json.dumps(event, indent=2)) 13 | return main(event) 14 | 15 | 16 | def main(event): 17 | root_ou_id = event.get("rootOuId") 18 | ou_id = event.get("ouId") 19 | account_id = event.get("accountId") 20 | 21 | move_account(root_ou_id, ou_id, account_id) 22 | 23 | return event 24 | 25 | 26 | def move_account(root_ou_id, ou_id, account_id): 27 | client = get_boto_client('organizations') 28 | debug_print("Trying to move account....") 29 | client.move_account( 30 | AccountId=account_id, 31 | SourceParentId=root_ou_id, 32 | DestinationParentId=ou_id 33 | ) 34 | -------------------------------------------------------------------------------- /src/notifyAdmins.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import boto3 4 | import json 5 | import time 6 | from debug import debug_print 7 | from debug import error_print 8 | from botoHelper import get_boto_client 9 | 10 | 11 | def handler(event, context): 12 | debug_print(json.dumps(event, indent=2)) 13 | return main(event) 14 | 15 | 16 | def main(event): 17 | data = event[0] 18 | account_id = data.get("accountId") 19 | admin_account_role_name = data.get("adminAccountRole") 20 | admin_account_number = data.get("adminAccount") 21 | 22 | message = "New account with ID: {} was created successfully. Role: {} with Trust to Account: {} was setup for Admin Access.".format( 23 | account_id, admin_account_role_name, admin_account_number) 24 | debug_print(message) 25 | postMessageToSns("Account was created", message) 26 | 27 | return data 28 | 29 | 30 | def postMessageToSns(subject, message): 31 | sns_topic = os.environ['ADMIN_SNS_TOPIC'] 32 | client = boto3.client('sns') 33 | 34 | client.publish( 35 | TopicArn=sns_topic, 36 | Message=message, 37 | Subject=subject 38 | ) 39 | -------------------------------------------------------------------------------- /src/notifyOwner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import boto3 4 | import json 5 | import time 6 | from debug import debug_print 7 | from debug import error_print 8 | from botoHelper import get_boto_client 9 | 10 | 11 | def handler(event, context): 12 | debug_print(json.dumps(event, indent=2)) 13 | return main(event) 14 | 15 | 16 | def main(event): 17 | data = event[0] 18 | account_id = data.get("accountId") 19 | iam_user = data.get("iamUser") 20 | iam_password = data.get("iamPassword") 21 | 22 | body_text = "New account with ID: {} was created successfully for you. A IAM User: {} with Password: {} has been created. Change password after first logon. Navigate to https://{}.signin.aws.amazon.com/console/ to sign in to the console.".format( 23 | account_id, iam_user, iam_password, account_id) 24 | 25 | body_html = """ 26 | 27 |
28 | 29 |A user has been created for you.
31 |Username: {}
32 |Password: {}
33 |Change password at first login!
34 |Navigate to https://{}.signin.aws.amazon.com/console/ to sign in to the console.
35 | 36 | 37 | """.format(account_id, iam_user, iam_password, account_id) 38 | 39 | sendEmail(os.environ['SENDER'], iam_user, 40 | "AWS Account was created", body_text, body_html) 41 | 42 | return data 43 | 44 | 45 | def sendEmail(sender, recipient, subject, body_text, body_html): 46 | 47 | charset = "UTF-8" 48 | 49 | # Create a new SES resource and specify a region. 50 | client = get_boto_client('ses') 51 | 52 | # Try to send the email. 53 | # Provide the contents of the email. 54 | client.send_email( 55 | Destination={ 56 | 'ToAddresses': [ 57 | recipient, 58 | ], 59 | }, 60 | Message={ 61 | 'Body': { 62 | 'Html': { 63 | 'Charset': charset, 64 | 'Data': body_html, 65 | }, 66 | 'Text': { 67 | 'Charset': charset, 68 | 'Data': body_text, 69 | }, 70 | }, 71 | 'Subject': { 72 | 'Charset': charset, 73 | 'Data': subject, 74 | }, 75 | }, 76 | Source=sender 77 | ) 78 | -------------------------------------------------------------------------------- /src/sendErrorNotification.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import boto3 4 | import json 5 | import time 6 | from debug import debug_print 7 | from debug import error_print 8 | from botoHelper import get_boto_client 9 | 10 | 11 | def handler(event, context): 12 | debug_print(json.dumps(event, indent=2)) 13 | return main(event) 14 | 15 | 16 | def main(event): 17 | postErrorMessageToSns("Failure during account creation", 18 | "There was an error during account creation. Check the logs for more details. ") 19 | return event 20 | 21 | 22 | def postErrorMessageToSns(subject, message): 23 | sns_topic = os.environ['ERROR_SNS_TOPIC'] 24 | client = boto3.client('sns') 25 | 26 | client.publish( 27 | TopicArn=sns_topic, 28 | Message=message, 29 | Subject=subject 30 | ) 31 | -------------------------------------------------------------------------------- /src/storeAccountData.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import boto3 4 | import json 5 | import time 6 | from debug import debug_print 7 | from debug import error_print 8 | from botoHelper import get_boto_client 9 | 10 | 11 | def handler(event, context): 12 | debug_print(json.dumps(event, indent=2)) 13 | return main(event) 14 | 15 | 16 | def main(event): 17 | bucket = os.environ["ACCOUNT_DATA_BUCKET"] 18 | ou = event.get("ouName") 19 | account_id = event.get("accountId") 20 | account_name = event.get("accountName") 21 | account_id = event.get("accountId") 22 | client = get_boto_client('s3') 23 | client.put_object( 24 | Body=json.dumps(event), 25 | Bucket=bucket, 26 | Key="{}/{}/{}.json".format(ou, account_name, account_id), 27 | ServerSideEncryption='AES256' 28 | ) 29 | 30 | return event 31 | --------------------------------------------------------------------------------