├── .gitignore ├── README.md ├── assets ├── button-in-progress.svg ├── button-ready.svg └── infra_v2.png ├── docker ├── Dockerfile ├── change-deployment-bucket.py └── run.sh ├── functions ├── handler.js ├── href.js ├── image-proxy.js ├── services │ └── dynamodb.js └── utils │ ├── extractProjectName.js │ └── responses.js ├── package-lock.json ├── package.json ├── scripts ├── deploy-proxy.js ├── gen-passwd.sh └── push-image.sh ├── serverless.yml └── static ├── 404.html └── in-progress.html /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | password.yml 9 | user-data.json 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploy with Serverless 2 | 3 | Infrastructure for processing Serverless projects and creating one-click deployments. 4 | 5 | ![Infra](assets/infra_v2.png?raw=true "Infrastructure Overview") 6 | 7 | ## Demo 8 | [![](https://v26mdkczs6.execute-api.us-east-1.amazonaws.com/dev/image?url=https://github.com/RafalWilinski/serverless-medium-text-to-speech)](https://v26mdkczs6.execute-api.us-east-1.amazonaws.com/dev/template?url=http://github.com/RafalWilinski/serverless-medium-text-to-speech) 9 | 10 | 11 | *Clicking button above will start deploy procedure of [serverless-medium-text-to-speech](https://github.com/RafalWilinski/serverless-medium-text-to-speech) project on your AWS Account via CloudFormation* 12 | 13 | ### Goal 14 | Goal of the project is to create a mechanism for 1-click deployments, similar to Deploy on Heroku or Create Stack (CloudFormation) buttons, for Serverless Framework based projects. 15 | 16 | ## Adding to your own project 17 | In `README.md`, add following button/image: 18 | ``` 19 | [![](https://kkcohgzei0.execute-api.us-east-1.amazonaws.com/dev/image?url=)](https://kkcohgzei0.execute-api.us-east-1.amazonaws.com/dev/template?url=) 20 | ``` 21 | 22 | All the magic like building project, uploading artifacts or creating CloudFormation template is handled automatically! 23 | 24 | You can add following params to URL in order to change build procedure: 25 | - `before` - command invoked before `serverless package --stage dev`, by default it's `null` 26 | - `package` - by default `serverless package --stage dev` 27 | - `after` - command invoked after `serverless package --stage dev`, by default it's `null` 28 | 29 | E.g.: 30 | ```url 31 | https://kkcohgzei0.execute-api.us-east-1.amazonaws.com/dev/image 32 | ?url=http://github.com/RafalWilinski/serverless-medium-text-to-speech 33 | &before=npm%20run%20build 34 | &package=serverless%20package%20--stage%20prod 35 | &after=rm%20-fr%20node_modules 36 | ``` 37 | 38 | ## Development 39 | ### Prerequisites 40 | - Node 41 | - AWS Account, AWS CLI & credentials Set 42 | - Serverless Framework 43 | - Docker 44 | 45 | ### Setup 46 | ```bash 47 | git clone https://github.com/RafalWilinski/deploy-with-serverless 48 | cd deploy-with-serverless 49 | npm install 50 | ``` 51 | 52 | ### Commands 53 | - `npm run deploy` - Deploys whole infrastructure, builds Docker image which processes projects and pushes that image to ECR 54 | - `npm run gen-passwd` - generates a random password for user which interacts with ECS 55 | - `npm run deploy-img` - deploys image asset to public bucket 56 | - `npm run deploy-static` - deploys 404 page 57 | - `npm run deploy-assets` - deploys both imgs and static pages 58 | 59 | 60 | -------------------------------------------------------------------------------- /assets/button-in-progress.svg: -------------------------------------------------------------------------------- 1 | 2 |
Deploy with Serverless Framework
[Not supported by viewer]
Building...
[Not supported by viewer]
-------------------------------------------------------------------------------- /assets/button-ready.svg: -------------------------------------------------------------------------------- 1 | 2 |
Deploy with Serverless Framework
[Not supported by viewer]
-------------------------------------------------------------------------------- /assets/infra_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RafalWilinski/deploy-with-serverless/3d8e2d19a6f94fafebd1ece99cbf76d067c76956/assets/infra_v2.png -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.6 2 | 3 | RUN apt-get update && \ 4 | apt-get upgrade -y 5 | 6 | RUN apt-get install -y \ 7 | less \ 8 | man \ 9 | python \ 10 | python-pip \ 11 | python-dev \ 12 | python-yaml 13 | 14 | RUN pip install awscli 15 | RUN echo "export PATH=~/.local/bin:$PATH" > ~/.bashrc 16 | RUN ["npm", "install", "-g", "serverless"] 17 | 18 | COPY ./run.sh / 19 | COPY ./change-deployment-bucket.py / 20 | ENTRYPOINT [ "/run.sh" ] 21 | -------------------------------------------------------------------------------- /docker/change-deployment-bucket.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import io 3 | import sys 4 | 5 | with open("serverless.yml", 'r') as stream: 6 | data_loaded = yaml.load(stream) 7 | service_name = data_loaded['service'] 8 | 9 | data_loaded['provider']['deploymentBucket'] = sys.argv[1] 10 | 11 | with io.open('serverless.yml', 'w', encoding='utf8') as outfile: 12 | yaml.dump(data_loaded, outfile, default_flow_style=False, allow_unicode=True) 13 | 14 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | timestamp() { 4 | date +%s 5 | } 6 | 7 | aws configure set region us-east-1 8 | 9 | # Clone repository and change directory 10 | git clone $REPO_URL src 11 | cd src 12 | 13 | # Install dependencies 14 | echo 'Installing dependencies...' 15 | npm install 16 | 17 | # Apply optional build commands like babel or webpack 18 | # eval $BEFORE_CMD 19 | 20 | # Change SLS bucket 21 | python ../change-deployment-bucket.py $BUCKET 22 | 23 | # Run `serverless package --stage dev`, this might be overriden 24 | # eval $PACKAGE_CMD 25 | serverless package --stage dev 26 | 27 | # eval $AFTER_CMD 28 | 29 | # Go to artifacts & compiled Cloudformation template path 30 | cd .serverless 31 | 32 | pwd 33 | 34 | # Upload CFN Template 35 | echo 'Uploading CFN template...' 36 | aws s3 sync . s3://$BUCKET --exclude "*.zip" --acl public-read 37 | 38 | # Put dynamodb item 39 | aws dynamodb put-item \ 40 | --table-name serverless-projects \ 41 | --item '{ 42 | "url": {"S": "'"$REPO_URL"'"}, 43 | "name": {"S": "'"$REPO_NAME"'"}, 44 | "bucket": {"S": "'"$BUCKET"'"}, 45 | "inProgress": {"BOOL": false} 46 | }' 47 | -------------------------------------------------------------------------------- /functions/handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const AWS = require("aws-sdk"); 4 | const fs = require("fs"); 5 | const path = require("path"); 6 | const extractProjectName = require("./utils/extractProjectName"); 7 | const response = require("./utils/responses"); 8 | const DynamoDB = require("./services/dynamodb"); 9 | 10 | const Batch = new AWS.Batch(); 11 | 12 | module.exports.run = (json, context, callback) => { 13 | const timestamp = Math.round(+new Date() / 1000); 14 | const name = extractProjectName(json.url); 15 | 16 | console.log(`URL: ${json.url}, Name: ${name}`); 17 | 18 | Batch.submitJob({ 19 | jobDefinition: `${process.env.JOB_DEFINITON_NAME}`, 20 | jobName: name, 21 | jobQueue: process.env.JOB_QUEUE, 22 | containerOverrides: { 23 | environment: [ 24 | { 25 | name: "REPO_URL", 26 | value: json.url 27 | }, 28 | { 29 | name: "REPO_NAME", 30 | value: name 31 | }, 32 | { 33 | name: "BEFORE_CMD", 34 | value: json.before || 'echo "Before cmd not specified"' 35 | }, 36 | { 37 | name: "AFTER_CMD", 38 | value: json.after || 'echo "After cmd not specified"' 39 | }, 40 | { 41 | name: "PACKAGE_CMD", 42 | value: json.package || "serverless package --stage dev" 43 | }, 44 | { 45 | name: "BUCKET", 46 | value: json.bucket 47 | } 48 | ] 49 | } 50 | }) 51 | .promise() 52 | .then(data => { 53 | console.log(data); 54 | return response.dataCode( 55 | 200, 56 | JSON.stringify({ message: "OK" }), 57 | callback 58 | ); 59 | }) 60 | .catch(error => { 61 | console.error(error); 62 | return response.dataCode(200, JSON.stringify({ error }), callback); 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /functions/href.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const DynamoDB = require("./services/dynamodb"); 4 | const responses = require("./utils/responses"); 5 | 6 | const return404 = callback => 7 | responses.redirect( 8 | "https://s3.amazonaws.com/deploy-with-serverless/404.html", 9 | callback 10 | ); 11 | 12 | // Redirects to latest CFN template 13 | module.exports.run = (event, context, callback) => { 14 | DynamoDB.get({ 15 | url: event.queryStringParameters.url 16 | }) 17 | .then(item => { 18 | if (!item) { 19 | return return404(callback); 20 | } 21 | 22 | if (item.inProgress) { 23 | return responses.redirect( 24 | "https://s3.amazonaws.com/deploy-with-serverless/in-progress.html", 25 | callback 26 | ); 27 | } 28 | 29 | const url = [ 30 | "https://console.aws.amazon.com/cloudformation/home?region=us-east-1", 31 | `#/stacks/new?stackName=${item.name}`, 32 | `&templateURL=https://s3.amazonaws.com/${item.bucket}`, 33 | "/cloudformation-template-update-stack.json" 34 | ].join(""); 35 | 36 | return responses.redirect(url, callback); 37 | }) 38 | .catch(error => { 39 | console.error(error); 40 | return return404(callback); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /functions/image-proxy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const AWS = require("aws-sdk"); 4 | const DynamoDB = require("./services/dynamodb"); 5 | const response = require("./utils/responses"); 6 | const extractProjectName = require("./utils/extractProjectName"); 7 | const Lambda = new AWS.Lambda(); 8 | const S3 = new AWS.S3(); 9 | 10 | const createBucket = Bucket => 11 | S3.createBucket({ 12 | Bucket, 13 | ACL: "public-read" 14 | }).promise(); 15 | 16 | const returnReady = callback => 17 | response.redirect( 18 | "https://s3.amazonaws.com/deploy-with-serverless/button-ready.svg", 19 | callback 20 | ); 21 | 22 | module.exports.run = (event, context, callback) => { 23 | // TODO: Check if built project is up to date 24 | 25 | const timestamp = +new Date(); 26 | const url = event.queryStringParameters.url; 27 | const before = event.queryStringParameters.before; 28 | const pkg = event.queryStringParameters.package; 29 | const after = event.queryStringParameters.after; 30 | const bucket = `${extractProjectName(url)}-${timestamp}`; 31 | 32 | DynamoDB.get({ 33 | url 34 | }) 35 | .then(item => { 36 | if (!item) { 37 | console.log("Project not found, submitting job..."); 38 | 39 | console.log(bucket); 40 | 41 | createBucket(bucket) 42 | .then(() => { 43 | return Lambda.invoke({ 44 | FunctionName: "deploy-with-serverless-dev-handler", 45 | Payload: JSON.stringify({ 46 | url, 47 | before, 48 | package: pkg, 49 | after, 50 | bucket 51 | }) 52 | }) 53 | .promise() 54 | .then(() => { 55 | DynamoDB.put({ 56 | inProgress: true, 57 | url, 58 | name: extractProjectName(url), 59 | bucket 60 | }).then(data => { 61 | console.log(data); 62 | return returnReady(callback); 63 | }); 64 | }); 65 | }) 66 | .catch(error => { 67 | console.error(error); 68 | return returnReady(callback); 69 | }); 70 | } 71 | 72 | console.log("Project already built..."); 73 | 74 | return returnReady(callback); 75 | }) 76 | .catch(error => { 77 | console.error(error); 78 | return returnReady(callback); 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /functions/services/dynamodb.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | const DynamoDB = new AWS.DynamoDB.DocumentClient({ region: "us-east-1" }); 3 | 4 | const put = Item => 5 | DynamoDB.put( 6 | Object.assign( 7 | {}, 8 | { Item }, 9 | { 10 | TableName: process.env.PROJECTS_TABLE 11 | } 12 | ) 13 | ).promise(); 14 | 15 | const get = Key => 16 | DynamoDB.get( 17 | Object.assign( 18 | {}, 19 | { Key }, 20 | { 21 | TableName: process.env.PROJECTS_TABLE 22 | } 23 | ) 24 | ) 25 | .promise() 26 | .then(data => data.Item); 27 | 28 | module.exports = { 29 | put, 30 | get 31 | }; 32 | -------------------------------------------------------------------------------- /functions/utils/extractProjectName.js: -------------------------------------------------------------------------------- 1 | /* 2 | * In: https://github.com/serverless/serverless 3 | * Out: serverless-serverless 4 | */ 5 | const extractProjectName = url => { 6 | const arr = url.split("/"); 7 | return `${arr[4]}`; 8 | }; 9 | 10 | module.exports = extractProjectName; 11 | -------------------------------------------------------------------------------- /functions/utils/responses.js: -------------------------------------------------------------------------------- 1 | const dataCode = (statusCode, body, callback) => { 2 | return callback(null, { 3 | statusCode, 4 | body: JSON.stringify(body) 5 | }); 6 | }; 7 | 8 | const redirect = (Location, callback) => { 9 | return callback(null, { 10 | statusCode: 307, 11 | body: "", 12 | headers: { 13 | Location, 14 | "Cache-Control": "max-age=60", 15 | ETag: String(Math.random() * 1000000) 16 | } 17 | }); 18 | }; 19 | 20 | module.exports = { 21 | dataCode, 22 | redirect 23 | }; 24 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deploy-with-serverless", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "argparse": { 8 | "version": "1.0.9", 9 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", 10 | "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", 11 | "dev": true, 12 | "requires": { 13 | "sprintf-js": "1.0.3" 14 | } 15 | }, 16 | "aws-sdk": { 17 | "version": "2.130.0", 18 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.130.0.tgz", 19 | "integrity": "sha1-8Mbafb2poVywwr6zAw9toqTAPDY=", 20 | "dev": true, 21 | "requires": { 22 | "buffer": "4.9.1", 23 | "crypto-browserify": "1.0.9", 24 | "events": "1.1.1", 25 | "jmespath": "0.15.0", 26 | "querystring": "0.2.0", 27 | "sax": "1.2.1", 28 | "url": "0.10.3", 29 | "uuid": "3.1.0", 30 | "xml2js": "0.4.17", 31 | "xmlbuilder": "4.2.1" 32 | } 33 | }, 34 | "balanced-match": { 35 | "version": "1.0.0", 36 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 37 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 38 | "dev": true 39 | }, 40 | "base64-js": { 41 | "version": "1.2.1", 42 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", 43 | "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", 44 | "dev": true 45 | }, 46 | "brace-expansion": { 47 | "version": "1.1.8", 48 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", 49 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", 50 | "dev": true, 51 | "requires": { 52 | "balanced-match": "1.0.0", 53 | "concat-map": "0.0.1" 54 | } 55 | }, 56 | "buffer": { 57 | "version": "4.9.1", 58 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", 59 | "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", 60 | "dev": true, 61 | "requires": { 62 | "base64-js": "1.2.1", 63 | "ieee754": "1.1.8", 64 | "isarray": "1.0.0" 65 | } 66 | }, 67 | "concat-map": { 68 | "version": "0.0.1", 69 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 70 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 71 | "dev": true 72 | }, 73 | "crypto-browserify": { 74 | "version": "1.0.9", 75 | "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", 76 | "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=", 77 | "dev": true 78 | }, 79 | "events": { 80 | "version": "1.1.1", 81 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 82 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", 83 | "dev": true 84 | }, 85 | "fs.realpath": { 86 | "version": "1.0.0", 87 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 88 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 89 | "dev": true 90 | }, 91 | "glob": { 92 | "version": "7.1.2", 93 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 94 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 95 | "dev": true, 96 | "requires": { 97 | "fs.realpath": "1.0.0", 98 | "inflight": "1.0.6", 99 | "inherits": "2.0.3", 100 | "minimatch": "3.0.4", 101 | "once": "1.4.0", 102 | "path-is-absolute": "1.0.1" 103 | } 104 | }, 105 | "ieee754": { 106 | "version": "1.1.8", 107 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", 108 | "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", 109 | "dev": true 110 | }, 111 | "inflight": { 112 | "version": "1.0.6", 113 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 114 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 115 | "dev": true, 116 | "requires": { 117 | "once": "1.4.0", 118 | "wrappy": "1.0.2" 119 | } 120 | }, 121 | "inherits": { 122 | "version": "2.0.3", 123 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 124 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 125 | "dev": true 126 | }, 127 | "isarray": { 128 | "version": "1.0.0", 129 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 130 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 131 | "dev": true 132 | }, 133 | "jmespath": { 134 | "version": "0.15.0", 135 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 136 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", 137 | "dev": true 138 | }, 139 | "lodash": { 140 | "version": "4.17.4", 141 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 142 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", 143 | "dev": true 144 | }, 145 | "minimatch": { 146 | "version": "3.0.4", 147 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 148 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 149 | "dev": true, 150 | "requires": { 151 | "brace-expansion": "1.1.8" 152 | } 153 | }, 154 | "once": { 155 | "version": "1.4.0", 156 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 157 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 158 | "dev": true, 159 | "requires": { 160 | "wrappy": "1.0.2" 161 | } 162 | }, 163 | "path-is-absolute": { 164 | "version": "1.0.1", 165 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 166 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 167 | "dev": true 168 | }, 169 | "querystring": { 170 | "version": "0.2.0", 171 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 172 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", 173 | "dev": true 174 | }, 175 | "sax": { 176 | "version": "1.2.1", 177 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 178 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", 179 | "dev": true 180 | }, 181 | "serverless-stack-output": { 182 | "version": "0.2.0", 183 | "resolved": "https://registry.npmjs.org/serverless-stack-output/-/serverless-stack-output-0.2.0.tgz", 184 | "integrity": "sha1-lReHJ7SwmZ7OCSUcsk6qAc3Npj0=", 185 | "dev": true, 186 | "requires": { 187 | "tomlify-j0.4": "2.2.1", 188 | "yamljs": "0.3.0" 189 | } 190 | }, 191 | "sprintf-js": { 192 | "version": "1.0.3", 193 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 194 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 195 | "dev": true 196 | }, 197 | "tomlify-j0.4": { 198 | "version": "2.2.1", 199 | "resolved": "https://registry.npmjs.org/tomlify-j0.4/-/tomlify-j0.4-2.2.1.tgz", 200 | "integrity": "sha512-0kCocYX8ujnbK6jQ9e+g9GLiCIfVkFaCB3DCTQDP7J79gPVZVmZgQZ/KUNe1a6hUfrmHHaErVGUjedfpaX5EZw==", 201 | "dev": true 202 | }, 203 | "url": { 204 | "version": "0.10.3", 205 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 206 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 207 | "dev": true, 208 | "requires": { 209 | "punycode": "1.3.2", 210 | "querystring": "0.2.0" 211 | }, 212 | "dependencies": { 213 | "punycode": { 214 | "version": "1.3.2", 215 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 216 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", 217 | "dev": true 218 | } 219 | } 220 | }, 221 | "uuid": { 222 | "version": "3.1.0", 223 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", 224 | "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", 225 | "dev": true 226 | }, 227 | "wrappy": { 228 | "version": "1.0.2", 229 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 230 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 231 | "dev": true 232 | }, 233 | "xml2js": { 234 | "version": "0.4.17", 235 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz", 236 | "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", 237 | "dev": true, 238 | "requires": { 239 | "sax": "1.2.1", 240 | "xmlbuilder": "4.2.1" 241 | } 242 | }, 243 | "xmlbuilder": { 244 | "version": "4.2.1", 245 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", 246 | "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", 247 | "dev": true, 248 | "requires": { 249 | "lodash": "4.17.4" 250 | } 251 | }, 252 | "yamljs": { 253 | "version": "0.3.0", 254 | "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", 255 | "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", 256 | "dev": true, 257 | "requires": { 258 | "argparse": "1.0.9", 259 | "glob": "7.1.2" 260 | } 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deploy-with-serverless", 3 | "version": "0.0.2", 4 | "description": "Architecture behind Deploy with Serverless button", 5 | "main": "index.js", 6 | "scripts": { 7 | "deploy": "serverless deploy", 8 | "gen-passwd": "./scripts/gen-passwd.sh", 9 | "deploy-imgs": "aws s3 cp ./assets s3://deploy-with-serverless --acl public-read --recursive --exclude \"*.png\"", 10 | "deploy-static": "aws s3 cp ./static s3://deploy-with-serverless --acl public-read --recursive", 11 | "deploy-assets": "npm run deploy-imgs && npm run deploy-static" 12 | }, 13 | "author": "Rafal Wilinski ", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "aws-sdk": "^2.130.0", 17 | "serverless-stack-output": "^0.2.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scripts/deploy-proxy.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const AWS = require('aws-sdk'); 3 | const spawn = require('child_process').spawn; 4 | const STS = new AWS.STS(); 5 | 6 | const handler = (data) => { 7 | console.log('Building & pushing docker image...'); 8 | 9 | STS.getCallerIdentity({}).promise().then((payload) => { 10 | const deploy = spawn('/bin/sh', ['./scripts/push-image.sh', data.accessKey, data.secretKey, payload.Account, 'us-east-1']); 11 | 12 | deploy.stdout.on('data', (data) => console.log(data.toString())); 13 | deploy.stderr.on('data', (data) => console.error(data.toString())); 14 | deploy.on('close', (code) => console.log(`Process exited: ${code}`)); 15 | }).catch(console.error); 16 | } 17 | 18 | module.exports = { handler } 19 | -------------------------------------------------------------------------------- /scripts/gen-passwd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | rm -f ./password.yml 5 | 6 | passwd=$(cat /dev/urandom | env LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) 7 | printf "password: %s" "$passwd" > ./password.yml 8 | 9 | printf "Password generated!" 10 | -------------------------------------------------------------------------------- /scripts/push-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ACCESS_KEY=$1 3 | SECRET_KEY=$2 4 | AWS_ACCOUNT_ID=$3 5 | REGION=$4 6 | 7 | NAME=serverless-batch:latest 8 | 9 | aws configure set profile.serverless-batch.aws_access_key_id $ACCESS_KEY 10 | aws configure set profile.serverless-batch.aws_secret_access_key $SECRET_KEY 11 | aws configure set profile.serverless-batch.region $REGION 12 | 13 | eval $(aws ecr get-login --no-include-email --profile serverless-batch --region $REGION) 14 | 15 | docker build -t $NAME ./docker 16 | docker tag $NAME $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$NAME 17 | docker push $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$NAME 18 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: deploy-with-serverless 2 | 3 | plugins: 4 | - serverless-stack-output 5 | 6 | custom: 7 | queueName: serverless-projects-batch-queue 8 | projectsTableName: serverless-projects 9 | jobDefinitionName: serverless-batch-job-definition 10 | keyPair: RafalWilinski-personal-ssh-key 11 | 12 | output: 13 | handler: scripts/deploy-proxy.handler 14 | file: user-data.json 15 | 16 | provider: 17 | name: aws 18 | runtime: nodejs6.10 19 | region: us-east-1 20 | environment: 21 | PROJECTS_TABLE: ${self:custom.projectsTableName} 22 | JOB_QUEUE: ${self:custom.queueName} 23 | JOB_DEFINITON_NAME: ${self:custom.jobDefinitionName} 24 | iamRoleStatements: 25 | - Effect: 'Allow' 26 | Action: 27 | - 'dynamodb:GetItem' 28 | - 'dynamodb:PutItem' 29 | - 'batch:SubmitJob' 30 | - 'lambda:InvokeFunction' 31 | - 's3:*' 32 | Resource: '*' 33 | 34 | functions: 35 | handler: 36 | handler: functions/handler.run 37 | memory: 128 38 | image-proxy: 39 | handler: functions/image-proxy.run 40 | memory: 128 41 | events: 42 | - http: 43 | path: /image 44 | method: GET 45 | cors: true 46 | href: 47 | handler: functions/href.run 48 | memory: 128 49 | events: 50 | - http: 51 | path: /template 52 | method: GET 53 | cors: true 54 | 55 | resources: 56 | Resources: 57 | 58 | # S3 59 | DeployWithServerlessBucket: 60 | Type: AWS::S3::Bucket 61 | Properties: 62 | BucketName: deploy-with-serverless 63 | AccessControl: PublicRead 64 | 65 | # DynamoDB 66 | ServerlessTemplatesTable: 67 | Type: AWS::DynamoDB::Table 68 | Properties: 69 | TableName: ${self:custom.projectsTableName} 70 | AttributeDefinitions: 71 | - AttributeName: url 72 | AttributeType: S 73 | KeySchema: 74 | - AttributeName: url 75 | KeyType: HASH 76 | ProvisionedThroughput: 77 | ReadCapacityUnits: 1 78 | WriteCapacityUnits: 1 79 | 80 | # Networking 81 | ServerlessBatchVPC: 82 | Type: AWS::EC2::VPC 83 | Properties: 84 | CidrBlock: "10.0.0.0/16" 85 | EnableDnsSupport: true 86 | EnableDnsHostnames: true 87 | Tags: 88 | - Key: project 89 | Value: "deploy-with-serverless" 90 | ServerlessBatchInternetGateway: 91 | Type: AWS::EC2::InternetGateway 92 | Properties: 93 | Tags: 94 | - Key: project 95 | Value: "deploy-with-serverless" 96 | ServerlessBatchVPCIPGWAttachment: 97 | Type: AWS::EC2::VPCGatewayAttachment 98 | Properties: 99 | InternetGatewayId: 100 | Ref: ServerlessBatchInternetGateway 101 | VpcId: 102 | Ref: ServerlessBatchVPC 103 | DependsOn: 104 | - ServerlessBatchInternetGateway 105 | - ServerlessBatchVPC 106 | ServerlessBatchRouteTable: 107 | Type: AWS::EC2::RouteTable 108 | Properties: 109 | VpcId: 110 | Ref: ServerlessBatchVPC 111 | Tags: 112 | - Key: project 113 | Value: "deploy-with-serverless" 114 | ServerlessBatchSubnetRouteTableAssoc: 115 | Type: AWS::EC2::SubnetRouteTableAssociation 116 | Properties: 117 | RouteTableId: 118 | Ref: ServerlessBatchRouteTable 119 | SubnetId: 120 | Ref: ServerlessBatchSubnet 121 | ServerlessBatchInternetRoute: 122 | Type: AWS::EC2::Route 123 | Properties: 124 | DestinationCidrBlock: 0.0.0.0/0 125 | GatewayId: 126 | Ref: ServerlessBatchInternetGateway 127 | RouteTableId: 128 | Ref: ServerlessBatchRouteTable 129 | DependsOn: ServerlessBatchInternetGateway 130 | ServerlessBatchSubnet: 131 | Type: AWS::EC2::Subnet 132 | Properties: 133 | CidrBlock: "10.0.0.0/24" 134 | MapPublicIpOnLaunch: true 135 | VpcId: 136 | Ref: ServerlessBatchVPC 137 | Tags: 138 | - Key: project 139 | Value: "deploy-with-serverless" 140 | ServerlessBatchSecurityGroup: 141 | Type: AWS::EC2::SecurityGroup 142 | Properties: 143 | GroupName: ServerlessBatchSecurityGroup 144 | GroupDescription: Security Group for AWS Batch for Serverless-deploy-button project 145 | VpcId: 146 | Ref: ServerlessBatchVPC 147 | SecurityGroupEgress: 148 | - 149 | IpProtocol: tcp 150 | FromPort: "0" 151 | ToPort: "60000" 152 | CidrIp: "0.0.0.0/0" 153 | SecurityGroupIngress: 154 | - 155 | IpProtocol: tcp 156 | FromPort: "22" 157 | ToPort: "22" 158 | CidrIp: "0.0.0.0/0" 159 | Tags: 160 | - Key: project 161 | Value: "deploy-with-serverless" 162 | 163 | # ECR Repository 164 | ServerlessBatchRepository: 165 | Type: AWS::ECR::Repository 166 | Properties: 167 | RepositoryName: "serverless-batch" 168 | RepositoryPolicyText: 169 | Version: "2012-10-17" 170 | Statement: 171 | - 172 | Sid: AllowPushPull 173 | Effect: Allow 174 | Principal: 175 | AWS: 176 | - 'Fn::GetAtt': 177 | - ServerlessBatchUser 178 | - Arn 179 | Action: 180 | - "ecr:GetDownloadUrlForLayer" 181 | - "ecr:BatchGetImage" 182 | - "ecr:BatchCheckLayerAvailability" 183 | - "ecr:PutImage" 184 | - "ecr:InitiateLayerUpload" 185 | - "ecr:UploadLayerPart" 186 | - "ecr:CompleteLayerUpload" 187 | DependsOn: ServerlessBatchUser 188 | 189 | # IAM 190 | ServerlessBatchUser: 191 | Type: AWS::IAM::User 192 | Properties: 193 | LoginProfile: 194 | Password: ${file(./password.yml):password} 195 | UserName: ServerlessBatchUser 196 | ServerlessBatchUserPolicy: 197 | Type: AWS::IAM::Policy 198 | Properties: 199 | PolicyName: ServerlessBatchUserPolicy 200 | PolicyDocument: 201 | Version: "2012-10-17" 202 | Statement: 203 | - 204 | Effect: "Allow" 205 | Action: 206 | - ecr:GetAuthorizationToken 207 | - ecr:InitiateLayerUpload 208 | Resource: "*" 209 | Users: 210 | - Ref: ServerlessBatchUser 211 | DependsOn: ServerlessBatchUser 212 | ServerlessBatchUserKeys: 213 | Type: AWS::IAM::AccessKey 214 | Properties: 215 | UserName: ServerlessBatchUser 216 | DependsOn: ServerlessBatchUser 217 | ServerlessBatchServiceRole: # Standard AWS Batch Role 218 | Type: AWS::IAM::Role 219 | Properties: 220 | RoleName: ServerlessBatchServiceRole 221 | AssumeRolePolicyDocument: 222 | Version: '2012-10-17' 223 | Statement: 224 | - Effect: Allow 225 | Principal: 226 | Service: 227 | - batch.amazonaws.com 228 | Action: sts:AssumeRole 229 | Policies: 230 | - PolicyName: ServerlessBatchPolicy 231 | PolicyDocument: 232 | Version: '2012-10-17' 233 | Statement: 234 | - Effect: Allow 235 | Resource: '*' 236 | Action: 237 | - ec2:DescribeAccountAttributes 238 | - ec2:DescribeInstances 239 | - ec2:DescribeSubnets 240 | - ec2:DescribeSecurityGroups 241 | - ec2:DescribeKeyPairs 242 | - ec2:DescribeImages 243 | - ec2:DescribeImageAttribute 244 | - ec2:DescribeSpotFleetInstances 245 | - ec2:DescribeSpotFleetRequests 246 | - ec2:DescribeSpotPriceHistory 247 | - ec2:RequestSpotFleet 248 | - ec2:CancelSpotFleetRequests 249 | - ec2:ModifySpotFleetRequest 250 | - ec2:TerminateInstances 251 | - autoscaling:DescribeAccountLimits 252 | - autoscaling:DescribeAutoScalingGroups 253 | - autoscaling:DescribeLaunchConfigurations 254 | - autoscaling:DescribeAutoScalingInstances 255 | - autoscaling:CreateLaunchConfiguration 256 | - autoscaling:CreateAutoScalingGroup 257 | - autoscaling:UpdateAutoScalingGroup 258 | - autoscaling:SetDesiredCapacity 259 | - autoscaling:DeleteLaunchConfiguration 260 | - autoscaling:DeleteAutoScalingGroup 261 | - autoscaling:CreateOrUpdateTags 262 | - autoscaling:SuspendProcesses 263 | - autoscaling:PutNotificationConfiguration 264 | - autoscaling:TerminateInstanceInAutoScalingGroup 265 | - ecs:* 266 | - logs:CreateLogGroup 267 | - logs:CreateLogStream 268 | - logs:PutLogEvents 269 | - logs:DescribeLogGroups 270 | - iam:GetInstanceProfile 271 | - iam:PassRole 272 | - ecr:* 273 | - dynamodb:PutItem 274 | - dynamodb:DescribeTable 275 | 276 | # AWS Batch 277 | ServerlessBatchComputeEnvironment: 278 | Type: AWS::Batch::ComputeEnvironment 279 | Properties: 280 | Type: MANAGED 281 | ComputeEnvironmentName: ServerlessBatchComputeEnvironment 282 | ServiceRole: 283 | Fn::GetAtt: 284 | - ServerlessBatchServiceRole 285 | - Arn 286 | State: ENABLED 287 | ComputeResources: 288 | MinvCpus: 0 289 | MaxvCpus: 4 290 | DesiredvCpus: 0 291 | Ec2KeyPair: ${self:custom.keyPair} 292 | Type: "EC2" 293 | ImageId: ami-ec33cc96 # ECS Optimized AMI 294 | InstanceRole: ecsInstanceRole 295 | InstanceTypes: 296 | - m3.medium 297 | SecurityGroupIds: 298 | - Ref: ServerlessBatchSecurityGroup 299 | Subnets: 300 | - Ref: ServerlessBatchSubnet 301 | ServerlessBatchJobQueue: 302 | Type: "AWS::Batch::JobQueue" 303 | Properties: 304 | ComputeEnvironmentOrder: 305 | - ComputeEnvironment: ServerlessBatchComputeEnvironment 306 | Order: 1 307 | Priority: 1 308 | State: ENABLED 309 | JobQueueName: ${self:custom.queueName} 310 | DependsOn: ServerlessBatchComputeEnvironment 311 | ServerlessBatchJobDefinition: 312 | Type: 'AWS::Batch::JobDefinition' 313 | Properties: 314 | Type: container 315 | JobDefinitionName: ${self:custom.jobDefinitionName} 316 | ContainerProperties: 317 | Memory: 1024 318 | Privileged: true 319 | Vcpus: 1 320 | Image: 321 | Fn::Join: 322 | - "" 323 | - - Ref: AWS::AccountId 324 | - '.dkr.ecr.us-east-1.amazonaws.com/serverless-batch:latest' 325 | RetryStrategy: 326 | Attempts: 1 327 | Outputs: 328 | accessKey: 329 | Description: Access Key 330 | Value: 331 | Ref: ServerlessBatchUserKeys 332 | secretKey: 333 | Description: Secret Key 334 | Value: 335 | Fn::GetAtt: 336 | - ServerlessBatchUserKeys 337 | - SecretAccessKey 338 | -------------------------------------------------------------------------------- /static/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 404 5 | 6 | 7 | 8 | 9 | 10 | 11 | 22 | 23 | 24 |
25 |
26 | 404 🙈 27 |
28 |
29 | Sorry, we couldn't find this build... 30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /static/in-progress.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Build in progress... 5 | 6 | 7 | 8 | 9 | 10 | 11 | 22 | 23 | 24 |
25 |
26 | 🙈 27 |
28 |
29 | Sorry, this build is still in progress... 30 |
31 |
32 | 33 | 34 | --------------------------------------------------------------------------------