Skip to content

Commit 315fe3f

Browse files
author
Frank Schmid
committed
Stabilized APIG deployment. Use configurable CF APIG stage resource.
1 parent 91f19ee commit 315fe3f

File tree

3 files changed

+172
-141
lines changed

3 files changed

+172
-141
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ in API Gateway, so it is not to be confused with Serverless stages.
5959
Thus an alias deployment will create an API Gateway stage with the alias name
6060
as name.
6161

62+
### API Gateway stage and deployment
63+
64+
The created API Gateway stage has the stage variables SERVERLESS_STAGE and
65+
SERVERLESS_ALIAS set to the corresponding values.
66+
67+
Upcoming: There will be a configuration possibility to configure the APIG
68+
stage parameters separately soon.
69+
6270
## Reference the current alias in your service
6371

6472
You can reference the currently deployed alias with `${self:provider.alias}` in
@@ -288,6 +296,7 @@ and _serverless.service.provider.deployedAliasTemplates[]_.
288296

289297
## Version history
290298

299+
* upcoming: APIG support fixed. Support external IAM roles.
291300
* 0.3.4-alpha1 Bugfixes. IAM policy consolitaion. Show master alias information.
292301
* 0.3.3-alpha1 Bugfixes. Allow manual resource overrides. Allow methods attached to APIG root resource.
293302
* 0.3.2-alpha1 Allow initial project creation with activated alias plugin

lib/aliasRestructureStack.js

Lines changed: 1 addition & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -152,147 +152,7 @@ module.exports = {
152152
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
153153
},
154154

155-
aliasHandleApiGateway(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
156-
157-
const stackName = this._provider.naming.getStackName();
158-
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
159-
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
160-
const userResources = _.get(this._serverless.service, 'resources', { Resources: {}, Outputs: {} });
161-
162-
// Check if our current deployment includes an API deployment
163-
let exposeApi = _.includes(_.keys(stageStack.Resources), 'ApiGatewayRestApi');
164-
const aliasResources = [];
165-
166-
if (!exposeApi) {
167-
// Check if we have any aliases deployed that reference the API.
168-
if (_.some(aliasStackTemplates, template => _.find(template.Resources, [ 'Type', 'AWS::ApiGateway::Deployment' ]))) {
169-
// Fetch the Api resource from the current stack
170-
stageStack.Resources.ApiGatewayRestApi = currentTemplate.Resources.ApiGatewayRestApi;
171-
exposeApi = true;
172-
}
173-
}
174-
175-
if (exposeApi) {
176-
177-
this.options.verbose && this._serverless.cli.log('Processing API');
178-
179-
// Export the API for the alias stacks
180-
stageStack.Outputs.ApiGatewayRestApi = {
181-
Description: 'API Gateway API',
182-
Value: { Ref: 'ApiGatewayRestApi' },
183-
Export: {
184-
Name: `${stackName}-ApiGatewayRestApi`
185-
}
186-
};
187-
188-
// Export the root resource for the API
189-
stageStack.Outputs.ApiGatewayRestApiRootResource = {
190-
Description: 'API Gateway API root resource',
191-
Value: { 'Fn::GetAtt': [ 'ApiGatewayRestApi', 'RootResourceId' ] },
192-
Export: {
193-
Name: `${stackName}-ApiGatewayRestApiRootResource`
194-
}
195-
};
196-
197-
// Move the API deployment into the alias stack. The alias is the owner of the APIG stage.
198-
const deployment = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::ApiGateway::Deployment' ]));
199-
if (!_.isEmpty(deployment)) {
200-
const deploymentName = _.keys(deployment)[0];
201-
const obj = deployment[deploymentName];
202-
obj.Properties.StageName = this._alias;
203-
obj.Properties.RestApiId = { 'Fn::ImportValue': `${stackName}-ApiGatewayRestApi` };
204-
aliasResources.push(deployment);
205-
delete stageStack.Resources[deploymentName];
206-
}
207-
208-
// Fetch lambda permissions, methods and resources. These have to be updated later to allow the aliased functions.
209-
const apiLambdaPermissions = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::Lambda::Permission' ]));
210-
const apiMethods = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::ApiGateway::Method' ]));
211-
const apiResources = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::ApiGateway::Resource' ]));
212-
const aliases = _.assign({}, _.pickBy(aliasStack.Resources, [ 'Type', 'AWS::Lambda::Alias' ]));
213-
const versions = _.assign({}, _.pickBy(aliasStack.Resources, [ 'Type', 'AWS::Lambda::Version' ]));
214-
215-
// Adjust resources
216-
_.forOwn(apiResources, (resource, name) => {
217-
resource.Properties.RestApiId = { 'Fn::ImportValue': `${stackName}-ApiGatewayRestApi` };
218-
// Check parent id. If it references the API root, use the imported api root resource id.
219-
if (_.has(resource, 'Properties.ParentId.Fn::GetAtt') && resource.Properties.ParentId['Fn::GetAtt'][0] === 'ApiGatewayRestApi') {
220-
resource.Properties.ParentId = { 'Fn::ImportValue': `${stackName}-ApiGatewayRestApiRootResource` };
221-
}
222-
223-
delete stageStack.Resources[name];
224-
});
225-
226-
// Adjust method API and target function
227-
_.forOwn(apiMethods, (method, name) => {
228-
229-
// Relink to function alias in case we have a lambda endpoint
230-
if (_.includes([ 'AWS', 'AWS_PROXY' ], _.get(method, 'Properties.Integration.Type'))) {
231-
// For methods it is a bit tricky to find the related function name. There is no direct link.
232-
const uriParts = method.Properties.Integration.Uri['Fn::Join'][1];
233-
const funcIndex = _.findIndex(uriParts, part => _.has(part, 'Fn::GetAtt'));
234-
const functionName = uriParts[funcIndex]['Fn::GetAtt'][0].replace(/LambdaFunction$/, '');
235-
const aliasName = _.find(_.keys(aliases), alias => _.startsWith(alias, functionName));
236-
237-
uriParts[funcIndex] = { Ref: aliasName };
238-
}
239-
240-
// If the method is located on the root resource, set the parent correctly
241-
if (_.has(method, 'Properties.ResourceId.Fn::GetAtt') && method.Properties.ResourceId['Fn::GetAtt'][0] === 'ApiGatewayRestApi') {
242-
method.Properties.ResourceId = { 'Fn::ImportValue': `${stackName}-ApiGatewayRestApiRootResource` };
243-
}
244-
245-
method.Properties.RestApiId = { 'Fn::ImportValue': `${stackName}-ApiGatewayRestApi` };
246-
247-
// Check for user resource overrides
248-
if (_.has(userResources.Resources, name)) {
249-
_.merge(method, userResources.Resources[name]);
250-
delete userResources.Resources[name];
251-
}
252-
253-
delete stageStack.Resources[name];
254-
});
255-
256-
// Adjust permission to reference the function aliases
257-
_.forOwn(apiLambdaPermissions, (permission, name) => {
258-
const functionName = name.replace(/LambdaPermissionApiGateway$/, '');
259-
const versionName = _.find(_.keys(versions), version => _.startsWith(version, functionName));
260-
const aliasName = _.find(_.keys(aliases), alias => _.startsWith(alias, functionName));
261-
262-
// Adjust references and alias permissions
263-
permission.Properties.FunctionName = { Ref: aliasName };
264-
permission.Properties.SourceArn = {
265-
'Fn::Join': [
266-
'',
267-
[
268-
'arn:aws:execute-api:',
269-
{ Ref: 'AWS::Region' },
270-
':',
271-
{ Ref: 'AWS::AccountId' },
272-
':',
273-
{ 'Fn::ImportValue': `${stackName}-ApiGatewayRestApi` },
274-
'/*/*'
275-
]
276-
]
277-
};
278-
279-
// Add dependency on function version
280-
permission.DependsOn = [ versionName, aliasName ];
281-
282-
delete stageStack.Resources[name];
283-
});
284-
285-
// Add all alias stack owned resources
286-
aliasResources.push(apiResources);
287-
aliasResources.push(apiMethods);
288-
aliasResources.push(apiLambdaPermissions);
289-
290-
}
291-
292-
_.forEach(aliasResources, resource => _.assign(aliasStack.Resources, resource));
293-
294-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
295-
},
155+
aliasHandleApiGateway: require('./stackops/apiGateway'),
296156

297157
aliasHandleUserResources: require('./stackops/userResources'),
298158

lib/stackops/apiGateway.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
'use strict';
2+
3+
/**
4+
* Handle APIG resources.
5+
* Keep all resources that are used somewhere and remove the ones that are not
6+
* referenced anymore.
7+
*/
8+
9+
const _ = require('lodash');
10+
const BbPromise = require('bluebird');
11+
12+
module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
13+
const stackName = this._provider.naming.getStackName();
14+
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
15+
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
16+
const userResources = _.get(this._serverless.service, 'resources', { Resources: {}, Outputs: {} });
17+
18+
// Check if our current deployment includes an API deployment
19+
let exposeApi = _.includes(_.keys(stageStack.Resources), 'ApiGatewayRestApi');
20+
const aliasResources = [];
21+
22+
if (!exposeApi) {
23+
// Check if we have any aliases deployed that reference the API.
24+
if (_.some(aliasStackTemplates, template => _.find(template.Resources, [ 'Type', 'AWS::ApiGateway::Deployment' ]))) {
25+
// Fetch the Api resource from the current stack
26+
stageStack.Resources.ApiGatewayRestApi = currentTemplate.Resources.ApiGatewayRestApi;
27+
exposeApi = true;
28+
}
29+
}
30+
31+
if (exposeApi) {
32+
33+
this.options.verbose && this._serverless.cli.log('Processing API');
34+
35+
// Export the API for the alias stacks
36+
stageStack.Outputs.ApiGatewayRestApi = {
37+
Description: 'API Gateway API',
38+
Value: { Ref: 'ApiGatewayRestApi' },
39+
Export: {
40+
Name: `${stackName}-ApiGatewayRestApi`
41+
}
42+
};
43+
44+
// Export the root resource for the API
45+
stageStack.Outputs.ApiGatewayRestApiRootResource = {
46+
Description: 'API Gateway API root resource',
47+
Value: { 'Fn::GetAtt': [ 'ApiGatewayRestApi', 'RootResourceId' ] },
48+
Export: {
49+
Name: `${stackName}-ApiGatewayRestApiRootResource`
50+
}
51+
};
52+
53+
// FIXME: Upgrade warning. Should be removed after some time has passed.
54+
if (_.some(_.reduce(aliasStackTemplates, (result, template) => {
55+
_.merge(result, template.Resources);
56+
return result;
57+
}, {}), [ 'Type', 'AWS::ApiGateway::Method' ]) ||
58+
_.find(currentAliasStackTemplate.Resources, [ 'Type', 'AWS::ApiGateway::Method' ])) {
59+
throw new this._serverless.classes.Error('ALIAS PLUGIN ALPHA CHANGE: APIG deployment had to be changed. Please remove the alias stacks and the APIG stage for the alias in CF (AWS console) and redeploy. Sorry!');
60+
}
61+
62+
// Move the API deployment into the alias stack. The alias is the owner of the APIG stage.
63+
const deployment = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::ApiGateway::Deployment' ]));
64+
if (!_.isEmpty(deployment)) {
65+
const deploymentName = _.keys(deployment)[0];
66+
const obj = deployment[deploymentName];
67+
68+
delete obj.Properties.StageName;
69+
obj.Properties.RestApiId = { 'Fn::ImportValue': `${stackName}-ApiGatewayRestApi` };
70+
obj.DependsOn = [];
71+
72+
aliasResources.push(deployment);
73+
delete stageStack.Resources[deploymentName];
74+
75+
// Create stage resource
76+
const stageResource = {
77+
Type: 'AWS::ApiGateway::Stage',
78+
Properties: {
79+
StageName: this._alias,
80+
DeploymentId: {
81+
Ref: deploymentName
82+
},
83+
RestApiId: {
84+
'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`
85+
},
86+
Variables: {
87+
SERVERLESS_ALIAS: this._alias,
88+
SERVERLESS_STAGE: this._stage
89+
}
90+
},
91+
DependsOn: [ deploymentName ]
92+
};
93+
aliasResources.push({ ApiGatewayStage: stageResource });
94+
95+
}
96+
97+
// Fetch lambda permissions, methods and resources. These have to be updated later to allow the aliased functions.
98+
const apiLambdaPermissions = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::Lambda::Permission' ]));
99+
const apiMethods = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::ApiGateway::Method' ]));
100+
//const apiResources = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::ApiGateway::Resource' ]));
101+
const aliases = _.assign({}, _.pickBy(aliasStack.Resources, [ 'Type', 'AWS::Lambda::Alias' ]));
102+
const versions = _.assign({}, _.pickBy(aliasStack.Resources, [ 'Type', 'AWS::Lambda::Version' ]));
103+
104+
// Adjust method API and target function
105+
_.forOwn(apiMethods, (method, name) => {
106+
107+
// Relink to function alias in case we have a lambda endpoint
108+
if (_.includes([ 'AWS', 'AWS_PROXY' ], _.get(method, 'Properties.Integration.Type'))) {
109+
// For methods it is a bit tricky to find the related function name. There is no direct link.
110+
const uriParts = method.Properties.Integration.Uri['Fn::Join'][1];
111+
const funcIndex = _.findIndex(uriParts, part => _.has(part, 'Fn::GetAtt'));
112+
113+
uriParts.splice(funcIndex + 1, 0, `:${this._alias}`);
114+
}
115+
116+
// Check for user resource overrides
117+
if (_.has(userResources.Resources, name)) {
118+
_.merge(method, userResources.Resources[name]);
119+
delete userResources.Resources[name];
120+
}
121+
122+
stageStack.Resources[name] = method;
123+
});
124+
125+
// Adjust permission to reference the function aliases
126+
_.forOwn(apiLambdaPermissions, (permission, name) => {
127+
const functionName = name.replace(/LambdaPermissionApiGateway$/, '');
128+
const versionName = _.find(_.keys(versions), version => _.startsWith(version, functionName));
129+
const aliasName = _.find(_.keys(aliases), alias => _.startsWith(alias, functionName));
130+
131+
// Adjust references and alias permissions
132+
permission.Properties.FunctionName = { Ref: aliasName };
133+
permission.Properties.SourceArn = {
134+
'Fn::Join': [
135+
'',
136+
[
137+
'arn:aws:execute-api:',
138+
{ Ref: 'AWS::Region' },
139+
':',
140+
{ Ref: 'AWS::AccountId' },
141+
':',
142+
{ 'Fn::ImportValue': `${stackName}-ApiGatewayRestApi` },
143+
'/*/*'
144+
]
145+
]
146+
};
147+
148+
// Add dependency on function version
149+
permission.DependsOn = [ versionName, aliasName ];
150+
151+
delete stageStack.Resources[name];
152+
});
153+
154+
// Add all alias stack owned resources
155+
aliasResources.push(apiLambdaPermissions);
156+
157+
}
158+
159+
_.forEach(aliasResources, resource => _.assign(aliasStack.Resources, resource));
160+
161+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
162+
};

0 commit comments

Comments
 (0)