Skip to content

Commit 03f5cef

Browse files
Support stack management using assumed roles
1 parent 32fc07b commit 03f5cef

9 files changed

Lines changed: 180 additions & 40 deletions

File tree

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,9 @@ src/stackuchin/template.yaml
137137
src/stackuchin/pipeline.yaml
138138

139139
src/stackuchin/template.json
140+
141+
src/stackuchin/cloudformation-template.yaml
142+
143+
src/stackuchin/input.yaml
144+
145+
src/stackuchin/test.py

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM python:3.7-alpine
22

3-
RUN pip install stackuchin==1.5.6
3+
RUN pip install stackuchin==1.6.0
44

55
VOLUME /project
66

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,19 @@ another-stack-name:
147147
Team: DevOps
148148
MaintainerEmail: support@rungutan.com
149149
MaintainerTeam: Rungutan
150+
151+
stack-different-account-assume-role:
152+
Account: 456445654456
153+
Region: us-east-1
154+
Template: some-folder/cloudformation-some-other-template.yaml
155+
AssumedRoleArn: arn:aws:iam::456445654456:role/SomeRoleThatCanBeAssumed
156+
# Stack without readable parameters.
157+
Parameters: {}
158+
Tags:
159+
Environment: UTILITIES
160+
Team: DevOps
161+
MaintainerEmail: support@rungutan.com
162+
MaintainerTeam: Rungutan
150163
```
151164

152165
## Running it as a pipeline

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
here = path.abspath(path.dirname(__file__))
66

7-
current_version = str('1.5.6')
7+
current_version = str('1.6.0')
88

99
# Get the long description from the README file
1010
with open(path.join(here, 'README.md'), encoding='utf-8') as f:

src/stackuchin/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def __init__(self):
4343

4444
# noinspection PyMethodMayBeStatic
4545
def version(self):
46-
print("1.5.6")
46+
print("1.6.0")
4747

4848
# noinspection PyMethodMayBeStatic
4949
def create(self):

src/stackuchin/create.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
def create(profile_name, stack_file, stack_name, secret, slack_webhook_url,
1616
s3_bucket, s3_prefix, only_errors, from_pipeline=False):
17+
1718
aws_region = None
1819
aws_account = None
1920
stacks = None
@@ -108,14 +109,20 @@ def create(profile_name, stack_file, stack_name, secret, slack_webhook_url,
108109
print(exc)
109110
exit(1)
110111

112+
sts = boto3.client('sts')
113+
iam_user = sts.get_caller_identity()['Arn']
114+
111115
# Verify integrity of stack
112116
stacks = None
117+
aws_role_arn = None
113118
try:
114119
with open(stack_file, 'r') as stack_stream:
115120
stacks = yaml.safe_load(stack_stream)
116121
aws_region = stacks[stack_name]['Region']
117122
aws_account = stacks[stack_name]['Account']
118123
stack_template = stacks[stack_name]['Template']
124+
if 'AssumedRoleArn' in stacks[stack_name]:
125+
aws_role_arn = stacks[stack_name]['AssumedRoleArn']
119126
except yaml.YAMLError as exc:
120127
print(exc)
121128
alert(stack_name,
@@ -124,14 +131,52 @@ def create(profile_name, stack_file, stack_name, secret, slack_webhook_url,
124131
aws_region, aws_account, action, profile_name, slack_webhook_url)
125132
exit(1)
126133

134+
# Set role
135+
role_session = None
136+
if aws_role_arn is not None:
137+
try:
138+
if profile_name is not None:
139+
boto3.setup_default_session(profile_name=profile_name)
140+
141+
# Get current timestamp
142+
current_date = datetime.utcnow().isoformat()
143+
current_date = current_date.replace(':', '-').replace('.', '-')
144+
145+
# Assume role
146+
sts_client = boto3.client('sts')
147+
assumed_role_object = sts_client.assume_role(
148+
RoleArn=aws_role_arn,
149+
RoleSessionName="AssumeRoleSession-{}".format(current_date)
150+
)
151+
credentials = assumed_role_object['Credentials']
152+
role_session = boto3.Session(
153+
aws_access_key_id=credentials['AccessKeyId'],
154+
aws_secret_access_key=credentials['SecretAccessKey'],
155+
aws_session_token=credentials['SessionToken']
156+
)
157+
iam_user = aws_role_arn
158+
except Exception as exc:
159+
print(exc)
160+
alert(stack_name,
161+
"Unable to assume role {} for stack {}. Exception = {}".format(
162+
aws_role_arn, stack_name, exc),
163+
aws_region, aws_account, action, profile_name, slack_webhook_url)
164+
exit(1)
165+
127166
# Connect to AWS CloudFormation
128-
cf_client = boto3.client('cloudformation', region_name=aws_region)
167+
cf_client = boto3.client('cloudformation', region_name=aws_region) if role_session is None else \
168+
role_session.client('cloudformation', region_name=aws_region)
169+
170+
# Create AWS S3 Resource
171+
s3_session = boto3.resource('s3', region_name=aws_region) if role_session is None else \
172+
role_session.resource('s3', region_name=aws_region)
129173

130174
# Get parameter and tags from stack
131175
stack_parameters, stack_tags, stack_template_url = result(stack_file, stack_name, secret,
132176
s3_bucket, s3_prefix,
133177
aws_region, aws_account,
134-
action, profile_name, slack_webhook_url)
178+
action, profile_name, slack_webhook_url,
179+
s3_session)
135180

136181
# Check if stack exists and if it is in "ROLLBACK_COMPLETE" status, delete it
137182
try:
@@ -147,7 +192,7 @@ def create(profile_name, stack_file, stack_name, secret, slack_webhook_url,
147192

148193
except Exception as exc:
149194
print(exc)
150-
alert(stack_name, exc, aws_region, aws_account, action, profile_name, slack_webhook_url)
195+
alert(stack_name, exc, aws_region, aws_account, action, profile_name, slack_webhook_url, iam_user)
151196
exit(1)
152197
except botocore.exceptions.ClientError as exc:
153198
if "Stack with id {} does not exist".format(stack_name) in str(exc):
@@ -156,19 +201,20 @@ def create(profile_name, stack_file, stack_name, secret, slack_webhook_url,
156201
print(exc)
157202
alert(stack_name,
158203
"Unable to check if stack already exists. Exception = {}".format(exc),
159-
aws_region, aws_account, action, profile_name, slack_webhook_url)
204+
aws_region, aws_account, action, profile_name, slack_webhook_url, iam_user)
160205
exit(1)
161206
except Exception as exc:
207+
print("aici crapa ?!")
162208
print(exc)
163209
alert(stack_name,
164210
"Unable to check if stack already exists. Exception = {}".format(exc),
165-
aws_region, aws_account, action, profile_name, slack_webhook_url)
211+
aws_region, aws_account, action, profile_name, slack_webhook_url, iam_user)
166212
exit(1)
167213

168214
# Create the stack
169215
if not only_errors:
170216
print("CREATE_STARTED for stack {}".format(stack_name))
171-
alert(stack_name, None, aws_region, aws_account, "CREATE_STARTED", profile_name, slack_webhook_url)
217+
alert(stack_name, None, aws_region, aws_account, "CREATE_STARTED", profile_name, slack_webhook_url, iam_user)
172218
waiter = None
173219
try:
174220
if stack_template_url['type'] == 'TemplateURL':
@@ -197,7 +243,7 @@ def create(profile_name, stack_file, stack_name, secret, slack_webhook_url,
197243
print(exc)
198244
alert(stack_name,
199245
"Unable to start stack creation process. Exception = {}".format(exc),
200-
aws_region, aws_account, action, profile_name, slack_webhook_url)
246+
aws_region, aws_account, action, profile_name, slack_webhook_url, iam_user)
201247
exit(1)
202248

203249
# Waiting for stack to finish creating
@@ -225,11 +271,11 @@ def create(profile_name, stack_file, stack_name, secret, slack_webhook_url,
225271
"CREATE_FAILED",
226272
"\n".join(failure_reasons)
227273
),
228-
aws_region, aws_account, action, profile_name, slack_webhook_url)
274+
aws_region, aws_account, action, profile_name, slack_webhook_url, iam_user)
229275
exit(1)
230276

231277
if not only_errors:
232278
print("CREATE_COMPLETE for stack {}".format(stack_name))
233-
alert(stack_name, None, aws_region, aws_account, "CREATE_COMPLETE", profile_name, slack_webhook_url)
279+
alert(stack_name, None, aws_region, aws_account, "CREATE_COMPLETE", profile_name, slack_webhook_url, iam_user)
234280

235281
return True

src/stackuchin/delete.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,20 @@ def delete(profile_name, stack_file, stack_name,
106106
print(exc)
107107
exit(1)
108108

109+
sts = boto3.client('sts')
110+
iam_user = sts.get_caller_identity()['Arn']
111+
109112
# Verify integrity of stack
110113
stacks = None
114+
aws_role_arn = None
111115
try:
112116
with open(stack_file, 'r') as stack_stream:
113117
stacks = yaml.safe_load(stack_stream)
114118
aws_region = stacks[stack_name]['Region']
115119
aws_account = stacks[stack_name]['Account']
116120
stack_template = stacks[stack_name]['Template']
121+
if 'AssumedRoleArn' in stacks[stack_name]:
122+
aws_role_arn = stacks[stack_name]['AssumedRoleArn']
117123
except yaml.YAMLError as exc:
118124
print(exc)
119125
alert(stack_name,
@@ -122,8 +128,41 @@ def delete(profile_name, stack_file, stack_name,
122128
aws_region, aws_account, action, profile_name, slack_webhook_url)
123129
exit(1)
124130

131+
# Set role
132+
role_session = None
133+
if aws_role_arn is not None:
134+
try:
135+
if profile_name is not None:
136+
boto3.setup_default_session(profile_name=profile_name)
137+
138+
# Get current timestamp
139+
current_date = datetime.utcnow().isoformat()
140+
current_date = current_date.replace(':', '-').replace('.', '-')
141+
142+
# Assume role
143+
sts_client = boto3.client('sts')
144+
assumed_role_object = sts_client.assume_role(
145+
RoleArn=aws_role_arn,
146+
RoleSessionName="AssumeRoleSession-{}".format(current_date)
147+
)
148+
credentials = assumed_role_object['Credentials']
149+
role_session = boto3.Session(
150+
aws_access_key_id=credentials['AccessKeyId'],
151+
aws_secret_access_key=credentials['SecretAccessKey'],
152+
aws_session_token=credentials['SessionToken']
153+
)
154+
iam_user = aws_role_arn
155+
except Exception as exc:
156+
print(exc)
157+
alert(stack_name,
158+
"Unable to assume role {} for stack {}. Exception = {}".format(
159+
aws_role_arn, stack_name, exc),
160+
aws_region, aws_account, action, profile_name, slack_webhook_url)
161+
exit(1)
162+
125163
# Connect to AWS CloudFormation
126-
cf_client = boto3.client('cloudformation', region_name=aws_region)
164+
cf_client = boto3.client('cloudformation', region_name=aws_region) if role_session is None else \
165+
role_session.client('cloudformation', region_name=aws_region)
127166

128167
# Check if stack exists
129168
stack_id = None
@@ -136,7 +175,7 @@ def delete(profile_name, stack_file, stack_name,
136175
print(exc)
137176
alert(stack_name,
138177
"Unable to check if stack already exists. Exception = {}".format(exc),
139-
aws_region, aws_account, action, profile_name, slack_webhook_url)
178+
aws_region, aws_account, action, profile_name, slack_webhook_url, iam_user)
140179
exit(1)
141180

142181
# Disable termination protection
@@ -146,21 +185,21 @@ def delete(profile_name, stack_file, stack_name,
146185
print(exc)
147186
alert(stack_name,
148187
"Unable to disable termination protection. Exception = {}".format(exc),
149-
aws_region, aws_account, action, profile_name, slack_webhook_url)
188+
aws_region, aws_account, action, profile_name, slack_webhook_url, iam_user)
150189
exit(1)
151190

152191
# Delete the stack
153192
waiter = None
154193
if not only_errors:
155194
print("DELETE_STARTED for stack {}".format(stack_name))
156-
alert(stack_name, None, aws_region, aws_account, "DELETE_STARTED", profile_name, slack_webhook_url)
195+
alert(stack_name, None, aws_region, aws_account, "DELETE_STARTED", profile_name, slack_webhook_url, iam_user)
157196
try:
158197
cf_client.delete_stack(StackName=stack_name, ClientRequestToken=client_token)
159198
except Exception as exc:
160199
print(exc)
161200
alert(stack_name,
162201
"Unable to start stack deletion process. Exception = {}".format(exc),
163-
aws_region, aws_account, action, profile_name, slack_webhook_url)
202+
aws_region, aws_account, action, profile_name, slack_webhook_url, iam_user)
164203
exit(1)
165204

166205
# Waiting for stack to finish deleting
@@ -188,11 +227,11 @@ def delete(profile_name, stack_file, stack_name,
188227
"DELETE_FAILED",
189228
"\n".join(failure_reasons)
190229
),
191-
aws_region, aws_account, action, profile_name, slack_webhook_url)
230+
aws_region, aws_account, action, profile_name, slack_webhook_url, iam_user)
192231
exit(1)
193232

194233
if not only_errors:
195234
print("DELETE_COMPLETE for stack {}".format(stack_name))
196-
alert(stack_name, None, aws_region, aws_account, "DELETE_COMPLETE", profile_name, slack_webhook_url)
235+
alert(stack_name, None, aws_region, aws_account, "DELETE_COMPLETE", profile_name, slack_webhook_url, iam_user)
197236

198237
return True

0 commit comments

Comments
 (0)