Skip to content

Commit

Permalink
Merge pull request #87 from gcasilva/main
Browse files Browse the repository at this point in the history
Fix account creation when account already exists
  • Loading branch information
gcasilva authored May 20, 2024
2 parents fc2105a + a8b9ffd commit 58f2f35
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 97 deletions.
21 changes: 13 additions & 8 deletions guide/content/deployment-steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,25 @@ description: Deployment steps

Wait for the CloudFormation status to change to `CREATE_COMPLETE` state.

## Launch using Customizations for Control Tower or as a Stack Set {#launch-cfct}
## Launch on AWS Organizations member accounts using AWS CloudFormation Stacksets
If you're using this solution in an AWS organization that doesn't use AWS Control Tower, you need to create IAM roles to [Set up basic permissions for stack set operations](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs-self-managed.html#stacksets-prereqs-accountsetup) so that this ABI solution can be deployed to all member accounts in the AWS Organizations or to specific accounts or OUs you select.
a. You need to create an IAM role (AWSCloudFormationStackSetAdministrationRole) in your management account to establish a trusted relationship between the account you're administering the stack set from and the account you're deploying stack instances to. The CloudFormation template to create this role is [available here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs-self-managed.html#stacksets-prereqs-accountsetup).
b. You need to create an IAM execution role (AWSCloudFormationStackSetExecutionRole) for AWS CloudFormation to deploy the StackSets across all member accounts with in the organization. You can use [this CloudFormation template](https://s3.amazonaws.com/cloudformation-stackset-sample-templates-us-east-1/AWSCloudFormationStackSetExecutionRole.yml) and deploy the stack acoss the organization using instructions from [Create a stack set with service-managed permissions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-getting-started-create.html#stacksets-orgs-associate-stackset-with-org)
c. From your Management Account create your AWS CloudFormation StackSets and chose Self-service permissions and use `AWSCloudFormationStackSetExecutionRole` for the IAM admin role name and `AWSCloudFormationStackSetExecutionRole` for the IAM execution role name and then you can select the CloudFormation template from `https://github.com/aws-ia/cfn-abi-spotbynetapp-cloudcheckr/blob/main/templates/CCBuiltIn.yaml`.
[AWS CloudFormation StackSets Self-service permissions](/images/stack-set-admin.png)

You can use CfCT to deploy the templates provided with the AWS Built-in package.

### Prerequisites
## Launch using Customizations for Control Tower (CfCT) {#launch-cfct}

Customizations for AWS Control Tower combines AWS Control Tower and other highly available, trusted AWS services to help customers set up a secure, multiaccount AWS environment according to AWS best practices. You can add customizations to your AWS Control Tower landing zone using an AWS CloudFormation template and service control policies (SCPs). You can deploy the custom template and policies to individual accounts and organizational units (OUs) within your organization.

The CfCT solution does not launch resources on the management account. Therefore, you must create the role with required permissions in the management account.
CfCT also integrates with AWS Control Tower lifecycle events to hlep ensure that resource deployments stay in sync with your landing zone. For example, when you create a new account using AWS Control Tower account factory, CfCT deploys all of the resources that are attached to the account.

Control Tower permissions:
(/images/control-tower-admin.png)
The templates provided by this ABI package are deployable through CfCT.

StackSet permissions:
### Prerequisites

(/images/stack-set-admin.png)
The CfCT solution can't launch resources in the management account by default. You need select pCreateAWSControlTowerExecutionRole : true to allow the stack to create the role or must manually create a role in that account that has necessary permissions.

### How it works

Expand Down
122 changes: 37 additions & 85 deletions lambda_functions/source/awsCreateAccount/awsCreateAccount.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import threading
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
response = {'accountId': None}
timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context])
Expand All @@ -27,31 +30,30 @@ def lambda_handler(event, context):
account_aliases, account_number = get_account_name()
accountName = account_aliases[0] if account_aliases else account_number
bearerToken = get_access_token("https://auth-"+Environment+".cloudcheckr.com/auth/connect/token", APIKey, APISecret)
response = createAccount(customerNumber, accountName, bearerToken, Environment)
if response.get('accountId') is None:
sendResponse = send_response(event, context, 'FAILED', {'Error': 'An error occurred during the Lambda execution: ' + response['body']})
return {
'statusCode': 500,
'body': 'An error occurred during the Lambda execution: ' + response['body']
}
existingAccountId = check_existing_account(customerNumber, bearerToken, account_number, Environment)
if existingAccountId:
response['accountId'] = existingAccountId
logger.info(f"Account already exists. CloudCheckr Account ID: {existingAccountId}")
else:
response = createAccount(customerNumber, accountName, bearerToken, Environment)
if response.get('accountId') is None:
send_response(event, context, 'FAILED', {'Error': 'An error occurred during the Lambda execution: ' + response['body']})
return {'statusCode': 500, 'body': 'An error occurred during the Lambda execution: ' + response['body']}

except Exception as e:
logger.error(f"Lambda execution error: {e}")
timer.cancel()
sendResponse = send_response(event, context, 'FAILED', {'Error': 'An error occurred during the Lambda execution: ' + str(e)})
return {
'statusCode': 500,
'body': 'An error occurred during the Lambda execution: ' + str(e)
}
send_response(event, context, 'FAILED', {'Error': str(e)})
return {'statusCode': 500, 'body': str(e)}

finally:
timer.cancel()
sendResponse = send_response(event, context, 'SUCCESS', {'accountNumber': response['accountId']})
send_response(event, context, 'SUCCESS', {'accountNumber': response['accountId']})

def timeout(event, context):
logging.error('Execution is about to time out, sending failure response to CloudFormation')
logger.error('Execution is about to time out, sending failure response to CloudFormation')
send_response(event, context, 'FAILED', {'Error': 'Execution is about to time out'})


def send_response(event, context, response_status, response_data):
response_body = json.dumps({
'Status': response_status,
Expand All @@ -72,98 +74,48 @@ def send_response(event, context, response_status, response_data):
with urllib.request.urlopen(req) as f:
pass

def getPreviousAccountNameID(customer_number, bearer_token, accountName, Environment):
url = f"https://api-"+Environment+".cloudcheckr.com/customer/v1/customers/"+customer_number+"/account-management/accounts?search="+accountName
def check_existing_account(customer_number, bearer_token, provider_identifier, Environment):
url = f"https://api-"+Environment+".cloudcheckr.com/customer/v1/customers/"+customer_number+"/account-management/accounts?search="+provider_identifier
headers = {
'Accept': 'text/plain',
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + bearer_token
}
try:
request = urllib.request.Request(url, headers=headers, method='GET')
with urllib.request.urlopen(request, timeout=15) as response:
response_text = response.read().decode()
response_json = json.loads(response_text)
if 'items' in response_json and len(response_json['items']) > 0:
account_id = response_json['items'][0].get('id')
print(f"Account ID found: {account_id}")
return account_id
else:
print("No accounts found matching the search.")
return None
except urllib.error.HTTPError as e:
print(f"HTTP Error while retrieving account ID: {e}")
return None
response_json = json.loads(response.read().decode())
for item in response_json.get('items', []):
if item.get('providerIdentifier') == provider_identifier:
return item.get('id')
return None
except Exception as e:
print(f"Unexpected error: {e}")
logger.error(f"Error checking existing accounts: {e}")
return None


def createAccount(customer_number, accountName, bearer_token, Environment):
url = f"https://api-"+Environment+".cloudcheckr.com/customer/v1/customers/"+customer_number+"/account-management/accounts"
payload = json.dumps({
"item": {
"name": accountName,
"provider": "AWS"
}
})
headers = {
'Accept': 'text/plain',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + bearer_token
}
payload = json.dumps({"item": {"name": accountName, "provider": "AWS"}})
headers = {'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + bearer_token}
try:
request = urllib.request.Request(url, data=payload.encode(), headers=headers, method='POST')
with urllib.request.urlopen(request, timeout=15) as response:
response_text = response.read().decode()
response_json = json.loads(response_text)

if 'id' in response_json:
return {
'statusCode': 200,
'body': 'completed!',
'accountId': response_json['id'],
'bearerToken': bearer_token
}

return {'statusCode': 200, 'body': 'No ID found, but no errors', 'accountId': None, 'bearerToken': bearer_token}

except urllib.error.HTTPError as e:
print(f"HTTP Error: {e.code} {e.reason}")
if e.code == 400:
account_id = getPreviousAccountNameID(customer_number, bearer_token, accountName, Environment)
if account_id:
return {'statusCode': 200, 'body': 'Account ID retrieved from existing account', 'accountId': account_id}
else:
return {'statusCode': 500, 'body': 'Failed to retrieve existing account ID', 'accountId': None}
else:
return {'statusCode': e.code, 'body': str(e.reason), 'accountId': None}
response_json = json.loads(response.read().decode())
return {'statusCode': 200, 'body': 'Account created!', 'accountId': response_json.get('id'), 'bearerToken': bearer_token}
except Exception as e:
print(f"Unexpected error: {e}")
return {'statusCode': 500, 'body': 'An unexpected error occurred', 'accountId': None}
logger.error(f"Error creating account: {e}")
return {'statusCode': 500, 'body': str(e), 'accountId': None}

def get_access_token(url, client_id, client_secret):
auth_header = base64.b64encode(f"{client_id}:{client_secret}".encode("utf-8")).decode("utf-8")
data = urllib.parse.urlencode({"grant_type": "client_credentials"}).encode("utf-8")

req = urllib.request.Request(
url,
data=data,
headers={
"Authorization": f"Basic {auth_header}",
"Content-Type": "application/x-www-form-urlencoded"
},
method='POST'
)

req = urllib.request.Request(url, data=data, headers={"Authorization": f"Basic {auth_header}", "Content-Type": "application/x-www-form-urlencoded"}, method='POST')
with urllib.request.urlopen(req) as response:
response_json = json.loads(response.read().decode())
return response_json["access_token"]
return json.loads(response.read().decode())["access_token"]

def get_account_name():
iam = boto3.client('iam')
response = iam.list_account_aliases()
account_aliases = iam.list_account_aliases().get('AccountAliases', [])
account_number = boto3.client('sts').get_caller_identity()['Account']
print(response['AccountAliases'], account_number)
return response['AccountAliases'], account_number

logger.info(f"Account aliases: {account_aliases}, Account number: {account_number}")
return account_aliases, account_number
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

import json
import base64
import boto3
import urllib.request
import urllib.parse
import threading
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
response = None
Expand All @@ -35,7 +38,7 @@ def lambda_handler(event, context):

except Exception as e:
timer.cancel()
print("Error: ", e)
logger.error(f"Error: {e}")
sendResponse = send_response(event, context, 'FAILED', {'Error': 'An error occurred during the Lambda execution: ' + str(e)})
return {
'statusCode': 500,
Expand All @@ -53,10 +56,14 @@ def timeout(event, context):

def credentialAccount(customerNumber, accountNumber, RoleArn, bearerToken, Environment):
url = f"https://api-"+Environment+".cloudcheckr.com/credential/v1/customers/"+customerNumber+"/accounts/"+accountNumber+"/credentials/aws"

aws_partition = get_aws_partition()
if aws_partition == "aws-us-gov":
regionGroup = "GovUs"
else:
regionGroup = "Commercial"
payload = json.dumps({
"item": {
"regionGroup": "Commercial",
"regionGroup": regionGroup,
"crossAccountRole": {
"roleArn": RoleArn
}
Expand Down Expand Up @@ -121,3 +128,9 @@ def get_access_token(url, client_id, client_secret):
response = urllib.request.urlopen(request)
response_json = json.loads(response.read().decode())
return response_json["access_token"]

def get_aws_partition():
session = boto3.session.Session()
region_name = session.region_name
partition = session.get_partition_for_region(region_name)
return partition
1 change: 0 additions & 1 deletion templates/CCBuiltIn.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ Parameters:
pEnvironment:
Type: String
Description: CloudCheckr Environment(US, EU, AU, GOV)
NoEcho: true
pCustomerNumber:
AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$
Type: String
Expand Down

0 comments on commit 58f2f35

Please sign in to comment.