Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial API Gateway Construct Library #665

Merged
merged 16 commits into from
Sep 17, 2018
2 changes: 1 addition & 1 deletion packages/@aws-cdk/applet-js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 19 additions & 6 deletions packages/@aws-cdk/assert/lib/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@ import cdk = require('@aws-cdk/cdk');
import api = require('@aws-cdk/cx-api');
import { StackInspector } from './inspector';

export function expect(stack: api.SynthesizedStack | cdk.Stack): StackInspector {
export function expect(stack: api.SynthesizedStack | cdk.Stack, skipValidation = false): StackInspector {
// Can't use 'instanceof' here, that breaks if we have multiple copies
// of this library.
const sstack: api.SynthesizedStack = isStackClassInstance(stack) ? {
name: 'test',
template: stack.toCloudFormation(),
metadata: {}
} : stack;
let sstack: api.SynthesizedStack;

if (isStackClassInstance(stack)) {
if (!skipValidation) {
const errors = stack.validateTree();
if (errors.length > 0) {
throw new Error(`Stack validation failed:\n${errors.map(e => `${e.message} at: ${e.source.parent}`).join('\n')}`);
}
}

sstack = {
name: 'test',
template: stack.toCloudFormation(),
metadata: {}
};
} else {
sstack = stack;
}

return new StackInspector(sstack);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/assert/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

179 changes: 177 additions & 2 deletions packages/@aws-cdk/aws-apigateway/README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,177 @@
## The CDK Construct Library for AWS API Gateway
This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project.
## CDK Construct Library for Amazon API Gateway

Amazon API Gateway is a fully managed service that makes it easy for developers
to publish, maintain, monitor, and secure APIs at any scale. Create an API to
access data, business logic, or functionality from your back-end services, such
as applications running on Amazon Elastic Compute Cloud (Amazon EC2), code
running on AWS Lambda, or any web application.

### Defining APIs

APIs are defined as a hierarchy of resources and methods. `addResource` and
`addMethod` can be used to build this hierarchy. The root resource is
`api.root`.

For example, the following code defines an API that includes the following HTTP
endpoints: `ANY /, GET /books`, `POST /books`, `GET /books/{book_id}`, `DELETE /books/{book_id}`.

```ts
const api = new apigateway.RestApi(this, 'books-api');

api.root.addMethod('ANY');

const books = api.root.addResource('books');
books.addMethod('GET');
eladb marked this conversation as resolved.
Show resolved Hide resolved
books.addMethod('POST');

const book = books.addResource('{book_id}');
book.addMethod('GET');
book.addMethod('DELETE');
```

### Integration Targets

Methods are associated with backend integrations, which are invoked when this
method is called. API Gateway supports the following integrations:

* `MockIntegration` - can be used to test APIs. This is the default
integration if one is not specified.
* `LambdaIntegration` - can be used to invoke an AWS Lambda function.
* `AwsIntegration` - can be used to invoke arbitrary AWS service APIs.
* `HttpIntegration` - can be used to invoke HTTP endpoints.

The following example shows how to integrate the `GET /book/{book_id}` method to
an AWS Lambda function:

```ts
const getBookHandler = new lambda.Function(...);
const getBookIntegration = new apigateway.LambdaIntegration(getBookHandler);
book.addMethod('GET', getBookIntegration);
```

Integration options can be optionally be specified:

```ts
const getBookIntegration = new apigateway.LambdaIntegration(getBookHandler, {
contentHandling: apigateway.ContentHandling.ConvertToText, // convert to base64
credentialsPassthrough: true, // use caller identity to invoke the function
});
```

Method options can optionally be specified when adding methods:

```ts
book.addMethod('GET', getBookIntegration, {
authorizationType: apigateway.AuthorizationType.IAM,
apiKeyRequired: true
});
```

#### Default Integration and Method Options

The `defaultIntegration` and `defaultMethodOptions` properties can be used to
eladb marked this conversation as resolved.
Show resolved Hide resolved
configure a default integration at any resource level. These options will be
used when defining method under this resource (recursively) with undefined
integration or options.

> If not defined, the default integration is `MockIntegration`. See reference
documentation for default method options.

The following example defines the `booksBackend` integration as a default
integration. This means that all API methods that do not explicitly define an
integration will be routed to this AWS Lambda function.

```ts
const booksBackend = new apigateway.LambdaIntegration(...);
const api = new apigateway.RestApi(this, 'books', {
defaultIntegration: booksBackend
});

const books = new api.root.addResource('books');
books.addMethod('GET'); // integrated with `booksBackend`
books.addMethod('POST'); // integrated with `booksBackend`

const book = books.addResource('{book_id}');
book.addMethod('GET'); // integrated with `booksBackend`
```

### Deployments

By default, the `RestApi` construct will automatically create an API Gateway
[Deployment] and a "prod" [Stage] which represent the API configuration you defined in
your CDK app. This means that when you deploy your app, your API can be accessed
from the public internet via the stage URL.
eladb marked this conversation as resolved.
Show resolved Hide resolved

The URL of your API can be obtained from the attribute `restApi.url`, and is
also exported as an `Output` from your stack, so it's printed when you `cdk
deploy` your app:

```
$ cdk deploy
...
books.booksapiEndpointE230E8D5 = https://6lyktd4lpk.execute-api.us-east-1.amazonaws.com/prod/
```

To disable this behavior, you can set `{ deploy: false }` when creating your
API. This means that the API will not be deployed and a stage will not be
created for it. You will need to manually define a `apigateway.Deployment` and
`apigateway.Stage` resources.

Use the `deployOptions` property to customize the deployment options of your
API.

The following example will configure API Gateway to emit logs and data traces to
AWS CloudWatch for all API calls:

> By default, an IAM role will be created and associated with API Gateway to
allow it to write logs and metrics to AWS CloudWatch `cloudWatchRole` is set to
`false`.

```ts
const api = new apigateway.RestApi(this, 'books', {
deployOptions: {
loggingLevel: apigateway.MethodLoggingLevel.Info,
dataTraceEnabled: true
}
})
```

#### Deeper dive: invalidation of deployments

API Gateway deployments are an immutable snapshot of the API. This means that we
want to automatically create a new deployment resource every time the API model
defined in our CDK app changes.

In order to achieve that, the AWS CloudFormation logical ID of the
`AWS::ApiGateway::Deployment` resource is dynamically calculated by hashing the
API configuration (resources, methods). This means that when the configuration
changes (i.e. a resource or method are added, configuration is changed), a new
logical ID will be assigned to the deployment resource. This will cause
CloudFormation to create a new deployment resource.

By default, old deployments are _deleted_. You can set `retainDeployments: true`
to allow users revert the stage to an old deployment.
eladb marked this conversation as resolved.
Show resolved Hide resolved

[Deployment]: https://docs.aws.amazon.com/apigateway/api-reference/resource/deployment/
[Stage]: https://docs.aws.amazon.com/apigateway/api-reference/resource/stage/

### TODO

The following features are not supported yet by this library:
eladb marked this conversation as resolved.
Show resolved Hide resolved

- [ ] Swagger/Open API models
- [ ] [Authorizers](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-authorizer.html)
- [ ] Method options: [`RequestValidatorId`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-requestvalidatorid), [`RequestModels`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-requestmodels), [`RequestParameters`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-requestparameters), [`MethodResponses`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-methodresponses)
- [ ] [Custom domains](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-domainname.html)
- [ ] [API keys](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html)
- [ ] [Base path mapping](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-basepathmapping.html)
- [ ] [Client certificates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-clientcertificate.html)
- [ ] Documentation ([part](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-documentationpart.html) and [version](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-documentationversion.html))
- [ ] [Model](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-model.html)
- [ ] [Request validators](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-requestvalidator.html)
- [ ] [Usage plans](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-usageplan.html)
- [ ] [VPC Links](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-vpclink.html)

----

This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project.
166 changes: 166 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/deployment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import cdk = require('@aws-cdk/cdk');
import crypto = require('crypto');
import { cloudformation, DeploymentId } from './apigateway.generated';
import { RestApiRef } from './restapi-ref';

export interface DeploymentProps {
/**
* The Rest API to deploy.
*/
api: RestApiRef;

/**
* A description of the purpose of the API Gateway deployment.
*/
description?: string;

/**
* When an API Gateway model is updated, a new deployment will automatically be created.
* If this is true (default), the old API Gateway Deployment resource will not be deleted.
* This will allow manually reverting back to a previous deployment in case for example
*
* @default false
*/
retainDeployments?: boolean;
}

/**
* A Deployment of a REST API.
*
* An immutable representation of a RestApi resource that can be called by users
* using Stages. A deployment must be associated with a Stage for it to be
* callable over the Internet.
*
* Normally, you don't need to define deployments manually. The RestApi
* construct manages a Deployment resource that represents the latest model. It
* can be accessed through `restApi.latestDeployment` (unless `deploy: false` is
* set when defining the `RestApi`).
*
* If you manually define this resource, you will need to know that since
* deployments are immutable, as long as the resource's logical ID doesn't
* change, the deployment will represent the snapshot in time in which the
* resource was created. This means that if you modify the RestApi model (i.e.
* add methods or resources), these changes will not be reflected unless a new
* deployment resource is created.
*
* To achieve this behavior, the method `addToLogicalId(data)` can be used to
* augment the logical ID generated for the deployment resource such that it
* will include arbitrary data. This is done automatically for the
* `restApi.latestDeployment` deployment.
*
* Furthermore, since a deployment does not reference any of the REST API
* resources and methods, CloudFormation will likely provision it before these
* resources are created, which means that it will represent a "half-baked"
* model. Use the `addDependency(dep)` method to circumvent that. This is done
* automatically for the `restApi.latestDeployment` deployment.
*/
export class Deployment extends cdk.Construct implements cdk.IDependable {
public readonly deploymentId: DeploymentId;
public readonly api: RestApiRef;

/**
* Allows taking a dependency on this construct.
*/
public readonly dependencyElements = new Array<cdk.IDependable>();

private readonly resource: LatestDeploymentResource;

constructor(parent: cdk.Construct, id: string, props: DeploymentProps) {
super(parent, id);

this.resource = new LatestDeploymentResource(this, 'Resource', {
description: props.description,
restApiId: props.api.restApiId,
});

if (props.retainDeployments) {
this.resource.options.deletionPolicy = cdk.DeletionPolicy.Retain;
eladb marked this conversation as resolved.
Show resolved Hide resolved
}

this.api = props.api;
this.deploymentId = new DeploymentId(() => this.resource.ref);
this.dependencyElements.push(this.resource);
}

/**
* Adds a dependency for this deployment. Should be called by all resources and methods
* so they are provisioned before this Deployment.
*/
public addDependency(dep: cdk.IDependable) {
this.resource.addDependency(dep);
}

/**
* Adds a component to the hash that determines this Deployment resource's
* logical ID.
*
* This should be called by constructs of the API Gateway model that want to
* invalidate the deployment when their settings change. The component will
* be resolve()ed during synthesis so tokens are welcome.
*/
public addToLogicalId(data: any) {
this.resource.addToLogicalId(data);
}
}

class LatestDeploymentResource extends cloudformation.DeploymentResource {
private originalLogicalId: string;
private customLogicalId?: string;
private hashComponents = new Array<any>();

constructor(parent: cdk.Construct, id: string, props: cloudformation.DeploymentResourceProps) {
super(parent, id, props);

this.originalLogicalId = this.logicalId;

Object.defineProperties(this, {
logicalId: {
eladb marked this conversation as resolved.
Show resolved Hide resolved
get: () => {
if (!this.customLogicalId) {
throw new Error('The logical ID of this resource cannot be evaluated eagerly. Use: new cdk.Token(() => foo.logicalId)');
}
return this.customLogicalId;
}
},
ref: {
get: () => new cdk.CloudFormationToken(() => ({ Ref: this.customLogicalId }))
},
});
}

/**
* Allows adding arbitrary data to the hashed logical ID of this deployment.
* This can be used to couple the deployment to the API Gateway model.
*/
public addToLogicalId(data: unknown) {

// if the construct is locked, it means we are already synthesizing and then
// we can't modify the hash because we might have already calculated it.
if (this.locked) {
throw new Error('Cannot modify the logical ID when the construct is locked');
}

this.hashComponents.push(data);
}

/**
* Hooks into synthesis to calculate a logical ID that hashes all the components
* add via `addToLogicalId`.
*/
public validate() {
eladb marked this conversation as resolved.
Show resolved Hide resolved
// if hash components were added to the deployment, we use them to calculate
// a logical ID for the deployment resource.
if (this.hashComponents.length === 0) {
this.customLogicalId = this.originalLogicalId;
} else {
const md5 = crypto.createHash('md5');
this.hashComponents
.map(c => cdk.resolve(c))
.forEach(c => md5.update(JSON.stringify(c)));

this.customLogicalId = this.originalLogicalId + md5.digest("hex");
}

return [];
}
}
Loading