From 53d61bbc9c96708147dc4d2e285eb8122409d700 Mon Sep 17 00:00:00 2001 From: Chris Garvis Date: Fri, 25 Aug 2023 14:13:43 -0400 Subject: [PATCH] feat(apigateway): L2 construct for Sagemaker Integration (#25459) Sagemaker integration for ApiGateway. Pulled the IEndpoint from the alpha module. Stabilizes `sagemaker.IEndpoint` from `aws-sagemaker-alpha` so that it can be used in the integration. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-sagemaker-alpha/lib/endpoint.ts | 27 ++------ packages/aws-cdk-lib/aws-apigateway/README.md | 3 +- .../aws-apigateway/lib/integrations/index.ts | 1 + .../lib/integrations/sagemaker.ts | 64 +++++++++++++++++++ .../lib/integrations/stepfunctions.ts | 2 +- .../test/integrations/sagemaker.test.ts | 56 ++++++++++++++++ .../aws-cdk-lib/aws-sagemaker/lib/endpoint.ts | 27 ++++++++ .../aws-cdk-lib/aws-sagemaker/lib/index.ts | 3 +- .../rosetta/aws_apigateway/default.ts-fixture | 17 ++--- .../aws_apigateway/stepfunctions.ts-fixture | 4 +- 10 files changed, 169 insertions(+), 35 deletions(-) create mode 100644 packages/aws-cdk-lib/aws-apigateway/lib/integrations/sagemaker.ts create mode 100644 packages/aws-cdk-lib/aws-apigateway/test/integrations/sagemaker.test.ts create mode 100644 packages/aws-cdk-lib/aws-sagemaker/lib/endpoint.ts diff --git a/packages/@aws-cdk/aws-sagemaker-alpha/lib/endpoint.ts b/packages/@aws-cdk/aws-sagemaker-alpha/lib/endpoint.ts index 3f716347e0ca7..2fcb1d79651a3 100644 --- a/packages/@aws-cdk/aws-sagemaker-alpha/lib/endpoint.ts +++ b/packages/@aws-cdk/aws-sagemaker-alpha/lib/endpoint.ts @@ -4,6 +4,7 @@ import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as cdk from 'aws-cdk-lib/core'; +import * as sagemaker from 'aws-cdk-lib/aws-sagemaker'; import { Construct } from 'constructs'; import { EndpointConfig, IEndpointConfig, InstanceProductionVariant } from './endpoint-config'; import { InstanceType } from './instance-type'; @@ -20,30 +21,12 @@ const BURSTABLE_INSTANCE_TYPE_PREFIXES = Object.entries(ec2.InstanceClass) .filter(([name, _]) => name.startsWith('T')) .map(([_, prefix]) => `ml.${prefix}.`); +// IEndpoint is stabilized so that it can be used in aws-apigateway SagemakerIntegration +// Exposing it again here so that there is no breakage to aws-sagemaker-alpha /** - * The interface for a SageMaker Endpoint resource. + * The Interface for a SageMaker Endpoint resource. */ -export interface IEndpoint extends cdk.IResource { - /** - * The ARN of the endpoint. - * - * @attribute - */ - readonly endpointArn: string; - - /** - * The name of the endpoint. - * - * @attribute - */ - readonly endpointName: string; - - /** - * Permits an IAM principal to invoke this endpoint - * @param grantee The principal to grant access to - */ - grantInvoke(grantee: iam.IGrantable): iam.Grant; -} +export interface IEndpoint extends sagemaker.IEndpoint {} /** * Represents the features common to all production variant types (e.g., instance, serverless) that diff --git a/packages/aws-cdk-lib/aws-apigateway/README.md b/packages/aws-cdk-lib/aws-apigateway/README.md index 70735b57ab344..81f4cb73318ec 100644 --- a/packages/aws-cdk-lib/aws-apigateway/README.md +++ b/packages/aws-cdk-lib/aws-apigateway/README.md @@ -257,9 +257,10 @@ 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. +- `LambdaIntegration` - can be used to invoke an AWS Lambda function. +- `SagemakerIntegration` - can be used to invoke Sagemaker Endpoints. The following example shows how to integrate the `GET /book/{book_id}` method to an AWS Lambda function: diff --git a/packages/aws-cdk-lib/aws-apigateway/lib/integrations/index.ts b/packages/aws-cdk-lib/aws-apigateway/lib/integrations/index.ts index 9ebc36bf92a58..7b8533e6857e4 100644 --- a/packages/aws-cdk-lib/aws-apigateway/lib/integrations/index.ts +++ b/packages/aws-cdk-lib/aws-apigateway/lib/integrations/index.ts @@ -3,4 +3,5 @@ export * from './lambda'; export * from './http'; export * from './mock'; export * from './stepfunctions'; +export * from './sagemaker'; export * from './request-context'; diff --git a/packages/aws-cdk-lib/aws-apigateway/lib/integrations/sagemaker.ts b/packages/aws-cdk-lib/aws-apigateway/lib/integrations/sagemaker.ts new file mode 100644 index 0000000000000..a42daa2efaca0 --- /dev/null +++ b/packages/aws-cdk-lib/aws-apigateway/lib/integrations/sagemaker.ts @@ -0,0 +1,64 @@ +import { AwsIntegration } from './aws'; +import * as iam from '../../../aws-iam'; +import { IEndpoint } from '../../../aws-sagemaker'; +import { IntegrationConfig, IntegrationOptions } from '../integration'; +import { Method } from '../method'; + +/** + * Options for SageMakerIntegration + */ +export interface SagemakerIntegrationOptions extends IntegrationOptions { +} + +/** + * Integrates an AWS Sagemaker Endpoint to an API Gateway method + * + * @example + * + * declare const resource: apigateway.Resource; + * declare const endpoint: sagemaker.IEndpoint; + * resource.addMethod('POST', new apigateway.SagemakerIntegration(endpoint)); + * + */ +export class SagemakerIntegration extends AwsIntegration { + private readonly endpoint: IEndpoint; + + constructor(endpoint: IEndpoint, options: SagemakerIntegrationOptions = {}) { + super({ + service: 'runtime.sagemaker', + path: `endpoints/${endpoint.endpointName}/invocations`, + options: { + credentialsRole: options.credentialsRole, + integrationResponses: [ + { statusCode: '200' }, + ], + ...options, + }, + }); + + this.endpoint = endpoint; + } + + public bind(method: Method): IntegrationConfig { + const bindResult = super.bind(method); + + const credentialsRole = bindResult.options?.credentialsRole ?? new iam.Role(method, 'SagemakerRole', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + description: 'Generated by CDK::ApiGateway::SagemakerIntegration', + }); + + this.endpoint.grantInvoke(credentialsRole); + + method.addMethodResponse({ + statusCode: '200', + }); + + return { + ...bindResult, + options: { + ...bindResult.options, + credentialsRole, + }, + }; + } +} diff --git a/packages/aws-cdk-lib/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/aws-cdk-lib/aws-apigateway/lib/integrations/stepfunctions.ts index 252322158d2b3..c843b795ad97a 100644 --- a/packages/aws-cdk-lib/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/aws-cdk-lib/aws-apigateway/lib/integrations/stepfunctions.ts @@ -8,11 +8,11 @@ import { Token } from '../../../core'; import { IntegrationConfig, IntegrationOptions, PassthroughBehavior } from '../integration'; import { Method } from '../method'; import { Model } from '../model'; + /** * Options when configuring Step Functions synchronous integration with Rest API */ export interface StepFunctionsExecutionIntegrationOptions extends IntegrationOptions { - /** * Which details of the incoming request must be passed onto the underlying state machine, * such as, account id, user identity, request id, etc. The execution input will include a new key `requestContext`: diff --git a/packages/aws-cdk-lib/aws-apigateway/test/integrations/sagemaker.test.ts b/packages/aws-cdk-lib/aws-apigateway/test/integrations/sagemaker.test.ts new file mode 100644 index 0000000000000..14e53f89d974f --- /dev/null +++ b/packages/aws-cdk-lib/aws-apigateway/test/integrations/sagemaker.test.ts @@ -0,0 +1,56 @@ +import { Template } from '../../../assertions'; +import * as iam from '../../../aws-iam'; +import * as sagemaker from '../../../aws-sagemaker'; +import * as cdk from '../../../core'; +import * as apigateway from '../../lib'; + +describe('SageMaker Integration', () => { + test('minimal setup', () => { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'my-api'); + const endpoint = new FakeEndpoint(stack, 'FakeEndpoint'); + + // WHEN + const integration = new apigateway.SagemakerIntegration(endpoint); + api.root.addMethod('POST', integration); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { + Integration: { + IntegrationHttpMethod: 'POST', + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':runtime.sagemaker:path/endpoints/endpointName/invocations', + ], + ], + }, + }, + }); + }); +}); + +class FakeEndpoint extends cdk.Resource implements sagemaker.IEndpoint { + public readonly endpointArn = 'endpointArn'; + + public readonly endpointName = 'endpointName'; + + public grantInvoke(grantee: iam.IGrantable) { + return iam.Grant.addToPrincipal({ + grantee, + actions: ['sagemaker:InvokeEndpoint'], + resourceArns: [this.endpointArn], + }); + } +} diff --git a/packages/aws-cdk-lib/aws-sagemaker/lib/endpoint.ts b/packages/aws-cdk-lib/aws-sagemaker/lib/endpoint.ts new file mode 100644 index 0000000000000..a336427caf629 --- /dev/null +++ b/packages/aws-cdk-lib/aws-sagemaker/lib/endpoint.ts @@ -0,0 +1,27 @@ +import * as iam from '../../aws-iam'; +import { IResource } from '../../core'; + +/** + * The interface for a SageMaker Endpoint resource. + */ +export interface IEndpoint extends IResource { + /** + * The ARN of the endpoint. + * + * @attribute + */ + readonly endpointArn: string; + + /** + * The name of the endpoint. + * + * @attribute + */ + readonly endpointName: string; + + /** + * Permits an IAM principal to invoke this endpoint + * @param grantee The principal to grant access to + */ + grantInvoke(grantee: iam.IGrantable): iam.Grant; +} diff --git a/packages/aws-cdk-lib/aws-sagemaker/lib/index.ts b/packages/aws-cdk-lib/aws-sagemaker/lib/index.ts index f1fbeac96cc60..d01b5737ce0ee 100644 --- a/packages/aws-cdk-lib/aws-sagemaker/lib/index.ts +++ b/packages/aws-cdk-lib/aws-sagemaker/lib/index.ts @@ -1 +1,2 @@ -export * from './sagemaker.generated'; \ No newline at end of file +export * from './sagemaker.generated'; +export * from './endpoint'; diff --git a/packages/aws-cdk-lib/rosetta/aws_apigateway/default.ts-fixture b/packages/aws-cdk-lib/rosetta/aws_apigateway/default.ts-fixture index 03215bb08180f..c5c06c6bb459e 100644 --- a/packages/aws-cdk-lib/rosetta/aws_apigateway/default.ts-fixture +++ b/packages/aws-cdk-lib/rosetta/aws_apigateway/default.ts-fixture @@ -1,14 +1,15 @@ // Fixture with packages imported, but nothing else import { Construct } from 'constructs'; import { Duration, Stack } from 'aws-cdk-lib'; -import apigateway = require('aws-cdk-lib/aws-apigateway'); -import cognito = require('aws-cdk-lib/aws-cognito'); -import lambda = require('aws-cdk-lib/aws-lambda'); -import iam = require('aws-cdk-lib/aws-iam'); -import s3 = require('aws-cdk-lib/aws-s3'); -import ec2 = require('aws-cdk-lib/aws-ec2'); -import logs = require('aws-cdk-lib/aws-logs'); -import stepfunctions = require('aws-cdk-lib/aws-stepfunctions'); +import * as apigateway from 'aws-cdk-lib/aws-apigateway'; +import * as cognito from 'aws-cdk-lib/aws-cognito'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import * as stepfunctions from 'aws-cdk-lib/aws-stepfunctions'; +import * as sagemaker from 'aws-cdk-lib/aws-sagemaker'; class Fixture extends Stack { constructor(scope: Construct, id: string) { diff --git a/packages/aws-cdk-lib/rosetta/aws_apigateway/stepfunctions.ts-fixture b/packages/aws-cdk-lib/rosetta/aws_apigateway/stepfunctions.ts-fixture index a9db92f540f96..c989039e643a5 100644 --- a/packages/aws-cdk-lib/rosetta/aws_apigateway/stepfunctions.ts-fixture +++ b/packages/aws-cdk-lib/rosetta/aws_apigateway/stepfunctions.ts-fixture @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { Stack } from 'aws-cdk-lib'; -import apigateway = require('aws-cdk-lib/aws-apigateway'); -import stepfunctions = require('aws-cdk-lib/aws-stepfunctions'); +import * as apigateway from 'aws-cdk-lib/aws-apigateway'; +import * as stepfunctions from 'aws-cdk-lib/aws-stepfunctions'; class Fixture extends Stack { constructor(scope: Construct, id: string) {