From 75186b068831112d3addf37b27c572fa50024723 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Wed, 21 Dec 2022 19:11:02 +0100 Subject: [PATCH] Add Redis cache --- src/cache/Redis.ts | 47 ++ src/index.ts | 4 + test/cache/Redis.test.ts | 15 + test/cache/__snapshots__/Redis.test.ts.snap | 678 ++++++++++++++++++++ 4 files changed, 744 insertions(+) create mode 100644 src/cache/Redis.ts create mode 100644 test/cache/Redis.test.ts create mode 100644 test/cache/__snapshots__/Redis.test.ts.snap diff --git a/src/cache/Redis.ts b/src/cache/Redis.ts new file mode 100644 index 0000000..2053b91 --- /dev/null +++ b/src/cache/Redis.ts @@ -0,0 +1,47 @@ +import { CfnCacheCluster, CfnCacheClusterProps, CfnSubnetGroup } from 'aws-cdk-lib/aws-elasticache'; +import { Names } from 'aws-cdk-lib'; +import { Port, SecurityGroup } from 'aws-cdk-lib/aws-ec2'; +import { Construct } from 'constructs'; +import { VpcForServerlessApp } from '../vpc/VpcForServerlessApp'; + +export type RedisProps = Partial & { + vpc: VpcForServerlessApp; +}; + +export class Redis extends CfnCacheCluster { + constructor(scope: Construct, id: string, props: RedisProps) { + const securityGroup = new SecurityGroup(scope, `${id}SecurityGroup`, { + vpc: props.vpc, + description: 'Security group for Redis', + allowAllOutbound: false, + allowAllIpv6Outbound: false, + }); + + const stackId = Names.uniqueResourceName(securityGroup, { + maxLength: 100, + }); + const subnetGroup = new CfnSubnetGroup(scope, `${id}SubnetGroup`, { + cacheSubnetGroupName: `${stackId}${id}SubnetGroup`, + description: 'Subnet group for Redis', + // Isolated subnets don't have a route to the internet (unlike private), this is what we want + subnetIds: props.vpc.isolatedSubnets.map((subnet) => subnet.subnetId), + }); + + props.vpc.appSecurityGroup.connections.allowTo( + securityGroup, + Port.tcp(props.port ?? 6379), + 'Allow Lambda functions to connect to Redis' + ); + + super(scope, id, { + engine: 'redis', + cacheNodeType: 'cache.t3.micro', + numCacheNodes: 1, + vpcSecurityGroupIds: [securityGroup.securityGroupId], + cacheSubnetGroupName: subnetGroup.cacheSubnetGroupName, + ...props, + }); + + this.addDependsOn(subnetGroup); + } +} diff --git a/src/index.ts b/src/index.ts index cc21191..92814f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,9 @@ +import { Redis } from './cache/Redis'; import { ConsoleFunction } from './function/ConsoleFunction'; import { PhpFpmFunction, PhpFpmFunctionProps } from './function/PhpFpmFunction'; import { PhpFunction, PhpFunctionProps } from './function/PhpFunction'; import { packagePhpCode } from './package'; +import { VpcForServerlessApp } from './vpc/VpcForServerlessApp'; export { PhpFunction, @@ -10,4 +12,6 @@ export { PhpFpmFunctionProps, ConsoleFunction, packagePhpCode, + VpcForServerlessApp, + Redis, }; diff --git a/test/cache/Redis.test.ts b/test/cache/Redis.test.ts new file mode 100644 index 0000000..97eec9b --- /dev/null +++ b/test/cache/Redis.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; +import { cleanupTemplate, compileTestStack } from '../helper'; +import { Redis, VpcForServerlessApp } from '../../src'; + +describe('Redis', () => { + it('builds', () => { + const template = compileTestStack((stack) => { + new Redis(stack, 'Redis', { + vpc: new VpcForServerlessApp(stack, 'Vpc'), + }); + }).toJSON(); + + expect(cleanupTemplate(template).Resources).toMatchSnapshot(); + }); +}); diff --git a/test/cache/__snapshots__/Redis.test.ts.snap b/test/cache/__snapshots__/Redis.test.ts.snap new file mode 100644 index 0000000..2d9be76 --- /dev/null +++ b/test/cache/__snapshots__/Redis.test.ts.snap @@ -0,0 +1,678 @@ +// Vitest Snapshot v1 + +exports[`Redis > builds 1`] = ` +{ + "Redis": { + "DependsOn": [ + "RedisSubnetGroup", + ], + "Properties": { + "CacheNodeType": "cache.t3.micro", + "CacheSubnetGroupName": "appRedisSecurityGroupC4E36EDARedisSubnetGroup", + "Engine": "redis", + "NumCacheNodes": 1, + "VpcSecurityGroupIds": [ + { + "Fn::GetAtt": [ + "RedisSecurityGroupB05951F6", + "GroupId", + ], + }, + ], + }, + "Type": "AWS::ElastiCache::CacheCluster", + }, + "RedisSecurityGroupB05951F6": { + "Properties": { + "GroupDescription": "Security group for Redis", + "SecurityGroupEgress": [ + { + "CidrIp": "255.255.255.255/32", + "Description": "Disallow all traffic", + "FromPort": 252, + "IpProtocol": "icmp", + "ToPort": 86, + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "RedisSecurityGroupfromappVpcAppSecurityGroup561DA17763795CDFD72F": { + "Properties": { + "Description": "Allow Lambda functions to connect to Redis", + "FromPort": 6379, + "GroupId": { + "Fn::GetAtt": [ + "RedisSecurityGroupB05951F6", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "VpcAppSecurityGroup3F3CF9D8", + "GroupId", + ], + }, + "ToPort": 6379, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, + "RedisSubnetGroup": { + "Properties": { + "CacheSubnetGroupName": "appRedisSecurityGroupC4E36EDARedisSubnetGroup", + "Description": "Subnet group for Redis", + "SubnetIds": [ + { + "Ref": "VpcIsolatedSubnet1SubnetE48C5737", + }, + ], + }, + "Type": "AWS::ElastiCache::SubnetGroup", + }, + "Vpc8378EB38": { + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc", + }, + ], + }, + "Type": "AWS::EC2::VPC", + }, + "VpcAppSecurityGroup3F3CF9D8": { + "Properties": { + "GroupDescription": "Security group for Lambda functions", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1", + }, + { + "CidrIpv6": "::/0", + "Description": "Allow all outbound ipv6 traffic by default", + "IpProtocol": "-1", + }, + ], + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "VpcAppSubnet1DefaultRoute7574356B": { + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA", + }, + "RouteTableId": { + "Ref": "VpcAppSubnet1RouteTable30EDBD03", + }, + }, + "Type": "AWS::EC2::Route", + }, + "VpcAppSubnet1RouteTable30EDBD03": { + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc/AppSubnet1", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::RouteTable", + }, + "VpcAppSubnet1RouteTableAssociation95F593A9": { + "Properties": { + "RouteTableId": { + "Ref": "VpcAppSubnet1RouteTable30EDBD03", + }, + "SubnetId": { + "Ref": "VpcAppSubnet1Subnet7D2CF347", + }, + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation", + }, + "VpcAppSubnet1Subnet7D2CF347": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "", + }, + ], + }, + "CidrBlock": "10.0.1.0/24", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "App", + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private", + }, + { + "Key": "Name", + "Value": "app/Vpc/AppSubnet1", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::Subnet", + }, + "VpcIGWD7BA715C": { + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc", + }, + ], + }, + "Type": "AWS::EC2::InternetGateway", + }, + "VpcIsolatedSubnet1RouteTable4771E3E5": { + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc/IsolatedSubnet1", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::RouteTable", + }, + "VpcIsolatedSubnet1RouteTableAssociationD300FCBB": { + "Properties": { + "RouteTableId": { + "Ref": "VpcIsolatedSubnet1RouteTable4771E3E5", + }, + "SubnetId": { + "Ref": "VpcIsolatedSubnet1SubnetE48C5737", + }, + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation", + }, + "VpcIsolatedSubnet1SubnetE48C5737": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "", + }, + ], + }, + "CidrBlock": "10.0.2.0/28", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Isolated", + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated", + }, + { + "Key": "Name", + "Value": "app/Vpc/IsolatedSubnet1", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::Subnet", + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "DependsOn": [ + "VpcVPCGWBF912B6E", + ], + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C", + }, + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E", + }, + }, + "Type": "AWS::EC2::Route", + }, + "VpcPublicSubnet1EIPD7E02669": { + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc/PublicSubnet1", + }, + ], + }, + "Type": "AWS::EC2::EIP", + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "DependsOn": [ + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1RouteTableAssociation97140677", + ], + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId", + ], + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4", + }, + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc/PublicSubnet1", + }, + ], + }, + "Type": "AWS::EC2::NatGateway", + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc/PublicSubnet1", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::RouteTable", + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E", + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4", + }, + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation", + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "", + }, + ], + }, + "CidrBlock": "10.0.0.0/24", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public", + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public", + }, + { + "Key": "Name", + "Value": "app/Vpc/PublicSubnet1", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::Subnet", + }, + "VpcVPCGWBF912B6E": { + "Properties": { + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C", + }, + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::VPCGatewayAttachment", + }, +} +`; + +exports[`VpcForServerlessApp > builds 1`] = ` +{ + "Vpc8378EB38": { + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc", + }, + ], + }, + "Type": "AWS::EC2::VPC", + }, + "VpcAppSecurityGroup3F3CF9D8": { + "Properties": { + "GroupDescription": "Security group for Lambda functions", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1", + }, + { + "CidrIpv6": "::/0", + "Description": "Allow all outbound ipv6 traffic by default", + "IpProtocol": "-1", + }, + ], + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "VpcAppSubnet1DefaultRoute7574356B": { + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA", + }, + "RouteTableId": { + "Ref": "VpcAppSubnet1RouteTable30EDBD03", + }, + }, + "Type": "AWS::EC2::Route", + }, + "VpcAppSubnet1RouteTable30EDBD03": { + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc/AppSubnet1", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::RouteTable", + }, + "VpcAppSubnet1RouteTableAssociation95F593A9": { + "Properties": { + "RouteTableId": { + "Ref": "VpcAppSubnet1RouteTable30EDBD03", + }, + "SubnetId": { + "Ref": "VpcAppSubnet1Subnet7D2CF347", + }, + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation", + }, + "VpcAppSubnet1Subnet7D2CF347": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "", + }, + ], + }, + "CidrBlock": "10.0.1.0/24", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "App", + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private", + }, + { + "Key": "Name", + "Value": "app/Vpc/AppSubnet1", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::Subnet", + }, + "VpcIGWD7BA715C": { + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc", + }, + ], + }, + "Type": "AWS::EC2::InternetGateway", + }, + "VpcIsolatedSubnet1RouteTable4771E3E5": { + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc/IsolatedSubnet1", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::RouteTable", + }, + "VpcIsolatedSubnet1RouteTableAssociationD300FCBB": { + "Properties": { + "RouteTableId": { + "Ref": "VpcIsolatedSubnet1RouteTable4771E3E5", + }, + "SubnetId": { + "Ref": "VpcIsolatedSubnet1SubnetE48C5737", + }, + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation", + }, + "VpcIsolatedSubnet1SubnetE48C5737": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "", + }, + ], + }, + "CidrBlock": "10.0.2.0/28", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Isolated", + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated", + }, + { + "Key": "Name", + "Value": "app/Vpc/IsolatedSubnet1", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::Subnet", + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "DependsOn": [ + "VpcVPCGWBF912B6E", + ], + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C", + }, + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E", + }, + }, + "Type": "AWS::EC2::Route", + }, + "VpcPublicSubnet1EIPD7E02669": { + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc/PublicSubnet1", + }, + ], + }, + "Type": "AWS::EC2::EIP", + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "DependsOn": [ + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1RouteTableAssociation97140677", + ], + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId", + ], + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4", + }, + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc/PublicSubnet1", + }, + ], + }, + "Type": "AWS::EC2::NatGateway", + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "app/Vpc/PublicSubnet1", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::RouteTable", + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E", + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4", + }, + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation", + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "", + }, + ], + }, + "CidrBlock": "10.0.0.0/24", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public", + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public", + }, + { + "Key": "Name", + "Value": "app/Vpc/PublicSubnet1", + }, + ], + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::Subnet", + }, + "VpcVPCGWBF912B6E": { + "Properties": { + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C", + }, + "VpcId": { + "Ref": "Vpc8378EB38", + }, + }, + "Type": "AWS::EC2::VPCGatewayAttachment", + }, +} +`;