# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Amazon Software License (the "License"). # You may not use this file except in compliance with the License. # A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # # or in the "license" file accompanying this file. This file is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either # express or implied. See the License for the specific language governing # permissions and limitations under the License. Description: "(SO0046) - The AWS CloudFormation template for deployment of the %%SOLUTION_NAME%%. Version %%VERSION%%" Parameters: CodeCommitRepository: Description: Enter the repository name that will be created Type: String AllowedPattern : '[a-zA-Z0-9-]+' Default: smart-product-reference-architecture API: Description: Solution API deployment Type: String AllowedValues: - 'true' - 'false' Default: 'true' Defender: Description: Solution IoT Defender deployment Type: String AllowedValues: - 'true' - 'false' Default: 'true' Event: Description: Solution Event deployment Type: String AllowedValues: - 'true' - 'false' Default: 'true' JITR: Description: Solution Just-In-Time-Registration deployment Type: String AllowedValues: - 'true' - 'false' Default: 'true' WebApp: Description: Solution Web Console deploymemt Type: String AllowedValues: - 'true' - 'false' Default: 'true' Telemetry: Description: Solution Telemetry deployment Type: String AllowedValues: - 'true' - 'false' Default: 'true' Version: Description: Solution Version Type: String Default: v0.0.1 Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Pipeline Configuration Parameters: - CodeCommitRepository - Label: default: Solution Feature Configuration Parameters: - API - Defender - Event - JITR - WebApp - Telemetry - Version ParameterLabels: CodeCommitRepository: default: CodeCommit Repository Name WebApp: default: Web Console Version: default: Solution Version Mappings: BuildEnv: Images: Standard3: "aws/codebuild/standard:3.0" CDK: StackName: "SmartProductSolutionStack" Metrics: SendAnonymousUsage: 'true' SourceCode: Source: S3Bucket: "%%BUCKET_NAME%%" KeyPrefix: "%%SOLUTION_NAME%%/%%VERSION%%" Resources: # CICD resources [start] DevOutputBucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Properties: PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True Metadata: cfn_nag: rules_to_suppress: - id: W35 reason: "validated and does not require access logging to be configured." - id: W41 reason: "validated and does not require to encrypt the bucket." - id: W51 reason: "The dev output bucket does not require bucket policy because the bucket is only accessed by CodeBuild and CodePipeline." SmartProductRepo: Type: AWS::CodeCommit::Repository DeletionPolicy: Retain Properties: RepositoryName: !Ref CodeCommitRepository RepositoryDescription: smart product reference architecture ArtifactS3Bucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Properties: PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True VersioningConfiguration: Status: Enabled Metadata: cfn_nag: rules_to_suppress: - id: W35 reason: "validated and does not require access logging to be configured." - id: W41 reason: "validated and does not require to encrypt the bucket." - id: W51 reason: "The artifact bucket does not require bucket policy because the bucket is only accessed by CodeBuild and CodePipeline." DevCodePipeline: Type: AWS::CodePipeline::Pipeline DependsOn: CICDCustomResource Properties: Name: !Sub ${CodeCommitRepository} RoleArn: !Sub ${CodePipelineRole.Arn} ArtifactStore: Type: S3 Location: !Ref ArtifactS3Bucket Stages: - Name: Source Actions: - Name: CodeCommit ActionTypeId: Category: Source Owner: AWS Version: '1' Provider: CodeCommit OutputArtifacts: - Name: SourceApp Configuration: RepositoryName: !Ref CodeCommitRepository BranchName: master PollForSourceChanges: false - Name: Build Actions: - Name: AssetBuild InputArtifacts: - Name: SourceApp ActionTypeId: Category: Build Owner: AWS Version: '1' Provider: CodeBuild OutputArtifacts: - Name: BuiltApp Configuration: ProjectName: !Ref CodeBuild - Name: Deploy Actions: - Name: CDKDeploy InputArtifacts: - Name: BuiltApp ActionTypeId: Category: Build Owner: AWS Version: '1' Provider: CodeBuild Configuration: ProjectName: !Ref CodeBuildCDK # CICD resources [end] # CodeBuild projects [start] CodeBuild: Type: AWS::CodeBuild::Project Properties: Name: !Sub ${CodeCommitRepository} Description: 'build project for smart product' ServiceRole: !Sub ${CodeBuildRole.Arn} EncryptionKey: !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3 Source: Type: CODEPIPELINE BuildSpec: | version: 0.2 phases: install: runtime-versions: nodejs: 12 commands: - echo "This buildspec is based on image - aws/codebuild/standard:2.0" - node -v - apt-get update - apt-get install jq pre_build: commands: - echo "Installing dependencies and executing unit tests - `pwd`" - cd deployment && chmod +x ./run-unit-tests.sh && ./run-unit-tests.sh - echo "Installing dependencies and executing unit tests completed `date`" build: commands: - echo "Starting build `date` in `pwd`" - VERSION=`jq -r .default.version ./custom-deployment/cdk-manifest.json` - echo "Deployment version $VERSION" - chmod +x ./build-s3-dist.sh && ./build-s3-dist.sh $BUILD_OUTPUT_BUCKET $SOLUTION_NAME $VERSION - echo "Build completed `date`" post_build: commands: - echo "Copying S3 assets to /$BUILD_OUTPUT_BUCKET/$SOLUTION_NAME/$VERSION" - aws s3 cp ./global-s3-assets/ s3://$BUILD_OUTPUT_BUCKET/$SOLUTION_NAME/$VERSION/ --recursive --acl bucket-owner-full-control - aws s3 cp ./regional-s3-assets/ s3://$BUILD_OUTPUT_BUCKET/$SOLUTION_NAME/$VERSION/ --recursive --acl bucket-owner-full-control - echo "Copying assets to output bucket complete `date`" artifacts: files: - deployment/**/* - source/**/* - LICENSE.txt - NOTICE.txt - README.md - CODE_OF_CONDUCT.md - CONTRIBUTING.md - CHANGELOG.md - .github/PULL_REQUEST_TEMPLATE.md - buildspec.yml Environment: ComputeType: BUILD_GENERAL1_SMALL Image: !FindInMap [ BuildEnv, Images, Standard3 ] Type: LINUX_CONTAINER EnvironmentVariables: - Name: BUILD_OUTPUT_BUCKET Value: !Ref DevOutputBucket - Name: SOLUTION_NAME Value: "%%SOLUTION_NAME%%" Artifacts: Name: !Sub ${CodeCommitRepository} Type: CODEPIPELINE CodeBuildCDK: Type: AWS::CodeBuild::Project Properties: Name: !Sub ${CodeCommitRepository}-CDK Description: 'CDK project to deploy custom components for smart product' ServiceRole: !Sub ${CodeBuildRole.Arn} EncryptionKey: !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3 Source: Type: CODEPIPELINE BuildSpec: | version: 0.2 phases: install: runtime-versions: nodejs: 12 ruby: 2.6 commands: - echo "This buildspec is based on image - aws/codebuild/standard:2.0" - node --version - npm install -g cdk@1.22.0 - cdk --version - echo "Installing cfn_nag" - gem install cfn-nag - cfn_nag --version build: commands: - echo "Installing CDK dependencies - `pwd`" - cd deployment/custom-deployment && npm install - echo "bootstrap CDK environment `date`" - ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -f5 -d:) - cdk bootstrap aws://$ACCOUNT_ID/$AWS_REGION --toolkit-stack-name SmartProductCDKToolkit - echo "Running CDK synth and cfn_nag to validate template" - cdk synth - cfn_nag_scan --input-path ./cdk.out/SmartProductSolutionStack.template.json - echo "Running CDK app `date`" - cdk deploy --toolkit-stack-name SmartProductCDKToolkit --require-approval never --verbose artifacts: files: - deployment/**/* - source/**/* - LICENSE.txt - NOTICE.txt - README.md - CODE_OF_CONDUCT.md - CONTRIBUTING.md - .github/PULL_REQUEST_TEMPLATE.md - buildspec.yml Environment: ComputeType: BUILD_GENERAL1_SMALL Image: !FindInMap [ BuildEnv, Images, Standard3 ] Type: LINUX_CONTAINER EnvironmentVariables: - Name: BUILD_OUTPUT_BUCKET Value: !Ref DevOutputBucket - Name: MAIN_STACK_NAME Value: !FindInMap [BuildEnv, CDK, StackName] Artifacts: Name: CodeBuild-CDK-output Type: CODEPIPELINE # CodeBuild projects [end] # CICD helper resources [start] CICDHelper: Type: "AWS::Lambda::Function" Properties: Description: "Smart Product Solution CICD helper" Code: S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "Source", "S3Bucket"], Ref: "AWS::Region"]] S3Key: !Join ["/", [!FindInMap ["SourceCode", "Source", "KeyPrefix"], "smart-product-cicd.zip"]] Handler: index.handler Runtime: nodejs12.x Role: !GetAtt CICDHelperRole.Arn Timeout: 300 MemorySize: 256 Environment: Variables: CODE_BUCKET: !Join ["", [!FindInMap ["SourceCode", "Source", "S3Bucket"], "-", !Ref "AWS::Region"]] CODE_KEY: !FindInMap ["SourceCode", "Source", "KeyPrefix"] CODE_SOURCE: "smart-product-solution.zip" CODECOMMIT_REPO: !Sub ${SmartProductRepo.Name} LOGGING_LEVEL: 2 CICDCustomResource: Type: "Custom::CreateCommit" DependsOn: CICDHelperPolicy Properties: ServiceToken: !GetAtt CICDHelper.Arn CDKManifestGenerator: Type: "Custom::ManifestGenerator" DependsOn: CICDCustomResource Properties: ServiceToken: !GetAtt CICDHelper.Arn API: !Ref API DEFENDER: !Ref Defender EVENT: !Ref Event JITR: !Ref JITR WEB_APP: !Ref WebApp TELEMETRY: !Ref Telemetry VERSION: !Ref Version ANONYMOUS_METRICS: !FindInMap [BuildEnv, Metrics, SendAnonymousUsage] CICDDeleteStack: Type: "Custom::DeleteStack" DependsOn: CICDHelperPolicy Properties: ServiceToken: !GetAtt CICDHelper.Arn STACK_NAME: !FindInMap [BuildEnv, CDK, StackName] # Custom resource [end] # IAM resources [start] CICDHelperRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" CICDHelperPolicy: Type: "AWS::IAM::ManagedPolicy" Metadata: cfn_nag: rules_to_suppress: - id: W13 reason: "Reviewed, actions split per service, and which cannot be applied at resource level. Because of CDK deployment we do not know the exact resource ARN" - id: F5 reason: "Reviewed, need permissions to delete various IAM resources" Properties: Description: "Policy for the Smart Product CICD Lambda function." PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: - !Join ["", ["arn:aws:logs:", Ref: "AWS::Region", ":", Ref: "AWS::AccountId", ":log-group:/aws/lambda/", !Ref CICDHelper, ":*"]] - Effect: "Allow" Action: - "codecommit:CreateCommit" - "codecommit:PutFile" - "codecommit:GetBranch" Resource: - !Sub ${SmartProductRepo.Arn} - Effect: "Allow" Action: - "apigateway:Delete*" Resource: - !Sub "arn:aws:apigateway:${AWS::Region}::*" - Effect: "Allow" Action: - "cloudformation:DeleteStack" - "cloudformation:Describe*" - "cloudformation:Delete*" Resource: - !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:*" - Effect: "Allow" Action: - "cognito-idp:DeleteUser" - "cognito-idp:DeleteUserPool" - "cognito-idp:DeleteUserPoolClient" Resource: - !Sub "arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:*" - Effect: "Allow" Action: - "dynamodb:DeleteTable" - "dynamodb:Describe*" Resource: - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:*" - Effect: "Allow" Action: - "iam:Delete*" - "iam:Describe*" - "iam:DetachRolePolicy" Resource: - !Sub "arn:aws:iam::${AWS::AccountId}:*" - Effect: "Allow" Action: - "iot:DeleteTopicRule" - "iotanalytics:DeleteChannel" - "iotanalytics:DeleteDataset" - "iotanalytics:DeleteDatastore" - "iotanalytics:DeletePipeline" - "iotanalytics:Describe*" Resource: - !Sub "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:*" - !Sub "arn:aws:iotanalytics:${AWS::Region}:${AWS::AccountId}:*" - Effect: "Allow" Action: - "lambda:DeleteFunction" - "lambda:InvokeFunction" - "lambda:RemovePermission" Resource: - !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:*" - Effect: "Allow" Action: - "sns:DeleteTopic" - "sns:GetTopicAttributes" Resource: - !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:*" - Effect: "Allow" Action: - "s3:DeleteBucketPolicy" Resource: - !Sub "arn:aws:s3:::*" - Effect: "Allow" Action: - "cloudfront:Get*" - "cloudfront:GetCloudFrontOriginAccessIdentityConfig" - "cloudfront:Delete*" - "cloudfront:UpdateDistribution" Resource: - "*" - Effect: "Allow" Action: - "s3:GetObject" Resource: - !Join ["", ["arn:aws:s3:::", !FindInMap ["SourceCode", "Source", "S3Bucket"], "-", !Ref "AWS::Region", "/", !FindInMap ["SourceCode", "Source", "KeyPrefix"], "/smart-product-solution.zip"]] Roles: - Ref: "CICDHelperRole" CodePipelineRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: | { "Statement": [{ "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "codepipeline.amazonaws.com" } }] } Policies: - PolicyName: !Sub ${CodeCommitRepository}-codepipeline PolicyDocument: !Sub | { "Statement": [ { "Action": [ "s3:GetBucketVersioning" ], "Resource": [ "arn:aws:s3:::${ArtifactS3Bucket}" ], "Effect": "Allow" }, { "Action": [ "s3:PutObject", "s3:GetObject", "S3:GetObjectVersion" ], "Resource": [ "arn:aws:s3:::${ArtifactS3Bucket}/*" ], "Effect": "Allow" }, { "Action": [ "codecommit:BatchGetRepositories", "codecommit:UploadArchive", "codecommit:Get*", "codecommit:List*", "codecommit:GitPull" ], "Resource": "arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${CodeCommitRepository}", "Effect": "Allow" }, { "Action": [ "codebuild:BatchGetBuilds", "codebuild:StartBuild" ], "Resource": [ "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${CodeBuild}", "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${CodeBuildCDK}" ], "Effect": "Allow" } ] } CodeBuildRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: | { "Statement": [{ "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "codebuild.amazonaws.com" } }] } Policies: - PolicyName: !Sub ${CodeCommitRepository}-codebuild PolicyDocument: !Sub | { "Statement": [ { "Effect": "Allow", "Resource": [ "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*" ], "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ] }, { "Effect": "Allow", "Resource": [ "arn:aws:s3:::${ArtifactS3Bucket}/*", "arn:aws:s3:::${DevOutputBucket}/*" ], "Action": [ "s3:GetObject", "s3:GetObjectVersion", "s3:PutObject" ] }, { "Effect": "Allow", "Resource": "arn:aws:apigateway:${AWS::Region}::*", "Action": [ "apigateway:Post", "apigateway:Get*", "apigateway:Put", "apigateway:Patch", "apigateway:Create", "apigateway:Delete" ] }, { "Effect": "Allow", "Resource": "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:*", "Action": [ "cloudformation:GetTemplate", "cloudformation:DescribeStacks", "cloudformation:DescribeStackEvents", "cloudformation:CreateChangeSet", "cloudformation:ExecuteChangeSet", "cloudformation:DescribeChangeSet", "cloudformation:CreateStack", "cloudformation:Delete*" ] }, { "Effect": "Allow", "Resource": "arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:*", "Action": [ "cognito-idp:CreateUser", "cognito-idp:CreateUserPoolClient", "cognito-idp:DeleteUser", "cognito-idp:DeleteUserPool", "cognito-idp:DeleteUserPoolClient" ] }, { "Effect": "Allow", "Resource": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:*", "Action": [ "dynamodb:CreateTable", "dynamodb:DescribeTable", "dynamodb:DeleteTable" ] }, { "Effect": "Allow", "Resource": "arn:aws:iam::${AWS::AccountId}:*", "Action": [ "iam:CreateRole", "iam:GetRole", "iam:DeleteRole", "iam:GetRolePolicy", "iam:PutRolePolicy", "iam:DeleteRolePolicy", "iam:PassRole", "iam:DetachRolePolicy", "iam:AttachRolePolicy" ] }, { "Effect": "Allow", "Resource": "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:*", "Action": [ "iot:CreateTopicRule", "iot:ReplaceTopicRule", "iot:DeleteTopicRule", "iot:GetTopicRule" ] }, { "Effect": "Allow", "Resource": "arn:aws:iotanalytics:${AWS::Region}:${AWS::AccountId}:*", "Action": [ "iotanalytics:CreateChannel", "iotanalytics:DescribeChannel", "iotanalytics:CreateDataset", "iotanalytics:DescribeDataset", "iotanalytics:CreateDatastore", "iotanalytics:DescribeDatastore", "iotanalytics:CreatePipeline", "iotanalytics:UpdatePipeline", "iotanalytics:DescribePipeline", "iotanalytics:ListTagsForResource", "iotanalytics:DeleteChannel", "iotanalytics:DeleteDataset", "iotanalytics:DeleteDatastore", "iotanalytics:DeletePipeline" ] }, { "Effect": "Allow", "Resource": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:*", "Action": [ "lambda:CreateFunction", "lambda:AddPermission", "lambda:GetFunction", "lambda:GetFunctionConfiguration", "lambda:InvokeFunction", "lambda:RemovePermission", "lambda:DeleteFunction", "lambda:UpdateFunctionCode" ] }, { "Effect": "Allow", "Resource": "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:*", "Action": [ "sns:DeleteTopic", "sns:CreateTopic", "sns:GetTopicAttributes", "sns:ListTopics" ] }, { "Effect": "Allow", "Resource": [ "arn:aws:s3:::*" ], "Action": [ "s3:CreateBucket", "s3:Put*", "s3:Get*", "s3:List*", "s3:DeleteBucketPolicy" ] }, { "Effect": "Allow", "Resource": "*", "Action": [ "cognito-idp:CreateUserPool", "cloudfront:UpdateDistribution", "cloudfront:Create*", "cloudfront:Get*", "cloudfront:Delete*", "cloudfront:TagResource" ] } ] } AmazonCloudWatchEventRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - events.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: cwe-pipeline-execution PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: codepipeline:StartPipelineExecution Resource: !Join [ '', [ 'arn:aws:codepipeline:', !Ref 'AWS::Region', ':', !Ref 'AWS::AccountId', ':', !Ref DevCodePipeline ] ] # IAM resources [start] # CloudWatch resources [start] AmazonCloudWatchEventRule: Type: AWS::Events::Rule Properties: EventPattern: source: - aws.codecommit detail-type: - 'CodeCommit Repository State Change' resources: - !Join [ '', [ 'arn:aws:codecommit:', !Ref 'AWS::Region', ':', !Ref 'AWS::AccountId', ':', !Ref CodeCommitRepository ] ] detail: event: - referenceCreated - referenceUpdated referenceType: - branch referenceName: - master Targets: - Arn: !Join [ '', [ 'arn:aws:codepipeline:', !Ref 'AWS::Region', ':', !Ref 'AWS::AccountId', ':', !Ref DevCodePipeline ] ] RoleArn: !GetAtt AmazonCloudWatchEventRole.Arn Id: codepipeline-smart-product Outputs: SolutionPipeline: Description: "Pipeline created for this solution." Value: !Sub ${CodeCommitRepository} PipelineBuildImage: Description: "Image used to build this pipeline" Value: !FindInMap [ BuildEnv, Images, Standard3 ] PipelineS3Artifacts: Description: "Bucket for CodePipeline to store build artifacts" Value: !Ref ArtifactS3Bucket DistArtifactBucket: Description: "Bucket prefix that custom CFN templates will reference for artifacts (template will append [region-name] to this prefix)" Value: !Ref DevOutputBucket