├── .gitignore ├── LICENSE.md ├── README.md ├── docs ├── app-overview.png ├── codepipeline.png └── pipeline-overview.png ├── gulpfile.js ├── package.json └── pipeline ├── cfn ├── api-gateway.json ├── app.json ├── dynamodb.json ├── iam.json ├── master.json ├── pipeline.json └── site.json ├── index.js ├── lambda ├── index.js └── package.json └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | dist.zip 4 | .idea/ 5 | *.iml 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2020 Stelligent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | This project deploys [dromedary](https://github.com/stelligent/dromedary) in AWS Lambda with API Gateway as the interface to demonstrate serverless architecture. It also demonstrates the use of CodePipeline with Lambdas to continuously deliver changes made in the source code in a serverless manner. We've also written a three-part blog series on the topic here: [Serverless Delivery: Architecture (Part 1)](http://www.stelligent.com/agile/serverless-delivery-architecture-part-1/), [Serverless Delivery: Bootstrapping the Pipeline (Part 2)](http://www.stelligent.com/agile/serverless-delivery-bootstrapping-the-pipeline-part-2/) and [Serverless Delivery: Orchestrating the Pipeline (Part 3)](http://www.stelligent.com/agile/serverless-delivery-orchestrating-the-pipeline-part-3/). 3 | 4 | # Architecture Overview 5 | The application is split into 2 separate parts for deployment: 6 | 7 | * **API** - deployed as a Lambda function using API Gateway for the front end. 8 | * **Static Content** - deployed into an S3 bucket with website hosting enabled. 9 | 10 | Additionally, a `config.json` file is generated and deployed into the S3 bucket containing the endpoint to use for the API in API Gateway. 11 | 12 | ![app-overview](docs/app-overview.png) 13 | 14 | # Pipeline Overview 15 | The pipeline consists of the following steps: 16 | 17 | * **commit** - a commit in GitHub triggers a new CodePipeline job. The source is downloaded from GitHub and then pushed into an S3 bucket as a zip file. 18 | * **npm lambda** - a lambda is executed that downloads the source zip file, runs `npm install` to get he dependencies and then uploads the source+dependencies to S3 as a tarball. 19 | * **gulp lambda(s)** - a lambda is executed that downloads the source+dependencies tarball from S3, extracts it, then runs a gulp task 20 | 21 | The details of what happens in the gulp task is completely owned by the `gulpfile.js` in the source code. This provides decoupling of the pipeline from the app and allows the pipeline template to be used by any gulp project. 22 | 23 | ![pipeline-overview](docs/pipeline-overview.png) 24 | 25 | Here's a sample of what the pipeline looks like in AWS CodePipeline console: 26 | 27 | ![pipeline-example](docs/codepipeline.png) 28 | 29 | 30 | # Launching Pipeline 31 | 32 | To integrate with GitHub, AWS CodePipeline uses OAuth tokens. Generate your token at [GitHub](https://github.com/settings/tokens) and ensure you enable the following two scopes: 33 | * `admin:repo_hook`, which is used to detect when you have committed and pushed changes to the repository 34 | * `repo`, which is used to read and pull artifacts from public and private repositories into a pipeline 35 | 36 | You can launch via the console: [![Launch Pipeline stack](https://s3.amazonaws.com/stelligent-training-public/public/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#cstack=sn~dromedary-serverless|turl~https://s3-us-west-2.amazonaws.com/dromedary-serverless/master.json) 37 | 38 | Or you can launch by using `gulp` in this repo: 39 | 40 | * **PREREQUISITES -** You need Node.js installed. 41 | * Install Node.js: `sudo yum install nodejs npm --enablerepo=epel` (For OS X, check out [nodejs.org](https://nodejs.org/en/download/)) 42 | * Update NPM: `curl -L https://npmjs.org/install.sh | sudo sh` 43 | * Install Gulp: `sudo npm install -g gulp` 44 | * Download this repo and then run `npm install` first to install all dependent modules. 45 | * Bring the pipeline up with `gulp pipeline:up --token=XXXXXXXXXXXXXXXXX` 46 | * You can run `gulp pipeline:wait` to wait for the stack to come up, and then `gulp pipeline:status` to get the outputs and `gulp pipeline:stacks` to see what applicaiton stacks the pipeline has currently running. 47 | * To tear everything down, run `gulp pipeline:teardown` 48 | * By default, the stack name will be **dromedary-serverless**. You can change this by passing `--stackName=my-stack-name` to any of the above gulp commands. 49 | 50 | # Development 51 | To publish pipeline template/lambda changes, run `gulp publish`. You may want to choose a different bucket to publish the templates and lambda code to via the `--templateBucket` argument. 52 | 53 | # Todo 54 | * Tighten up IAM policies in CFN 55 | -------------------------------------------------------------------------------- /docs/app-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stelligent/dromedary-serverless/c2cf953c09b8d24c69134487ef25615b846e38ef/docs/app-overview.png -------------------------------------------------------------------------------- /docs/codepipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stelligent/dromedary-serverless/c2cf953c09b8d24c69134487ef25615b846e38ef/docs/codepipeline.png -------------------------------------------------------------------------------- /docs/pipeline-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stelligent/dromedary-serverless/c2cf953c09b8d24c69134487ef25615b846e38ef/docs/pipeline-overview.png -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var gulp = require('gulp'); 4 | var gutil = require('gulp-util'); 5 | var gcallback = require('gulp-callback'); 6 | var install = require('gulp-install'); 7 | var zip = require('gulp-zip'); 8 | var del = require('del'); 9 | var AWS = require('aws-sdk'); 10 | var fs = require('fs'); 11 | var runSequence = require('run-sequence'); 12 | 13 | 14 | var opts = { 15 | region: (gutil.env.region || 'us-west-2'), 16 | stackName: (gutil.env.stackName || 'dromedary-serverless'), 17 | cfnBucket: (gutil.env.templateBucket || 'serverless-pipeline'), 18 | testSiteFQDN: 'drom-test.elasticoperations.com', 19 | prodSiteFQDN: 'drom-prod.elasticoperations.com', 20 | hostedZoneId: 'Z3809G91N7QZJE', //TODO: get this programatically 21 | distSitePath: 'dist/site.zip', 22 | distLambdaPath: 'dist/lambda.zip', 23 | distSwaggerPath: 'dist/swagger.json', 24 | gulpTestTask: 'test-functional', 25 | gulpPackageTask: 'package', 26 | githubToken: gutil.env.token, 27 | githubUser: 'stelligent', 28 | githubRepo: 'dromedary', 29 | githubBranch: 'master' 30 | } 31 | var util = require('./pipeline/util.js') 32 | var gpipeline = require('./pipeline') 33 | gpipeline.registerTasks(gulp,opts); 34 | 35 | var lambda = new AWS.Lambda(); 36 | var s3 = new AWS.S3(); 37 | var dist = 'dist'; 38 | 39 | 40 | gulp.task('lambda:clean', function(cb) { 41 | return del([dist],{force: true}, cb); 42 | }); 43 | 44 | gulp.task('lambda:js', function() { 45 | return gulp.src([__dirname+'/pipeline/lambda/index.js']) 46 | .pipe(gulp.dest(dist+'/lambda/')); 47 | }); 48 | 49 | gulp.task('lambda:install', function() { 50 | return gulp.src(__dirname+'/pipeline/lambda/package.json') 51 | .pipe(gulp.dest(dist+'/lambda/')) 52 | .pipe(install({production: true})); 53 | }); 54 | 55 | gulp.task('lambda:zip', ['lambda:js','lambda:install'], function() { 56 | return gulp.src(['!'+dist+'/lambda/package.json','!'+dist+'/**/aws-sdk{,/**}',dist+'/lambda/**/*']) 57 | .pipe(zip('pipeline-lambda.zip')) 58 | .pipe(gulp.dest(dist)); 59 | }); 60 | 61 | gulp.task('lambda:upload', ['lambda:gulpUpload', 'lambda:npmUpload']); 62 | 63 | gulp.task('lambda:gulpUpload', ['lambda:zip'], function() { 64 | return uploadLambda('CodePipelineGulpLambdaArn'); 65 | }); 66 | gulp.task('lambda:deployUpload', ['lambda:zip'], function() { 67 | return uploadLambda('CodePipelineDeployLambdaArn'); 68 | }); 69 | gulp.task('lambda:npmUpload', ['lambda:zip'], function() { 70 | return uploadLambda('CodePipelineNpmLambdaArn'); 71 | }); 72 | 73 | 74 | // Tasks to provision the pipeline 75 | gulp.task('cfn:templatesBucket', function(cb) { 76 | s3.headBucket({ Bucket: opts.cfnBucket }, function(err, data) { 77 | if (err) { 78 | if(err.statusCode == 404) { 79 | s3.createBucket({ 80 | Bucket: opts.cfnBucket, 81 | CreateBucketConfiguration: { 82 | LocationConstraint: opts.region 83 | } 84 | }, function(err, data) { 85 | if (err) { 86 | cb(err); 87 | } else { 88 | console.log('Created bucket: '+opts.cfnBucket); 89 | cb(); 90 | } 91 | }); 92 | } else { 93 | cb(err); 94 | } 95 | } else { 96 | console.log('Bucket already exists:'+ opts.cfnBucket); 97 | cb(); 98 | } 99 | }); 100 | }); 101 | 102 | gulp.task('cfn:templates',['cfn:templatesBucket'], function() { 103 | return util.uploadToS3(__dirname+'/pipeline/cfn',opts.cfnBucket); 104 | }); 105 | gulp.task('lambda:uploadS3', ['lambda:zip','cfn:templatesBucket'], function(cb) { 106 | var path = dist+'/pipeline-lambda.zip'; 107 | var params = { 108 | Bucket: opts.cfnBucket, 109 | Key: 'pipeline-lambda.zip', 110 | ACL: 'public-read', 111 | Body: fs.readFileSync(path) 112 | } 113 | 114 | s3.putObject(params, function(err, data) { 115 | if (err) { 116 | cb(err); 117 | } else { 118 | cb(); 119 | } 120 | }); 121 | }); 122 | 123 | gulp.task('publish',['cfn:templates','lambda:uploadS3'], function() { 124 | }); 125 | 126 | gulp.task('launch',['publish'], function(callback) { 127 | runSequence('pipeline:up',callback); 128 | }); 129 | 130 | function uploadLambda(lambdaArnOutputKey) { 131 | return util.getSubStackOutput(opts.stackName,'PipelineStack',lambdaArnOutputKey) 132 | .then(function(pipelineFunctionArn) { 133 | var params = { 134 | FunctionName: pipelineFunctionArn, 135 | Publish: true, 136 | ZipFile: fs.readFileSync(dist + '/pipeline-lambda.zip') 137 | }; 138 | 139 | console.log("About to update function..." + pipelineFunctionArn); 140 | 141 | return new Promise(function (resolve, reject) { 142 | lambda.updateFunctionCode(params, function (err, data) { 143 | if (err) { 144 | reject(err); 145 | } else { 146 | console.log("Updated lambda to version: " + data.Version); 147 | resolve(); 148 | } 149 | }); 150 | }); 151 | }); 152 | } 153 | 154 | 155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dromedary-serverless", 3 | "version": "0.1.1", 4 | "description": "Deploy dromedary to AWS Lambda", 5 | "main": "app.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+ssh://git@github.com/stelligent/dromedary-serverless.git" 9 | }, 10 | "author": "Stelligent Systems, LLC", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/stelligent/dromedary-serverless/issues" 14 | }, 15 | "homepage": "https://github.com/stelligent/dromedary-serverless#readme", 16 | "dependencies": { 17 | "aws-sdk": "^2.2.34", 18 | "mime": "^1.3.4", 19 | "promise": "^7.1.1", 20 | "chalk": "^1.1.1" 21 | }, 22 | "devDependencies": { 23 | "del": "^1.2.1", 24 | "gulp": "^3.9.0", 25 | "gulp-callback": "0.0.3", 26 | "gulp-install": "^0.5.0", 27 | "gulp-util": "^3.0.7", 28 | "gulp-zip": "^3.1.0", 29 | "run-sequence": "^1.1.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pipeline/cfn/api-gateway.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"apigateway for the application", 4 | "Parameters":{ 5 | "BucketName":{ 6 | "Type":"String", 7 | "Description":"The name of the bucket to install lambdas from" 8 | }, 9 | "TestAppLambdaName":{ 10 | "Type":"String", 11 | "Description":"The name of the test lambda to integrate the gateway with" 12 | }, 13 | "ProdAppLambdaName":{ 14 | "Type":"String", 15 | "Description":"The name of the prod lambda to integrate the gateway with" 16 | }, 17 | "TestDDBTableName":{ 18 | "Type":"String", 19 | "Description":"The DynamoDB table name for test stage" 20 | }, 21 | "ProdDDBTableName":{ 22 | "Type":"String", 23 | "Description":"The DynamoDB table name for prod stage" 24 | }, 25 | "ApiIntegrationCredentialsRole": { 26 | "Type":"String", 27 | "Description":"Role used by api gateway for calling app lambda function" 28 | } 29 | }, 30 | "Resources" : { 31 | "RestApi": { 32 | "Type": "AWS::ApiGateway::RestApi", 33 | "Properties": { 34 | "Description": "REST Api", 35 | "Name": { 36 | "Fn::Join": [ 37 | "-", 38 | [ 39 | "Api", 40 | { 41 | "Ref": "AWS::StackName" 42 | } 43 | ] 44 | ] 45 | } 46 | } 47 | }, 48 | "RootResource": { 49 | "Type": "AWS::ApiGateway::Resource", 50 | "Properties": { 51 | "PathPart": "{subpath}", 52 | "ParentId": { 53 | "Fn::GetAtt": [ 54 | "RestApi", 55 | "RootResourceId" 56 | ] 57 | }, 58 | "RestApiId": { 59 | "Ref": "RestApi" 60 | } 61 | } 62 | }, 63 | "RootGetMethod": { 64 | "Type": "AWS::ApiGateway::Method", 65 | "Properties": { 66 | "ApiKeyRequired": false, 67 | "AuthorizationType": "NONE", 68 | "HttpMethod": "GET", 69 | "ResourceId": { 70 | "Ref": "RootResource" 71 | }, 72 | "RestApiId": { 73 | "Ref": "RestApi" 74 | }, 75 | "RequestModels": {}, 76 | "RequestParameters": { 77 | "method.request.path.subpath": true 78 | }, 79 | "MethodResponses": [ 80 | { 81 | "StatusCode": "200", 82 | "ResponseModels": {}, 83 | "ResponseParameters": { 84 | "method.response.header.Access-Control-Allow-Origin": true, 85 | "method.response.header.Access-Control-Allow-Methods": true, 86 | "method.response.header.Content-Type": true 87 | } 88 | } 89 | ], 90 | "Integration": { 91 | "Type": "AWS", 92 | "IntegrationHttpMethod": "POST", 93 | "Uri": { 94 | "Fn::Join": [ 95 | "", 96 | [ 97 | "arn:aws:apigateway:", 98 | { 99 | "Ref": "AWS::Region" 100 | }, 101 | ":lambda:path/2015-03-31/functions/", 102 | "arn:aws:lambda:", 103 | { 104 | "Ref": "AWS::Region" 105 | }, 106 | ":", 107 | { 108 | "Ref": "AWS::AccountId" 109 | }, 110 | ":function:", 111 | "${stageVariables.AppFunctionName}:${stageVariables.AppVersion}/invocations" 112 | ] 113 | ] 114 | }, 115 | "Credentials": { 116 | "Ref": "ApiIntegrationCredentialsRole" 117 | }, 118 | "RequestTemplates": { 119 | "application/json": { 120 | "Fn::Join": [ 121 | "\n", 122 | [ 123 | "{", 124 | " \"stage\": \"$context.stage\",", 125 | " \"request-id\": \"$context.requestId\",", 126 | " \"api-id\": \"$context.apiId\",", 127 | " \"resource-path\": \"$context.resourcePath\",", 128 | " \"resource-id\": \"$context.resourceId\",", 129 | " \"http-method\": \"$context.httpMethod\",", 130 | " \"source-ip\": \"$context.identity.sourceIp\",", 131 | " \"user-agent\": \"$context.identity.userAgent\",", 132 | " \"account-id\": \"$context.identity.accountId\",", 133 | " \"api-key\": \"$context.identity.apiKey\",", 134 | " \"caller\": \"$context.identity.caller\",", 135 | " \"user\": \"$context.identity.user\",", 136 | " \"user-arn\": \"$context.identity.userArn\",", 137 | " \"queryString\": \"$input.params().querystring\",", 138 | " \"headers\": \"$input.params().header\",", 139 | " \"pathParams\": \"$input.params().path\",", 140 | " \"allParams\": \"$input.params()\",", 141 | " \"ddbTableName\": \"$stageVariables.DDBTableName\"", 142 | "}" 143 | ] 144 | ] 145 | } 146 | }, 147 | "RequestParameters": { 148 | "integration.request.path.subpath": "method.request.path.subpath" 149 | }, 150 | "IntegrationResponses": [ 151 | { 152 | "StatusCode": "200", 153 | "SelectionPattern": ".*", 154 | "ResponseTemplates": { 155 | "application/json": "$util.base64Decode( $input.path('$.payload') )" 156 | }, 157 | "ResponseParameters": { 158 | "method.response.header.Access-Control-Allow-Origin": "'*'", 159 | "method.response.header.Access-Control-Allow-Methods": "'GET, OPTIONS'", 160 | "method.response.header.Content-Type": "integration.response.body.contentType" 161 | } 162 | } 163 | ] 164 | } 165 | } 166 | }, 167 | "ApiTestDeployment": { 168 | "DependsOn": [ "RootGetMethod" ], 169 | "Type" : "AWS::ApiGateway::Deployment", 170 | "Properties" : { 171 | "RestApiId": { "Ref": "RestApi" }, 172 | "StageName": "test", 173 | "StageDescription" : { 174 | "StageName": "test", 175 | "Variables": { 176 | "DDBTableName": { "Ref": "TestDDBTableName" }, 177 | "AppFunctionName": { "Ref":"TestAppLambdaName" }, 178 | "AppVersion":"test" 179 | } 180 | } 181 | } 182 | }, 183 | "ApiProdDeployment": { 184 | "DependsOn": [ "RootGetMethod" ], 185 | "Type": "AWS::ApiGateway::Deployment", 186 | "Properties": { 187 | "RestApiId": { 188 | "Ref": "RestApi" 189 | }, 190 | "StageName": "prod", 191 | "StageDescription": { 192 | "StageName": "prod", 193 | "Variables": { 194 | "DDBTableName": { 195 | "Ref": "ProdDDBTableName" 196 | }, 197 | "AppFunctionName": { 198 | "Ref": "ProdAppLambdaName" 199 | }, 200 | "AppVersion": "prod" 201 | } 202 | } 203 | } 204 | } 205 | }, 206 | "Outputs" : { 207 | "StackName":{ 208 | "Value":{ "Ref":"AWS::StackName" } 209 | }, 210 | "ApiName": { 211 | "Value": { "Ref": "RestApi"} 212 | }, 213 | "TestApiUrl": { 214 | "Value": { "Fn::Join": [ "", [ "https://", { "Ref": "RestApi"}, ".execute-api.",{"Ref":"AWS::Region"},".amazonaws.com/test"] ] } 215 | }, 216 | "ProdApiUrl": { 217 | "Value": { "Fn::Join": [ "", [ "https://", { "Ref": "RestApi"}, ".execute-api.",{"Ref":"AWS::Region"},".amazonaws.com/prod"] ] } 218 | } 219 | } 220 | } -------------------------------------------------------------------------------- /pipeline/cfn/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"lambda for the application", 4 | "Parameters":{ 5 | "AppRole":{ 6 | "Type":"String", 7 | "Description":"The role for the function to run as." 8 | }, 9 | "AppTimeout":{ 10 | "Type":"Number", 11 | "Description":"The name to use for the app lambda function.", 12 | "Default": 10 13 | }, 14 | "AppMemorySize":{ 15 | "Type":"Number", 16 | "Description":"The size to use for the lambda.", 17 | "Default": 384 18 | } 19 | }, 20 | "Resources" : { 21 | "AppLambda": { 22 | "Type" : "AWS::Lambda::Function", 23 | "Properties" : { 24 | "Code" : { 25 | "ZipFile": { "Fn::Join": ["\n", [ 26 | "exports.handler = function(event, context) {", 27 | " context.fail(new Error(500));", 28 | "};" 29 | ]]} 30 | }, 31 | "Description" : "serverless application", 32 | "Handler" : "index.handler", 33 | "MemorySize" : { "Ref":"AppMemorySize"}, 34 | "Timeout" : { "Ref":"AppTimeout"}, 35 | "Role" : {"Ref":"AppRole"}, 36 | "Runtime" : "nodejs4.3" 37 | } 38 | } 39 | }, 40 | "Outputs" : { 41 | "StackName":{ 42 | "Value":{ "Ref":"AWS::StackName" } 43 | }, 44 | "AppLambdaName" : { 45 | "Value" : { "Ref" : "AppLambda" }, 46 | "Description" : "Lambda name" 47 | }, 48 | "AppLambdaArn" : { 49 | "Value" : { "Fn::GetAtt" : [ "AppLambda", "Arn" ] }, 50 | "Description" : "Lambda Arn" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pipeline/cfn/dynamodb.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"DynamoDB Table", 4 | "Parameters":{ 5 | "ReadCapacityUnits":{ 6 | "Type":"Number", 7 | "Description":"Provisioned Read Capacity Units", 8 | "Default":"5" 9 | }, 10 | "WriteCapacityUnits":{ 11 | "Type":"Number", 12 | "Description":"Provisioned Write Capacity Units", 13 | "Default":"5" 14 | } 15 | }, 16 | "Resources":{ 17 | "Table":{ 18 | "Type":"AWS::DynamoDB::Table", 19 | "Properties":{ 20 | "AttributeDefinitions":[ 21 | { 22 | "AttributeName":"site_name", 23 | "AttributeType":"S" 24 | }, 25 | { 26 | "AttributeName":"color_name", 27 | "AttributeType":"S" 28 | } 29 | ], 30 | "KeySchema":[ 31 | { 32 | "AttributeName":"site_name", 33 | "KeyType":"HASH" 34 | }, 35 | { 36 | "AttributeName":"color_name", 37 | "KeyType":"RANGE" 38 | } 39 | ], 40 | "ProvisionedThroughput":{ 41 | "ReadCapacityUnits":{ 42 | "Ref":"ReadCapacityUnits" 43 | }, 44 | "WriteCapacityUnits":{ 45 | "Ref":"WriteCapacityUnits" 46 | } 47 | } 48 | } 49 | } 50 | }, 51 | "Outputs":{ 52 | "TableName":{ 53 | "Description":"Name of DynamoDB Table", 54 | "Value":{ "Ref":"Table" } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /pipeline/cfn/iam.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"iam roles & policies and instance-profiles", 4 | "Resources":{ 5 | "AppLambdaTrustRole":{ 6 | "Type":"AWS::IAM::Role", 7 | "Properties":{ 8 | "AssumeRolePolicyDocument":{ 9 | "Statement":[ 10 | { 11 | "Sid":"1", 12 | "Effect":"Allow", 13 | "Principal":{ 14 | "Service":[ 15 | "lambda.amazonaws.com" 16 | ] 17 | }, 18 | "Action":"sts:AssumeRole" 19 | } 20 | ] 21 | }, 22 | "Path":"/", 23 | "Policies":[ 24 | { 25 | "PolicyName": "AppLambdaPolicy", 26 | "PolicyDocument": { 27 | "Version": "2012-10-17", 28 | "Statement": [ 29 | { 30 | "Action": [ 31 | "dynamodb:DeleteItem", 32 | "dynamodb:GetItem", 33 | "dynamodb:PutItem", 34 | "dynamodb:Query", 35 | "dynamodb:Scan", 36 | "dynamodb:BatchGetItem", 37 | "dynamodb:BatchWriteItem", 38 | "dynamodb:DescribeTable", 39 | "dynamodb:UpdateItem" 40 | ], 41 | "Effect": "Allow", 42 | "Resource": "*" 43 | }, 44 | { 45 | "Action": [ 46 | "logs:CreateLogGroup", 47 | "logs:CreateLogStream", 48 | "logs:PutLogEvents" 49 | ], 50 | "Effect": "Allow", 51 | "Resource": "*" 52 | } 53 | ] 54 | } 55 | } 56 | ] 57 | } 58 | }, 59 | "CodePipelineRole":{ 60 | "Type":"AWS::IAM::Role", 61 | "Properties":{ 62 | "AssumeRolePolicyDocument":{ 63 | "Statement":[ 64 | { 65 | "Sid":"1", 66 | "Effect":"Allow", 67 | "Principal":{ 68 | "Service":[ 69 | "codepipeline.amazonaws.com" 70 | ] 71 | }, 72 | "Action":"sts:AssumeRole" 73 | } 74 | ] 75 | }, 76 | "Path":"/", 77 | "Policies":[ 78 | { 79 | "PolicyName":"CodePipelinePolicy", 80 | "PolicyDocument":{ 81 | "Version":"2012-10-17", 82 | "Statement":[ 83 | { 84 | "Action":[ 85 | "s3:GetObject", 86 | "s3:GetObjectVersion", 87 | "s3:GetBucketVersioning" 88 | ], 89 | "Resource":"*", 90 | "Effect":"Allow" 91 | }, 92 | { 93 | "Action":[ 94 | "s3:PutObject" 95 | ], 96 | "Resource":[ 97 | "arn:aws:s3:::codepipeline*" 98 | ], 99 | "Effect":"Allow" 100 | }, 101 | { 102 | "Action":[ 103 | "cloudwatch:*", 104 | "s3:*", 105 | "cloudformation:*", 106 | "iam:PassRole" 107 | ], 108 | "Resource":"*", 109 | "Effect":"Allow" 110 | }, 111 | { 112 | "Action":[ 113 | "lambda:InvokeFunction", 114 | "lambda:ListFunctions" 115 | ], 116 | "Resource":"*", 117 | "Effect":"Allow" 118 | } 119 | ] 120 | } 121 | } 122 | ] 123 | } 124 | }, 125 | "CodePipelineLambdaRole": { 126 | "Type": "AWS::IAM::Role", 127 | "Properties": { 128 | "AssumeRolePolicyDocument": { 129 | "Version": "2012-10-17", 130 | "Statement": [ 131 | { 132 | "Effect": "Allow", 133 | "Principal": { 134 | "Service": [ 135 | "lambda.amazonaws.com" 136 | ] 137 | }, 138 | "Action": [ 139 | "sts:AssumeRole" 140 | ] 141 | } 142 | ] 143 | }, 144 | "Path": "/", 145 | "Policies": [ 146 | { 147 | "PolicyName": "LambdaPolicy", 148 | "PolicyDocument": { 149 | "Version": "2012-10-17", 150 | "Statement": [ 151 | { 152 | "Effect": "Allow", 153 | "Action": [ 154 | "logs:*" 155 | ], 156 | "Resource": [ 157 | "arn:aws:logs:*:*:*" 158 | ] 159 | }, 160 | { 161 | "Effect": "Allow", 162 | "Action": [ 163 | "codepipeline:GetJobDetails", 164 | "codepipeline:PutJobSuccessResult", 165 | "codepipeline:PutJobFailureResult" 166 | ], 167 | "Resource": [ 168 | "*" 169 | ] 170 | }, 171 | { 172 | "Action":[ 173 | "s3:*", 174 | "apigateway:*", 175 | "lambda:*", 176 | "dynamodb:*", 177 | "cloudformation:*", 178 | "iam:*" 179 | ], 180 | "Resource":"*", 181 | "Effect":"Allow" 182 | } 183 | ] 184 | } 185 | } 186 | ] 187 | } 188 | }, 189 | "ApiIntegrationCredentialsRole": { 190 | "Type": "AWS::IAM::Role", 191 | "Properties": { 192 | "AssumeRolePolicyDocument": { 193 | "Version" : "2012-10-17", 194 | "Statement": [ 195 | { 196 | "Effect": "Allow", 197 | "Principal": { 198 | "Service": [ 199 | "apigateway.amazonaws.com" 200 | ] 201 | }, 202 | "Action": [ 203 | "sts:AssumeRole" 204 | ] 205 | } 206 | ] 207 | }, 208 | "Path": "/", 209 | "Policies": [ 210 | { 211 | "PolicyName": "ApiGatewayIntegrationLambda", 212 | "PolicyDocument": { 213 | "Version": "2012-10-17", 214 | "Statement": [ 215 | { 216 | "Effect": "Allow", 217 | "Action": "lambda:InvokeFunction", 218 | "Resource": "*" 219 | } 220 | ] 221 | } 222 | } 223 | ] 224 | } 225 | } 226 | }, 227 | "Outputs":{ 228 | "AppLambdaRoleArn":{ 229 | "Description":"The ARN of the Application Lambda Trust Role, which is needed to configure Lambda Function", 230 | "Value":{ 231 | "Fn::GetAtt":[ "AppLambdaTrustRole", "Arn" ] 232 | } 233 | }, 234 | "CodePipelineRoleArn":{ 235 | "Description":"The ARN of the Pipeline Trust Role, which is needed to configure Pipeline", 236 | "Value":{ 237 | "Fn::GetAtt":[ "CodePipelineRole", "Arn" ] 238 | } 239 | }, 240 | "CodePipelineLambdaRoleArn":{ 241 | "Description":"The ARN of the Pipeline custom action role, which is needed to configure Pipeline custom actions", 242 | "Value":{ 243 | "Fn::GetAtt":[ "CodePipelineLambdaRole", "Arn" ] 244 | } 245 | }, 246 | "ApiIntegrationCredentialsRole":{ 247 | "Description":"The ARN of the role that API Gateway assumes for running lambda app", 248 | "Value":{ 249 | "Fn::GetAtt":[ "ApiIntegrationCredentialsRole", "Arn" ] 250 | } 251 | } 252 | } 253 | } -------------------------------------------------------------------------------- /pipeline/cfn/master.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Master pipeline stack that calls nested stacks", 4 | "Parameters":{ 5 | "TemplateBucketName":{ 6 | "Type":"String", 7 | "Description":"S3 bucket name for all the CloudFormation templates used in the pipeline.", 8 | "Default": "dromedary-serverless" 9 | }, 10 | "HostedZoneId":{ 11 | "Type":"AWS::Route53::HostedZone::Id", 12 | "Description":"The existing hosted zone id to create the test and prod hostnames in." 13 | }, 14 | "TestSiteFQDN":{ 15 | "Type":"String", 16 | "Description":"Unique name to use for bucket and Route53 to serve static website content for test stage.", 17 | "AllowedPattern":"^[a-z0-9-]+(\\.[a-z0-9-]+){2,}$", 18 | "ConstraintDescription":"Must be valid hostname (like www.foo.com)" 19 | }, 20 | "ProdSiteFQDN":{ 21 | "Type":"String", 22 | "Description":"Unique name to use for bucket and Route53 to serve static website content for production stage.", 23 | "AllowedPattern":"^[a-z0-9-]+(\\.[a-z0-9-]+){2,}$", 24 | "ConstraintDescription":"Must be valid hostname (like www.foo.com)" 25 | }, 26 | "GitHubToken":{ 27 | "NoEcho":"true", 28 | "Type":"String", 29 | "Description":"Secret. It might look something like 9b189a1654643522561f7b3ebd44a1531a4287af OAuthToken with access to Repo. Go to https://github.com/settings/tokens" 30 | }, 31 | "GitHubUser":{ 32 | "Type":"String", 33 | "Description":"GitHub UserName", 34 | "Default":"stelligent" 35 | }, 36 | "GitHubRepo":{ 37 | "Type":"String", 38 | "Description":"GitHub Repo to pull from. Only the Name. not the URL", 39 | "Default":"dromedary" 40 | }, 41 | "GitHubBranch":{ 42 | "Type":"String", 43 | "Description":"Branch to use from Repo. Only the Name. not the URL", 44 | "Default":"master" 45 | }, 46 | "GulpPackageTask":{ 47 | "Type":"String", 48 | "Description":"Gulp task name to package the app", 49 | "Default":"package" 50 | }, 51 | "GulpTestTask":{ 52 | "Type":"String", 53 | "Description":"Gulp task name for acceptance testing", 54 | "Default":"test-functional" 55 | }, 56 | "DistSitePath":{ 57 | "Type":"String", 58 | "Description":"Path where GulpPackageTask places the site artifact", 59 | "Default":"dist/site.zip" 60 | }, 61 | "DistLambdaPath":{ 62 | "Type":"String", 63 | "Description":"Path where GulpPackageTask places the lambda artifact", 64 | "Default":"dist/lambda.zip" 65 | }, 66 | "DistSwaggerPath":{ 67 | "Type":"String", 68 | "Description":"Path where GulpPackageTask places the lambda artifact", 69 | "Default":"dist/swagger.json" 70 | } 71 | }, 72 | "Metadata" : { 73 | "AWS::CloudFormation::Interface" : { 74 | "ParameterGroups" : [ 75 | { 76 | "Label" : { "default" : "App Configuration" }, 77 | "Parameters" : [ "HostedZoneId","TestSiteFQDN","ProdSiteFQDN" ] 78 | }, 79 | { 80 | "Label" : { "default" : "GitHub Configuration" }, 81 | "Parameters" : [ "GitHubToken","GitHubUser", "GitHubRepo", "GitHubBranch" ] 82 | }, 83 | { 84 | "Label" : { "default" : "Gulp Configuration" }, 85 | "Parameters" : [ "GulpPackageTask", "GulpTestTask", "DistSitePath", "DistLambdaPath", "DistSwaggerPath"] 86 | }, 87 | { 88 | "Label" : { "default":"CloudFormation Configuration" }, 89 | "Parameters" : [ "TemplateBucketName" ] 90 | } 91 | ], 92 | "ParameterLabels" : { 93 | "GitHubToken" : { "default" : "OAuth2 Token" }, 94 | "GitHubUser" : { "default" : "User Name" }, 95 | "GitHubRepo" : { "default" : "Repository Name" }, 96 | "GitHubBranch" : { "default" : "Branch Name" }, 97 | "GulpPackageTask":{ "default": "Unit Test and Package Task"}, 98 | "GulpTestTask":{ "default": "Functional Testing Task"}, 99 | "DistSitePath":{ "default": "Path to Site Dist"}, 100 | "DistLambdaPath":{ "default": "Path to Lambda Dist"}, 101 | "DistSwaggerPath":{ "default": "Path to Swagger Dist"}, 102 | "TemplateBucketName" : { "default" : "CFN Template Bucket Name" }, 103 | "TestSiteFQDN" : { "default" : "Test DNS Name" }, 104 | "ProdSiteFQDN" : { "default" : "Production DNS Name" }, 105 | "HostedZoneId" : { "default" : "Hosted Zone" } 106 | } 107 | } 108 | }, 109 | "Mappings" : { 110 | "EndpointMap" : { 111 | "us-east-1": { 112 | "s3": "https://s3.amazonaws.com" 113 | }, 114 | "us-west-2": { 115 | "s3": "https://s3-us-west-2.amazonaws.com" 116 | }, 117 | "eu-west-1": { 118 | "s3": "https://s3-eu-west-1.amazonaws.com" 119 | }, 120 | "ap-northeast-1": { 121 | "s3": "https://s3-ap-northeast-1.amazonaws.com" 122 | } 123 | } 124 | }, 125 | "Resources":{ 126 | "PipelineStack":{ 127 | "Type":"AWS::CloudFormation::Stack", 128 | "DependsOn": [ "IAMStack" ], 129 | "Properties":{ 130 | "TemplateURL": { "Fn::Join": ["/", [{ "Fn::FindInMap":[ "EndpointMap", { "Ref":"AWS::Region" }, "s3" ] }, { "Ref":"TemplateBucketName"}, "pipeline.json"]] }, 131 | "TimeoutInMinutes":"60", 132 | "Parameters":{ 133 | "LambdaBucketName":{ 134 | "Ref": "TemplateBucketName" 135 | }, 136 | "GitHubToken":{ 137 | "Ref": "GitHubToken" 138 | }, 139 | "GitHubUser":{ 140 | "Ref": "GitHubUser" 141 | }, 142 | "GitHubRepo":{ 143 | "Ref": "GitHubRepo" 144 | }, 145 | "GitHubBranch":{ 146 | "Ref": "GitHubBranch" 147 | }, 148 | "GulpPackageTask":{ 149 | "Ref": "GulpPackageTask" 150 | }, 151 | "GulpTestTask":{ 152 | "Ref": "GulpTestTask" 153 | }, 154 | "DistSitePath":{ 155 | "Ref": "DistSitePath" 156 | }, 157 | "DistLambdaPath":{ 158 | "Ref": "DistLambdaPath" 159 | }, 160 | "DistSwaggerPath":{ 161 | "Ref": "DistSwaggerPath" 162 | }, 163 | "TestBucketName":{ 164 | "Ref": "TestSiteFQDN" 165 | }, 166 | "TestEndpoint":{ 167 | "Fn::GetAtt" : [ "TestS3Stack", "Outputs.SiteUrl" ] 168 | }, 169 | "ProdBucketName":{ 170 | "Ref": "ProdSiteFQDN" 171 | }, 172 | "ProdEndpoint":{ 173 | "Fn::GetAtt" : [ "ProdS3Stack", "Outputs.SiteUrl" ] 174 | }, 175 | "ApiName":{ 176 | "Fn::GetAtt" : [ "ApiGatewayStack", "Outputs.ApiName" ] 177 | }, 178 | "TestApiUrl":{ 179 | "Fn::GetAtt" : [ "ApiGatewayStack", "Outputs.TestApiUrl" ] 180 | }, 181 | "ProdApiUrl":{ 182 | "Fn::GetAtt" : [ "ApiGatewayStack", "Outputs.ProdApiUrl" ] 183 | }, 184 | "TestAppFunctionName":{ 185 | "Fn::GetAtt" : [ "TestAppStack", "Outputs.AppLambdaArn" ] 186 | }, 187 | "ProdAppFunctionName":{ 188 | "Fn::GetAtt" : [ "ProdAppStack", "Outputs.AppLambdaArn" ] 189 | }, 190 | "CodePipelineRoleArn":{ 191 | "Fn::GetAtt" : [ "IAMStack", "Outputs.CodePipelineRoleArn" ] 192 | }, 193 | "CodePipelineLambdaRoleArn":{ 194 | "Fn::GetAtt" : [ "IAMStack", "Outputs.CodePipelineLambdaRoleArn" ] 195 | } 196 | } 197 | } 198 | }, 199 | "TestDynamoDBStack":{ 200 | "Type":"AWS::CloudFormation::Stack", 201 | "Properties":{ 202 | "TemplateURL": { "Fn::Join": ["/", [{ "Fn::FindInMap":[ "EndpointMap", { "Ref":"AWS::Region" }, "s3" ] }, { "Ref":"TemplateBucketName"}, "dynamodb.json"]] }, 203 | "TimeoutInMinutes":"60" 204 | } 205 | }, 206 | "ProdDynamoDBStack":{ 207 | "Type":"AWS::CloudFormation::Stack", 208 | "Properties":{ 209 | "TemplateURL": { "Fn::Join": ["/", [{ "Fn::FindInMap":[ "EndpointMap", { "Ref":"AWS::Region" }, "s3" ] }, { "Ref":"TemplateBucketName"}, "dynamodb.json"]] }, 210 | "TimeoutInMinutes":"60" 211 | } 212 | }, 213 | "ApiGatewayStack":{ 214 | "Type":"AWS::CloudFormation::Stack", 215 | "Properties":{ 216 | "TemplateURL": { "Fn::Join": ["/", [{ "Fn::FindInMap":[ "EndpointMap", { "Ref":"AWS::Region" }, "s3" ] }, { "Ref":"TemplateBucketName"}, "api-gateway.json"]] }, 217 | "TimeoutInMinutes":"60", 218 | "Parameters":{ 219 | "BucketName":{ "Ref":"TemplateBucketName" }, 220 | "TestDDBTableName":{ "Fn::GetAtt": ["TestDynamoDBStack", "Outputs.TableName"]}, 221 | "ProdDDBTableName":{ "Fn::GetAtt": ["ProdDynamoDBStack", "Outputs.TableName"]}, 222 | "TestAppLambdaName":{ "Fn::GetAtt" : [ "TestAppStack", "Outputs.AppLambdaName" ] }, 223 | "ProdAppLambdaName":{ "Fn::GetAtt" : [ "ProdAppStack", "Outputs.AppLambdaName" ] }, 224 | "ApiIntegrationCredentialsRole":{ "Fn::GetAtt" : [ "IAMStack", "Outputs.ApiIntegrationCredentialsRole" ] } 225 | } 226 | } 227 | }, 228 | "TestAppStack":{ 229 | "Type":"AWS::CloudFormation::Stack", 230 | "Properties":{ 231 | "TemplateURL": { "Fn::Join": ["/", [{ "Fn::FindInMap":[ "EndpointMap", { "Ref":"AWS::Region" }, "s3" ] }, { "Ref":"TemplateBucketName"}, "app.json"]] }, 232 | "TimeoutInMinutes":"60", 233 | "Parameters":{ 234 | "AppRole":{ 235 | "Fn::GetAtt" : [ "IAMStack", "Outputs.AppLambdaRoleArn" ] 236 | } 237 | } 238 | } 239 | }, 240 | "ProdAppStack":{ 241 | "Type":"AWS::CloudFormation::Stack", 242 | "Properties":{ 243 | "TemplateURL": { "Fn::Join": ["/", [{ "Fn::FindInMap":[ "EndpointMap", { "Ref":"AWS::Region" }, "s3" ] }, { "Ref":"TemplateBucketName"}, "app.json"]] }, 244 | "TimeoutInMinutes":"60", 245 | "Parameters":{ 246 | "AppRole":{ 247 | "Fn::GetAtt" : [ "IAMStack", "Outputs.AppLambdaRoleArn" ] 248 | } 249 | } 250 | } 251 | }, 252 | "TestS3Stack":{ 253 | "Type":"AWS::CloudFormation::Stack", 254 | "Properties":{ 255 | "TemplateURL": { "Fn::Join": ["/", [{ "Fn::FindInMap":[ "EndpointMap", { "Ref":"AWS::Region" }, "s3" ] }, { "Ref":"TemplateBucketName"}, "site.json"]] }, 256 | "TimeoutInMinutes":"60", 257 | "Parameters":{ 258 | "HostedZoneId":{ "Ref":"HostedZoneId" }, 259 | "SiteBucketName":{ "Ref":"TestSiteFQDN" } 260 | } 261 | } 262 | }, 263 | "ProdS3Stack":{ 264 | "Type":"AWS::CloudFormation::Stack", 265 | "Properties":{ 266 | "TemplateURL": { "Fn::Join": ["/", [{ "Fn::FindInMap":[ "EndpointMap", { "Ref":"AWS::Region" }, "s3" ] }, { "Ref":"TemplateBucketName"}, "site.json"]] }, 267 | "TimeoutInMinutes":"60", 268 | "Parameters":{ 269 | "HostedZoneId":{ "Ref":"HostedZoneId" }, 270 | "SiteBucketName":{ "Ref":"ProdSiteFQDN" } 271 | } 272 | } 273 | }, 274 | "IAMStack":{ 275 | "Type":"AWS::CloudFormation::Stack", 276 | "Properties":{ 277 | "TemplateURL": { "Fn::Join": ["/", [{ "Fn::FindInMap":[ "EndpointMap", { "Ref":"AWS::Region" }, "s3" ] }, { "Ref":"TemplateBucketName"}, "iam.json"]] }, 278 | "TimeoutInMinutes":"60" 279 | } 280 | } 281 | }, 282 | "Outputs":{ 283 | "TestSiteBucket" : { 284 | "Value" : { "Fn::GetAtt" : [ "TestS3Stack", "Outputs.SiteBucket" ] } , 285 | "Description" : "Test Site Bucket - Empty before deleting stack" 286 | }, 287 | "ProdSiteBucket" : { 288 | "Value" : { "Fn::GetAtt" : [ "ProdS3Stack", "Outputs.SiteBucket" ] } , 289 | "Description" : "Prod Site Bucket - Empty before deleting stack" 290 | }, 291 | "TestSiteUrl" : { 292 | "Value" : { "Fn::GetAtt" : [ "TestS3Stack", "Outputs.SiteUrl" ] } , 293 | "Description" : "Test Site Url" 294 | }, 295 | "ProdSiteUrl" : { 296 | "Value" : { "Fn::GetAtt" : [ "ProdS3Stack", "Outputs.SiteUrl" ] } , 297 | "Description" : "Production Site Url" 298 | }, 299 | "PipelineBucket" : { 300 | "Value" : { "Fn::GetAtt" : [ "PipelineStack", "Outputs.ArtifactBucket" ] } , 301 | "Description" : "Pipeline Artifact Bucket - Empty before deleting stack" 302 | }, 303 | "PipelineUrl": { 304 | "Value": { 305 | "Fn::Join": [ 306 | "", 307 | [ 308 | "https://console.aws.amazon.com/codepipeline/home?region=", 309 | { "Ref": "AWS::Region" }, 310 | "#/view/", 311 | { "Fn::GetAtt" : [ "PipelineStack", "Outputs.PipelineName" ] } 312 | ] 313 | ] 314 | }, 315 | "Description" : "Pipeline Url" 316 | } 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /pipeline/cfn/pipeline.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"CodePipeline definition", 4 | "Parameters":{ 5 | "LambdaBucketName":{ 6 | "Type":"String", 7 | "Description":"S3 bucket name for the lambda function used in the pipeline actions." 8 | }, 9 | "GitHubToken":{ 10 | "NoEcho":"true", 11 | "Type":"String", 12 | "Description":"Secret. It might look something like 9b189a1654643522561f7b3ebd44a1531a4287af OAuthToken with access to Repo. Go to https://github.com/settings/tokens" 13 | }, 14 | "GitHubUser":{ 15 | "Type":"String", 16 | "Description":"GitHub UserName" 17 | }, 18 | "GitHubRepo":{ 19 | "Type":"String", 20 | "Description":"GitHub Repo to pull from. Only the Name. not the URL" 21 | }, 22 | "GitHubBranch":{ 23 | "Type":"String", 24 | "Description":"Branch to use from Repo. Only the Name. not the URL" 25 | }, 26 | "GulpPackageTask":{ 27 | "Type":"String", 28 | "Description":"Gulp task name for Unit Test and Packing" 29 | }, 30 | "GulpTestTask":{ 31 | "Type":"String", 32 | "Description":"Gulp task name for Functional Testing" 33 | }, 34 | "DistSitePath":{ 35 | "Type":"String", 36 | "Description":"Path to dist artifact", 37 | "Default":"dist/site.zip" 38 | }, 39 | "DistLambdaPath":{ 40 | "Type":"String", 41 | "Description":"Path to lambda artifact", 42 | "Default":"dist/lambda.zip" 43 | }, 44 | "DistSwaggerPath":{ 45 | "Type":"String", 46 | "Description":"Path to swagger artifact", 47 | "Default":"dist/swagger.json" 48 | }, 49 | "TestBucketName":{ 50 | "Type":"String", 51 | "Description":"Name of the s3 bucket for test" 52 | }, 53 | "TestEndpoint":{ 54 | "Type":"String", 55 | "Description":"The endpoint for testing" 56 | }, 57 | "ProdBucketName":{ 58 | "Type":"String", 59 | "Description":"Name of the s3 bucket for test" 60 | }, 61 | "ProdEndpoint":{ 62 | "Type":"String", 63 | "Description":"The endpoint for testing" 64 | }, 65 | "ApiName":{ 66 | "Type":"String", 67 | "Description":"Name of the api gateway" 68 | }, 69 | "TestAppFunctionName":{ 70 | "Type":"String", 71 | "Description":"Name of the test application lambda function" 72 | }, 73 | "ProdAppFunctionName":{ 74 | "Type":"String", 75 | "Description":"Name of the prod application lambda function" 76 | }, 77 | "CodePipelineRoleArn":{ 78 | "Type":"String", 79 | "Description":"The Arn of a role for code pipeline to run as" 80 | }, 81 | "CodePipelineLambdaRoleArn":{ 82 | "Type":"String", 83 | "Description":"The Arn of a role for code pipeline lambda actions to run as" 84 | }, 85 | "TestApiUrl":{ 86 | "Type":"String", 87 | "Description":"The URL to the test api for config.json" 88 | }, 89 | "ProdApiUrl":{ 90 | "Type":"String", 91 | "Description":"The URL to the production api for config.json" 92 | } 93 | }, 94 | "Mappings" : { 95 | "EndpointMap" : { 96 | "us-east-1": { 97 | "s3": "https://s3.amazonaws.com" 98 | }, 99 | "us-west-2": { 100 | "s3": "https://s3-us-west-2.amazonaws.com" 101 | }, 102 | "eu-west-1": { 103 | "s3": "https://s3-eu-west-1.amazonaws.com" 104 | }, 105 | "ap-northeast-1": { 106 | "s3": "https://s3-ap-northeast-1.amazonaws.com" 107 | } 108 | } 109 | }, 110 | "Resources": { 111 | 112 | "CodePipelineNpmLambda":{ 113 | "Type":"AWS::Lambda::Function", 114 | "Properties":{ 115 | "Code" : { 116 | "S3Bucket":{ "Ref": "LambdaBucketName" }, 117 | "S3Key": "pipeline-lambda.zip" 118 | }, 119 | "Role": { "Ref": "CodePipelineLambdaRoleArn" }, 120 | "Description":"Run NPM tasks for pipeline", 121 | "Timeout":300, 122 | "Handler":"index.npmHandler", 123 | "Runtime" : "nodejs4.3", 124 | "MemorySize":1536 125 | } 126 | }, 127 | "CodePipelineGulpLambda":{ 128 | "Type":"AWS::Lambda::Function", 129 | "Properties":{ 130 | "Code" : { 131 | "S3Bucket":{ "Ref": "LambdaBucketName" }, 132 | "S3Key": "pipeline-lambda.zip" 133 | }, 134 | "Role": { "Ref": "CodePipelineLambdaRoleArn" }, 135 | "Description":"Run gulp tasks for pipeline", 136 | "Timeout":300, 137 | "Handler":"index.gulpHandler", 138 | "Runtime" : "nodejs4.3", 139 | "MemorySize":1536 140 | } 141 | }, 142 | "CodePipelineDeployLambda":{ 143 | "Type":"AWS::Lambda::Function", 144 | "Properties":{ 145 | "Code" : { 146 | "S3Bucket":{ "Ref": "LambdaBucketName" }, 147 | "S3Key": "pipeline-lambda.zip" 148 | }, 149 | "Role": { "Ref": "CodePipelineLambdaRoleArn" }, 150 | "Description":"Run deploy tasks for pipeline", 151 | "Timeout":300, 152 | "Handler":"index.deployHandler", 153 | "Runtime" : "nodejs4.3", 154 | "MemorySize":1536 155 | } 156 | }, 157 | "ArtifactBucket": { 158 | "Type": "AWS::S3::Bucket", 159 | "Properties": { 160 | } 161 | }, 162 | "ServerlessPipeline": { 163 | "Type": "AWS::CodePipeline::Pipeline", 164 | "DependsOn": [ 165 | "CodePipelineNpmLambda", 166 | "CodePipelineGulpLambda" 167 | ], 168 | "Properties": { 169 | "DisableInboundStageTransitions": [ ], 170 | "RoleArn": { "Ref": "CodePipelineRoleArn" }, 171 | "Stages": [ 172 | { 173 | "Name": "Source", 174 | "Actions": [ 175 | { 176 | "InputArtifacts": [], 177 | "Name": "Source", 178 | "ActionTypeId": { 179 | "Category": "Source", 180 | "Owner": "ThirdParty", 181 | "Version": "1", 182 | "Provider": "GitHub" 183 | }, 184 | "Configuration": { 185 | "Owner": { 186 | "Ref": "GitHubUser" 187 | }, 188 | "Repo": { 189 | "Ref": "GitHubRepo" 190 | }, 191 | "Branch": { 192 | "Ref": "GitHubBranch" 193 | }, 194 | "OAuthToken": { 195 | "Ref": "GitHubToken" 196 | } 197 | }, 198 | "OutputArtifacts": [ 199 | { 200 | "Name": "SourceOutput" 201 | } 202 | ], 203 | "RunOrder": 1 204 | } 205 | ] 206 | }, 207 | { 208 | "Name": "Commit", 209 | "Actions": [ 210 | { 211 | "InputArtifacts":[ 212 | { 213 | "Name": "SourceOutput" 214 | } 215 | ], 216 | "Name":"Dependencies", 217 | "ActionTypeId":{ 218 | "Category":"Invoke", 219 | "Owner":"AWS", 220 | "Version":"1", 221 | "Provider":"Lambda" 222 | }, 223 | "Configuration":{ 224 | "FunctionName":{ 225 | "Ref":"CodePipelineNpmLambda" 226 | }, 227 | "UserParameters":"subcommand=install" 228 | }, 229 | "OutputArtifacts": [ 230 | { 231 | "Name": "SourceInstalledOutput" 232 | } 233 | ], 234 | "RunOrder":1 235 | }, 236 | { 237 | "InputArtifacts":[ 238 | { 239 | "Name": "SourceInstalledOutput" 240 | } 241 | ], 242 | "Name":"TestAndPackage", 243 | "ActionTypeId":{ 244 | "Category":"Invoke", 245 | "Owner":"AWS", 246 | "Version":"1", 247 | "Provider":"Lambda" 248 | }, 249 | "Configuration":{ 250 | "FunctionName":{ 251 | "Ref":"CodePipelineGulpLambda" 252 | }, 253 | "UserParameters": { "Fn::Join": ["", ["task=", { "Ref":"GulpPackageTask"}, 254 | "&DistSiteOutput=", {"Ref":"DistSitePath"}, 255 | "&DistLambdaOutput=", {"Ref":"DistLambdaPath"}, 256 | "&DistSwaggerOutput=", {"Ref":"DistSwaggerPath"} 257 | ]] } 258 | }, 259 | "OutputArtifacts": [ 260 | { 261 | "Name": "DistSiteOutput" 262 | }, 263 | { 264 | "Name": "DistLambdaOutput" 265 | }, 266 | { 267 | "Name": "DistSwaggerOutput" 268 | } 269 | ], 270 | "RunOrder":2 271 | } 272 | ] 273 | }, 274 | { 275 | "Name": "Acceptance", 276 | "Actions": [ 277 | { 278 | "InputArtifacts":[ 279 | { 280 | "Name": "DistLambdaOutput" 281 | } 282 | ], 283 | "Name":"DeployTestApp", 284 | "ActionTypeId":{ 285 | "Category":"Invoke", 286 | "Owner":"AWS", 287 | "Version":"1", 288 | "Provider":"Lambda" 289 | }, 290 | "Configuration":{ 291 | "FunctionName":{ 292 | "Ref":"CodePipelineDeployLambda" 293 | }, 294 | "UserParameters": { "Fn::Join": ["", ["type=lambda&alias=test&function=", { "Ref":"TestAppFunctionName"}]] } 295 | }, 296 | "OutputArtifacts": [ 297 | ], 298 | "RunOrder":1 299 | }, 300 | { 301 | "InputArtifacts":[ 302 | { 303 | "Name": "DistSwaggerOutput" 304 | } 305 | ], 306 | "Name":"DeployTestAPI", 307 | "ActionTypeId":{ 308 | "Category":"Invoke", 309 | "Owner":"AWS", 310 | "Version":"1", 311 | "Provider":"Lambda" 312 | }, 313 | "Configuration":{ 314 | "FunctionName":{ 315 | "Ref":"CodePipelineDeployLambda" 316 | }, 317 | "UserParameters": { "Fn::Join": ["", ["type=apigateway&stage=test&name=", { "Ref":"ApiName"}]] } 318 | }, 319 | "OutputArtifacts": [ 320 | ], 321 | "RunOrder":1 322 | }, 323 | { 324 | "InputArtifacts":[ 325 | { 326 | "Name": "DistSiteOutput" 327 | } 328 | ], 329 | "Name":"DeployTestSite", 330 | "ActionTypeId":{ 331 | "Category":"Invoke", 332 | "Owner":"AWS", 333 | "Version":"1", 334 | "Provider":"Lambda" 335 | }, 336 | "Configuration":{ 337 | "FunctionName":{ 338 | "Ref":"CodePipelineDeployLambda" 339 | }, 340 | "UserParameters": { "Fn::Join": ["", ["type=s3&bucket=", { "Ref":"TestBucketName"},"&apiBaseurl=",{"Fn::Join":["",[{"Ref":"TestApiUrl"},"/"]]} ]]} 341 | }, 342 | "OutputArtifacts": [ 343 | ], 344 | "RunOrder": 2 345 | }, 346 | { 347 | "InputArtifacts":[ 348 | { 349 | "Name": "SourceInstalledOutput" 350 | } 351 | ], 352 | "Name":"FunctionalTest", 353 | "ActionTypeId":{ 354 | "Category":"Invoke", 355 | "Owner":"AWS", 356 | "Version":"1", 357 | "Provider":"Lambda" 358 | }, 359 | "Configuration":{ 360 | "FunctionName":{ 361 | "Ref":"CodePipelineGulpLambda" 362 | }, 363 | "UserParameters": { "Fn::Join": ["", ["task=", { "Ref":"GulpTestTask"}, 364 | "&Env.TARGET_URL=", {"Ref":"TestEndpoint"} 365 | ]] } 366 | }, 367 | "OutputArtifacts": [ 368 | ], 369 | "RunOrder":3 370 | } 371 | ] 372 | }, 373 | { 374 | "Name": "Production", 375 | "Actions": [ 376 | { 377 | "InputArtifacts":[ 378 | { 379 | "Name": "DistLambdaOutput" 380 | } 381 | ], 382 | "Name":"DeployProdApp", 383 | "ActionTypeId":{ 384 | "Category":"Invoke", 385 | "Owner":"AWS", 386 | "Version":"1", 387 | "Provider":"Lambda" 388 | }, 389 | "Configuration":{ 390 | "FunctionName":{ 391 | "Ref":"CodePipelineDeployLambda" 392 | }, 393 | "UserParameters": { "Fn::Join": ["", ["type=lambda&alias=prod&function=", { "Ref":"ProdAppFunctionName"}]] } 394 | }, 395 | "OutputArtifacts": [ 396 | ], 397 | "RunOrder":1 398 | }, 399 | { 400 | "InputArtifacts":[ 401 | { 402 | "Name": "DistSwaggerOutput" 403 | } 404 | ], 405 | "Name":"DeployProdAPI", 406 | "ActionTypeId":{ 407 | "Category":"Invoke", 408 | "Owner":"AWS", 409 | "Version":"1", 410 | "Provider":"Lambda" 411 | }, 412 | "Configuration":{ 413 | "FunctionName":{ 414 | "Ref":"CodePipelineDeployLambda" 415 | }, 416 | "UserParameters": { "Fn::Join": ["", ["type=apigateway&stage=prod&name=", { "Ref":"ApiName"}]] } 417 | }, 418 | "OutputArtifacts": [ 419 | ], 420 | "RunOrder":1 421 | }, 422 | { 423 | "InputArtifacts":[ 424 | { 425 | "Name": "DistSiteOutput" 426 | } 427 | ], 428 | "Name":"DeployProdSite", 429 | "ActionTypeId":{ 430 | "Category":"Invoke", 431 | "Owner":"AWS", 432 | "Version":"1", 433 | "Provider":"Lambda" 434 | }, 435 | "Configuration":{ 436 | "FunctionName":{ 437 | "Ref":"CodePipelineDeployLambda" 438 | }, 439 | "UserParameters": { "Fn::Join": ["", ["type=s3&bucket=", { "Ref":"ProdBucketName"},"&apiBaseurl=",{"Fn::Join":["",[{"Ref":"ProdApiUrl"},"/"]]} ]]} 440 | }, 441 | "OutputArtifacts": [ 442 | ], 443 | "RunOrder": 2 444 | }, 445 | { 446 | "InputArtifacts":[ 447 | { 448 | "Name": "SourceInstalledOutput" 449 | } 450 | ], 451 | "Name":"FunctionalTest", 452 | "ActionTypeId":{ 453 | "Category":"Invoke", 454 | "Owner":"AWS", 455 | "Version":"1", 456 | "Provider":"Lambda" 457 | }, 458 | "Configuration":{ 459 | "FunctionName":{ 460 | "Ref":"CodePipelineGulpLambda" 461 | }, 462 | "UserParameters": { "Fn::Join": ["", ["task=", { "Ref":"GulpTestTask"}, 463 | "&Env.TARGET_URL=", {"Ref":"ProdEndpoint"} 464 | ]] } 465 | }, 466 | "OutputArtifacts": [ 467 | ], 468 | "RunOrder":3 469 | } 470 | ] 471 | } 472 | ], 473 | "ArtifactStore": { 474 | "Type": "S3", 475 | "Location": { "Ref": "ArtifactBucket" } 476 | } 477 | } 478 | } 479 | }, 480 | "Outputs":{ 481 | "CodePipelineNpmLambdaArn" : { 482 | "Value" : { "Fn::GetAtt" : [ "CodePipelineNpmLambda", "Arn" ] }, 483 | "Description" : "NPM Lambda Arn" 484 | }, 485 | "CodePipelineGulpLambdaArn" : { 486 | "Value" : { "Fn::GetAtt" : [ "CodePipelineGulpLambda", "Arn" ] }, 487 | "Description" : "Gulp Lambda Arn" 488 | }, 489 | "CodePipelineDeployLambdaArn" : { 490 | "Value" : { "Fn::GetAtt" : [ "CodePipelineDeployLambda", "Arn" ] }, 491 | "Description" : "Deploy Lambda Arn" 492 | }, 493 | "ArtifactBucket": { 494 | "Value": { "Ref": "ArtifactBucket" } 495 | }, 496 | "PipelineName": { 497 | "Value": { 498 | "Ref": "ServerlessPipeline" 499 | } 500 | } 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /pipeline/cfn/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"s3 buckets for sites at each stage", 4 | "Parameters":{ 5 | "HostedZoneId":{ 6 | "Type":"AWS::Route53::HostedZone::Id", 7 | "Description":"The hosted zone id to create this record in" 8 | }, 9 | "SiteBucketName":{ 10 | "Type":"String", 11 | "Description":"The bucket name to use for the site" 12 | } 13 | }, 14 | "Mappings" : { 15 | "RegionMap" : { 16 | "us-east-1" : { "S3hostedzoneID" : "Z3AQBSTGFYJSTF", "websiteendpoint" : "s3-website-us-east-1.amazonaws.com" }, 17 | "us-west-1" : { "S3hostedzoneID" : "Z2F56UZL2M1ACD", "websiteendpoint" : "s3-website-us-west-1.amazonaws.com" }, 18 | "us-west-2" : { "S3hostedzoneID" : "Z3BJ6K6RIION7M", "websiteendpoint" : "s3-website-us-west-2.amazonaws.com" }, 19 | "eu-west-1" : { "S3hostedzoneID" : "Z1BKCTXD74EZPE", "websiteendpoint" : "s3-website-eu-west-1.amazonaws.com" }, 20 | "ap-southeast-1" : { "S3hostedzoneID" : "Z3O0J2DXBE1FTB", "websiteendpoint" : "s3-website-ap-southeast-1.amazonaws.com" }, 21 | "ap-southeast-2" : { "S3hostedzoneID" : "Z1WCIGYICN2BYD", "websiteendpoint" : "s3-website-ap-southeast-2.amazonaws.com" }, 22 | "ap-northeast-1" : { "S3hostedzoneID" : "Z2M4EHUR26P7ZW", "websiteendpoint" : "s3-website-ap-northeast-1.amazonaws.com" }, 23 | "sa-east-1" : { "S3hostedzoneID" : "Z31GFT0UA1I2HV", "websiteendpoint" : "s3-website-sa-east-1.amazonaws.com" } 24 | } 25 | }, 26 | "Resources" : { 27 | "SiteBucket" : { 28 | "Type" : "AWS::S3::Bucket", 29 | "Properties" : { 30 | "AccessControl" : "PublicRead", 31 | "BucketName" : { "Ref":"SiteBucketName" }, 32 | "WebsiteConfiguration" : { 33 | "IndexDocument" : "index.html" 34 | } 35 | } 36 | }, 37 | "SiteRecord": { 38 | "Type": "AWS::Route53::RecordSetGroup", 39 | "Properties": { 40 | "HostedZoneId": {"Ref":"HostedZoneId"}, 41 | "RecordSets": [ 42 | { 43 | "Name": {"Fn::Join":["",[{"Ref": "SiteBucketName"},"."]]}, 44 | "Type": "A", 45 | "AliasTarget": { 46 | "HostedZoneId": {"Fn::FindInMap" : [ "RegionMap", { "Ref" : "AWS::Region" }, "S3hostedzoneID"]}, 47 | "DNSName": {"Fn::FindInMap" : [ "RegionMap", { "Ref" : "AWS::Region" }, "websiteendpoint"]} 48 | } 49 | } 50 | ] 51 | } 52 | } 53 | }, 54 | "Outputs" : { 55 | "StackName":{ 56 | "Value":{ "Ref":"AWS::StackName" } 57 | }, 58 | "SiteUrl" : { 59 | "Value" : { "Fn::Join" : [ "", ["http://",{"Ref":"SiteBucketName"} ]] }, 60 | "Description" : "Site URL" 61 | }, 62 | "SiteBucket" : { 63 | "Value" : {"Ref":"SiteBucket"}, 64 | "Description" : "Site Bucket" 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /pipeline/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk'); 4 | var fs = require('fs'); 5 | var mime = require('mime'); 6 | var chalk = require('chalk'); 7 | var Promise = require('promise'); 8 | 9 | var util = require('./util.js'); 10 | 11 | exports.registerTasks = function ( gulp, opts ) { 12 | util.init(opts.region); 13 | var cloudFormation = new AWS.CloudFormation(); 14 | 15 | var stackName = opts.stackName || 'serverless-pipeline'; 16 | var cfnBucket = opts.cfnBucket || 'serverless-pipeline'; 17 | var taskPrefix = opts.taskPrefix || 'pipeline'; 18 | 19 | 20 | gulp.task(taskPrefix+':up', function() { 21 | return util.getStack(stackName).then(function(stack) { 22 | var action, status = stack && stack.StackStatus; 23 | if (!status || status === 'DELETE_COMPLETE') { 24 | action = 'createStack'; 25 | } else if (status.match(/(CREATE|UPDATE|UPDATE_ROLLBACK)_COMPLETE/)) { 26 | action = 'updateStack'; 27 | } else { 28 | return console.error('Stack "' + stackName + '" is currently in ' + status + ' status and can not be deployed.'); 29 | } 30 | 31 | 32 | var s3Endpoint = (opts.region=='us-east-1'?'https://s3.amazonaws.com':'https://s3-'+opts.region+'.amazonaws.com'); 33 | var s3BucketURL = s3Endpoint+'/'+cfnBucket; 34 | 35 | var params = { 36 | StackName: stackName, 37 | Capabilities: ['CAPABILITY_IAM'], 38 | Parameters: [ 39 | { 40 | ParameterKey: "GitHubUser", 41 | ParameterValue: opts.githubUser 42 | }, 43 | { 44 | ParameterKey: "GitHubToken", 45 | ParameterValue: opts.githubToken 46 | }, 47 | { 48 | ParameterKey: "GitHubRepo", 49 | ParameterValue: opts.githubRepo 50 | }, 51 | { 52 | ParameterKey: "GitHubBranch", 53 | ParameterValue: opts.githubBranch 54 | }, 55 | { 56 | ParameterKey: "GulpPackageTask", 57 | ParameterValue: opts.gulpPackageTask 58 | }, 59 | { 60 | ParameterKey: "GulpTestTask", 61 | ParameterValue: opts.gulpTestTask 62 | }, 63 | { 64 | ParameterKey: "HostedZoneId", 65 | ParameterValue: opts.hostedZoneId 66 | }, 67 | { 68 | ParameterKey: "TestSiteFQDN", 69 | ParameterValue: opts.testSiteFQDN 70 | }, 71 | { 72 | ParameterKey: "ProdSiteFQDN", 73 | ParameterValue: opts.prodSiteFQDN 74 | }, 75 | { 76 | ParameterKey: "DistSitePath", 77 | ParameterValue: opts.distSitePath 78 | }, 79 | { 80 | ParameterKey: "DistLambdaPath", 81 | ParameterValue: opts.distLambdaPath 82 | }, 83 | { 84 | ParameterKey: "DistSwaggerPath", 85 | ParameterValue: opts.distSwaggerPath 86 | }, 87 | { 88 | ParameterKey: "TemplateBucketName", 89 | ParameterValue: cfnBucket 90 | } 91 | ], 92 | TemplateURL: s3BucketURL+"/master.json" 93 | }; 94 | params.Parameters = params.Parameters.filter(function(p) { return p.ParameterValue; }); 95 | 96 | return new Promise(function(resolve,reject) { 97 | cloudFormation[action](params, function(err) { 98 | if (err) { 99 | reject(err); 100 | } else { 101 | var a = action === 'createStack' ? 'creation' : 'update'; 102 | console.log('Stack ' + a + ' in progress.'); 103 | resolve(); 104 | } 105 | }); 106 | 107 | }); 108 | }); 109 | }); 110 | 111 | gulp.task(taskPrefix+':emptyArtifacts', function() { 112 | return util.getSubStackOutput(stackName,'PipelineStack','ArtifactBucket') 113 | .then(function(bucketName) { 114 | return util.emptyBucket(bucketName); 115 | }).catch(function(){ 116 | return true; 117 | }); 118 | }); 119 | 120 | gulp.task(taskPrefix+':emptyTestSite', function() { 121 | return util.getSubStackOutput(stackName,'TestS3Stack','SiteBucket') 122 | .then(function(bucketName) { 123 | return util.emptyBucket(bucketName); 124 | }).catch(function(){ 125 | return true; 126 | }); 127 | }); 128 | 129 | gulp.task(taskPrefix+':emptyProdSite', function() { 130 | return util.getSubStackOutput(stackName,'ProdS3Stack','SiteBucket') 131 | .then(function(bucketName) { 132 | return util.emptyBucket(bucketName); 133 | }).catch(function(){ 134 | return true; 135 | }); 136 | }); 137 | 138 | gulp.task(taskPrefix+':down', [taskPrefix+':emptyArtifacts',taskPrefix+':emptyTestSite',taskPrefix+':emptyProdSite'], function(cb) { 139 | return util.getStack(stackName).then(function() { 140 | return new Promise(function(resolve,reject) { 141 | cloudFormation.deleteStack({StackName: stackName}, function(err) { 142 | if(err) 143 | reject(err); 144 | else { 145 | console.log('Stack deletion in progress.'); 146 | resolve(); 147 | } 148 | }); 149 | }); 150 | }); 151 | }); 152 | 153 | gulp.task(taskPrefix+':wait', function(cb) { 154 | var checkFunction = function() { 155 | util.getStack(stackName).then(function(stack) { 156 | if(!stack || /_IN_PROGRESS$/.test(stack.StackStatus)) { 157 | console.log(" StackStatus = "+(stack!=null?stack.StackStatus:'NOT_FOUND')); 158 | setTimeout(checkFunction, 5000); 159 | } else { 160 | console.log("Final StackStatus = "+stack.StackStatus); 161 | cb(); 162 | } 163 | }); 164 | }; 165 | 166 | checkFunction(); 167 | }); 168 | 169 | gulp.task(taskPrefix+':status', function() { 170 | return util.getStack(stackName) 171 | .then(function(stack) { 172 | if (!stack) { 173 | return console.error('Stack does not exist: ' + stackName); 174 | } 175 | console.log('Status: '+stack.StackStatus); 176 | console.log('Outputs: '); 177 | stack.Outputs.forEach(function (output) { 178 | console.log(' '+output.OutputKey+' = '+output.OutputValue); 179 | }); 180 | console.log(''); 181 | console.log('Use gulp pipeline:log to view full event log'); 182 | 183 | }); 184 | }); 185 | 186 | gulp.task(taskPrefix+':log', function() { 187 | return util.getStack(stackName) 188 | .then(function(stack){ 189 | if (!stack) { 190 | return console.log('Stack does not exist: ' + stackName); 191 | } 192 | 193 | cloudFormation.describeStackEvents({StackName: stackName}, function(err, data) { 194 | if (!data) { 195 | console.log('No log info available for ' + stackName); 196 | return; 197 | } 198 | var events = data.StackEvents; 199 | events.sort(function(a, b) { 200 | return new Date(a.Timestamp).getTime() - new Date(b.Timestamp).getTime(); 201 | }); 202 | events.forEach(function(event) { 203 | event.Timestamp = new Date(event.Timestamp).toLocaleString().replace(',', ''); 204 | event.ResourceType = '[' + event.ResourceType + ']'; 205 | console.log(event.Timestamp+' '+event.ResourceStatus+' '+event.LogicalResourceId+event.ResourceType+' '+event.ResourceStatusReason); 206 | }); 207 | }); 208 | 209 | }); 210 | }); 211 | }; 212 | 213 | -------------------------------------------------------------------------------- /pipeline/lambda/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs'); 4 | 5 | var AWS = require('aws-sdk'); 6 | 7 | var yauzl = require("yauzl"); // for .zip 8 | var mkdirp = require("mkdirp"); // for .zip 9 | var path = require("path"); // for .zip 10 | 11 | var tar = require('tar'); // for .tar.gz 12 | var zlib = require('zlib'); // for .tar.gz 13 | var fstream = require("fstream"); // for .tar.gz 14 | 15 | var mime = require('mime'); // for S3 bucket upload 16 | var moment = require('moment'); // for config version 17 | 18 | var childProcess = require('child_process'); // for exec 19 | var querystring = require('querystring'); // for user parameters 20 | var Promise = require('promise'); // for sanity! 21 | 22 | 23 | if(!AWS.config.region) { 24 | AWS.config.region = process.env.AWS_DEFAULT_REGION; 25 | } 26 | var codepipeline = new AWS.CodePipeline(); 27 | var lambda = new AWS.Lambda(); 28 | var s3 = new AWS.S3({maxRetries: 10, signatureVersion: "v4"}); 29 | 30 | // run npm 31 | exports.npmHandler = function( event, context ) { 32 | doAction(npmAction, event, context); 33 | }; 34 | 35 | // run gulp 36 | exports.gulpHandler = function( event, context ) { 37 | doAction(gulpAction, event, context); 38 | }; 39 | 40 | // run deploy 41 | exports.deployHandler = function( event, context ) { 42 | doAction(deployAction, event, context); 43 | }; 44 | 45 | // run an action 46 | function doAction(actionFunction, event, context) { 47 | console.log(JSON.stringify(event)); 48 | 49 | var promise; 50 | try { 51 | promise = actionFunction(event["CodePipeline.job"]) 52 | } catch (e) { 53 | promise = Promise.reject(e); 54 | } 55 | 56 | handlePromise(promise, event, context); 57 | }; 58 | 59 | // handle promise by notifying code pipeline 60 | function handlePromise(promise, event, context) { 61 | promise 62 | .then(function() { 63 | console.log("Success!"); 64 | 65 | var params = { 66 | jobId: event["CodePipeline.job"].id 67 | }; 68 | codepipeline.putJobSuccessResult(params, function(err, data) { 69 | if(err) { 70 | context.fail(err); 71 | } else { 72 | context.succeed("Action complete."); 73 | } 74 | }); 75 | }).catch( function(message) { 76 | var userParams = querystring.parse( event["CodePipeline.job"].data.actionConfiguration.configuration.UserParameters ); 77 | var retrys = parseInt(userParams['retrys']) || 0 78 | var continuationToken = parseInt(event["CodePipeline.job"].data.continuationToken) || 0; 79 | 80 | 81 | console.log("Prior attempts="+continuationToken+" and retrys="+retrys); 82 | if(continuationToken < retrys) { 83 | console.log("Retrying later."); 84 | 85 | var params = { 86 | jobId: event["CodePipeline.job"].id, 87 | continuationToken: (continuationToken+1).toString() 88 | }; 89 | codepipeline.putJobSuccessResult(params, function(err, data) { 90 | if(err) { 91 | context.fail(err); 92 | } else { 93 | context.succeed("Action complete."); 94 | } 95 | }); 96 | 97 | } else { 98 | var m = JSON.stringify(message); 99 | console.error("Failure: "+m); 100 | 101 | var params = { 102 | jobId: event["CodePipeline.job"].id, 103 | failureDetails: { 104 | message: m, 105 | type: 'JobFailed', 106 | externalExecutionId: context.invokeid 107 | } 108 | }; 109 | 110 | codepipeline.putJobFailureResult(params, function(err, data) { 111 | context.fail(m); 112 | }); 113 | } 114 | }); 115 | 116 | }; 117 | 118 | // run npm 119 | // 120 | // return: promise 121 | function npmAction(jobDetails) { 122 | var userParams = querystring.parse( jobDetails.data.actionConfiguration.configuration.UserParameters ); 123 | var artifactName = 'SourceOutput'; 124 | var artifactZipPath = '/tmp/source.zip'; 125 | var artifactExtractPath = '/tmp/source/'; 126 | 127 | var outArtifactName = 'SourceInstalledOutput'; 128 | var outArtifactTarballPath = '/tmp/source_installed.tar.gz'; 129 | 130 | return downloadInputArtifact(jobDetails, artifactName, artifactZipPath) 131 | .then(function () { 132 | return rmdir(artifactExtractPath); 133 | }).then(function () { 134 | return extractZip(artifactZipPath, artifactExtractPath); 135 | }).then(function () { 136 | return installNpm(artifactExtractPath); 137 | }).then(function () { 138 | var subcommand = userParams['subcommand']; 139 | return runNpm(artifactExtractPath, subcommand); 140 | }).then(function () { 141 | return packTarball(artifactExtractPath, outArtifactTarballPath); 142 | }).then(function () { 143 | return uploadOutputArtifact(jobDetails, outArtifactName, outArtifactTarballPath); 144 | }); 145 | } 146 | 147 | 148 | // run gulp 149 | // 150 | // return: promise 151 | function gulpAction(jobDetails) { 152 | var userParams = querystring.parse( jobDetails.data.actionConfiguration.configuration.UserParameters ); 153 | var artifactName = 'SourceInstalledOutput'; 154 | var artifactZipPath = '/tmp/source_installed.tar.gz'; 155 | var artifactExtractPath = '/tmp/source_installed/'; 156 | 157 | return downloadInputArtifact(jobDetails, artifactName, artifactZipPath) 158 | .then(function () { 159 | return rmdir(artifactExtractPath); 160 | }).then(function () { 161 | return extractTarball(artifactZipPath, artifactExtractPath); 162 | }).then(function () { 163 | return installNpm(artifactExtractPath); 164 | }).then(function () { 165 | var env = {}; 166 | for(var key in userParams) { 167 | var match = key.match(/^Env\.(.+)$/); 168 | if(match) { 169 | env[match[1]] = userParams[key]; 170 | } 171 | } 172 | 173 | return runGulp(artifactExtractPath, userParams.task, env); 174 | }).then(function() { 175 | var rtn = Promise.resolve(true); 176 | jobDetails.data.outputArtifacts.forEach(function(artifact) { 177 | rtn = rtn.then(function() { 178 | return uploadOutputArtifact(jobDetails, artifact.name, artifactExtractPath + userParams[artifact.name]); 179 | }) 180 | }); 181 | 182 | return rtn; 183 | }); 184 | } 185 | 186 | 187 | 188 | // run deploy 189 | // 190 | // return: promise 191 | function deployAction(jobDetails) { 192 | var userParams = querystring.parse(jobDetails.data.actionConfiguration.configuration.UserParameters); 193 | switch (userParams.type) { 194 | case 's3': 195 | return deployS3Action(jobDetails,userParams.bucket,userParams.apiBaseurl); 196 | case 'lambda': 197 | return deployLambdaAction(jobDetails,userParams.alias,userParams.function); 198 | case 'apigateway': 199 | return deployApiGatewayAction(jobDetails,userParams.stage,userParams.name); 200 | default: 201 | return Promise.reject(); 202 | } 203 | } 204 | 205 | function deployS3Action(jobDetails,bucketName,apiBaseurl) { 206 | var artifactZipPath = '/tmp/dist.zip'; 207 | var artifactExtractPath = '/tmp/dist/'; 208 | var artifactName = 'DistSiteOutput'; 209 | 210 | return downloadInputArtifact(jobDetails, artifactName, artifactZipPath) 211 | .then(function () { 212 | return rmdir(artifactExtractPath); 213 | }).then(function () { 214 | return extractZip(artifactZipPath, artifactExtractPath); 215 | }).then(function () { 216 | return uploadToS3(artifactExtractPath,bucketName); 217 | }).then(function () { 218 | // TODO: get version from github artifact, get apiBaseurl from ??? 219 | var version = ""; 220 | return uploadConfig(apiBaseurl,version,bucketName); 221 | }); 222 | } 223 | 224 | 225 | function deployLambdaAction(jobDetails,alias,functionName) { 226 | var artifactZipPath = '/tmp/dist.zip'; 227 | var artifactName = 'DistLambdaOutput'; 228 | 229 | return downloadInputArtifact(jobDetails, artifactName, artifactZipPath) 230 | .then(function () { 231 | return uploadLambda(artifactZipPath,alias,functionName); 232 | }); 233 | }; 234 | 235 | function deployApiGatewayAction(jobDetails,stageName,apiName) { 236 | var artifactPath = '/tmp/dist.json'; 237 | var artifactName = 'DistSwaggerOutput'; 238 | 239 | return downloadInputArtifact(jobDetails, artifactName, artifactPath) 240 | .then(function () { 241 | // upload swagger to api gateway 242 | return true; 243 | }); 244 | }; 245 | 246 | 247 | // get codepipeline job details from aws 248 | // 249 | // return: promise 250 | function getJobDetails(jobId) { 251 | console.log("Getting CodePipeline Job Details for '"+jobId+"'"); 252 | return new Promise(function (resolve, reject) { 253 | var params = { jobId: jobId }; 254 | codepipeline.getJobDetails(params, function(err, data) { 255 | if(err) reject(err); 256 | else resolve(data); 257 | }); 258 | }); 259 | } 260 | 261 | // get s3 object 262 | // 263 | // return: promise 264 | function getS3Object(params, dest) { 265 | return new Promise(function(resolve,reject) { 266 | console.log("Getting S3 Object '" + params.Bucket+"/"+params.Key + "' to '"+dest+"'"); 267 | var file = fs.createWriteStream(dest); 268 | s3.getObject(params) 269 | .createReadStream() 270 | .on('error', reject) 271 | .pipe(file) 272 | .on('error', reject) 273 | .on('close', resolve); 274 | }); 275 | } 276 | 277 | // put s3 object 278 | // 279 | // return: promise 280 | function putS3Object(params, path) { 281 | return new Promise(function(resolve,reject) { 282 | console.log("Putting S3 Object '" + params.Bucket+"/"+params.Key + "' from '"+path+"'"); 283 | params.Body = fs.createReadStream(path); 284 | s3.putObject(params, function(err, data) { 285 | if(err) { 286 | reject(err); 287 | } else { 288 | resolve(data); 289 | } 290 | }); 291 | }); 292 | } 293 | 294 | function uploadOutputArtifact(jobDetails, artifactName, path) { 295 | console.log("Uploading output artifact '" + artifactName + "' from '"+path+"'"); 296 | 297 | // Get the output artifact 298 | var artifact = null; 299 | jobDetails.data.outputArtifacts.forEach(function (a) { 300 | if (a.name == artifactName) { 301 | artifact = a; 302 | } 303 | }); 304 | 305 | if (artifact != null && artifact.location.type == 'S3') { 306 | var params = { 307 | Bucket: artifact.location.s3Location.bucketName, 308 | Key: artifact.location.s3Location.objectKey 309 | }; 310 | return putS3Object(params, path); 311 | } else { 312 | return Promise.reject("Unknown Source Type:" + JSON.stringify(sourceOutput)); 313 | } 314 | } 315 | 316 | function uploadToS3(dir,bucket) { 317 | console.log("Uploading directory '" + dir + "' to '"+bucket+"'"); 318 | 319 | var rtn = Promise.resolve(true); 320 | var files = fs.readdirSync(dir); 321 | files.forEach(function(file) { 322 | var path = dir + '/' + file; 323 | if (!fs.statSync(path).isDirectory()) { 324 | var params = { 325 | Bucket: bucket, 326 | Key: file, 327 | ACL: 'public-read', 328 | ContentType: mime.lookup(path), 329 | CacheControl: 'no-cache, no-store, must-revalidate', 330 | Expires: 0, 331 | } 332 | 333 | rtn = rtn.then(function() { 334 | return putS3Object(params, path); 335 | }) 336 | } 337 | }); 338 | return rtn; 339 | } 340 | 341 | function uploadLambda(zipPath,alias,functionArn) { 342 | return new Promise(function(resolve,reject) { 343 | console.log("Uploading lambda '" + zipPath + "' to '"+functionArn+"'"); 344 | var params = { 345 | FunctionName: functionArn, 346 | Publish: true, 347 | ZipFile: fs.readFileSync(zipPath) 348 | }; 349 | 350 | lambda.updateFunctionCode(params, function(err, data) { 351 | if (err) { 352 | reject(err); 353 | } else { 354 | console.log("Tagging lambda version '"+data.Version+"' with '"+alias+"'"); 355 | var aliasParams = { 356 | FunctionName: functionArn, 357 | FunctionVersion: data.Version, 358 | Name: alias 359 | }; 360 | lambda.deleteAlias({'FunctionName': functionArn, Name: alias}, function() { 361 | lambda.createAlias(aliasParams, function (err) { 362 | if (err) { 363 | reject(err); 364 | } else { 365 | resolve(); 366 | } 367 | }); 368 | }); 369 | } 370 | }); 371 | 372 | }); 373 | } 374 | 375 | function uploadConfig(apiBaseurl, version,bucketName) { 376 | if(version == "") { 377 | // default to a timestamp 378 | version = moment().format('YYYYMMDD-HHmmss'); 379 | } 380 | 381 | var config = { 382 | apiBaseurl: apiBaseurl, 383 | version: version 384 | } 385 | 386 | var params = { 387 | Bucket: bucketName, 388 | Key: 'config.json', 389 | ACL: 'public-read', 390 | CacheControl: 'no-cache, no-store, must-revalidate', 391 | Expires: 0, 392 | ContentType: 'application/javascript', 393 | Body: JSON.stringify(config) 394 | } 395 | 396 | return new Promise(function(resolve,reject) { 397 | console.log("Putting Config '" + params.Bucket+"/"+params.Key + "' from: "+params.Body); 398 | 399 | s3.putObject(params, function (err, data) { 400 | if(err) { 401 | reject(err); 402 | } else { 403 | resolve(data); 404 | } 405 | }); 406 | }); 407 | } 408 | 409 | 410 | // get input artifact 411 | // 412 | // return: promise 413 | function downloadInputArtifact(jobDetails, artifactName, dest) { 414 | console.log("Downloading input artifact '" + artifactName + "' to '"+dest+"'"); 415 | 416 | // Get the input artifact 417 | var artifact = null; 418 | jobDetails.data.inputArtifacts.forEach(function (a) { 419 | if (a.name == artifactName) { 420 | artifact = a; 421 | } 422 | }); 423 | 424 | if (artifact != null && artifact.location.type == 'S3') { 425 | var params = { 426 | Bucket: artifact.location.s3Location.bucketName, 427 | Key: artifact.location.s3Location.objectKey 428 | }; 429 | return getS3Object(params, dest); 430 | } else { 431 | return Promise.reject("Unknown Source Type:" + JSON.stringify(sourceOutput)); 432 | } 433 | } 434 | 435 | function packTarball(sourceDirectory, destPath) { 436 | return new Promise(function (resolve, reject) { 437 | console.log("Creating tarball '"+destPath+"' from '"+sourceDirectory+"'"); 438 | 439 | 440 | var packer = tar.Pack({ noProprietary: true, fromBase: true }) 441 | .on('error', reject); 442 | 443 | var gzip = zlib.createGzip() 444 | .on('error', reject) 445 | 446 | var destFile = fs.createWriteStream(destPath) 447 | .on('error', reject) 448 | .on('close', resolve); 449 | 450 | fstream.Reader({ path: sourceDirectory, type: "Directory" }) 451 | .on('error', reject) 452 | .pipe(packer) 453 | .pipe(gzip) 454 | .pipe(destFile); 455 | }); 456 | } 457 | 458 | function extractTarball(sourcePath,destDirectory) { 459 | return new Promise(function (resolve, reject) { 460 | console.log("Extracting tarball '" + sourcePath+ "' to '" + destDirectory + "'"); 461 | 462 | var sourceFile = fs.createReadStream(sourcePath) 463 | .on('error', reject); 464 | 465 | var gunzip = zlib.createGunzip() 466 | .on('error', reject) 467 | 468 | var extractor = tar.Extract({path: destDirectory}) 469 | .on('error', reject) 470 | .on('end', resolve); 471 | 472 | sourceFile 473 | .pipe(gunzip) 474 | .pipe(extractor); 475 | }); 476 | } 477 | 478 | function rmdir(dir) { 479 | if(!dir || dir == '/') { 480 | throw new Error('Invalid directory '+dir); 481 | } 482 | 483 | console.log("Cleaning directory '"+dir+"'"); 484 | return exec('rm -rf '+dir); 485 | } 486 | 487 | // extract zip to directory 488 | // 489 | // return: promise 490 | function extractZip(sourceZip,destDirectory) { 491 | return new Promise(function (resolve, reject) { 492 | console.log("Extracting zip: '"+sourceZip+"' to '"+destDirectory+"'"); 493 | 494 | yauzl.open(sourceZip, {lazyEntries: true}, function(err, zipfile) { 495 | if (err) throw err; 496 | zipfile.readEntry(); 497 | zipfile.on("error", reject); 498 | zipfile.on("end", resolve); 499 | zipfile.on("entry", function(entry) { 500 | if (/\/$/.test(entry.fileName)) { 501 | // directory file names end with '/' 502 | mkdirp(destDirectory+'/'+entry.fileName, function(err) { 503 | if (err) throw err; 504 | zipfile.readEntry(); 505 | }); 506 | } else { 507 | // file entry 508 | zipfile.openReadStream(entry, function(err, readStream) { 509 | if (err) throw err; 510 | // ensure parent directory exists 511 | mkdirp(destDirectory+'/'+path.dirname(entry.fileName), function(err) { 512 | if (err) throw err; 513 | readStream.pipe(fs.createWriteStream(destDirectory+'/'+entry.fileName)); 514 | readStream.on("end", function() { 515 | zipfile.readEntry(); 516 | }); 517 | }); 518 | }); 519 | } 520 | }); 521 | }); 522 | }); 523 | } 524 | 525 | // install NPM 526 | // 527 | // return: promise 528 | function installNpm(destDirectory) { 529 | console.log("Installing npm into '" + destDirectory + "'"); 530 | return exec('cp -r '+__dirname+'/node_modules ' + destDirectory, {cwd: destDirectory}); 531 | } 532 | 533 | // run npm install 534 | // 535 | // return: promise 536 | function runNpm(packageDirectory, subcommand) { 537 | console.log("Running 'npm "+subcommand+"' in '"+packageDirectory+"'"); 538 | var env = {}; 539 | env['HOME']= '/tmp'; 540 | return exec('node '+packageDirectory+'/node_modules/npm/bin/npm-cli.js '+subcommand, {cwd: packageDirectory, env: env}); 541 | } 542 | 543 | // run gulp 544 | // 545 | // return: promise 546 | function runGulp(packageDirectory, task, env) { 547 | console.log("Running gulp task '" + task + "' in '"+packageDirectory+"'"); 548 | // clone the env, append npm to path 549 | for (var e in process.env) env[e] = process.env[e]; 550 | env['PATH'] += (':'+packageDirectory+'/node_modules/.bin/'); 551 | env['PREFIX'] = packageDirectory+'/global-prefix'; 552 | env['HOME']= '/tmp'; 553 | return exec('node '+packageDirectory+'/node_modules/gulp/bin/gulp.js --no-color '+task,{cwd: packageDirectory, env: env}); 554 | } 555 | 556 | 557 | // run shell script 558 | // 559 | function exec(command,options) { 560 | return new Promise(function (resolve, reject) { 561 | var child = childProcess.exec(command,options); 562 | 563 | var lastMessage = "" 564 | child.stdout.on('data', function(data) { 565 | lastMessage = data.toString('utf-8'); 566 | process.stdout.write(data); 567 | }); 568 | child.stderr.on('data', function(data) { 569 | lastMessage = data.toString('utf-8'); 570 | process.stderr.write(data); 571 | }); 572 | child.on('close', function (code) { 573 | if(!code) { 574 | resolve(true); 575 | } else { 576 | reject("Error("+code+") - "+lastMessage); 577 | } 578 | }); 579 | }); 580 | } 581 | 582 | -------------------------------------------------------------------------------- /pipeline/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-pipeline-actions", 3 | "version": "0.1.0", 4 | "description": "Lambda to run gulp within codepipeline", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+ssh://git@github.com/cplee/serverless-pipeline.git" 9 | }, 10 | "main": "index.js", 11 | "dependencies": { 12 | "aws-sdk": "^2.2.36", 13 | "fstream": "^1.0.8", 14 | "mime": "^1.3.4", 15 | "mkdirp": "^0.5.1", 16 | "moment": "^2.11.2", 17 | "npm": "^2.0", 18 | "path": "^0.12.7", 19 | "promise": "^7.1.1", 20 | "tar": "^2.2.1", 21 | "yauzl": "^2.4.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pipeline/util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var AWS = require('aws-sdk'); 4 | var fs = require('fs'); 5 | var mime = require('mime'); 6 | var chalk = require('chalk'); 7 | var Promise = require('promise'); 8 | 9 | function Util() {}; 10 | module.exports = Util; 11 | 12 | 13 | var s3, cloudFormation; 14 | 15 | Util.init = function(region) { 16 | AWS.config.region = region 17 | s3 = new AWS.S3(); 18 | cloudFormation = new AWS.CloudFormation(); 19 | }; 20 | 21 | Util.getStack = function(stackName) { 22 | return new Promise(function(resolve,reject) { 23 | cloudFormation.describeStacks({StackName: stackName}, function(err, data) { 24 | if (err || data.Stacks == null) { 25 | resolve(null); 26 | } else { 27 | resolve(data.Stacks.find(function(s) { return s.StackName === stackName || s.StackId === stackName; })); 28 | } 29 | }); 30 | }); 31 | }; 32 | 33 | 34 | Util.getSubStackOutput = function(stackName,subStackName,outputKey) { 35 | return Util.getStackResource(stackName,subStackName) 36 | .then(function(subStackResource) { 37 | return Util.getStackOutput(subStackResource.PhysicalResourceId, outputKey); 38 | }); 39 | }; 40 | 41 | Util.getStackOutput = function(stackName, outputKey) { 42 | return Util.getStack(stackName) 43 | .then(function(stack) { 44 | if(stack) { 45 | try { 46 | return stack.Outputs.find(function (o) { return o.OutputKey === outputKey }).OutputValue; 47 | } catch (e) { 48 | return null; 49 | } 50 | } 51 | }); 52 | }; 53 | 54 | Util.getStackResource = function(stackName, resourceName) { 55 | return new Promise(function(resolve) { 56 | cloudFormation.describeStackResources({StackName: stackName, LogicalResourceId: resourceName}, function(err, data) { 57 | if (err || data.StackResources == null) { 58 | resolve(null); 59 | } else { 60 | resolve(data.StackResources.find(function (r) { return r.LogicalResourceId === resourceName })); 61 | } 62 | }); 63 | }); 64 | }; 65 | 66 | Util.emptyBucket = function(bucketName) { 67 | return new Promise(function(resolve,reject){ 68 | s3.listObjects({Bucket: bucketName}, function(err, data) { 69 | if (err) { 70 | resolve(true); 71 | } else { 72 | 73 | var objects = data.Contents.map(function (c) { return { Key: c.Key }}); 74 | var params = { 75 | Bucket: bucketName, 76 | Delete: { 77 | Objects: objects 78 | } 79 | }; 80 | 81 | if(objects.length > 0) { 82 | s3.deleteObjects(params, function(err) { 83 | if (err) { 84 | reject(err); 85 | } else { 86 | resolve(true); 87 | } 88 | }); 89 | } else { 90 | resolve(true); 91 | } 92 | } 93 | }); 94 | 95 | }); 96 | }; 97 | 98 | 99 | Util.uploadToS3 = function(dir,bucketName) { 100 | return new Promise(function(resolve,reject) { 101 | var files = fs.readdirSync(dir); 102 | var respCount = 0; 103 | files.forEach(function(file) { 104 | var path = dir + '/' + file; 105 | if (!fs.statSync(path).isDirectory()) { 106 | console.log("Uploading: "+ path); 107 | var params = { 108 | Bucket: bucketName, 109 | Key: file, 110 | ACL: 'public-read', 111 | ContentType: mime.lookup(path), 112 | Body: fs.readFileSync(path) 113 | } 114 | 115 | s3.putObject(params, function(err, data) { 116 | if (err) { 117 | console.log(err, err.stack); 118 | } 119 | 120 | if(++respCount >= files.length) { 121 | resolve(true); 122 | } 123 | }); 124 | } else { 125 | respCount++; 126 | } 127 | }); 128 | 129 | if(files.length==0) { 130 | resolve(true); 131 | } 132 | 133 | }); 134 | }; 135 | 136 | 137 | --------------------------------------------------------------------------------