Skip to content

Commit bb809a8

Browse files
committed
Initial commit 🎉
1 parent 04e9756 commit bb809a8

File tree

6 files changed

+365
-23
lines changed

6 files changed

+365
-23
lines changed

README.md

+88-15
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,100 @@
11
# ember-cli-deploy-fastboot-api-lambda
22

3-
This README outlines the details of collaborating on this Ember addon.
3+
An ambitious ember-cli-deploy plugin for serving Ember FastBoot Applications entirely from within AWS Lambda/API Gateway (assets and all!).
4+
5+
## Background
6+
API Gateway [now supports the handling binary payloads](https://aws.amazon.com/about-aws/whats-new/2016/11/binary-data-now-supported-by-api-gateway/), which means an end-to-end fastboot hosting solution can now be achieved through API gateway and Lambda without the use of S3 for serving static files. This is what this addon aims to achieve.
7+
8+
## Prerequisites
9+
- You have [ember-fastboot](https://ember-fastboot.com) installed and configured within your Ember app.
10+
- You have an [AWS account](https://aws.amazon.com/free) setup.
11+
- You have the [AWS CLI](https://aws.amazon.com/cli) installed and configured.
412

513
## Installation
614

7-
* `git clone <repository-url>` this repository
8-
* `cd ember-cli-deploy-fastboot-api-lambda`
9-
* `npm install`
10-
* `bower install`
15+
* Install the ember-cli-deploy addons
16+
```
17+
ember install ember-cli-deploy
18+
ember install ember-cli-deploy-build
19+
ember install ember-cli-deploy-fastboot-api-lambda
20+
```
21+
22+
## Configuration
23+
24+
* Configure the deployment variables
25+
```
26+
// config/deploy.js
27+
ENV['fastboot-api-lambda'] = {
28+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
29+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
30+
31+
lambdaFunction: 'my-ember-app', // Lambda functions name
32+
region: 'us-east-1' // Region where lambda is deployed
33+
};
34+
```
35+
36+
* Create the lambda function
37+
38+
* Open the [AWS Lambda console](https://console.aws.amazon.com/lambda).
39+
* Select the region that you defined in your deploy variables above.
40+
* Create a blank lambda, with the name you defined in your deploy variables above.
41+
* Handler => `index.handler`.
42+
* Role => `Create a custom role`. Give it a name and use the default policy document.
43+
* Memory => `128`.
44+
* Timeout => `30 seconds`.
45+
* Select `Next` and then select `Create function`.
46+
47+
* Create the API Gateway Proxy
48+
49+
* Open the [AWS API Gateway console](https://console.aws.amazon.com/apigateway).
50+
* Select the region that you defined in your deploy variables above.
51+
* Select `New API` and give it a name
52+
* Select Binary Support. Click `Edit`. Add `*/*` and click `Save`.
53+
* Create proxy method:
54+
* Under resources, click `/`, then click `Actions => Create Method`. Select `Any`.
55+
* Click the `Any label`, choose Integration type `lambda`, check the `Use Lambda Proxy integration` checkbox, and finally select your lambda function's region and name.
56+
* Create proxy resource:
57+
* Under resources, click `/`, then click `Actions => Create Resource`. Select `Any`.
58+
* Select `Configure as proxy resource`, and select `Enable API Gateway CORS`.
59+
* Select Integration type `Lambda Function Proxy`, and finally select your lambda function's region and name.
60+
* Under resources, click `Actions => Deploy API`. Select a new stage and give it the name `fastboot`. Hit `Deploy`. You will now see the `Invoke URL`. This is where you site will be hosted.
61+
62+
* Ember Application
63+
* The `rootURL` must match the stage name you selected when creating the api gateway. Otherwise the `link-to` helper wont work.
64+
```
65+
// config/environment.js
66+
var ENV = {
67+
rootURL: '/fastboot/'
68+
}
69+
```
70+
71+
* Configuration is done! 🎉
72+
73+
## Deployment
74+
75+
Is as simple as going:
76+
77+
`ember deploy production --activate --verbose=true`
78+
79+
80+
## Caveats
81+
82+
Just a word of warning.. just because this architecture is possible, doesn't make it the optimal for all use-cases.
83+
Lambda functions suffer from a cold start delay, which can make there response times unpredictable.
84+
85+
86+
## Sites using this addon
1187

12-
## Running
88+
* [nzsupps.co.nz](https://nzsupps.co.nz)
1389

14-
* `ember serve`
15-
* Visit your app at [http://localhost:4200](http://localhost:4200).
90+
*Feel free to make a pull request if you would like your site added to the list!*
1691

17-
## Running Tests
1892

19-
* `npm test` (Runs `ember try:each` to test your addon against multiple Ember versions)
20-
* `ember test`
21-
* `ember test --server`
2293

23-
## Building
94+
## Credit
95+
[ember-cli-deploy-fastboot-lambda](https://github.com/bustlelabs/ember-cli-deploy-fastboot-lambda) for providing the base upload logic.
2496

25-
* `ember build`
97+
## Information
98+
For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/).
2699

27-
For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/).
100+
For more information on using ember-cli-deploy, visit [https://github.com/ember-cli-deploy/ember-cli-deploy](https://github.com/ember-cli-deploy/ember-cli-deploy).

assets/lambda-package/index.js

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// ember-cli-deploy-fastboot-api-lambda
2+
3+
var config = require('./config.json');
4+
var mime = require('mime');
5+
var fs = require('fs-promise');
6+
var FastBoot = require('fastboot');
7+
8+
var fancyACacheYeh = {
9+
yes: 'max-age=63072000, public',
10+
no: 'max-age=0, public'
11+
};
12+
13+
var defaults = {
14+
distPath: 'dist',
15+
path: '/',
16+
host: 'localhost',
17+
assetsPath: '/assets/',
18+
standardExtensions: [
19+
'html',
20+
'css',
21+
'js',
22+
'json',
23+
'xml',
24+
'ico',
25+
'txt',
26+
'map'
27+
],
28+
headers: {
29+
'Content-Type': 'text/html;charset=UTF-8',
30+
'Cache-Control': fancyACacheYeh.no
31+
},
32+
fastBootOptions: {
33+
request: {
34+
headers: {},
35+
get: function() {}
36+
},
37+
response: {}
38+
}
39+
};
40+
41+
// Merge defaults with config overrides
42+
var standardExtensions = defaults.standardExtensions.concat(config.standardExtensions || []);
43+
var fallbackPath = config.defaultPath || defaults.path;
44+
45+
// Instantiate Fastboot server
46+
var app = new FastBoot({ distPath: defaults.distPath });
47+
48+
exports.handler = function(event, context, callback) {
49+
console.log('INFO event:', event);
50+
51+
var path = event.path || fallbackPath;
52+
var staticPath = defaults.distPath + '/' + path;
53+
54+
console.log('INFO path:', path);
55+
console.log('INFO staticPath:', staticPath);
56+
57+
return fs.readFile(staticPath)
58+
59+
// STATIC FILE LOGIC
60+
.then(function(fileBuffer) {
61+
62+
// 1. Look up files content type.
63+
var contentType = mime.lookup(staticPath);
64+
65+
//2. Get file extension.
66+
var extension = mime.extension(contentType);
67+
68+
//3. If it isn't a standard file, then base64 encode it.
69+
var shouldEncode = standardExtensions.indexOf(extension) < 0;
70+
71+
//4. Determine if the item is fingerprinted/cacheable
72+
var shouldCache = staticPath.includes(defaults.assetsPath);
73+
74+
//5. Set encoding value
75+
var encoding = shouldEncode ? 'base64' : 'utf8';
76+
77+
//6. Create headers
78+
var headers = {
79+
'Content-Type': contentType,
80+
'Cache-Control': shouldCache ? fancyACacheYeh.yes : fancyACacheYeh.no
81+
};
82+
83+
//7. Create body
84+
var body = fileBuffer.toString(encoding);
85+
86+
//8. Create final output
87+
var payload = {
88+
statusCode: 200,
89+
headers: headers,
90+
body: body,
91+
isBase64Encoded: shouldEncode
92+
};
93+
94+
console.log('INFO: contentType:', contentType);
95+
console.log('INFO: extension:', extension);
96+
console.log('INFO: standardExtensions:', standardExtensions);
97+
console.log('INFO: shouldEncode:', shouldEncode);
98+
console.log('INFO: shouldCache:', shouldCache);
99+
console.log('INFO: encoding:', encoding);
100+
101+
return callback(null, payload);
102+
})
103+
104+
// GO FASTBOOT GO!
105+
.catch(function() {
106+
107+
// 1. Create options
108+
var options = defaults.fastBootOptions;
109+
options.request.headers = event.headers || {};
110+
options.request.headers.host = (event.headers || {}).Host || defaults.host;
111+
if (event.cookie) {
112+
options.request.headers.cookie = event.cookie;
113+
}
114+
115+
console.log('INFO: options:', options);
116+
117+
// 2. Fire up fastboot server
118+
return app.visit(path, options)
119+
.then(function(result) {
120+
return result.html()
121+
.then(function(html) {
122+
123+
console.log('INFO: html:', html);
124+
125+
// 3. Create headers object
126+
var headers = Object.assign(result.headers.headers, defaults.headers);
127+
128+
console.log('INFO: headers:', headers);
129+
130+
// 4. Create payload
131+
var payload = {
132+
statusCode: result.statusCode,
133+
headers: headers,
134+
body: html
135+
};
136+
137+
// 5. Profit ???
138+
return callback(null, payload);
139+
});
140+
})
141+
.catch(err => callback(err));
142+
143+
});
144+
145+
};

assets/lambda-package/package.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"dependencies": {
3+
"fastboot": "1.0.0-rc.0",
4+
"fs-promise": "0.5.0",
5+
"mine": "0.1.0"
6+
}
7+
}

index.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
/* jshint node: true */
22
'use strict';
33

4+
var FastbootAPILambdaDeployPlugin = require('./lib/fastboot-api-lambda-deploy-plugin');
5+
46
module.exports = {
5-
name: 'ember-cli-deploy-fastboot-api-lambda'
7+
name: 'ember-cli-deploy-fastboot-api-lambda',
8+
9+
createDeployPlugin: function(options) {
10+
return new FastbootAPILambdaDeployPlugin({
11+
name: options.name
12+
});
13+
}
614
};
+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
'use strict';
2+
3+
const DEFAULT_REGION = 'us-west-2';
4+
5+
const DeployPlugin = require('ember-cli-deploy-plugin');
6+
const fs = require('fs-promise');
7+
const path = require('path');
8+
const AWS = require('aws-sdk');
9+
const RSVP = require('rsvp');
10+
const exec = RSVP.denodeify(require('child_process').exec);
11+
12+
module.exports = DeployPlugin.extend({
13+
requiredConfig: ['lambdaFunction'],
14+
15+
_getConfig(context) {
16+
const config = Object.assign({}, context.config['fastboot-api-lambda']);
17+
delete config.accessKeyId;
18+
delete config.secretAccessKey;
19+
return config;
20+
},
21+
22+
_getPaths(context) {
23+
const addonRootPath = path.join(__dirname, '..');
24+
const projectRootPath = context.project.root;
25+
26+
const skeletonPath = path.join(addonRootPath, 'assets/lambda-package');
27+
const tempPath = path.join(projectRootPath, 'tmp/lambda-package');
28+
29+
return {
30+
addonRootPath,
31+
projectRootPath,
32+
skeletonPath,
33+
tempPath
34+
};
35+
},
36+
37+
didBuild: function(context) {
38+
const config = this._getConfig(context);
39+
const paths = this._getPaths(context);
40+
41+
const addonRootPath = paths.addonRootPath;
42+
const projectRootPath = paths.projectRootPath
43+
const skeletonPath = paths.skeletonPath;
44+
const tempPath = paths.tempPath;
45+
46+
return RSVP.resolve()
47+
.then(() => this.log(`1/5. Cloning skeleton FastBoot server`))
48+
.then(() => fs.copy(skeletonPath, tempPath))
49+
50+
.then(() => this.log(`2/5. Installing FastBoot server dependencies`))
51+
.then(() => exec("npm install --production", { cwd: tempPath }))
52+
53+
.then(() => this.log(`3/5. Cloning config into FastBoot server directory`))
54+
.then(() => {
55+
const json = JSON.stringify(config);
56+
return fs.writeFile(`${tempPath}/config.json`, json, 'utf8');
57+
})
58+
59+
.then(() => this.log(`4/5. Cloning FastBoot build into FastBoot server directory`))
60+
.then(() => fs.copy(context.distDir, `${tempPath}/dist`))
61+
62+
.then(() => this.log(`5/5. Installing dependencies of the FastBoot build`))
63+
.then(() => exec('npm install --production', { cwd: `${tempPath}/dist` }))
64+
65+
.then(() => this.log(`API FastBoot lambda production bundle successfully built`));
66+
},
67+
68+
activate: function(context) {
69+
const config = this._getConfig(context);
70+
const tempPath = this._getPaths(context).tempPath;
71+
72+
const lambdaFunction = config.lambdaFunction;
73+
const region = config.region || DEFAULT_REGION;
74+
75+
const Lambda = new AWS.Lambda({ region: region });
76+
const UpdateLambdaFunc = RSVP.denodeify(Lambda.updateFunctionCode.bind(Lambda));
77+
78+
return RSVP.resolve()
79+
.then(() => this.log('1/3. zipping up API FastBoot lambda bundle'))
80+
.then(() => exec("zip -qr lambda-package.zip *", { cwd: tempPath }))
81+
.then(() => exec("mv lambda-package.zip ../", { cwd: tempPath }))
82+
83+
.then(() => this.log('2/3. Reading zip file into file buffer'))
84+
.then(() => fs.readFile(`${tempPath}.zip`))
85+
86+
.then(fileBuf => {
87+
this.log(`3/3. Uploading zip to ${lambdaFunction} lambda to ${region} region`);
88+
return UpdateLambdaFunc({
89+
FunctionName: lambdaFunction,
90+
ZipFile: fileBuf
91+
});
92+
})
93+
94+
.then(() => this.log(`API FastBoot lambda production bundle successfully uploaded to "${lambdaFunction}" lambda function to region "${region}" 🚀`));
95+
}
96+
});

0 commit comments

Comments
 (0)