diff --git a/API.md b/API.md index 509b966..749b7d0 100644 --- a/API.md +++ b/API.md @@ -213,6 +213,105 @@ List of domains created by the internal service stack and shared with the api ga --- +### InternalWebsite + +#### Initializers + +```typescript +import { InternalWebsite } from 'cdk-internal-gateway' + +new InternalWebsite(scope: Construct, id: string, props: InternalWebsiteProps) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| scope | constructs.Construct | *No description.* | +| id | string | *No description.* | +| props | InternalWebsiteProps | *No description.* | + +--- + +##### `scope`Required + +- *Type:* constructs.Construct + +--- + +##### `id`Required + +- *Type:* string + +--- + +##### `props`Required + +- *Type:* InternalWebsiteProps + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| toString | Returns a string representation of this construct. | + +--- + +##### `toString` + +```typescript +public toString(): string +``` + +Returns a string representation of this construct. + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| isConstruct | Checks if `x` is a construct. | + +--- + +##### ~~`isConstruct`~~ + +```typescript +import { InternalWebsite } from 'cdk-internal-gateway' + +InternalWebsite.isConstruct(x: any) +``` + +Checks if `x` is a construct. + +###### `x`Required + +- *Type:* any + +Any object. + +--- + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| node | constructs.Node | The tree node. | + +--- + +##### `node`Required + +```typescript +public readonly node: Node; +``` + +- *Type:* constructs.Node + +The tree node. + +--- + + ## Structs ### InternalApiGatewayProps @@ -439,5 +538,144 @@ SSLPolicy attached to the load balancer listener. --- +### InternalWebsiteProps + +Properties for InternalService. + +#### Initializer + +```typescript +import { InternalWebsiteProps } from 'cdk-internal-gateway' + +const internalWebsiteProps: InternalWebsiteProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| domains | aws-cdk-lib.aws_apigateway.IDomainName[] | List of custom domains names to be used for the API Gateway. | +| stage | string | Stage name used for all cloudformation resource names and internal aws resource names. | +| vpcEndpoint | aws-cdk-lib.aws_ec2.IInterfaceVpcEndpoint | VPC endpoint id of execute-api vpc endpoint. | +| apiBasePathMappingPath | string | Path for custom domain base path mapping that will be attached to the api gateway. | +| binaryMediaTypes | string[] | Binary media types for the internal api gateway. | +| minimumCompressionSize | number | minimum compression size for the internal api gateway. | +| sourcePath | string | Path of website folder containing the website`s sources. | +| bucketName | string | Name of s3 bucket to use for the website deployment. | +| websiteIndexDocument | string | Name of html index document used for the website. | + +--- + +##### `domains`Required + +```typescript +public readonly domains: IDomainName[]; +``` + +- *Type:* aws-cdk-lib.aws_apigateway.IDomainName[] + +List of custom domains names to be used for the API Gateway. + +--- + +##### `stage`Required + +```typescript +public readonly stage: string; +``` + +- *Type:* string + +Stage name used for all cloudformation resource names and internal aws resource names. + +--- + +##### `vpcEndpoint`Required + +```typescript +public readonly vpcEndpoint: IInterfaceVpcEndpoint; +``` + +- *Type:* aws-cdk-lib.aws_ec2.IInterfaceVpcEndpoint + +VPC endpoint id of execute-api vpc endpoint. + +This endpoint will be used to forward requests from the load balancer`s target group to the api gateway. + +--- + +##### `apiBasePathMappingPath`Optional + +```typescript +public readonly apiBasePathMappingPath: string; +``` + +- *Type:* string + +Path for custom domain base path mapping that will be attached to the api gateway. + +--- + +##### `binaryMediaTypes`Optional + +```typescript +public readonly binaryMediaTypes: string[]; +``` + +- *Type:* string[] + +Binary media types for the internal api gateway. + +--- + +##### `minimumCompressionSize`Optional + +```typescript +public readonly minimumCompressionSize: number; +``` + +- *Type:* number + +minimum compression size for the internal api gateway. + +--- + +##### `sourcePath`Required + +```typescript +public readonly sourcePath: string; +``` + +- *Type:* string + +Path of website folder containing the website`s sources. + +--- + +##### `bucketName`Optional + +```typescript +public readonly bucketName: string; +``` + +- *Type:* string + +Name of s3 bucket to use for the website deployment. + +--- + +##### `websiteIndexDocument`Optional + +```typescript +public readonly websiteIndexDocument: string; +``` + +- *Type:* string +- *Default:* index.html + +Name of html index document used for the website. + +--- + diff --git a/README.md b/README.md index 75e4cca..deebaa7 100644 --- a/README.md +++ b/README.md @@ -29,17 +29,34 @@ pip install pharindoko.cdk-internal-gateway ### Technical Details +Modularized approach with separate constructs + +- attach multiple InternalApiGateway and InternalWebsite constructs to the same Internal Service to save costs and keep flexibility + +**Internal Service Construct (mandatory construct):** + - creates an internal application loadbalancer - forwards traffic to VPC endpoint for execute-api - redirect http to https +- generates custom domains for the API Gateway +- generates certificates for the loadbalancer listener + +**Internal Api Gateway Construct:** + - provides a securely configured apigateway resource out of the box - attach your aws components to the internal apigateway resource - sets api gateway to PRIVATE mode - sets resource policies to only allow traffic from vpc endpoint -- generates and attaches custom domains to the API Gateway -- generates and attaches certificates to the the API Gateway and the loadbalancer -- modularized approach with separate constructs - - add multiple internal api gateways to the same internal service to save costs and keep flexibility +- attaches custom domains to the API Gateway +- attaches certificates to the the API Gateway and the loadbalancer + +**Internal Website Construct:** + +- makes your website internally accessible +- redeploys your website with a single cdk deploy +- provides a securely configured private s3 bucket out of box +- works with SPA applications (written with Vue, Angular) and static websites +- is an extension of the InternalApiGateway Construct ## Requirements @@ -133,7 +150,7 @@ pip install pharindoko.cdk-internal-gateway }) // create another stack that inherits from the InternalApiGateway - ... + ... ... } } @@ -152,11 +169,11 @@ pip install pharindoko.cdk-internal-gateway ## Costs -You have to expect basic infra costs for 2 components in this setup: +You have to expect basic infra costs for 2 components in this setup: | Count | Type | Estimated Costs | |---|---|---| |1 x| application load balancer | 20 $ | |2 x| network interfaces for the vpc endpoint | 16 $ | -A shared vpc can lower the costs as vpc endpoint and their network interfaces can be used together... +A shared vpc can lower the costs as vpc endpoint and their network interfaces can be used together... diff --git a/cdk-internal-gateway.drawio.png b/cdk-internal-gateway.drawio.png index 57b60d4..cb821d4 100644 Binary files a/cdk-internal-gateway.drawio.png and b/cdk-internal-gateway.drawio.png differ diff --git a/src/index.ts b/src/index.ts index 0f625b0..a494fa8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from "./internal-apigateway"; export * from "./internal-service"; +export * from "./internal-website"; diff --git a/src/internal-website.ts b/src/internal-website.ts new file mode 100644 index 0000000..8771216 --- /dev/null +++ b/src/internal-website.ts @@ -0,0 +1,143 @@ +import * as path from "path"; +import { + aws_apigateway as apigateway, + aws_iam as iam, + aws_s3 as s3, + aws_s3_deployment as s3deploy, + RemovalPolicy, +} from "aws-cdk-lib"; +import { BlockPublicAccess } from "aws-cdk-lib/aws-s3"; +import { Construct } from "constructs"; +import { + InternalApiGateway, + InternalApiGatewayProps, +} from "./internal-apigateway"; + +/** + * Properties for InternalService + */ +export interface InternalWebsiteProps extends InternalApiGatewayProps { + /** + * Path of website folder containing the website`s sources + */ + readonly sourcePath: string; + + /** + * Name of s3 bucket to use for the website deployment + */ + readonly bucketName?: string; + + /** + * Name of html index document used for the website + * + * @default index.html + */ + readonly websiteIndexDocument?: string; +} + +export class InternalWebsite extends InternalApiGateway { + constructor(scope: Construct, id: string, props: InternalWebsiteProps) { + super(scope, id, props); + + const bucket = new s3.Bucket(this, `WebsiteBucket-${id}`, { + versioned: true, + bucketName: props.bucketName || undefined, + publicReadAccess: false, + blockPublicAccess: BlockPublicAccess.BLOCK_ALL, + removalPolicy: RemovalPolicy.DESTROY, + websiteIndexDocument: props.websiteIndexDocument || "index.html", + autoDeleteObjects: true, + }); + + new s3deploy.BucketDeployment(this, `WebsiteDeployment-${id}`, { + sources: [s3deploy.Source.asset(path.normalize(props.sourcePath))], + destinationBucket: bucket, + }); + + const role = new iam.Role(this, `ApiGatewayReadOnlyRole-${id}`, { + assumedBy: new iam.ServicePrincipal("apigateway.amazonaws.com"), + }); + + role.addToPolicy( + new iam.PolicyStatement({ + resources: [`${bucket.bucketArn}`, `${bucket.bucketArn}/*`], + actions: ["s3:GetObject", "s3:GetObjectAcl", "s3:ListBucket"], + }) + ); + + const proxyIntegration = new apigateway.AwsIntegration({ + service: "s3", + integrationHttpMethod: "GET", + path: `${bucket.bucketName}/{proxy}`, + options: { + credentialsRole: role, + requestParameters: { + "integration.request.path.proxy": "method.request.path.proxy", + }, + integrationResponses: [ + { + statusCode: "200", + selectionPattern: "2..", + responseParameters: { + "method.response.header.Content-Type": + "integration.response.header.Content-Type", + }, + }, + { + statusCode: "403", + selectionPattern: "4..", + }, + ], + }, + }); + + const defaultIntegration = new apigateway.AwsIntegration({ + service: "s3", + integrationHttpMethod: "GET", + path: `${bucket.bucketName}/${ + props.websiteIndexDocument || "index.html" + }`, + options: { + credentialsRole: role, + integrationResponses: [ + { + statusCode: "200", + selectionPattern: "2..", + responseParameters: { + "method.response.header.Content-Type": + "integration.response.header.Content-Type", + }, + }, + { + statusCode: "403", + selectionPattern: "4..", + }, + ], + }, + }); + + const proxyMethodOptions = { + methodResponses: [ + { + statusCode: "200", + responseParameters: { + "method.response.header.Content-Type": true, + }, + }, + ], + requestParameters: { + "method.request.path.proxy": true, + }, + }; + + const proxyResource = this.apiGateway.root.addProxy({ + anyMethod: false, + defaultIntegration: proxyIntegration, + defaultMethodOptions: proxyMethodOptions, + }); + const defaultResource = this.apiGateway.root.resourceForPath("/"); + + defaultResource.addMethod("ANY", defaultIntegration, proxyMethodOptions); + proxyResource.addMethod("ANY", proxyIntegration, proxyMethodOptions); + } +} diff --git a/test/internal-website.test.ts b/test/internal-website.test.ts new file mode 100644 index 0000000..aaaac9e --- /dev/null +++ b/test/internal-website.test.ts @@ -0,0 +1,1117 @@ +import { + App, + aws_ec2 as ec2, + aws_route53 as route53, + Stack, +} from "aws-cdk-lib"; +import { Match, Template } from "aws-cdk-lib/assertions"; +import { InternalService, InternalWebsite } from "../src"; + +let app: App; +let stack: Stack; +let internalServiceStack: InternalService; +let vpcEndpointId: ec2.IInterfaceVpcEndpoint; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, "test", { + env: { + account: "123456789012", + region: "us-east-1", + }, + }); + + const vpc = ec2.Vpc.fromLookup(stack, "vpc", { vpcId: "vpc-1234567" }); + const internalSubnetIds = ["subnet-1234567890", "subnet-1234567890"]; + const hostedZone = route53.HostedZone.fromLookup(stack, "hostedzone", { + domainName: "test.aws1234.com", + privateZone: true, + vpcId: vpc.vpcId, + }); + + internalServiceStack = new InternalService(stack, "internalServiceStack", { + vpc: vpc, + subnetSelection: { + subnets: internalSubnetIds.map((ip, index) => + ec2.Subnet.fromSubnetId(stack, `Subnet${index}`, ip) + ), + }, + vpcEndpointIPAddresses: ["192.168.2.1", "192.168.2.2"], + subjectAlternativeNames: [ + "internalservice-dev.test.com", + "internalservice-dev.test2.com", + ], + hostedZone: hostedZone, + subDomain: "internalservice-dev", + }); + vpcEndpointId = ec2.InterfaceVpcEndpoint.fromInterfaceVpcEndpointAttributes( + stack, + "vpcEndpoint", + { + port: 443, + vpcEndpointId: "vpce-1234567890", + } + ); +}); + +test("Internal Website provider - set default values", () => { + new InternalWebsite(stack, "internalGatewayStack", { + stage: "dev", + domains: internalServiceStack.domains, + vpcEndpoint: vpcEndpointId, + sourcePath: "./test/website-sample", + }); + + const template = Template.fromStack(stack); + expect(template).toMatchInlineSnapshot(` + Object { + "Outputs": Object { + "internalGatewayStackGatewayinternalGatewayStackEndpoint4102CA04": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStack16717335", + }, + ".execute-api.us-east-1.", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStackDeploymentStagedev5F843F8F", + }, + "/", + ], + ], + }, + }, + "internalServiceStackDomainUrlinternalServiceStackB921B240": Object { + "Description": "service url", + "Export": Object { + "Name": "-DomainUrl", + }, + "Value": "https://internalservice-dev.test.aws1234.com", + }, + }, + "Parameters": Object { + "BootstrapVersion": Object { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": Object { + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": Object { + "DependsOn": Array [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-east-1", + "S3Key": "6ddcf10002539818a9256eff3fb2b22aa09298d8f946e26ba121c175a600c44e.zip", + }, + "Handler": "index.handler", + "Layers": Array [ + Object { + "Ref": "internalGatewayStackWebsiteDeploymentinternalGatewayStackAwsCliLayer57780F1C", + }, + ], + "Role": Object { + "Fn::GetAtt": Array [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", + "Arn", + ], + }, + "Runtime": "python3.9", + "Timeout": 900, + }, + "Type": "AWS::Lambda::Function", + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::cdk-hnb659fds-assets-123456789012-us-east-1", + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::cdk-hnb659fds-assets-123456789012-us-east-1/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "Roles": Array [ + Object { + "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": Object { + "DependsOn": Array [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-east-1", + "S3Key": "6babbac1f25446ab4660ead0ad5972e3a7742f50c6d8326af98a8bcd5d485335.zip", + }, + "Description": Object { + "Fn::Join": Array [ + "", + Array [ + "Lambda function for auto-deleting objects in ", + Object { + "Ref": "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09", + }, + " S3 bucket.", + ], + ], + }, + "Handler": "__entrypoint__.handler", + "MemorySize": 128, + "Role": Object { + "Fn::GetAtt": Array [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 900, + }, + "Type": "AWS::Lambda::Function", + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "internalGatewayStackApiGatewayReadOnlyRoleinternalGatewayStackCB025027": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "internalGatewayStackApiGatewayReadOnlyRoleinternalGatewayStackDefaultPolicy6443C420": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject", + "s3:GetObjectAcl", + "s3:ListBucket", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "internalGatewayStackApiGatewayReadOnlyRoleinternalGatewayStackDefaultPolicy6443C420", + "Roles": Array [ + Object { + "Ref": "internalGatewayStackApiGatewayReadOnlyRoleinternalGatewayStackCB025027", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "internalGatewayStackGatewayinternalGatewayStack16717335": Object { + "Properties": Object { + "Description": "This service serves an internal api gateway", + "EndpointConfiguration": Object { + "Types": Array [ + "PRIVATE", + ], + "VpcEndpointIds": Array [ + "vpce-1234567890", + ], + }, + "Name": "Gateway-internalGatewayStack", + "Policy": Object { + "Statement": Array [ + Object { + "Action": "execute-api:Invoke", + "Condition": Object { + "StringNotEquals": Object { + "aws:sourceVpce": "vpce-1234567890", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": "execute-api:/*/*/*", + }, + Object { + "Action": "execute-api:Invoke", + "Condition": Object { + "StringEquals": Object { + "aws:sourceVpce": "vpce-1234567890", + }, + }, + "Effect": "Allow", + "Principal": Object { + "AWS": "*", + }, + "Resource": "execute-api:/*/*/*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "internalGatewayStackGatewayinternalGatewayStackANY270EDB4A": Object { + "Properties": Object { + "AuthorizationType": "NONE", + "HttpMethod": "ANY", + "Integration": Object { + "Credentials": Object { + "Fn::GetAtt": Array [ + "internalGatewayStackApiGatewayReadOnlyRoleinternalGatewayStackCB025027", + "Arn", + ], + }, + "IntegrationHttpMethod": "GET", + "IntegrationResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": "integration.response.header.Content-Type", + }, + "SelectionPattern": "2..", + "StatusCode": "200", + }, + Object { + "SelectionPattern": "4..", + "StatusCode": "403", + }, + ], + "Type": "AWS", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:us-east-1:s3:path/", + Object { + "Ref": "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09", + }, + "/index.html", + ], + ], + }, + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "200", + }, + ], + "RequestParameters": Object { + "method.request.path.proxy": true, + }, + "ResourceId": Object { + "Fn::GetAtt": Array [ + "internalGatewayStackGatewayinternalGatewayStack16717335", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStack16717335", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "internalGatewayStackGatewayinternalGatewayStackAccountE55E37CC": Object { + "DeletionPolicy": "Retain", + "DependsOn": Array [ + "internalGatewayStackGatewayinternalGatewayStack16717335", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "internalGatewayStackGatewayinternalGatewayStackCloudWatchRoleA7C069E8", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + "UpdateReplacePolicy": "Retain", + }, + "internalGatewayStackGatewayinternalGatewayStackCloudWatchRoleA7C069E8": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + "UpdateReplacePolicy": "Retain", + }, + "internalGatewayStackGatewayinternalGatewayStackDeployment754882BEf8dca1f7e640b3bb960d81e0ff81b7e9": Object { + "DependsOn": Array [ + "internalGatewayStackGatewayinternalGatewayStackproxyANYFEAB94E3", + "internalGatewayStackGatewayinternalGatewayStackproxy9B658BE0", + "internalGatewayStackGatewayinternalGatewayStackANY270EDB4A", + ], + "Properties": Object { + "Description": "This service serves an internal api gateway", + "RestApiId": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStack16717335", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "internalGatewayStackGatewayinternalGatewayStackDeploymentStagedev5F843F8F": Object { + "DependsOn": Array [ + "internalGatewayStackGatewayinternalGatewayStackAccountE55E37CC", + ], + "Properties": Object { + "DeploymentId": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStackDeployment754882BEf8dca1f7e640b3bb960d81e0ff81b7e9", + }, + "RestApiId": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStack16717335", + }, + "StageName": "dev", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "internalGatewayStackGatewayinternalGatewayStackproxy9B658BE0": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "internalGatewayStackGatewayinternalGatewayStack16717335", + "RootResourceId", + ], + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStack16717335", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "internalGatewayStackGatewayinternalGatewayStackproxyANYFEAB94E3": Object { + "Properties": Object { + "AuthorizationType": "NONE", + "HttpMethod": "ANY", + "Integration": Object { + "Credentials": Object { + "Fn::GetAtt": Array [ + "internalGatewayStackApiGatewayReadOnlyRoleinternalGatewayStackCB025027", + "Arn", + ], + }, + "IntegrationHttpMethod": "GET", + "IntegrationResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": "integration.response.header.Content-Type", + }, + "SelectionPattern": "2..", + "StatusCode": "200", + }, + Object { + "SelectionPattern": "4..", + "StatusCode": "403", + }, + ], + "RequestParameters": Object { + "integration.request.path.proxy": "method.request.path.proxy", + }, + "Type": "AWS", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:us-east-1:s3:path/", + Object { + "Ref": "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09", + }, + "/{proxy}", + ], + ], + }, + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "200", + }, + ], + "RequestParameters": Object { + "method.request.path.proxy": true, + }, + "ResourceId": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStackproxy9B658BE0", + }, + "RestApiId": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStack16717335", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "internalGatewayStackWebsiteBucketinternalGatewayStackAutoDeleteObjectsCustomResource27685893": Object { + "DeletionPolicy": "Delete", + "DependsOn": Array [ + "internalGatewayStackWebsiteBucketinternalGatewayStackPolicy1913132B", + ], + "Properties": Object { + "BucketName": Object { + "Ref": "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09", + }, + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn", + ], + }, + }, + "Type": "Custom::S3AutoDeleteObjects", + "UpdateReplacePolicy": "Delete", + }, + "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "Tags": Array [ + Object { + "Key": "aws-cdk:auto-delete-objects", + "Value": "true", + }, + Object { + "Key": "aws-cdk:cr-owned:917df6a0", + "Value": "true", + }, + ], + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + "WebsiteConfiguration": Object { + "IndexDocument": "index.html", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + }, + "internalGatewayStackWebsiteBucketinternalGatewayStackPolicy1913132B": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::GetAtt": Array [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn", + ], + }, + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "internalGatewayStackWebsiteDeploymentinternalGatewayStackAwsCliLayer57780F1C": Object { + "Properties": Object { + "Content": Object { + "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-east-1", + "S3Key": "c409e6c5845f1f349df8cd84e160bf6f1c35d2b060b63e1f032f9bd39d4542cc.zip", + }, + "Description": "/opt/awscli/aws", + }, + "Type": "AWS::Lambda::LayerVersion", + }, + "internalGatewayStackWebsiteDeploymentinternalGatewayStackCustomResourceCE904C61": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "DestinationBucketName": Object { + "Ref": "internalGatewayStackWebsiteBucketinternalGatewayStackBF609A09", + }, + "Prune": true, + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", + "Arn", + ], + }, + "SourceBucketNames": Array [ + "cdk-hnb659fds-assets-123456789012-us-east-1", + ], + "SourceObjectKeys": Array [ + "387c754ea2daf522842e23f74232b2c455937afe2e90e7c8d173275b0706342a.zip", + ], + }, + "Type": "Custom::CDKBucketDeployment", + "UpdateReplacePolicy": "Delete", + }, + "internalGatewayStacktestinternalServiceStackApiGatewayCustomDomaininternalServiceStackCFCBFC75": Object { + "Properties": Object { + "BasePath": "", + "DomainName": Object { + "Ref": "internalServiceStackApiGatewayCustomDomaininternalServiceStack836326F5", + }, + "RestApiId": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStack16717335", + }, + "Stage": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStackDeploymentStagedev5F843F8F", + }, + }, + "Type": "AWS::ApiGateway::BasePathMapping", + }, + "internalGatewayStacktestinternalServiceStackDomaininternalservicedevtest2cominternalServiceStackB33D4C7F": Object { + "Properties": Object { + "BasePath": "", + "DomainName": Object { + "Ref": "internalServiceStackDomaininternalservicedevtest2cominternalServiceStack5EFB1D61", + }, + "RestApiId": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStack16717335", + }, + "Stage": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStackDeploymentStagedev5F843F8F", + }, + }, + "Type": "AWS::ApiGateway::BasePathMapping", + }, + "internalGatewayStacktestinternalServiceStackDomaininternalservicedevtestcominternalServiceStack590A0ACC": Object { + "Properties": Object { + "BasePath": "", + "DomainName": Object { + "Ref": "internalServiceStackDomaininternalservicedevtestcominternalServiceStackB6868874", + }, + "RestApiId": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStack16717335", + }, + "Stage": Object { + "Ref": "internalGatewayStackGatewayinternalGatewayStackDeploymentStagedev5F843F8F", + }, + }, + "Type": "AWS::ApiGateway::BasePathMapping", + }, + "internalServiceStackApiGatewayCustomDomaininternalServiceStack836326F5": Object { + "Properties": Object { + "DomainName": "internalservice-dev.test.aws1234.com", + "EndpointConfiguration": Object { + "Types": Array [ + "REGIONAL", + ], + }, + "RegionalCertificateArn": Object { + "Ref": "internalServiceStackSSLCertificateinternalServiceStack283B9A17", + }, + "SecurityPolicy": "TLS_1_2", + }, + "Type": "AWS::ApiGateway::DomainName", + }, + "internalServiceStackApplicationLoadBalancerinternalServiceStackA9484EF3": Object { + "Properties": Object { + "LoadBalancerAttributes": Array [ + Object { + "Key": "deletion_protection.enabled", + "Value": "false", + }, + ], + "Scheme": "internal", + "SecurityGroups": Array [ + Object { + "Fn::GetAtt": Array [ + "internalServiceStackLoadBalancerSecurityGroupinternalServiceStackB6066852", + "GroupId", + ], + }, + ], + "Subnets": Array [ + "subnet-1234567890", + "subnet-1234567890", + ], + "Type": "application", + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + }, + "internalServiceStackApplicationLoadBalancerinternalServiceStackListenerinternalServiceStackAC542223": Object { + "Properties": Object { + "Certificates": Array [ + Object { + "CertificateArn": Object { + "Ref": "internalServiceStackSSLCertificateinternalServiceStack283B9A17", + }, + }, + ], + "DefaultActions": Array [ + Object { + "TargetGroupArn": Object { + "Ref": "internalServiceStackTargetGroupinternalServiceStackB131C63B", + }, + "Type": "forward", + }, + ], + "LoadBalancerArn": Object { + "Ref": "internalServiceStackApplicationLoadBalancerinternalServiceStackA9484EF3", + }, + "Port": 443, + "Protocol": "HTTPS", + "SslPolicy": "ELBSecurityPolicy-FS-1-2-Res-2020-10", + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener", + }, + "internalServiceStackApplicationLoadBalancerinternalServiceStackRedirect80To4439382502D": Object { + "Properties": Object { + "DefaultActions": Array [ + Object { + "RedirectConfig": Object { + "Port": "443", + "Protocol": "HTTPS", + "StatusCode": "HTTP_301", + }, + "Type": "redirect", + }, + ], + "LoadBalancerArn": Object { + "Ref": "internalServiceStackApplicationLoadBalancerinternalServiceStackA9484EF3", + }, + "Port": 80, + "Protocol": "HTTP", + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener", + }, + "internalServiceStackDomaininternalservicedevtest2cominternalServiceStack5EFB1D61": Object { + "Properties": Object { + "DomainName": "internalservice-dev.test2.com", + "EndpointConfiguration": Object { + "Types": Array [ + "REGIONAL", + ], + }, + "RegionalCertificateArn": Object { + "Ref": "internalServiceStackSSLCertificateinternalServiceStack283B9A17", + }, + "SecurityPolicy": "TLS_1_2", + }, + "Type": "AWS::ApiGateway::DomainName", + }, + "internalServiceStackDomaininternalservicedevtestcominternalServiceStackB6868874": Object { + "Properties": Object { + "DomainName": "internalservice-dev.test.com", + "EndpointConfiguration": Object { + "Types": Array [ + "REGIONAL", + ], + }, + "RegionalCertificateArn": Object { + "Ref": "internalServiceStackSSLCertificateinternalServiceStack283B9A17", + }, + "SecurityPolicy": "TLS_1_2", + }, + "Type": "AWS::ApiGateway::DomainName", + }, + "internalServiceStackLoadBalancerSecurityGroupinternalServiceStackB6066852": Object { + "Properties": Object { + "GroupDescription": "security group for a load balancer", + "SecurityGroupEgress": Array [ + Object { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1", + }, + ], + "SecurityGroupIngress": Array [ + Object { + "CidrIp": "0.0.0.0/0", + "Description": "allow HTTPS traffic from anywhere", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443, + }, + Object { + "CidrIp": "0.0.0.0/0", + "Description": "Allow from anyone on port 80", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80, + }, + ], + "VpcId": "vpc-12345", + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "internalServiceStackRoute53RecordinternalServiceStack0A03BF43": Object { + "Properties": Object { + "AliasTarget": Object { + "DNSName": Object { + "Fn::Join": Array [ + "", + Array [ + "dualstack.", + Object { + "Fn::GetAtt": Array [ + "internalServiceStackApplicationLoadBalancerinternalServiceStackA9484EF3", + "DNSName", + ], + }, + ], + ], + }, + "HostedZoneId": Object { + "Fn::GetAtt": Array [ + "internalServiceStackApplicationLoadBalancerinternalServiceStackA9484EF3", + "CanonicalHostedZoneID", + ], + }, + }, + "HostedZoneId": "DUMMY", + "Name": "internalservice-dev.test.aws1234.com.", + "Type": "A", + }, + "Type": "AWS::Route53::RecordSet", + }, + "internalServiceStackSSLCertificateinternalServiceStack283B9A17": Object { + "Properties": Object { + "DomainName": "internalservice-dev.test.aws1234.com", + "SubjectAlternativeNames": Array [ + "internalservice-dev.test.com", + "internalservice-dev.test2.com", + ], + "Tags": Array [ + Object { + "Key": "Name", + "Value": "test/internalServiceStack/SSLCertificate-internalServiceStack", + }, + ], + "ValidationMethod": "DNS", + }, + "Type": "AWS::CertificateManager::Certificate", + }, + "internalServiceStackTargetGroupinternalServiceStackB131C63B": Object { + "Properties": Object { + "Port": 443, + "Protocol": "HTTPS", + "TargetGroupAttributes": Array [ + Object { + "Key": "stickiness.enabled", + "Value": "false", + }, + ], + "TargetType": "ip", + "Targets": Array [ + Object { + "Id": "192.168.2.1", + }, + Object { + "Id": "192.168.2.2", + }, + ], + "VpcId": "vpc-12345", + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + }, + }, + "Rules": Object { + "CheckBootstrapVersion": Object { + "Assertions": Array [ + Object { + "Assert": Object { + "Fn::Not": Array [ + Object { + "Fn::Contains": Array [ + Array [ + "1", + "2", + "3", + "4", + "5", + ], + Object { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, + } + `); +}); + +test("Api Gateway Stack provider - set optional parameters", () => { + new InternalWebsite(stack, "internalApiGatewayStackOptionalParameters", { + stage: "dev", + domains: internalServiceStack.domains, + vpcEndpoint: vpcEndpointId, + sourcePath: "./test/website-sample", + websiteIndexDocument: "test.html", + bucketName: "test-bucket", + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties( + "AWS::S3::Bucket", + Match.objectLike({ + WebsiteConfiguration: { + IndexDocument: "test.html", + }, + }) + ); + + template.hasResourceProperties( + "AWS::S3::Bucket", + Match.objectLike({ + BucketName: "test-bucket", + }) + ); + + template.hasResourceProperties( + "AWS::ApiGateway::Method", + Match.objectLike({ + Integration: { + Uri: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition", + }, + ":apigateway:us-east-1:s3:path/", + { + Ref: "internalApiGatewayStackOptionalParametersWebsiteBucketinternalApiGatewayStackOptionalParameters2269B1C9", + }, + "/test.html", + ], + ], + }, + }, + }) + ); +}); diff --git a/test/website-sample/index.html b/test/website-sample/index.html new file mode 100644 index 0000000..7d8ae2e --- /dev/null +++ b/test/website-sample/index.html @@ -0,0 +1,5 @@ + + + Hello World + +