Skip to content

Commit 8a01c3e

Browse files
authored
Merge pull request #109 from mattsb42-aws/depor
Decrypt Oracle deployment tooling
2 parents b3bf59c + d4a34ef commit 8a01c3e

File tree

4 files changed

+392
-2
lines changed

4 files changed

+392
-2
lines changed
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
version: 0.2
2+
phases:
3+
install:
4+
commands:
5+
- pip install tox
6+
build:
7+
commands:
8+
- cd decrypt_oracle
9+
- tox -e chalice-deploy
10+
artifacts:
11+
type: zip
12+
files:
13+
- decrypt_oracle/transformed.yaml

decrypt_oracle/.chalice/pipeline.py

+345
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
"""
2+
Generate the CloudFormation template for the deployment pipeline.
3+
"""
4+
import argparse
5+
import getpass
6+
import logging
7+
from typing import Iterable
8+
9+
import boto3
10+
import troposphere
11+
from awacs import (
12+
aws as AWS,
13+
awslambda as LAMBDA,
14+
cloudformation as CLOUDFORMATION,
15+
cloudwatch as CLOUDWATCH,
16+
codebuild as CODEBUILD,
17+
codepipeline as CODEPIPELINE,
18+
iam as IAM,
19+
logs as LOGS,
20+
s3 as S3,
21+
sts as STS,
22+
)
23+
from botocore.exceptions import ClientError
24+
from troposphere import GetAtt, Ref, Sub, Template, codebuild, codepipeline, iam, s3
25+
26+
APPLICATION_NAME = "AwsEncryptionSdkDecryptOraclePython"
27+
PIPELINE_STACK_NAME = "{}DeployPipeline".format(APPLICATION_NAME)
28+
CODEBUILD_IMAGE = "aws/codebuild/python:3.6.5"
29+
BUILDSPEC = "decrypt_oracle/.chalice/buildspec.yaml"
30+
GITHUB_REPO = "aws-encryption-sdk-python"
31+
WAITER_CONFIG = dict(Delay=10)
32+
_LOGGER = logging.getLogger("Decrypt Oracle Build Pipeline Deployer")
33+
34+
35+
class AllowEverywhere(AWS.Statement):
36+
"""Shortcut for creating IAM Statements that Allow to Resource "*"."""
37+
38+
def __init__(self, *args, **kwargs):
39+
my_kwargs = dict(Effect=AWS.Allow, Resource=["*"])
40+
my_kwargs.update(kwargs)
41+
super(AllowEverywhere, self).__init__(*args, **my_kwargs)
42+
43+
44+
def _service_assume_role(service: str) -> AWS.Policy:
45+
"""Build and return the IAM AssumeRolePolicy for use in service roles."""
46+
return AWS.Policy(
47+
Statement=[
48+
AWS.Statement(
49+
Effect=AWS.Allow,
50+
Action=[STS.AssumeRole],
51+
Principal=AWS.Principal("Service", ["{}.amazonaws.com".format(service)]),
52+
)
53+
]
54+
)
55+
56+
57+
def _codebuild_role() -> iam.Role:
58+
"""Build and return the IAM Role resource to be used by CodeBuild to run the build project."""
59+
policy = iam.Policy(
60+
"CodeBuildPolicy",
61+
PolicyName="CodeBuildPolicy",
62+
PolicyDocument=AWS.PolicyDocument(
63+
Statement=[
64+
AllowEverywhere(Action=[LOGS.CreateLogGroup, LOGS.CreateLogStream, LOGS.PutLogEvents]),
65+
AllowEverywhere(Action=[S3.GetObject, S3.GetObjectVersion, S3.PutObject]),
66+
]
67+
),
68+
)
69+
return iam.Role("CodeBuildRole", AssumeRolePolicyDocument=_service_assume_role(CODEBUILD.prefix), Policies=[policy])
70+
71+
72+
def _codebuild_builder(role: iam.Role, application_bucket: s3.Bucket) -> codebuild.Project:
73+
"""Build and return the CodeBuild Project resource to be used to build the decrypt oracle."""
74+
artifacts = codebuild.Artifacts(Type="CODEPIPELINE")
75+
environment = codebuild.Environment(
76+
ComputeType="BUILD_GENERAL1_SMALL",
77+
Image=CODEBUILD_IMAGE,
78+
Type="LINUX_CONTAINER",
79+
EnvironmentVariables=[codebuild.EnvironmentVariable(Name="APP_S3_BUCKET", Value=Ref(application_bucket))],
80+
)
81+
source = codebuild.Source(Type="CODEPIPELINE", BuildSpec=BUILDSPEC)
82+
return codebuild.Project(
83+
"{}Build".format(APPLICATION_NAME),
84+
Artifacts=artifacts,
85+
Environment=environment,
86+
Name=APPLICATION_NAME,
87+
ServiceRole=Ref(role),
88+
Source=source,
89+
)
90+
91+
92+
def _pipeline_role(buckets: Iterable[s3.Bucket]) -> iam.Role:
93+
"""Build and return the IAM Role resource to be used by CodePipeline to run the pipeline."""
94+
bucket_statements = [
95+
AWS.Statement(
96+
Effect=AWS.Allow,
97+
Action=[S3.GetBucketVersioning, S3.PutBucketVersioning],
98+
Resource=[GetAtt(bucket, "Arn") for bucket in buckets],
99+
),
100+
AWS.Statement(
101+
Effect=AWS.Allow,
102+
Action=[S3.GetObject, S3.PutObject],
103+
Resource=[Sub("${{{bucket}.Arn}}/*".format(bucket=bucket.title)) for bucket in buckets],
104+
),
105+
]
106+
policy = iam.Policy(
107+
"PipelinePolicy",
108+
PolicyName="PipelinePolicy",
109+
PolicyDocument=AWS.PolicyDocument(
110+
Statement=bucket_statements
111+
+ [
112+
AllowEverywhere(Action=[CLOUDWATCH.Action("*"), IAM.PassRole]),
113+
AllowEverywhere(Action=[LAMBDA.InvokeFunction, LAMBDA.ListFunctions]),
114+
AllowEverywhere(
115+
Action=[
116+
CLOUDFORMATION.CreateStack,
117+
CLOUDFORMATION.DeleteStack,
118+
CLOUDFORMATION.DescribeStacks,
119+
CLOUDFORMATION.UpdateStack,
120+
CLOUDFORMATION.CreateChangeSet,
121+
CLOUDFORMATION.DeleteChangeSet,
122+
CLOUDFORMATION.DescribeChangeSet,
123+
CLOUDFORMATION.ExecuteChangeSet,
124+
CLOUDFORMATION.SetStackPolicy,
125+
CLOUDFORMATION.ValidateTemplate,
126+
]
127+
),
128+
AllowEverywhere(Action=[CODEBUILD.BatchGetBuilds, CODEBUILD.StartBuild]),
129+
]
130+
),
131+
)
132+
return iam.Role(
133+
"CodePipelinesRole", AssumeRolePolicyDocument=_service_assume_role(CODEPIPELINE.prefix), Policies=[policy]
134+
)
135+
136+
137+
def _cloudformation_role() -> iam.Role:
138+
"""Build and return the IAM Role resource to be used by the pipeline to interact with CloudFormation."""
139+
policy = iam.Policy(
140+
"CloudFormationPolicy",
141+
PolicyName="CloudFormationPolicy",
142+
PolicyDocument=AWS.PolicyDocument(Statement=[AllowEverywhere(Action=[AWS.Action("*")])]),
143+
)
144+
return iam.Role(
145+
"CloudFormationRole", AssumeRolePolicyDocument=_service_assume_role(CLOUDFORMATION.prefix), Policies=[policy]
146+
)
147+
148+
149+
def _pipeline(
150+
pipeline_role: iam.Role,
151+
cfn_role: iam.Role,
152+
codebuild_builder: codebuild.Project,
153+
artifact_bucket: s3.Bucket,
154+
github_owner: str,
155+
github_branch: str,
156+
github_access_token: troposphere.AWSProperty,
157+
) -> codepipeline.Pipeline:
158+
"""Build and return the CodePipeline pipeline resource."""
159+
_source_output = "SourceOutput"
160+
get_source = codepipeline.Stages(
161+
Name="Source",
162+
Actions=[
163+
codepipeline.Actions(
164+
Name="PullSource",
165+
RunOrder="1",
166+
OutputArtifacts=[codepipeline.OutputArtifacts(Name=_source_output)],
167+
ActionTypeId=codepipeline.ActionTypeId(
168+
Category="Source", Owner="ThirdParty", Version="1", Provider="GitHub"
169+
),
170+
Configuration=dict(
171+
Owner=github_owner,
172+
Repo=GITHUB_REPO,
173+
OAuthToken=Ref(github_access_token),
174+
Branch=github_branch,
175+
PollForSourceChanges=True,
176+
),
177+
)
178+
],
179+
)
180+
_compiled_cfn_template = "CompiledCfnTemplate"
181+
_changeset_name = "{}ChangeSet".format(APPLICATION_NAME)
182+
_stack_name = "{}Stack".format(APPLICATION_NAME)
183+
do_build = codepipeline.Stages(
184+
Name="Build",
185+
Actions=[
186+
codepipeline.Actions(
187+
Name="BuildChanges",
188+
RunOrder="1",
189+
InputArtifacts=[codepipeline.InputArtifacts(Name=_source_output)],
190+
OutputArtifacts=[codepipeline.OutputArtifacts(Name=_compiled_cfn_template)],
191+
ActionTypeId=codepipeline.ActionTypeId(
192+
Category="Build", Owner="AWS", Version="1", Provider="CodeBuild"
193+
),
194+
Configuration=dict(ProjectName=Ref(codebuild_builder)),
195+
)
196+
],
197+
)
198+
stage_changeset = codepipeline.Actions(
199+
Name="StageChanges",
200+
RunOrder="1",
201+
ActionTypeId=codepipeline.ActionTypeId(Category="Deploy", Owner="AWS", Version="1", Provider="CloudFormation"),
202+
InputArtifacts=[codepipeline.InputArtifacts(Name=_compiled_cfn_template)],
203+
Configuration=dict(
204+
ActionMode="CHANGE_SET_REPLACE",
205+
ChangeSetName=_changeset_name,
206+
RoleArn=GetAtt(cfn_role, "Arn"),
207+
Capabilities="CAPABILITY_IAM",
208+
StackName=_stack_name,
209+
TemplatePath="{}::decrypt_oracle/transformed.yaml".format(_compiled_cfn_template),
210+
),
211+
)
212+
deploy_changeset = codepipeline.Actions(
213+
Name="Deploy",
214+
RunOrder="2",
215+
ActionTypeId=codepipeline.ActionTypeId(Category="Deploy", Owner="AWS", Version="1", Provider="CloudFormation"),
216+
Configuration=dict(
217+
ActionMode="CHANGE_SET_EXECUTE",
218+
ChangeSetName=_changeset_name,
219+
StackName=_stack_name,
220+
OutputFileName="StackOutputs.json",
221+
),
222+
OutputArtifacts=[codepipeline.OutputArtifacts(Name="AppDeploymentValues")],
223+
)
224+
deploy = codepipeline.Stages(Name="Deploy", Actions=[stage_changeset, deploy_changeset])
225+
artifact_store = codepipeline.ArtifactStore(Type="S3", Location=Ref(artifact_bucket))
226+
return codepipeline.Pipeline(
227+
"{}Pipeline".format(APPLICATION_NAME),
228+
RoleArn=GetAtt(pipeline_role, "Arn"),
229+
ArtifactStore=artifact_store,
230+
Stages=[get_source, do_build, deploy],
231+
)
232+
233+
234+
def _build_template(github_owner: str, github_branch: str) -> Template:
235+
"""Build and return the pipeline template."""
236+
template = Template(Description="CI/CD pipeline for Decrypt Oracle powered by the AWS Encryption SDK for Python")
237+
github_access_token = template.add_parameter(
238+
troposphere.Parameter(
239+
"GithubPersonalToken", Type="String", Description="Personal access token for the github repo.", NoEcho=True
240+
)
241+
)
242+
application_bucket = template.add_resource(s3.Bucket("ApplicationBucket"))
243+
artifact_bucket = template.add_resource(s3.Bucket("ArtifactBucketStore"))
244+
builder_role = template.add_resource(_codebuild_role())
245+
builder = template.add_resource(_codebuild_builder(builder_role, application_bucket))
246+
# add codepipeline role
247+
pipeline_role = template.add_resource(_pipeline_role(buckets=[application_bucket, artifact_bucket]))
248+
# add cloudformation deploy role
249+
cfn_role = template.add_resource(_cloudformation_role())
250+
# add codepipeline
251+
template.add_resource(
252+
_pipeline(
253+
pipeline_role=pipeline_role,
254+
cfn_role=cfn_role,
255+
codebuild_builder=builder,
256+
artifact_bucket=artifact_bucket,
257+
github_owner=github_owner,
258+
github_branch=github_branch,
259+
github_access_token=github_access_token,
260+
)
261+
)
262+
return template
263+
264+
265+
def _stack_exists(cloudformation) -> bool:
266+
"""Determine if the stack has already been deployed."""
267+
try:
268+
cloudformation.describe_stacks(StackName=PIPELINE_STACK_NAME)
269+
270+
except ClientError as error:
271+
if error.response["Error"]["Message"] == "Stack with id {name} does not exist".format(name=PIPELINE_STACK_NAME):
272+
return False
273+
raise
274+
275+
else:
276+
return True
277+
278+
279+
def _update_existing_stack(cloudformation, template: Template, github_token: str) -> None:
280+
"""Update a stack."""
281+
_LOGGER.info("Updating existing stack")
282+
283+
# 3. update stack
284+
cloudformation.update_stack(
285+
StackName=PIPELINE_STACK_NAME,
286+
TemplateBody=template.to_json(),
287+
Parameters=[dict(ParameterKey="GithubPersonalToken", ParameterValue=github_token)],
288+
Capabilities=["CAPABILITY_IAM"],
289+
)
290+
_LOGGER.info("Waiting for stack update to complete...")
291+
waiter = cloudformation.get_waiter("stack_update_complete")
292+
waiter.wait(StackName=PIPELINE_STACK_NAME, WaiterConfig=WAITER_CONFIG)
293+
_LOGGER.info("Stack update complete!")
294+
295+
296+
def _deploy_new_stack(cloudformation, template: Template, github_token: str) -> None:
297+
"""Deploy a new stack."""
298+
_LOGGER.info("Bootstrapping new stack")
299+
300+
# 2. deploy template
301+
cloudformation.create_stack(
302+
StackName=PIPELINE_STACK_NAME,
303+
TemplateBody=template.to_json(),
304+
Parameters=[dict(ParameterKey="GithubPersonalToken", ParameterValue=github_token)],
305+
Capabilities=["CAPABILITY_IAM"],
306+
)
307+
_LOGGER.info("Waiting for stack to deploy...")
308+
waiter = cloudformation.get_waiter("stack_create_complete")
309+
waiter.wait(StackName=PIPELINE_STACK_NAME, WaiterConfig=WAITER_CONFIG)
310+
_LOGGER.info("Stack deployment complete!")
311+
312+
313+
def _deploy_or_update_template(template: Template, github_token: str) -> None:
314+
"""Update a stack, deploying a new stack if nothing exists yet."""
315+
cloudformation = boto3.client("cloudformation")
316+
317+
if _stack_exists(cloudformation):
318+
return _update_existing_stack(cloudformation=cloudformation, template=template, github_token=github_token)
319+
320+
return _deploy_new_stack(cloudformation=cloudformation, template=template, github_token=github_token)
321+
322+
323+
def _setup_logging() -> None:
324+
"""Set up logging."""
325+
logging.basicConfig(level=logging.INFO)
326+
327+
328+
def main(args=None):
329+
"""Entry point for CLI."""
330+
_setup_logging()
331+
332+
parser = argparse.ArgumentParser(description="Pipeline deployer")
333+
parser.add_argument("--github-user", required=True, help="What Github user should be used?")
334+
parser.add_argument("--github-branch", required=False, default="master", help="What Github branch should be used?")
335+
336+
parsed = parser.parse_args(args)
337+
338+
access_token = getpass.getpass("Github personal token:")
339+
340+
template = _build_template(github_owner=parsed.github_user, github_branch=parsed.github_branch)
341+
_deploy_or_update_template(template=template, github_token=access_token)
342+
343+
344+
if __name__ == "__main__":
345+
main()

decrypt_oracle/setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ force_grid_wrap = 0
3939
combine_as_imports = True
4040
not_skip = __init__.py
4141
known_first_party = aws_encryption_sdk_decryption_oracle
42-
known_third_party =aws_encryption_sdk,aws_encryption_sdk_decrypt_oracle,aws_encryption_sdk_decryption_oracle,chalice,pytest,requests,setuptools
42+
known_third_party =awacs,aws_encryption_sdk,aws_encryption_sdk_decrypt_oracle,boto3,botocore,chalice,pytest,requests,setuptools,troposphere

0 commit comments

Comments
 (0)