From a806d211bee3eb9e775032cf7b9458ccfc507c51 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Mon, 18 May 2026 13:25:01 +0000 Subject: [PATCH] Add CloudFormation benchmark options --- deploy/aws/cloudformation/template.yaml | 100 +++++++++++++++++---- deploy/aws/cloudformation/template_test.go | 57 +++++++++--- 2 files changed, 125 insertions(+), 32 deletions(-) diff --git a/deploy/aws/cloudformation/template.yaml b/deploy/aws/cloudformation/template.yaml index 03d09dda..9af7b1cd 100644 --- a/deploy/aws/cloudformation/template.yaml +++ b/deploy/aws/cloudformation/template.yaml @@ -11,12 +11,17 @@ Metadata: - SubnetId - AllowedApiCidr - ApiPort + - EnableHttpIngress + - EnableHttpsIngress + - AllowedIngressCidr - Label: default: Instance Parameters: - InstanceType - RootVolumeSize - DataVolumeSize + - DataVolumeIops + - DataVolumeThroughput - AmiSsmParameter - Label: default: Access @@ -41,6 +46,12 @@ Metadata: default: Hypeman API access CIDR ApiPort: default: Hypeman API port + EnableHttpIngress: + default: Enable HTTP ingress + EnableHttpsIngress: + default: Enable HTTPS ingress + AllowedIngressCidr: + default: Hypeman ingress access CIDR EnableSSH: default: Enable SSH AllowedSshCidr: @@ -51,6 +62,10 @@ Metadata: default: Root volume size DataVolumeSize: default: Hypeman data volume size + DataVolumeIops: + default: Hypeman data volume IOPS + DataVolumeThroughput: + default: Hypeman data volume throughput HypemanVersion: default: Hypeman release HypemanBranch: @@ -83,6 +98,21 @@ Parameters: MinValue: 1 MaxValue: 65535 Description: Hypeman API port exposed to AllowedApiCidr. + EnableHttpIngress: + Type: String + Default: "false" + AllowedValues: ["true", "false"] + Description: Open port 80 from AllowedIngressCidr for Hypeman HTTP ingress traffic. + EnableHttpsIngress: + Type: String + Default: "false" + AllowedValues: ["true", "false"] + Description: Open port 443 from AllowedIngressCidr for Hypeman HTTPS ingress traffic. + AllowedIngressCidr: + Type: String + Default: 127.0.0.1/32 + Description: Client CIDR allowed to reach Hypeman ingress ports when enabled. Use your current public IP /32 or a trusted VPN CIDR; avoid 0.0.0.0/0. + AllowedPattern: "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$" EnableSSH: Type: String Default: "false" @@ -109,6 +139,18 @@ Parameters: MinValue: 50 MaxValue: 16384 Description: Hypeman data EBS volume size in GiB. This volume is formatted as XFS and mounted at /var/lib/hypeman. + DataVolumeIops: + Type: Number + Default: 3000 + MinValue: 3000 + MaxValue: 80000 + Description: Provisioned IOPS for the Hypeman data gp3 EBS volume. High values may require a larger DataVolumeSize. + DataVolumeThroughput: + Type: Number + Default: 125 + MinValue: 125 + MaxValue: 2000 + Description: Provisioned throughput in MiB/s for the Hypeman data gp3 EBS volume. High values may require higher DataVolumeIops. HypemanVersion: Type: String Default: latest @@ -128,6 +170,8 @@ Parameters: Conditions: UseSSH: !Equals [!Ref EnableSSH, "true"] + UseHttpIngress: !Equals [!Ref EnableHttpIngress, "true"] + UseHttpsIngress: !Equals [!Ref EnableHttpsIngress, "true"] Resources: HypemanSecurityGroup: @@ -141,6 +185,22 @@ Resources: ToPort: !Ref ApiPort CidrIp: !Ref AllowedApiCidr Description: Hypeman API + - !If + - UseHttpIngress + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: !Ref AllowedIngressCidr + Description: Hypeman HTTP ingress + - !Ref AWS::NoValue + - !If + - UseHttpsIngress + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: !Ref AllowedIngressCidr + Description: Hypeman HTTPS ingress + - !Ref AWS::NoValue - !If - UseSSH - IpProtocol: tcp @@ -186,9 +246,10 @@ Resources: Roles: - !Ref HypemanInstanceRole - # CloudFormation's typed EC2 resources do not expose CpuOptions.NestedVirtualization yet. - # This helper creates only the launch template that carries that EC2 API option; - # the Hypeman EC2 instance itself remains a normal stack-managed resource. + # CloudFormation's typed EC2 instance block device mapping does not expose gp3 + # throughput, and EC2 resources do not expose CpuOptions.NestedVirtualization + # yet. This helper creates a minimal launch template for those fields; the + # Hypeman EC2 instance itself remains a normal stack-managed resource. NestedVirtualizationLaunchTemplateRole: Type: AWS::IAM::Role Properties: @@ -285,12 +346,24 @@ Resources: stack_uuid = event["StackId"].rsplit("/", 1)[-1] return f"{event['ResourceProperties']['NamePrefix']}-{stack_uuid}" - def create_launch_template(name): + def create_launch_template(name, props): payload = { "Action": "CreateLaunchTemplate", "Version": "2016-11-15", "LaunchTemplateName": name, "LaunchTemplateData.CpuOptions.NestedVirtualization": "enabled", + "LaunchTemplateData.BlockDeviceMapping.1.DeviceName": "/dev/sda1", + "LaunchTemplateData.BlockDeviceMapping.1.Ebs.VolumeSize": props["RootVolumeSize"], + "LaunchTemplateData.BlockDeviceMapping.1.Ebs.VolumeType": "gp3", + "LaunchTemplateData.BlockDeviceMapping.1.Ebs.Encrypted": "true", + "LaunchTemplateData.BlockDeviceMapping.1.Ebs.DeleteOnTermination": "true", + "LaunchTemplateData.BlockDeviceMapping.2.DeviceName": "/dev/sdf", + "LaunchTemplateData.BlockDeviceMapping.2.Ebs.VolumeSize": props["DataVolumeSize"], + "LaunchTemplateData.BlockDeviceMapping.2.Ebs.VolumeType": "gp3", + "LaunchTemplateData.BlockDeviceMapping.2.Ebs.Iops": props["DataVolumeIops"], + "LaunchTemplateData.BlockDeviceMapping.2.Ebs.Throughput": props["DataVolumeThroughput"], + "LaunchTemplateData.BlockDeviceMapping.2.Ebs.Encrypted": "true", + "LaunchTemplateData.BlockDeviceMapping.2.Ebs.DeleteOnTermination": "true", "TagSpecification.1.ResourceType": "launch-template", "TagSpecification.1.Tag.1.Key": "Name", "TagSpecification.1.Tag.1.Value": name, @@ -326,7 +399,7 @@ Resources: return if request_type == "Update": delete_launch_template(physical_id) - data = create_launch_template(launch_template_name(event)) + data = create_launch_template(launch_template_name(event), event["ResourceProperties"]) send(event, context, "SUCCESS", data, physical_id=data["LaunchTemplateId"]) except Exception as exc: traceback.print_exc() @@ -337,6 +410,10 @@ Resources: Properties: ServiceToken: !GetAtt NestedVirtualizationLaunchTemplateFunction.Arn NamePrefix: hypeman + RootVolumeSize: !Ref RootVolumeSize + DataVolumeSize: !Ref DataVolumeSize + DataVolumeIops: !Ref DataVolumeIops + DataVolumeThroughput: !Ref DataVolumeThroughput HypemanHost: Type: AWS::EC2::Instance @@ -351,19 +428,6 @@ Resources: - !Ref HypemanSecurityGroup IamInstanceProfile: !Ref HypemanInstanceProfile KeyName: !If [UseSSH, !Ref KeyName, !Ref AWS::NoValue] - BlockDeviceMappings: - - DeviceName: /dev/sda1 - Ebs: - VolumeSize: !Ref RootVolumeSize - VolumeType: gp3 - Encrypted: true - DeleteOnTermination: true - - DeviceName: /dev/sdf - Ebs: - VolumeSize: !Ref DataVolumeSize - VolumeType: gp3 - Encrypted: true - DeleteOnTermination: true Tags: - Key: Name Value: !Sub ${AWS::StackName}-hypeman diff --git a/deploy/aws/cloudformation/template_test.go b/deploy/aws/cloudformation/template_test.go index b9473bb6..af304557 100644 --- a/deploy/aws/cloudformation/template_test.go +++ b/deploy/aws/cloudformation/template_test.go @@ -16,10 +16,15 @@ func TestQuickstartParameters(t *testing.T) { assertDefault(t, parameters, "InstanceType", "c8i.2xlarge") assertDefault(t, parameters, "AllowedApiCidr", "127.0.0.1/32") assertDefault(t, parameters, "ApiPort", "8080") + assertDefault(t, parameters, "EnableHttpIngress", "false") + assertDefault(t, parameters, "EnableHttpsIngress", "false") + assertDefault(t, parameters, "AllowedIngressCidr", "127.0.0.1/32") assertDefault(t, parameters, "EnableSSH", "false") assertDefault(t, parameters, "AllowedSshCidr", "127.0.0.1/32") assertDefault(t, parameters, "RootVolumeSize", "30") assertDefault(t, parameters, "DataVolumeSize", "100") + assertDefault(t, parameters, "DataVolumeIops", "3000") + assertDefault(t, parameters, "DataVolumeThroughput", "125") assertDefault(t, parameters, "HypemanVersion", "latest") assertDefault(t, parameters, "HypemanCliVersion", "latest") @@ -31,6 +36,10 @@ func TestQuickstartParameters(t *testing.T) { assertContains(t, scalar(t, apiCidr["Description"]), "current public IP /32") assertContains(t, scalar(t, apiCidr["Description"]), "avoid 0.0.0.0/0") + ingressCidr := requireMapping(t, parameters["AllowedIngressCidr"]) + assertContains(t, scalar(t, ingressCidr["Description"]), "current public IP /32") + assertContains(t, scalar(t, ingressCidr["Description"]), "avoid 0.0.0.0/0") + metadata := requireMapping(t, requireField(t, root, "Metadata")) cfnInterface := requireMapping(t, requireField(t, metadata, "AWS::CloudFormation::Interface")) groups := requireSequence(t, requireField(t, cfnInterface, "ParameterGroups")) @@ -54,8 +63,8 @@ func TestCloudFormationLaunchContract(t *testing.T) { securityGroup := requireMapping(t, requireField(t, resources, "HypemanSecurityGroup")) sgProperties := requireMapping(t, requireField(t, securityGroup, "Properties")) ingress := requireSequence(t, requireField(t, sgProperties, "SecurityGroupIngress")) - if len(ingress.Content) != 2 { - t.Fatalf("expected API ingress and conditional SSH ingress, got %d entries", len(ingress.Content)) + if len(ingress.Content) != 4 { + t.Fatalf("expected API ingress, HTTP ingress, HTTPS ingress, and SSH ingress, got %d entries", len(ingress.Content)) } apiIngress := requireMapping(t, ingress.Content[0]) @@ -63,7 +72,10 @@ func TestCloudFormationLaunchContract(t *testing.T) { assertRef(t, requireField(t, apiIngress, "ToPort"), "ApiPort") assertRef(t, requireField(t, apiIngress, "CidrIp"), "AllowedApiCidr") - sshIngress := ingress.Content[1] + assertConditionalIngress(t, ingress.Content[1], "UseHttpIngress", "80", "AllowedIngressCidr") + assertConditionalIngress(t, ingress.Content[2], "UseHttpsIngress", "443", "AllowedIngressCidr") + + sshIngress := ingress.Content[3] if sshIngress.Tag != "!If" { t.Fatalf("expected SSH ingress to be conditional !If, got %s", sshIngress.Tag) } @@ -87,6 +99,14 @@ func TestCloudFormationLaunchContract(t *testing.T) { zipFile := scalar(t, requireField(t, code, "ZipFile")) assertContains(t, zipFile, `"Action": "CreateLaunchTemplate"`) assertContains(t, zipFile, `"LaunchTemplateData.CpuOptions.NestedVirtualization": "enabled"`) + assertContains(t, zipFile, `"LaunchTemplateData.BlockDeviceMapping.2.Ebs.Iops": props["DataVolumeIops"]`) + assertContains(t, zipFile, `"LaunchTemplateData.BlockDeviceMapping.2.Ebs.Throughput": props["DataVolumeThroughput"]`) + + launchTemplateProperties := requireMapping(t, requireField(t, launchTemplate, "Properties")) + assertRef(t, requireField(t, launchTemplateProperties, "RootVolumeSize"), "RootVolumeSize") + assertRef(t, requireField(t, launchTemplateProperties, "DataVolumeSize"), "DataVolumeSize") + assertRef(t, requireField(t, launchTemplateProperties, "DataVolumeIops"), "DataVolumeIops") + assertRef(t, requireField(t, launchTemplateProperties, "DataVolumeThroughput"), "DataVolumeThroughput") host := requireMapping(t, requireField(t, resources, "HypemanHost")) if got := scalar(t, requireField(t, host, "Type")); got != "AWS::EC2::Instance" { @@ -97,17 +117,6 @@ func TestCloudFormationLaunchContract(t *testing.T) { assertGetAtt(t, requireField(t, hostLaunchTemplate, "LaunchTemplateId"), "NestedVirtualizationLaunchTemplate.LaunchTemplateId") assertGetAtt(t, requireField(t, hostLaunchTemplate, "Version"), "NestedVirtualizationLaunchTemplate.VersionNumber") - blockDeviceMappings := requireSequence(t, requireField(t, hostProperties, "BlockDeviceMappings")) - if len(blockDeviceMappings.Content) != 2 { - t.Fatalf("expected root and Hypeman data block device mappings, got %d", len(blockDeviceMappings.Content)) - } - dataDevice := requireMapping(t, blockDeviceMappings.Content[1]) - if got := scalar(t, requireField(t, dataDevice, "DeviceName")); got != "/dev/sdf" { - t.Fatalf("data device name = %q, want /dev/sdf", got) - } - dataEBS := requireMapping(t, requireField(t, dataDevice, "Ebs")) - assertRef(t, requireField(t, dataEBS, "VolumeSize"), "DataVolumeSize") - userData := nodeText(requireField(t, hostProperties, "UserData")) assertContains(t, userData, "curl -fsSL https://raw.githubusercontent.com/kernel/hypeman/main/scripts/install.sh | bash") assertContains(t, userData, "xfsprogs") @@ -144,6 +153,26 @@ func TestQuickstartOutputs(t *testing.T) { assertContains(t, scalar(t, requireField(t, requireMapping(t, outputs["CreateTokenCommand"]), "Value")), "hypeman-create-token") } +func assertConditionalIngress(t *testing.T, node *yaml.Node, condition, port, cidrRef string) { + t.Helper() + + if node.Tag != "!If" { + t.Fatalf("expected ingress to be conditional !If, got %s", node.Tag) + } + parts := requireSequence(t, node) + if got := scalar(t, parts.Content[0]); got != condition { + t.Fatalf("expected condition %q, got %q", condition, got) + } + rule := requireMapping(t, parts.Content[1]) + if got := scalar(t, requireField(t, rule, "FromPort")); got != port { + t.Fatalf("expected FromPort %s, got %q", port, got) + } + if got := scalar(t, requireField(t, rule, "ToPort")); got != port { + t.Fatalf("expected ToPort %s, got %q", port, got) + } + assertRef(t, requireField(t, rule, "CidrIp"), cidrRef) +} + func loadTemplate(t *testing.T) *yaml.Node { t.Helper()