Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/cli-reference/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

The `osls deploy` command deploys your entire service via CloudFormation. Run this command when you have made infrastructure changes (i.e., you edited `serverless.yml`). Use `osls deploy function -f myFunction` when you have made code changes and you want to quickly upload your updated code to AWS Lambda or just change function configuration.

With [`provider.deploymentMode: express`](../guides/deploying.md#deployment-mode), the stack operation completes as soon as the configuration is applied and resources may still be stabilizing when the command returns.

```bash
osls deploy
```
Expand Down
2 changes: 2 additions & 0 deletions docs/cli-reference/remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ The `osls remove` command will remove the deployed service, defined in your curr

If the stack has deletion protection enabled (see [`provider.deletionProtection`](../guides/deploying.md#deletion-protection)), the command fails before deleting anything.

With [`provider.deploymentMode: express`](../guides/deploying.md#deployment-mode), the stack is deleted in CloudFormation express mode and the command returns while resources may still be deleting.

```bash
osls remove
```
Expand Down
2 changes: 2 additions & 0 deletions docs/cli-reference/rollback.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ If `timestamp` is not specified, the CLI will show your existing deployments.

Rollback only works while the target deployment's artifacts still exist in the deployment bucket. osls keeps the most recent deployments and prunes older ones (the last `5` by default, configurable via `provider.deploymentBucket.maxPreviousDeploymentArtifacts`). Once a deployment's artifacts have been pruned, you can no longer roll back to it.

Rollback uses the [`provider.deploymentMode`](../guides/deploying.md#deployment-mode) from the current `serverless.yml`, not the one saved with the target deployment.

## Examples

### AWS
Expand Down
2 changes: 2 additions & 0 deletions docs/guides/compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ services:

As seen in the above example, it is possible to configure more than one dependency by providing `dependsOn` as a list.

Dependencies, explicit or from variables, only order the commands: a dependent service starts deploying as soon as the services it depends on have finished. With [`provider.deploymentMode: express`](./deploying.md#deployment-mode) on an upstream service, its stack completes before its resources have stabilized, so a dependent service that uses those resources while it deploys (an event source mapping on a stream, a custom resource that calls an endpoint, or networking such as a VPC and NAT gateway, for example) can fail and need to be deployed again. Removals run in reverse order, and an express removal returns while resources such as VPC network interfaces may not have been released yet, so removing the upstream service straight afterwards can fail with a dependency error until they are gone.

### Global commands

On top of `osls deploy`, the following commands can be run globally across all services:
Expand Down
28 changes: 28 additions & 0 deletions docs/guides/deploying.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,34 @@ provider:
deploymentMethod: direct
```

### Deployment mode

[CloudFormation express mode](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-express-mode.html) completes stack operations as soon as resource configuration is applied, without waiting for resources to stabilize. Deployments usually finish faster, but resources may still be initializing when the command returns. AWS positions it for development iteration; keep the default mode where a successful deployment must mean that resources are ready to serve traffic.

Enable it with `provider.deploymentMode`:

```yaml
provider:
name: aws
deploymentMode: express
```

The setting applies to every stack operation osls performs for the service: `osls deploy` (with either `deploymentMethod`), `osls rollback` and `osls remove`. `osls deploy function` does not use CloudFormation and is unaffected. No template changes are needed. CloudFormation still waits for custom resources to respond, and stack outputs that reference resource attributes are resolved before the operation completes. AWS documents no resource restrictions, but a resource that depends on another one being fully operational can fail; if that happens, use the default mode for that stage.

Rollback keeps working as in the default mode: a failed deployment is rolled back unless `provider.disableRollback` is `true`. This differs from CloudFormation's own express default, used by the AWS CLI and the CDK, which disables rollback unless you opt back in; the SAM CLI makes the same choice as osls. Keep rollback enabled unless you need to inspect failed resources: the constraints below only apply while it is disabled.

Keep in mind:

- `osls deploy` returns as soon as the configuration is applied. Resources such as CloudFront distributions may still be propagating when osls prints the service information, so a request made straight after the deploy can still reach the previous configuration. Express mode is not always faster: an express operation can still take tens of seconds for a single resource, and a resource that keeps failing can be retried for several minutes before the deployment fails.
- CloudFormation does not accept `OnFailure` in express mode, so osls cannot ask it to delete a stack whose creation failed, which it otherwise does with `deploymentMethod: direct`. This matters when a deployment creates the stack: the first deployment of a service creates the stack with the deployment bucket, and with a custom `provider.deploymentBucket` it creates the whole stack at once. A failed creation leaves the stack in `ROLLBACK_COMPLETE`; run `osls remove` before deploying again, as is already the case for change set deployments.
- With `disableRollback: true`, a failed express deployment leaves the stack in `CREATE_FAILED` or `UPDATE_FAILED`. Until an update succeeds, CloudFormation rejects every update that does not also use express mode with rollback disabled, and `RollbackStack` (`aws cloudformation rollback-stack`) is rejected too. Keep both settings, fix the problem and deploy again, with either deployment method. If `osls deploy` reports that there are no changes, deploy with `--force`.
- While rollback is disabled, CloudFormation rejects updates that replace a resource, for example changing a function's `name` (which also replaces its log group) or a DynamoDB table's or SQS queue's name. The deployment fails and leaves the stack in `UPDATE_FAILED`, and CloudFormation records the attempted properties, so reverting the change is treated as a replacement as well: a resource that keeps its physical name is deleted before it is created again, which discards a log group and its logs, and the creation can fail once more with `AlreadyExists` until the deletion has propagated. Deploy the reverted configuration with `deploymentMethod: direct` and `disableRollback: true` still set (`osls rollback --timestamp` uses the same path, while a change set built from the reverted configuration alone reports nothing to deploy), or remove and redeploy the service. Re-enable rollback before deploying the replacement.
- `provider.rollbackConfiguration` still applies, and the operation only completes after its monitoring period.
- Switching between express and the default mode is a per-deployment choice: after a successful operation, the next one can use either mode.
- `osls deploy --package` uses the value saved by `osls package`; re-run `osls package` after changing it. `osls rollback` and `osls remove` use the current `serverless.yml`.
- `osls remove` reports completion while resources may still be deleting in the background. Deploying a service that reuses the same physical resource names straight afterwards, including redeploying the same service right after `osls remove`, can fail with a name conflict.
- Nested stacks inherit the mode from the root stack.

### Deletion protection

Set `provider.deletionProtection` to have osls manage [CloudFormation termination protection](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-protect-stacks.html) for the service stack:
Expand Down
3 changes: 3 additions & 0 deletions docs/guides/serverless.yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ provider:
key: value
# Method used for CloudFormation deployments: 'changesets' or 'direct' (default: changesets)
deploymentMethod: direct
# CloudFormation deployment mode: 'standard' or 'express' (default: standard, values are case-sensitive)
# Express mode completes stack operations once resource configuration is applied, without waiting for resources to stabilize
deploymentMode: express
# Manage CloudFormation termination protection for the stack after each deploy (not managed by default).
# `true`/`false` applies to every stage; the `stages` form enables protection for the listed
# stages and disables it for all others.
Expand Down
7 changes: 7 additions & 0 deletions lib/plugins/aws/deploy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,13 @@ class AwsDeploy {
)}s)`
)}`
);
if (this.serverless.service.provider.deploymentMode === 'express') {
log.notice(
style.aside(
'Deployed with CloudFormation express mode. Resources may still be stabilizing in the background.'
)
);
}
writeText();
writeServiceOutputs(this.serverless.serviceOutputs);
writeServiceOutputs(this.serverless.servicePluginOutputs);
Expand Down
15 changes: 8 additions & 7 deletions lib/plugins/aws/lib/get-create-stack-params.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

module.exports = {
getCreateStackParams(options) {
const params = {
...this.getSharedStackActionParams(options),
OnFailure: 'DELETE',
};
const params = this.getSharedStackActionParams(options);

if (this.serverless.service.provider.disableRollback) {
delete params.OnFailure;
params.DisableRollback = this.serverless.service.provider.disableRollback;
// CloudFormation rejects OnFailure in express mode
if (this.serverless.service.provider.deploymentMode !== 'express') {
if (this.serverless.service.provider.disableRollback) {
params.DisableRollback = this.serverless.service.provider.disableRollback;
} else {
params.OnFailure = 'DELETE';
}
}

return params;
Expand Down
7 changes: 7 additions & 0 deletions lib/plugins/aws/lib/get-shared-stack-action-params.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ module.exports = {
params.RoleARN = customDeploymentRole;
}

if (this.serverless.service.provider.deploymentMode === 'express') {
params.DeploymentConfig = {
Mode: 'EXPRESS',
DisableRollback: Boolean(this.serverless.service.provider.disableRollback),
};
}

if (this.serverless.service.provider.notificationArns) {
params.NotificationARNs = this.serverless.service.provider.notificationArns;
} else {
Expand Down
5 changes: 4 additions & 1 deletion lib/plugins/aws/lib/get-update-stack-params.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ module.exports = {
params.RollbackConfiguration = this.serverless.service.provider.rollbackConfiguration;
}

if (this.serverless.service.provider.disableRollback) {
if (
this.serverless.service.provider.disableRollback &&
this.serverless.service.provider.deploymentMode !== 'express'
) {
params.DisableRollback = this.serverless.service.provider.disableRollback;
}
return params;
Expand Down
1 change: 1 addition & 0 deletions lib/plugins/aws/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,7 @@ class AwsProvider {
},
],
},
deploymentMode: { enum: ['standard', 'express'] },
deploymentPrefix: { type: 'string' },
disableRollback: { type: 'boolean' },
endpointType: {
Expand Down
7 changes: 7 additions & 0 deletions lib/plugins/aws/remove/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ class AwsRemove {
)}s)`
)}`
);
if (this.serverless.service.provider.deploymentMode === 'express') {
log.notice(
style.aside(
'Removed with CloudFormation express mode. Resources may still be deleting in the background.'
)
);
}
},
};
}
Expand Down
4 changes: 4 additions & 0 deletions lib/plugins/aws/remove/lib/stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ module.exports = {
params.RoleARN = customDeploymentRole;
}

if (this.serverless.service.provider.deploymentMode === 'express') {
params.DeploymentConfig = { Mode: 'EXPRESS' };
}

const cfData = {
StackId: stackName,
};
Expand Down
7 changes: 7 additions & 0 deletions lib/plugins/aws/rollback.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ class AwsRollback {
)}s)`
)}`
);
if (this.serverless.service.provider.deploymentMode === 'express') {
log.notice(
style.aside(
'Rolled back with CloudFormation express mode. Resources may still be stabilizing in the background.'
)
);
}
} else {
log.notice.skip(
`No updates to be performed. Rollback skipped. ${style.aside(
Expand Down
153 changes: 153 additions & 0 deletions test/unit/lib/plugins/aws/deploy/index.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const sinon = require('sinon');
const logEmitter = require('log/lib/emitter');

const runServerless = require('../../../../../utils/run-serverless');

Expand Down Expand Up @@ -229,6 +230,155 @@ describe('test/unit/lib/plugins/aws/deploy/index.test.js', () => {
});
});

describe('express deployment mode', () => {
async function deployExpress(options = {}) {
const createStackStub = sinon.stub().resolves({});
const updateStackStub = sinon.stub().resolves({});
const createChangeSetStub = sinon.stub().resolves({});
const executeChangeSetStub = sinon.stub().resolves({});
const logEvents = [];
const listener = (event) => logEvents.push(event);
logEmitter.on('log', listener);
try {
await runServerless({
fixture: 'function',
command: 'deploy',
awsSdkV3StubMap: {
...baseAwsSdkV3StubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
deleteObjects: {},
listObjectsV2: { Contents: [] },
upload: {},
headBucket: {},
},
CloudFormation: {
describeStacks: sinon
.stub()
.onFirstCall()
.throws(createCloudFormationValidationError('stack does not exist'))
.onSecondCall()
.resolves({ Stacks: [{}] }),
createStack: createStackStub,
updateStack: updateStackStub,
createChangeSet: createChangeSetStub,
executeChangeSet: executeChangeSetStub,
deleteChangeSet: {},
describeChangeSet: {
ChangeSetName: 'new-service-dev-change-set',
ChangeSetId: 'some-change-set-id',
StackName: 'new-service-dev',
Status: 'CREATE_COMPLETE',
},
describeStackEvents: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_COMPLETE',
},
],
},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
},
validateTemplate: {},
listStackResources: {},
},
},
configExt: {
service: 'new-service',
provider: { deploymentMode: 'express', ...options.provider },
},
});
} finally {
logEmitter.off('log', listener);
}
const noticeMessages = logEvents
.filter((event) => event.logger.level === 'notice')
.map((event) => String(event.messageTokens[0]));
return {
createStackStub,
updateStackStub,
createChangeSetStub,
executeChangeSetStub,
noticeMessages,
};
}

it('passes DeploymentConfig with rollback enabled on direct deployments', async () => {
const { createStackStub, updateStackStub, noticeMessages } = await deployExpress({
provider: { deploymentMethod: 'direct' },
});
const deploymentConfig = { Mode: 'EXPRESS', DisableRollback: false };
expect(createStackStub.getCall(0).args[0].DeploymentConfig).to.deep.equal(deploymentConfig);
expect(createStackStub.getCall(0).args[0]).to.not.have.property('OnFailure');
expect(updateStackStub.getCall(0).args[0].DeploymentConfig).to.deep.equal(deploymentConfig);
expect(noticeMessages.join('\n')).to.include('Deployed with CloudFormation express mode');
});

it('carries `disableRollback` in DeploymentConfig on direct deployments', async () => {
const { createStackStub, updateStackStub } = await deployExpress({
provider: { deploymentMethod: 'direct', disableRollback: true },
});
const deploymentConfig = { Mode: 'EXPRESS', DisableRollback: true };
expect(createStackStub.getCall(0).args[0].DeploymentConfig).to.deep.equal(deploymentConfig);
expect(createStackStub.getCall(0).args[0]).to.not.have.property('OnFailure');
expect(createStackStub.getCall(0).args[0]).to.not.have.property('DisableRollback');
expect(updateStackStub.getCall(0).args[0].DeploymentConfig).to.deep.equal(deploymentConfig);
expect(updateStackStub.getCall(0).args[0]).to.not.have.property('DisableRollback');
});

it('passes DeploymentConfig on change sets and executes them as before', async () => {
const { createChangeSetStub, executeChangeSetStub } = await deployExpress();
const deploymentConfig = { Mode: 'EXPRESS', DisableRollback: false };
expect(createChangeSetStub.getCall(0).args[0].DeploymentConfig).to.deep.equal(
deploymentConfig
);
expect(createChangeSetStub.getCall(1).args[0].DeploymentConfig).to.deep.equal(
deploymentConfig
);
expect(executeChangeSetStub.getCall(0).args[0]).to.deep.equal({
StackName: 'new-service-dev',
ChangeSetName: 'new-service-dev-change-set',
});
expect(executeChangeSetStub.getCall(1).args[0]).to.deep.equal({
StackName: 'new-service-dev',
ChangeSetName: 'new-service-dev-change-set',
});
});

it('keeps `disableRollback` on change set execution', async () => {
const { createChangeSetStub, executeChangeSetStub } = await deployExpress({
provider: { disableRollback: true },
});
const deploymentConfig = { Mode: 'EXPRESS', DisableRollback: true };
expect(createChangeSetStub.getCall(0).args[0].DeploymentConfig).to.deep.equal(
deploymentConfig
);
expect(createChangeSetStub.getCall(1).args[0].DeploymentConfig).to.deep.equal(
deploymentConfig
);
expect(executeChangeSetStub.getCall(0).args[0]).to.deep.equal({
StackName: 'new-service-dev',
ChangeSetName: 'new-service-dev-change-set',
DisableRollback: true,
});
expect(executeChangeSetStub.getCall(1).args[0]).to.deep.equal({
StackName: 'new-service-dev',
ChangeSetName: 'new-service-dev-change-set',
DisableRollback: true,
});
});
});

describe('with direct create/update calls', () => {
it('with nonexistent stack - first deploy', async () => {
const describeStacksStub = sinon
Expand Down Expand Up @@ -291,6 +441,9 @@ describe('test/unit/lib/plugins/aws/deploy/index.test.js', () => {

expect(createStackStub).to.be.calledOnce;
expect(updateStackStub).to.be.calledOnce;
expect(createStackStub.getCall(0).args[0].OnFailure).to.equal('DELETE');
expect(createStackStub.getCall(0).args[0]).to.not.have.property('DeploymentConfig');
expect(updateStackStub.getCall(0).args[0]).to.not.have.property('DeploymentConfig');
const createStackSends = getCloudFormationSends(awsSdkV3Stub, 'createStack');
const updateStackSends = getCloudFormationSends(awsSdkV3Stub, 'updateStack');
const validateTemplateSends = getCloudFormationSends(awsSdkV3Stub, 'validateTemplate');
Expand Down
Loading
Loading