Skip to content

Commit 9e2c26a

Browse files
Merge pull request #38 from serverless-components/add-retry-remove-openapi
Add retry limits to lambda creation and remove openapi auto-generation
2 parents b43d272 + 32b468c commit 9e2c26a

File tree

4 files changed

+135
-119
lines changed

4 files changed

+135
-119
lines changed

README.md

-17
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
- [x] **Custom Domain + SSL** - Auto-configure a custom domain w/ a free AWS ACM SSL certificate.
1515
- [x] **Team Collaboration** - Collaborate with your teamates with shared state and outputs.
1616
- [x] **Built-in Monitoring** - Monitor your express app right from the Serverless Dashboard.
17-
- [x] **(NEW) Auto-Generate An OpenAPI Spec On Each Deployment** - A new OpenAPI spec is generated after each deployment.
1817

1918
<br/>
2019

@@ -115,7 +114,6 @@ inputs:
115114
- arn:aws:second:layer
116115
domain: api.serverless.com # (optional) if the domain was registered via AWS Route53 on the account you are deploying to, it will automatically be set-up with your Express app's API Gateway, as well as a free AWS ACM SSL Cert.
117116
region: us-east-2 # (optional) aws region to deploy to. default is us-east-1.
118-
openApi: true # (optional) (experimental) Initialize the express app on each deployment, extract an OpenAPI V.3 specification, and add it to the outputs.
119117
```
120118
121119
Once you've chosen your configuration, run `serverless deploy` again (or simply just `serverless`) to deploy your changes.
@@ -221,21 +219,6 @@ If things aren't working, revert your code to the old code, remove the `traffic`
221219

222220
If things are working, keep the new code, remove the `traffic` configuration option, and deploy.
223221

224-
### Auto-Generate An OpenAPI V3 Specification From Your Express.js App
225-
226-
Version 1.5.0 introduced experimental support for auto-generating an OpenAPI specification from your Express app upon every deployment, then adding them to the `outputs` of your app.
227-
228-
This works by attempting to run your application on each deployment and extracting the routes you've defined in your Express app.
229-
230-
Currently, this feature is disabled by default, since it's experimental. To enable it, add `openApi: true` to your `serverless.yml` and ensure you are using the latest version of the Express coponent (>= 1.1.0).
231-
232-
Given a lot of things can happen in your application upon starting it up, this does not work consistently. For example, your environment variables will NOT be available during this process, which could cause your app not to initialize.
233-
234-
If it runs into an error trying to start your application, it will try its best to pass through useful errors to you so you can address what's blocking it from working. You can see these by running `serverless deploy --debug`
235-
236-
Overall, an OpenAPI specification generated by default is very powerful. This means you don't have to maintain that manually since it auto-updates on every deployment. (That's what serverless is all about!)
237-
238-
We will be adding many interesting features built on this. Extracting your endpoints and putting them into a common format was merely the first step...
239222

240223
### How To Debug CORS Errors
241224

serverless.component.yml

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: express
2-
version: 1.6.0
2+
version: 1.7.0
33
author: eahefnawy
44
org: serverlessinc
55
description: Deploys a serverless Express.js application onto AWS Lambda and AWS HTTP API.
@@ -13,7 +13,6 @@ actions:
1313
deploy:
1414
description: Deploy your Express.js application to AWS Lambda, AWS HTTP API and more.
1515
inputs:
16-
1716
src:
1817
type: src
1918
description: The folder containing the source code of your Express.js application.
@@ -110,10 +109,6 @@ actions:
110109
min: 0
111110
max: 1
112111

113-
openApi:
114-
type: boolean
115-
description: Automatically generate an OpenAPI specification on every deployment.
116-
117112
# remove
118113

119114
remove:

src/serverless.js

-3
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ class Express extends Component {
2727
async deploy(inputs) {
2828
const outputs = {};
2929

30-
// Defaults
31-
inputs.openApi = inputs.openApi === true;
32-
3330
// Check credentials exist
3431
if (Object.keys(this.credentials.aws).length === 0) {
3532
const msg =

src/utils.js

+134-93
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const getNakedDomain = (domain) => {
9494
* @param ${instance} instance - the component instance
9595
* @param ${object} config - the component config
9696
*/
97-
const packageExpress = async (instance, inputs, outputs) => {
97+
const packageExpress = async (instance, inputs) => {
9898
console.log('Packaging Express.js application...');
9999

100100
// unzip source zip file
@@ -106,10 +106,14 @@ const packageExpress = async (instance, inputs, outputs) => {
106106
console.log('Installing Express + AWS Lambda handler...');
107107
copySync(path.join(__dirname, '_express'), path.join(sourceDirectory, '_express'));
108108

109+
/**
110+
* DEPRECATED: This runs untrusted code and should not be used until we can find a way to do this more securely.
111+
*/
109112
// Attempt to infer data from the application
110-
if (inputs.openApi) {
111-
await infer(instance, inputs, outputs, sourceDirectory);
112-
}
113+
// if (inputs.openApi) {
114+
// console.log('Attempting to collect API routes and convert to OpenAPI format, since openAPI is set to 'true'')
115+
// await infer(instance, inputs, outputs, sourceDirectory);
116+
// }
113117

114118
// add sdk to the source directory, add original handler
115119
console.log('Installing Serverless Framework SDK...');
@@ -133,101 +137,106 @@ const packageExpress = async (instance, inputs, outputs) => {
133137
};
134138

135139
/*
140+
* DEPRECATED: This runs untrusted code and should not be used until we can find a way to do this more securely.
141+
*
136142
* Infer data from the Application by attempting to intiatlize it during deployment and extracting data.
137143
*
138144
* @param ${object} instance - the component instance
139145
* @param ${object} inputs - the component inputs
140146
*/
141-
const infer = async (instance, inputs, outputs, sourceDirectory) => {
142-
// Initialize application
143-
let app;
144-
try {
145-
app = require(path.join(sourceDirectory, './app.js'));
146-
} catch (error) {
147-
const msg = error.message;
148-
error.message = `OpenAPI auto-generation failed due to the Express Component not being able to start your app. To fix this, you can turn this feature off by specifying "inputs.openApi: false" or fix the following issue: ${msg}`;
149-
throw error;
150-
}
151-
152-
try {
153-
await generateOpenAPI(instance, inputs, outputs, app);
154-
} catch (error) {
155-
const msg = error.message;
156-
error.message = `OpenAPI auto-generation failed due to the Express Component not being able to start your app. To fix this, you can turn this feature off by specifying "inputs.openApi: false" or fix the following issue: ${msg}`;
157-
throw error;
158-
}
159-
};
147+
// const infer = async (instance, inputs, outputs, sourceDirectory) => {
148+
// // Initialize application
149+
// let app;
150+
// try {
151+
// // Load app
152+
// app = require(path.join(sourceDirectory, './app.js'));
153+
// } catch (error) {
154+
// const msg = error.message;
155+
// error.message = `OpenAPI auto-generation failed due to the Express Component not being able to start your app. To fix this, you can turn this feature off by specifying 'inputs.openApi: false' or fix the following issue: ${msg}`;
156+
// throw error;
157+
// }
158+
159+
// try {
160+
// await generateOpenAPI(instance, inputs, outputs, app);
161+
// } catch (error) {
162+
// const msg = error.message;
163+
// error.message = `OpenAPI auto-generation failed due to the Express Component not being able to start your app. To fix this, you can turn this feature off by specifying 'inputs.openApi: false' or fix the following issue: ${msg}`;
164+
// throw error;
165+
// }
166+
// };
160167

161168
/*
169+
* DEPRECATED: This runs untrusted code and should not be used until we can find a way to do this more securely.
170+
*
162171
* Generate an OpenAPI specification from the Application
163172
*
164173
* @param ${object} instance - the component instance
165174
* @param ${object} inputs - the component inputs
166175
*/
167-
const generateOpenAPI = async (instance, inputs, outputs, app) => {
168-
// Open API Version 3.0.3, found here: https://swagger.io/specification/
169-
// TODO: This is not complete, but the pieces that do exist are accurate.
170-
const openApi = {
171-
openapi: '3.0.3',
172-
info: {
173-
// title: null,
174-
// description: null,
175-
version: '0.0.1',
176-
},
177-
paths: {},
178-
};
179-
180-
// Parts of the OpenAPI spec that we may use these at a later date.
181-
// For now, they are unincorporated.
182-
// const oaServersObject = {
183-
// url: null,
184-
// description: null,
185-
// variables: {},
186-
// };
187-
// const oaComponentsObject = {
188-
// schemas: {},
189-
// responses: {},
190-
// parameters: {},
191-
// examples: {},
192-
// requestBodies: {},
193-
// headers: {},
194-
// securitySchemes: {},
195-
// links: {},
196-
// callbacks: {},
197-
// };
198-
// const oaPathItem = {
199-
// description: null,
200-
// summary: null,
201-
// operationId: null,
202-
// responses: {},
203-
// };
204-
205-
if (app && app._router && app._router.stack && app._router.stack.length) {
206-
app._router.stack.forEach((route) => {
207-
// This array holds all middleware layers, which include routes and more
208-
// First check if this 'layer' is an express route type, otherwise skip
209-
if (!route.route) return;
210-
211-
// Define key data
212-
const ePath = route.route.path;
213-
214-
if (['*', '/*'].indexOf(ePath) > -1) {
215-
return;
216-
}
217-
218-
// Save path
219-
openApi.paths[ePath] = openApi.paths[ePath] || {};
220-
221-
for (const method of Object.keys(route.route.methods)) {
222-
// Save method
223-
openApi.paths[ePath][method] = {};
224-
}
225-
});
226-
}
227-
228-
// Save to outputs
229-
outputs.api = openApi;
230-
};
176+
// const generateOpenAPI = async (instance, inputs, outputs, app) => {
177+
// // Open API Version 3.0.3, found here: https://swagger.io/specification/
178+
// // TODO: This is not complete, but the pieces that do exist are accurate.
179+
// const openApi = {
180+
// openapi: '3.0.3',
181+
// info: {
182+
// // title: null,
183+
// // description: null,
184+
// version: '0.0.1',
185+
// },
186+
// paths: {},
187+
// };
188+
189+
// // Parts of the OpenAPI spec that we may use these at a later date.
190+
// // For now, they are unincorporated.
191+
// // const oaServersObject = {
192+
// // url: null,
193+
// // description: null,
194+
// // variables: {},
195+
// // };
196+
// // const oaComponentsObject = {
197+
// // schemas: {},
198+
// // responses: {},
199+
// // parameters: {},
200+
// // examples: {},
201+
// // requestBodies: {},
202+
// // headers: {},
203+
// // securitySchemes: {},
204+
// // links: {},
205+
// // callbacks: {},
206+
// // };
207+
// // const oaPathItem = {
208+
// // description: null,
209+
// // summary: null,
210+
// // operationId: null,
211+
// // responses: {},
212+
// // };
213+
214+
// if (app && app._router && app._router.stack && app._router.stack.length) {
215+
// app._router.stack.forEach((route) => {
216+
// // This array holds all middleware layers, which include routes and more
217+
// // First check if this 'layer' is an express route type, otherwise skip
218+
// if (!route.route) return;
219+
220+
// // Define key data
221+
// const ePath = route.route.path;
222+
223+
// if (['*', '/*'].indexOf(ePath) > -1) {
224+
// return;
225+
// }
226+
227+
// // Save path
228+
// openApi.paths[ePath] = openApi.paths[ePath] || {};
229+
230+
// for (const method of Object.keys(route.route.methods)) {
231+
// // Save method
232+
// openApi.paths[ePath][method] = {};
233+
// }
234+
// });
235+
// }
236+
237+
// // Save to outputs
238+
// outputs.api = openApi;
239+
// };
231240

232241
/*
233242
* Fetches a lambda function by ARN
@@ -269,7 +278,10 @@ const getVpcConfig = (vpcConfig) => {
269278
* @param ${object} inputs - the component inputs
270279
* @param ${object} clients - the aws clients object
271280
*/
272-
const createLambda = async (instance, inputs, clients) => {
281+
const createLambda = async (instance, inputs, clients, retries = 0) => {
282+
// Track retries
283+
retries++;
284+
273285
const vpcConfig = getVpcConfig(inputs.vpc);
274286

275287
const params = {
@@ -302,16 +314,45 @@ const createLambda = async (instance, inputs, clients) => {
302314
instance.state.lambdaVersion = res.Version;
303315
} catch (e) {
304316
console.log(`Unable to create AWS Lambda due to: ${e.message}`);
317+
318+
// Handle known errors
319+
320+
if (e.message.includes('The role defined for the function cannot be assumed by Lambda')) {
321+
// This error can happen upon first creation. So sleeping is an acceptable solution. This code will retry multiple times.
322+
if (retries > 5) {
323+
console.log(
324+
'Attempted to retry Lambda creation 5 times, but the invalid role error persists. Aborting...'
325+
);
326+
327+
// Throw different errors, depending on whether the user is using a custom role
328+
if (instance.state.userRoleArn) {
329+
throw new Error(
330+
'Unable to create the AWS Lambda function which your Express.js app runs on. The reason is "the role defined for the function cannot be assumed by Lambda". This might be due to a missing or invalid "Trust Relationship" within the policy of the custom IAM Role you you are attempting to use. Try modifying that. If that doesn\'t work, this is an issue with AWS Lambda\'s APIs. We suggest trying to remove this instance by running "serverless remove" then redeploying to get around this.'
331+
);
332+
} else {
333+
throw new Error(
334+
'Unable to create the AWS Lambda function which your Express.js app runs on. The reason is "the role defined for the function cannot be assumed by Lambda". This is an issue with AWS Lambda\'s APIs. We suggest trying to remove this instance by running "serverless remove" then redeploying to get around this. This seems to be the only way users have gotten past this.'
335+
);
336+
}
337+
}
338+
}
339+
305340
if (
306-
e.message.includes('The role defined for the function cannot be assumed by Lambda') ||
307341
e.message.includes(
308342
'Lambda was unable to configure access to your environment variables because the KMS key is invalid'
309343
)
310344
) {
311-
// we need to wait around 2 seconds after the role is created before it can be assumed
312-
await sleep(2000);
313-
return createLambda(instance, inputs, clients);
345+
// This error can happen upon first creation. So sleeping is an acceptable solution. This code will retry multiple times.
346+
if (retries > 5) {
347+
console.log(
348+
'Attempted to retry Lambda creation 5 times, but the KMS error persists Aborting...'
349+
);
350+
throw new Error(
351+
'Unable to create the AWS Lambda function which your Express.js app runs on. The reason is "Lambda was unable to configure access to your environment variables because the KMS key is invalid". This is a known issue with AWS Lambda\'s APIs, and there is nothing the Serverless Framework can do to help with it at this time. We suggest trying to remove this instance by running "serverless remove" then redeploying to attempt to get around this.'
352+
);
353+
}
314354
}
355+
315356
throw e;
316357
}
317358
return null;
@@ -472,7 +513,7 @@ const findOrCreateCertificate = async (instance, clients) => {
472513
);
473514

474515
console.log(
475-
`Certificate for ${instance.state.nakedDomain} is in a "${certificate.Status}" status`
516+
`Certificate for ${instance.state.nakedDomain} is in a '${certificate.Status}' status`
476517
);
477518

478519
if (certificate.Status === 'PENDING_VALIDATION') {

0 commit comments

Comments
 (0)