diff --git a/integration/combination/test_function_with_capacity_provider.py b/integration/combination/test_function_with_capacity_provider.py new file mode 100644 index 0000000000..a559b5c9d0 --- /dev/null +++ b/integration/combination/test_function_with_capacity_provider.py @@ -0,0 +1,61 @@ +from unittest.case import skipIf + +import pytest + +from integration.config.service_names import LAMBDA_MANAGED_INSTANCES +from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support + + +@skipIf( + current_region_does_not_support([LAMBDA_MANAGED_INSTANCES]), + "LambdaManagedInstance is not supported in this testing region", +) +class TestFunctionWithCapacityProvider(BaseTest): + @pytest.fixture(autouse=True) + def companion_stack_outputs(self, get_companion_stack_outputs): + self.companion_stack_outputs = get_companion_stack_outputs + + def test_function_with_capacity_provider_custom_role(self): + """Test Lambda function with CapacityProviderConfig using custom operator role.""" + # Phase 1: Prepare parameters from companion stack + parameters = [ + self.generate_parameter("SubnetId", self.companion_stack_outputs["LMISubnetId"]), + self.generate_parameter("SecurityGroup", self.companion_stack_outputs["LMISecurityGroupId"]), + self.generate_parameter("KMSKeyArn", self.companion_stack_outputs["LMIKMSKeyArn"]), + ] + + # Phase 2: Deploy and verify against expected JSON + self.create_and_verify_stack("combination/function_lmi_custom", parameters) + + # Phase 3: Verify resource counts + lambda_resources = self.get_stack_resources("AWS::Lambda::Function") + self.assertEqual(len(lambda_resources), 1, "Should create exactly one Lambda function") + + capacity_provider_resources = self.get_stack_resources("AWS::Lambda::CapacityProvider") + self.assertEqual(len(capacity_provider_resources), 1, "Should create exactly one CapacityProvider") + + iam_role_resources = self.get_stack_resources("AWS::IAM::Role") + self.assertEqual(len(iam_role_resources), 2, "Should create exactly two IAM roles") + + def test_function_with_capacity_provider_default_role(self): + """Test Lambda function with CapacityProviderConfig using default operator role.""" + # Phase 1: Prepare parameters from companion stack + parameters = [ + self.generate_parameter("SubnetId", self.companion_stack_outputs["LMISubnetId"]), + self.generate_parameter("SecurityGroup", self.companion_stack_outputs["LMISecurityGroupId"]), + self.generate_parameter("KMSKeyArn", self.companion_stack_outputs["LMIKMSKeyArn"]), + ] + + # Phase 2: Deploy and verify against expected JSON + self.create_and_verify_stack("combination/function_lmi_default", parameters) + + # Phase 3: Verify resource counts + lambda_resources = self.get_stack_resources("AWS::Lambda::Function") + self.assertEqual(len(lambda_resources), 1, "Should create exactly one Lambda function") + + capacity_provider_resources = self.get_stack_resources("AWS::Lambda::CapacityProvider") + self.assertEqual(len(capacity_provider_resources), 2, "Should create exactly two CapacityProviders") + + iam_role_resources = self.get_stack_resources("AWS::IAM::Role") + self.assertEqual(len(iam_role_resources), 3, "Should create exactly three IAM roles") diff --git a/integration/config/service_names.py b/integration/config/service_names.py index b4a2c26261..4de385e0ea 100644 --- a/integration/config/service_names.py +++ b/integration/config/service_names.py @@ -30,6 +30,7 @@ STATE_MACHINE_CWE_CWS = "StateMachineCweCws" STATE_MACHINE_WITH_APIS = "StateMachineWithApis" LAMBDA_URL = "LambdaUrl" +LAMBDA_MANAGED_INSTANCES = "LambdaManagedInstances" LAMBDA_ENV_VARS = "LambdaEnvVars" EVENT_INVOKE_CONFIG = "EventInvokeConfig" API_KEY = "ApiKey" diff --git a/integration/resources/expected/combination/function_lmi_custom.json b/integration/resources/expected/combination/function_lmi_custom.json new file mode 100644 index 0000000000..52c2c0979f --- /dev/null +++ b/integration/resources/expected/combination/function_lmi_custom.json @@ -0,0 +1,18 @@ +[ + { + "LogicalResourceId": "MyCapacityProvider", + "ResourceType": "AWS::Lambda::CapacityProvider" + }, + { + "LogicalResourceId": "MyCapacityProviderCustomRole", + "ResourceType": "AWS::IAM::Role" + }, + { + "LogicalResourceId": "MyFunction", + "ResourceType": "AWS::Lambda::Function" + }, + { + "LogicalResourceId": "MyFunctionRole", + "ResourceType": "AWS::IAM::Role" + } +] diff --git a/integration/resources/expected/combination/function_lmi_default.json b/integration/resources/expected/combination/function_lmi_default.json new file mode 100644 index 0000000000..b39c5e9422 --- /dev/null +++ b/integration/resources/expected/combination/function_lmi_default.json @@ -0,0 +1,26 @@ +[ + { + "LogicalResourceId": "SimpleCapacityProvider", + "ResourceType": "AWS::Lambda::CapacityProvider" + }, + { + "LogicalResourceId": "SimpleCapacityProviderOperatorRole", + "ResourceType": "AWS::IAM::Role" + }, + { + "LogicalResourceId": "AdvancedCapacityProvider", + "ResourceType": "AWS::Lambda::CapacityProvider" + }, + { + "LogicalResourceId": "AdvancedCapacityProviderOperatorRole", + "ResourceType": "AWS::IAM::Role" + }, + { + "LogicalResourceId": "MyFunction", + "ResourceType": "AWS::Lambda::Function" + }, + { + "LogicalResourceId": "MyFunctionRole", + "ResourceType": "AWS::IAM::Role" + } +] diff --git a/integration/resources/templates/combination/function_lmi_custom.yaml b/integration/resources/templates/combination/function_lmi_custom.yaml new file mode 100644 index 0000000000..1f72cacfc4 --- /dev/null +++ b/integration/resources/templates/combination/function_lmi_custom.yaml @@ -0,0 +1,85 @@ +Parameters: + SubnetId: + Type: String + SecurityGroup: + Type: String + KMSKeyArn: + Type: String + +Resources: + MyCapacityProviderCustomRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: CapacityProviderOperatorRolePolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - ec2:AttachNetworkInterface + - ec2:CreateTags + - ec2:RunInstances + Resource: + - !Sub arn:${AWS::Partition}:ec2:*:*:instance/* + - !Sub arn:${AWS::Partition}:ec2:*:*:network-interface/* + - !Sub arn:${AWS::Partition}:ec2:*:*:volume/* + Condition: + StringEquals: + ec2:ManagedResourceOperator: scaler.lambda.amazonaws.com + - Effect: Allow + Action: + - ec2:DescribeAvailabilityZones + - ec2:DescribeCapacityReservations + - ec2:DescribeInstances + - ec2:DescribeInstanceStatus + - ec2:DescribeInstanceTypeOfferings + - ec2:DescribeInstanceTypes + - ec2:DescribeSecurityGroups + - ec2:DescribeSubnets + Resource: '*' + - Effect: Allow + Action: + - ec2:RunInstances + - ec2:CreateNetworkInterface + Resource: + - !Sub arn:${AWS::Partition}:ec2:*:*:subnet/* + - !Sub arn:${AWS::Partition}:ec2:*:*:security-group/* + - Effect: Allow + Action: + - ec2:RunInstances + Resource: + - !Sub arn:${AWS::Partition}:ec2:*:*:image/* + Condition: + Bool: + ec2:Public: 'true' + + MyCapacityProvider: + Type: AWS::Serverless::CapacityProvider + Properties: + CapacityProviderName: !Sub "${AWS::StackName}-cp" + VpcConfig: + SubnetIds: + - !Ref SubnetId + SecurityGroupIds: + - !Ref SecurityGroup + OperatorRole: !GetAtt MyCapacityProviderCustomRole.Arn + + MyFunction: + Type: AWS::Serverless::Function + Properties: + Runtime: nodejs22.x + Handler: index.handler + CodeUri: ${codeuri} + CapacityProviderConfig: + Arn: !GetAtt MyCapacityProvider.Arn + +Metadata: + SamTransformTest: true diff --git a/integration/resources/templates/combination/function_lmi_default.yaml b/integration/resources/templates/combination/function_lmi_default.yaml new file mode 100644 index 0000000000..8137a51b21 --- /dev/null +++ b/integration/resources/templates/combination/function_lmi_default.yaml @@ -0,0 +1,52 @@ +Parameters: + SubnetId: + Type: String + SecurityGroup: + Type: String + KMSKeyArn: + Type: String + +Resources: + SimpleCapacityProvider: + Type: AWS::Serverless::CapacityProvider + Properties: + VpcConfig: + SubnetIds: + - !Ref SubnetId + SecurityGroupIds: + - !Ref SecurityGroup + + MyFunction: + Type: AWS::Serverless::Function + Properties: + Runtime: nodejs22.x + Handler: index.handler + CodeUri: ${codeuri} + CapacityProviderConfig: + Arn: !GetAtt SimpleCapacityProvider.Arn + + AdvancedCapacityProvider: + Type: AWS::Serverless::CapacityProvider + Properties: + CapacityProviderName: !Sub "${AWS::StackName}-cp" + VpcConfig: + SubnetIds: + - !Ref SubnetId + SecurityGroupIds: + - !Ref SecurityGroup + InstanceRequirements: + Architectures: + - x86_64 + AllowedTypes: + - m5.large + - m5.xlarge + - m5.2xlarge + ScalingConfig: + MaxVCpuCount: 64 + AverageCPUUtilization: 70 + KmsKeyArn: !Ref KMSKeyArn + Tags: + Environment: Test + +Metadata: + SamTransformTest: true diff --git a/integration/setup/companion-stack.yaml b/integration/setup/companion-stack.yaml index 72a1232673..9f67bd3ffd 100644 --- a/integration/setup/companion-stack.yaml +++ b/integration/setup/companion-stack.yaml @@ -41,6 +41,106 @@ Resources: Type: AWS::S3::Bucket DeletionPolicy: Delete + LMIKMSKey: + Type: AWS::KMS::Key + Properties: + Description: KMS Key for Lambda Capacity Provider Resource + KeyPolicy: + Version: '2012-10-17' + Statement: + - Sid: Enable IAM User Permissions + Effect: Allow + Principal: + AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: kms:* + Resource: '*' + - Sid: Allow Lambda service + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: + - kms:Decrypt + - kms:DescribeKey + Resource: '*' # Lambda Managed Instances (LMI) VPC Resources + + LMIVpc: + Type: AWS::EC2::VPC + Properties: + CidrBlock: 10.0.0.0/16 + EnableDnsHostnames: true + EnableDnsSupport: true + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-VPC" + + LMIPrivateSubnet: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref LMIVpc + CidrBlock: 10.0.1.0/24 + AvailabilityZone: !Select [0, !GetAZs ''] + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-PrivateSubnet" + + LMIPrivateRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref LMIVpc + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-PrivateRT" + + LMIPrivateSubnetRouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref LMIPrivateSubnet + RouteTableId: !Ref LMIPrivateRouteTable + + LMISecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for capacity provider Lambda functions + VpcId: !Ref LMIVpc + SecurityGroupEgress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 10.0.0.0/16 + Description: Allow HTTPS within VPC + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-LambdaSG" + + VPCEndpointSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for VPC endpoints + VpcId: !Ref LMIVpc + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 10.0.0.0/16 + Description: Allow HTTPS from within VPC + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-VPCEndpointSG" + + CloudWatchLogsEndpoint: + Type: AWS::EC2::VPCEndpoint + Properties: + VpcId: !Ref LMIVpc + ServiceName: !Sub 'com.amazonaws.${AWS::Region}.logs' + VpcEndpointType: Interface + PrivateDnsEnabled: true + SubnetIds: + - !Ref LMIPrivateSubnet + SecurityGroupIds: + - !Ref VPCEndpointSecurityGroup + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-CloudWatchLogsEndpoint" Outputs: PreCreatedVpc: Description: Pre-created VPC that can be used inside other tests @@ -66,5 +166,14 @@ Outputs: Description: Pre-created S3 Bucket that can be used inside other tests Value: Ref: PreCreatedS3Bucket + LMISubnetId: + Description: Private subnet ID for Lambda functions + Value: !Ref LMIPrivateSubnet + LMISecurityGroupId: + Description: Security group ID for Lambda functions + Value: !Ref LMISecurityGroup + LMIKMSKeyArn: + Description: ARN of the KMS key for Capacity Provider + Value: !GetAtt LMIKMSKey.Arn Metadata: SamTransformTest: true