Skip to content

Commit 98f5d22

Browse files
committed
Implemented basic CloudFormation create/update
1 parent fde3f81 commit 98f5d22

File tree

4 files changed

+186
-27
lines changed

4 files changed

+186
-27
lines changed

LICENSE

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2017 SC5 Online. https://sc5.io
4+
5+
The following license applies to all parts of this software except as
6+
documented below:
7+
8+
====
9+
10+
Permission is hereby granted, free of charge, to any person obtaining a copy
11+
of this software and associated documentation files (the "Software"), to deal
12+
in the Software without restriction, including without limitation the rights
13+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
copies of the Software, and to permit persons to whom the Software is
15+
furnished to do so, subject to the following conditions:
16+
17+
The above copyright notice and this permission notice shall be included in all
18+
copies or substantial portions of the Software.
19+
20+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26+
SOFTWARE.
27+
28+
====
29+
30+
All files located in the node_modules and external directories are
31+
externally maintained libraries used by this software which have their
32+
own licenses; we recommend you read them, as their terms may differ from
33+
the terms above.

README.md

+10-7
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,12 @@ permanentResources:
7878
Name: ${self:service}-data
7979
```
8080

81+
The syntax of the Resources section is identical to the normal resources
82+
in `serverless.yml`, so you can just cut-paste resource definitions around.
8183

8284
The full name of the deployed CloudFormation stack will include the service
8385
name. If your service is called `my-service` and you deploy it to the `dev`
84-
stage, the additional stack would be called `my-service-permanent-dev`.
85-
86-
The syntax of the Resources section is identical to the normal resources
87-
in `serverless.yml`, so you can just cut-paste resource definitions around.
86+
stage, the additional stack would be called `my-service-dev-permanent`.
8887

8988
### Using Deploy: After stacks
9089

@@ -114,16 +113,20 @@ Your additional stacks will be deployed automatically when you run:
114113

115114
sls deploy
116115

116+
To deploy all additional stacks without deploying tje Serverless service, you can use:
117+
118+
sls deploy additionalstacks
119+
117120
To deploy an additional stack individually, you can use:
118121

119-
sls deploy additionalstack [stackname]
122+
sls deploy additionalstacks --stack [stackname]
120123

121124
If you want to remove an additional stack, you need to run:
122125

123-
sls remove additionalstack [stackname]
126+
sls remove additionalstacks --stack [stackname]
124127

125128
Or you can remove all additional stacks in the service with:
126129

127-
sls remove additionalstack -a
130+
sls remove additionalstacks
128131

129132
Alternatively, you can remove stacks manually in AWS CloudFormation Console.

index.js

+142-19
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,12 @@ class ServerlessPlugin {
1111
this.commands = {
1212
deploy: {
1313
commands: {
14-
additionalstack: {
14+
additionalstacks: {
1515
usage: 'Deploy additional stacks',
1616
lifecycleEvents: [
1717
'deploy',
1818
],
1919
options: {
20-
all: {
21-
usage: 'Deploy all additional stacks',
22-
shortcut: 'a',
23-
required: false,
24-
},
2520
stack: {
2621
usage: 'Additional stack name to deploy',
2722
shortcut: 'k',
@@ -36,7 +31,7 @@ class ServerlessPlugin {
3631
this.hooks = {
3732
'before:deploy:deploy': this.beforeDeployGlobal.bind(this),
3833
'after:deploy:deploy': this.afterDeployGlobal.bind(this),
39-
'deploy:additionalstack:deploy': this.deployAdditionalStackDeploy.bind(this),
34+
'deploy:additionalstacks:deploy': this.deployAdditionalStackDeploy.bind(this),
4035
}
4136
}
4237

@@ -90,16 +85,7 @@ class ServerlessPlugin {
9085
deployAdditionalStackDeploy() {
9186
const stacks = this.getAdditionalStacks()
9287

93-
if (this.options.all) {
94-
// Deploy all additional stacks
95-
if (Object.keys(stacks).length > 0) {
96-
this.serverless.cli.log('Deploying all additional stacks...')
97-
return this.deployStacks(stacks)
98-
} else {
99-
this.serverless.cli.log('No additional stacks defined. Add a custom.additionalStacks section to serverless.yml.')
100-
return Promise.resolve()
101-
}
102-
} else if (this.options.stack) {
88+
if (this.options.stack) {
10389
const stack = stacks[this.options.stack]
10490
if (stack) {
10591
this.serverless.cli.log('Deploying additional stack ' + this.options.stack + '...')
@@ -108,10 +94,22 @@ class ServerlessPlugin {
10894
return Promise.reject(new Error('Additional stack not found: ' + this.options.stack))
10995
}
11096
} else {
111-
return Promise.reject(new Error('Please specify either sls deploy additionalstack -a to deploy all additional stacks or -k [stackName] to deploy a single stack'))
97+
// Deploy all additional stacks
98+
if (Object.keys(stacks).length > 0) {
99+
this.serverless.cli.log('Deploying all additional stacks...')
100+
return this.deployStacks(stacks)
101+
} else {
102+
this.serverless.cli.log('No additional stacks defined. Add a custom.additionalStacks section to serverless.yml.')
103+
return Promise.resolve()
104+
}
112105
}
113106
}
114107

108+
// Generate a full name for an additional stack (used in AWS)
109+
getFullStackName(stackName) {
110+
return this.provider.naming.getStackName() + '-' + stackName
111+
}
112+
115113
// This deploys all the specified stacks
116114
deployStacks(stacks) {
117115
let promise = Promise.resolve()
@@ -126,7 +124,132 @@ class ServerlessPlugin {
126124

127125
// This is where we actually handle the deployment to AWS
128126
deployStack(stackName, stack) {
129-
this.serverless.cli.log('DEPLOYING NOW ' + stackName + ' ' + JSON.stringify(stack))
127+
// Generate the CloudFormation template
128+
const compiledCloudFormationTemplate = {
129+
"AWSTemplateFormatVersion": "2010-09-09",
130+
"Description": stack.Description || "Additional AWS CloudFormation template for this Serverless application",
131+
"Metadata": stack.Metadata || undefined,
132+
"Parameters": stack.Parameters || undefined,
133+
"Mappings": stack.Mappings || undefined,
134+
"Conditions": stack.Conditions || undefined,
135+
"Transform": stack.Transform || undefined,
136+
"Resources": stack.Resources || undefined,
137+
"Outputs": stack.Outputs || undefined,
138+
}
139+
140+
// Generate tags
141+
const stackTags = {
142+
STAGE: this.options.stage || this.serverless.service.provider.stage
143+
}
144+
if (typeof stack.Tags === 'object') {
145+
// Add custom tags
146+
Object.assign(stackTags, stack.Tags)
147+
}
148+
149+
// Generate full stack name
150+
const fullStackName = this.getFullStackName(stackName)
151+
152+
return this.describeStack(fullStackName)
153+
.then(stackStatus => {
154+
if (!stackStatus) {
155+
// Create stack
156+
this.serverless.cli.log('Creating CloudFormation stack ' + fullStackName + '...')
157+
console.log('TEMPL', compiledCloudFormationTemplate)
158+
console.log('TAGS', stackTags)
159+
return this.createStack(fullStackName, compiledCloudFormationTemplate, stackTags)
160+
} else {
161+
// Update stack
162+
this.serverless.cli.log('Updating CloudFormation stack ' + fullStackName + '...')
163+
return this.updateStack(fullStackName, compiledCloudFormationTemplate, stackTags)
164+
}
165+
})
166+
}
167+
168+
describeStack(fullStackName) {
169+
return this.provider.request(
170+
'CloudFormation',
171+
'describeStacks', {
172+
StackName: fullStackName,
173+
},
174+
this.options.stage,
175+
this.options.region
176+
)
177+
.then(response => {
178+
return response.Stacks && response.Stacks[0]
179+
})
180+
.then(null, err => {
181+
if (err.message && err.message.match(/does not exist$/)) {
182+
// Stack doesn't exist yet
183+
return null
184+
} else {
185+
// Some other error, let it throw
186+
return Promise.reject(err)
187+
}
188+
})
189+
}
190+
191+
createStack(fullStackName, compiledCloudFormationTemplate, stackTags) {
192+
// These are the same parameters that Serverless uses in https://github.com/serverless/serverless/blob/master/lib/plugins/aws/deploy/lib/createStack.js
193+
const params = {
194+
StackName: fullStackName,
195+
OnFailure: 'ROLLBACK',
196+
Capabilities: [
197+
'CAPABILITY_IAM',
198+
'CAPABILITY_NAMED_IAM',
199+
],
200+
Parameters: [],
201+
TemplateBody: JSON.stringify(compiledCloudFormationTemplate),
202+
Tags: Object.keys(stackTags).map((key) => ({ Key: key, Value: stackTags[key] })),
203+
}
204+
205+
return this.provider.request(
206+
'CloudFormation',
207+
'createStack',
208+
params,
209+
this.options.stage,
210+
this.options.region
211+
)
212+
}
213+
214+
updateStack(fullStackName, compiledCloudFormationTemplate, stackTags) {
215+
// These are the same parameters that Serverless uses in https://github.com/serverless/serverless/blob/master/lib/plugins/aws/lib/updateStack.js
216+
const params = {
217+
StackName: fullStackName,
218+
Capabilities: [
219+
'CAPABILITY_IAM',
220+
'CAPABILITY_NAMED_IAM',
221+
],
222+
Parameters: [],
223+
TemplateBody: JSON.stringify(compiledCloudFormationTemplate),
224+
Tags: Object.keys(stackTags).map((key) => ({ Key: key, Value: stackTags[key] })),
225+
}
226+
227+
return this.provider.request(
228+
'CloudFormation',
229+
'updateStack',
230+
params,
231+
this.options.stage,
232+
this.options.region
233+
)
234+
.then(null, err => {
235+
if (err.message && err.message.match(/^No updates/)) {
236+
// Stack is unchanged, ignore error
237+
return Promise.resolve()
238+
} else {
239+
return Promise.reject(err)
240+
}
241+
})
242+
}
243+
244+
waitForStack(fullStackName) {
245+
const readMore = () => {
246+
return describeStack(fullStackName)
247+
.then(response => {
248+
console.log('STATUS', response)
249+
setTimeout(readMore, 5000)
250+
})
251+
}
252+
return readMore()
130253
}
131254
}
132255

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "serverless-plugin-additional-stacks",
3-
"version": "1.0.0",
3+
"version": "0.1.0",
44
"main": "index.js",
55
"repository": {},
66
"license": "MIT"

0 commit comments

Comments
 (0)