# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # SPDX-License-Identifier: MIT-0 # # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from aws_cdk import ( Environment, Stack, Stage, aws_codebuild as codebuild, aws_codecommit as codecommit, pipelines, ) from constructs import Construct from mlops_infra.config.constants import ( APP_PREFIX, CODE_COMMIT_REPO_NAME, PIPELINE_BRANCH, DEFAULT_DEPLOYMENT_REGION, ) from mlops_infra.networking_stack import NetworkingStack from mlops_infra.sagemaker_studio_stack import SagemakerStudioStack class CoreStage(Stage): """ Core Stage """ def __init__(self, scope: Construct, id: str, deploy_sm_domain=True, **kwargs) -> None: super().__init__(scope, id, **kwargs) networking_stack = NetworkingStack(self, "networking", deploy_sm_domain, **kwargs) # TODO: If SM Studio is not created in the dev account, the mlops-sm-project-template Service Catalog Portfolio still expects an execution role in as SSM /mlops/role/lead which will have to be created manually and will need to have ssm:PutParameter policy if deploy_sm_domain: sagemaker_studio_stack = SagemakerStudioStack( self, "sagemaker-studio", vpc=networking_stack.primary_vpc, subnets=networking_stack.primary_vpc.private_subnets, **kwargs, ) class PipelineStack(Stack): """ Pipeline Stack Pipeline stack which provisions code pipeline for CICD deployments for the project resources. """ def __init__( self, scope: Construct, id: str, # cloud_assembly_artifact: codepipeline.Artifact, config_set: dict, **kwargs, ): super().__init__(scope, id, **kwargs) backend_repository = codecommit.Repository.from_repository_name( self, "ProjectInfraRepo", repository_name=CODE_COMMIT_REPO_NAME ) # create a CodePipeline resource which creates the following: # - source stage linked to the code commit repo listensing to the master branch update events # - build stage which runs cdk synth which generated the cloudformation resources in the pipeline artefact cloud assembly # - self mutation stage which handles pipeline changes based on latest pushed code to the repository # - assets stage which pushes zipped assets such as lambda function code, lambda layers and any other assets to a managed s3 bucket (the bucket that was created as part of cdk toolkit stack) pipeline = pipelines.CodePipeline( self, "Pipeline", self_mutation=True, cross_account_keys=True, pipeline_name=f"{APP_PREFIX}-infra-{PIPELINE_BRANCH}-{config_set['SET_NAME']}", docker_enabled_for_synth=True, docker_enabled_for_self_mutation=True, synth=pipelines.ShellStep( # build stage in code pipeline "Synth", input=pipelines.CodePipelineSource.code_commit(repository=backend_repository, branch=PIPELINE_BRANCH), commands=[ "npm install -g aws-cdk", "pip install -r requirements.txt", "cdk synth --no-lookups", ], ), ) pipeline.add_wave( "SecurityEvaluation", post=[ pipelines.CodeBuildStep( "cfn-nag", commands=[], input=pipeline.synth, primary_output_directory="./report", partial_build_spec=codebuild.BuildSpec.from_object( { "version": 0.2, "env": { "shell": "bash", "variables": { "TemplateFolder": "./**/*.template.json", "FAIL_BUILD": "false", }, }, "phases": { "install": { "runtime-versions": {"ruby": 3.1}, "commands": [ "export date=`date +%Y-%m-%dT%H:%M:%S.%NZ`", "echo Installing cfn_nag - `pwd`", "gem install cfn-nag", "echo cfn_nag installation complete `date`", ], }, "build": { "commands": [ "echo Starting cfn scanning `date` in `pwd`", "echo 'RulesToSuppress:\n- id: W58\n reason: W58 is an warning raised due to Lambda functions require permission to write CloudWatch Logs, although the lambda role contains the policy that support these permissions cgn_nag continues to through this problem (https://github.com/stelligent/cfn_nag/issues/422)' > cfn_nag_ignore.yml", # this is temporary solution to an issue with W58 rule with cfn_nag 'mkdir report || echo "dir report exists"', "SCAN_RESULT=$(cfn_nag_scan --fail-on-warnings --deny-list-path cfn_nag_ignore.yml --input-path ${TemplateFolder} -o json > ./report/cfn_nag.out.json && echo OK || echo FAILED)", "echo Completed cfn scanning `date`", "echo $SCAN_RESULT", "echo $FAIL_BUILD", """if [[ "$FAIL_BUILD" = "true" && "$SCAN_RESULT" = "FAILED" ]]; then printf "\n\nFailiing pipeline as possible insecure configurations were detected\n\n" && exit 1; fi""", ] }, }, } ), ) ], ) # add dev stage for resources that we want to deploy in this account and potentially in other accounts as well dev_stage = pipeline.add_stage( CoreStage( self, "dev", deploy_sm_domain="True", # config_set["CREATE_SM_DOMAIN"] == env=Environment(account=config_set["DEV_ACCOUNT"], region=DEFAULT_DEPLOYMENT_REGION), ) ) # add preprod stage for resources that we want to deploy in this account and potentially in other accounts as well preprod_stage = pipeline.add_stage( CoreStage( self, "preprod", deploy_sm_domain=False, env=Environment(account=config_set["PREPROD_ACCOUNT"], region=DEFAULT_DEPLOYMENT_REGION), ) ) # add prod stage for resources that we want to deploy in this account and potentially in other accounts as well prod_stage = pipeline.add_stage( CoreStage( self, "prod", deploy_sm_domain=False, env=Environment(account=config_set["PROD_ACCOUNT"], region=DEFAULT_DEPLOYMENT_REGION), ) )