Skip to content

Commit

Permalink
feat(core): resource overrides (escape hatch) (#784)
Browse files Browse the repository at this point in the history
Adds capabilities and documentation to allow users to add overrides
to synthesized resource definitions in case of gaps in L2s.

- resource.addOverride(path, value)
- resource.addPropertyOverride(propertyPath, value)
- resource.addDeletionOverride(p, v)
- resource.addPropertyDeletionOVerride(pp, v)
- xxxResource.propertyOverrides (of type XxxResourceProps)

Documented under the AWS Construct Library topic.

Fixes #606
  • Loading branch information
Elad Ben-Israel authored Sep 27, 2018
1 parent 9d00e36 commit 5054eef
Show file tree
Hide file tree
Showing 7 changed files with 783 additions and 16 deletions.
275 changes: 270 additions & 5 deletions docs/src/aws-construct-lib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,274 @@ were part of your app.

.. _cloudformation_layer:

AWS CloudFormation Layer
========================
Access the AWS CloudFormation Layer
===================================

This topic discusses ways to directly modify the underlying CloudFormation
resources at the AWS Construct Library. We also call this technique an "escape
hatch", as it allows users to "escape" from the abstraction boundary defined by
the AWS Construct and patch the underlying resources.

.. important::

**We do not recommend this method, as it breaks the abstraction layer and
might produce unexpected results**.

Furthermore, the internal implementation of an AWS construct is not part of
the API compatibility guarantees that we can make. This means that updates to
the construct library may break your code without a major version bump.

AWS constructs, such as :py:class:`Topic <@aws-cdk/aws-sns.Topic>`, encapsulate
one or more AWS CloudFormation resources behind their APIs. These resources are
also represented as constructs under the ``cloudformation`` namespace in each
library. For example, the :py:class:`@aws-cdk/aws-s3.Bucket` construct
encapsulates the :py:class:`@aws-cdk/aws-s3.cloudformation.BucketResource`. When
a stack that includes an AWS construct is synthesized, the CloudFormation
definition of the underlying resources are included in the resulting template.

Eventually, the APIs provided by AWS constructs are expected to support all the
services and capabilities offered by AWS, but we are aware that the library
still has many gaps both at the service level (some services don't have any
constructs yet) and at the resource level (an AWS construct exists, but some
features are missing).

.. note::

If you encounter a missing capability in the AWS Construct Library, whether
it is an entire library, a specific resource or a feature,
`raise an issue <https://github.com/awslabs/aws-cdk/issues/new>`_ on GitHub,
and letting us know.

This topic covers the following use cases:

- How to access the low-level CloudFormation resources encapsulated by an AWS construct
- How to specify resource options such as metadata, dependencies on resources
- How to add overrides to a CloudFormation resource and property definitions
- How to directly define low-level CloudFormation resources without an AWS construct

You can also find more information on how to work directly with the AWS
CloudFormation layer under :py:doc:`cloudformation`.

Accessing Low-level Resources
-----------------------------

You can use :py:meth:`construct.findChild(id) <@aws-cdk/cdk.Construct.findChild>`
to access any child of this construct by its construct ID. By convention, the "main"
resource of any AWS Construct is called ``"Resource"``.

The following example shows how to access the underlying S3 bucket resource
given an :py:class:`s3.Bucket <@aws-cdk/s3.Bucket>` construct:

.. code-block:: ts
// let's create an AWS bucket construct
const bucket = new s3.Bucket(this, 'MyBucket');
// we use our knowledge that the main construct is called "Resource" and
// that it's actual type is s3.cloudformation.BucketResource; const
const bucketResource = bucket.findResource('Resource') as s3.cloudformation.BucketResource;
At this point, ``bucketResource`` represents the low-level CloudFormation resource of type
:py:class:`s3.cloudformation.BucketResource <@aws-cdk/aws-s3.cloudformation.BucketResource>`
encapsulated by our bucket.

:py:meth:`construct.findChild(id) <@aws-cdk/cdk.Construct.findChild>` will fail
if the child could not be located, which means that if the underlying |l2| changes
the IDs or structure for some reason, synthesis fails.

It is also possible to use :py:meth:`construct.children <@aws-cdk/cdk.Construct.children>` for more
advanced queries. For example, we can look for a child that has a certain CloudFormation resource
type:

.. code-block:: ts
const bucketResource =
bucket.children.find(c => (c as cdk.Resource).resourceType === 'AWS::S3::Bucket')
as s3.cloudformation.BucketResource;
From that point, users are interacting with CloudFormation resource classes
(which extend :py:class:`cdk.Resource <@aws-cdk/cdk.Resource>`), so we will look
into how to use their APIs in order to modify the behavior of the AWS construct
at hand.

Resource Options
----------------

:py:class:`cdk.Resource <@aws-cdk/cdk.Resource>` has a few facilities for
setting resource options such as ``Metadata``, ``DependsOn``, etc.

For example, this code:

.. code-block:: ts
const bucketResource = bucket.findChild('Resource') as s3.cloudformation.BucketResource;
bucketResource.options.metadata = { MetadataKey: 'MetadataValue' };
bucketResource.options.updatePolicy = {
autoScalingRollingUpdate: {
pauseTime: '390'
}
};
bucketResource.addDependency(otherBucket.findChild('Resource') as cdk.Resource);
Synthesizes the following template:

.. code-block:: json
{
"Type": "AWS::S3::Bucket",
"DependsOn": [ "Other34654A52" ],
"UpdatePolicy": {
"AutoScalingRollingUpdate": {
"PauseTime": "390"
}
},
"Metadata": {
"MetadataKey": "MetadataValue"
}
}
Overriding Resource Properties
------------------------------

Each low-level resource in the CDK has a strongly-typed property called
``propertyOverrides``. It allows users to apply overrides that adhere to the
CloudFormation schema of the resource, and use code-completion and
type-checking.

You will normally use this mechanism when a certain feature is available at the
CloudFormation layer but is not exposed by the AWS Construct.

The following example sets a bucket's analytics configuration:

.. code-block:: ts
bucketResource.propertyOverrides.analyticsConfigurations = [
{
id: 'config1',
storageClassAnalysis: {
dataExport: {
outputSchemaVersion: '1',
destination: {
format: 'html',
bucketArn: otherBucket.bucketArn // use tokens freely
}
}
}
}
];
Raw Overrides
-------------

In cases the strongly-typed overrides are not sufficient, or, for example, if
the schema defined in CloudFormation is not up-to-date, the method
:py:meth:`cdk.Resource.addOverride(path, value) <@aws-cdk/cdk.Resource.addOverride>`
can be used to define an override that will by applied to the resource
definition during synthesis.

For example:

.. code-block:: ts
// define an override at the resource definition root, you can even modify the "Type"
// of the resource if needed.
bucketResource.addOverride('Type', 'AWS::S3::SpecialBucket');
// define an override for a property (both are equivalent operations):
bucketResource.addPropertyOverride('VersioningConfiguration.Status', 'NewStatus');
bucketResource.addOverride('Properties.VersioningConfiguration.Status', 'NewStatus');
// use dot-notation to define overrides in complex structures which will be merged
// with the values set by the higher-level construct
bucketResource.addPropertyOverride('LoggingConfiguration.DestinationBucketName', otherBucket.bucketName);
// it is also possible to assign a `null` value
bucketResource.addPropertyOverride('Foo.Bar', null);
Synthesizes to:

.. code-block:: json
{
"Type": "AWS::S3::SpecialBucket",
"Properties": {
"Foo": {
"Bar": null
},
"VersioningConfiguration": {
"Status": "NewStatus"
},
"LoggingConfiguration": {
"DestinationBucketName": {
"Ref": "Other34654A52"
}
}
}
}
Use ``undefined``, :py:meth:`cdk.Resource.addDeletionOverride <@aws-cdk/cdk.Resource.addDeletionOverride>`
or :py:meth:`cdk.Resource.addPropertyDeletionOverride <@aws-cdk/cdk.Resource.addPropertyDeletionOverride>`
to delete values:

.. code-block:: ts
const bucket = new s3.Bucket(this, 'MyBucket', {
versioned: true,
encryption: s3.BucketEncryption.KmsManaged
});
const bucketResource = bucket.findChild('Resource') as s3.cloudformation.BucketResource;
bucketResource.addPropertyOverride('BucketEncryption.ServerSideEncryptionConfiguration.0.EncryptEverythingAndAlways', true);
bucketResource.addPropertyDeletionOverride('BucketEncryption.ServerSideEncryptionConfiguration.0.ServerSideEncryptionByDefault');
Synthesizes to:

.. code-block:: json
"MyBucketF68F3FF0": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"EncryptEverythingAndAlways": true
}
]
},
"VersioningConfiguration": {
"Status": "Enabled"
}
}
}
Directly Defining CloudFormation Resources
-------------------------------------------

It is also possible to explicitly define CloudFormation resources in your stack.
To that end, instantiate one of the constructs under the ``cloudformation``
namespace of the dedicated library.

.. code-block:: ts
new s3.cloudformation.BucketResource(this, 'MyBucket', {
analyticsConfigurations: [
// ...
]
});
In the rare case where you want to define a resource that doesn't have a
corresponding ``cloudformation`` class (such as a new resource that was not yet
published in the CloudFormation resource specification), you can instantiate the
:py:class:`cdk.Resource <@aws-cdk/cdk.Resource>` object:

.. code-block:: ts
new cdk.Resource(this, 'MyBucket', {
type: 'AWS::S3::Bucket',
properties: {
AnalyticsConfiguration: [ /* ... */ ] // note the PascalCase here
}
});
Every module in the AWS Construct Library includes a ``cloudformation`` namespace which contains
low-level constructs which represent the low-level AWS CloudFormation semantics of this service.
See :py:doc:`cloudformation` for details.
22 changes: 22 additions & 0 deletions examples/cdk-examples-typescript/resource-overrides/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"app": "node index",
"context": {
"availability-zones:993655754359:us-east-1": [
"us-east-1a",
"us-east-1b",
"us-east-1c",
"us-east-1d",
"us-east-1e",
"us-east-1f"
],
"availability-zones:585695036304:us-east-1": [
"us-east-1a",
"us-east-1b",
"us-east-1c",
"us-east-1d",
"us-east-1e",
"us-east-1f"
],
"ssm:585695036304:us-east-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-0ff8a91507f77f867"
}
}
Loading

0 comments on commit 5054eef

Please sign in to comment.