aws-cloudformation / cloudformation-coverage-roadmap

The AWS CloudFormation Public Coverage Roadmap
https://aws.amazon.com/cloudformation/
Creative Commons Attribution Share Alike 4.0 International
1.1k stars 53 forks source link

Missing AWS::CodeBuild::Fleet ScalingConfiguration attribute #1998

Open nmussy opened 2 months ago

nmussy commented 2 months ago

Name of the resource

ScalingConfiguration

Resource name

AWS::CodeBuild::Fleet

Description

ScalingConfiguration

This field is currently implemented as scalingConfiguration in the API:

Other Details

This coverage gap is going to prevent me from being able to fully implement the CDK Fleet L2 Construct, see https://github.com/aws/aws-cdk/pull/29754

rgoltz commented 1 month ago

Hi Jimmy @nmussy - There is a update on the CloudFormation definition/documentation for AWS::CodeBuild::Fleet 👍. I can already see those props in CFN:

It seems to move fast 💯 - Just scalingConfiguration is pending/missing in CFN, isn't it?

nmussy commented 1 month ago

I've updated the issue, thanks for letting me know

FarrOut commented 1 month ago

I have developed a workaround using a custom resource to call the UpdateFleet API call after Cloudformation has provisioned the Fleet. Not pretty, but it works. Like me.

CDK code

codebuild_nest.py

from aws_cdk import (
    # Duration,
    NestedStack,
    CfnTag,
    CfnOutput,
    aws_codebuild as codebuild,
    aws_ec2 as ec2,
    Lazy,
    aws_iam as iam,
    RemovalPolicy,
)
from constructs import Construct
from motley.components.CICD.codebuild_updater import CodeBuildUpdater

class CodeBuildNest(NestedStack):

    def __init__(
        self,
        scope: Construct,
        construct_id: str,
        subnet_id: str,
        removal_policy: RemovalPolicy = RemovalPolicy.RETAIN,
        vpc: ec2.IVpc = None,
        **kwargs,
    ) -> None:
        super().__init__(scope, construct_id, **kwargs)

        subnet_arn = f"arn:aws:ec2:{self.region}:{self.account}:subnet/{subnet_id}"

        role = CodeBuildSecurityNest(
            self,
            "CodeBuildSecurityNest",
            subnet_id=subnet_id,
            removal_policy=removal_policy,
        ).role

        fleet = codebuild.CfnFleet(
            self,
            "MyCfnFleet",
            base_capacity=3,
            compute_type="BUILD_GENERAL1_SMALL",
            environment_type="LINUX_CONTAINER",
            tags=[CfnTag(key="Name", value="MyLinuxFleet")],
        )
        fleet.apply_removal_policy(removal_policy)

        # To avoid "Not authorized to perform DescribeSecurityGroups"
        # https://stackoverflow.com/a/60776576
        # fleet.add_depends_on(policy.node.default_child)
        # fleet.add_depends_on(role.node.default_child)
        fleet.add_depends_on(role.node.default_child)

        fleet.add_override("Properties.FleetVpcConfig.VpcId", vpc.vpc_id)
        fleet.add_override(
            "Properties.FleetVpcConfig.Subnets",
            [subnet_id],
        )

        sg = ec2.SecurityGroup(self, "BuildFleetSecurityGroup", vpc=vpc)
        fleet.add_override(
            "Properties.FleetVpcConfig.SecurityGroupIds", [sg.security_group_id]
        )

        fleet.add_override(
            "Properties.FleetServiceRole",
            role.role_arn,
        )

        fleet.add_override("Properties.OverflowBehavior", "QUEUE")

        CfnOutput(self, "FleetId", value=fleet.ref)
        CfnOutput(self, "FleetArn", value=fleet.attr_arn)
        CfnOutput(self, "FleetName", value=str(fleet.name))

        # Update fleet to enable ScalingConfiguration
        # DEMO
        params = {
            "arn": fleet.attr_arn,
            "scalingConfiguration": {
                "desiredCapacity": 6,
                "maxCapacity": 12,
                "scalingType": "TARGET_TRACKING_SCALING",
                "targetTrackingScalingConfigs": [
                    {"metricType": "FLEET_UTILIZATION_RATE", "targetValue": 80.0}
                ],
            },
        }

        updater = CodeBuildUpdater(
            self,
            "CodeBuildUpdater",
            parameters=params,
            service_role=role,
        )
        # Ensure Updater runs AFTER resource has been created.
        updater.node.add_dependency(fleet)        

class CodeBuildSecurityNest(NestedStack):

    def __init__(
        self,
        scope: Construct,
        construct_id: str,
        subnet_id: str,
        removal_policy: RemovalPolicy = RemovalPolicy.RETAIN,
        vpc: ec2.IVpc = None,
        **kwargs,
    ) -> None:
        super().__init__(scope, construct_id, **kwargs)

        subnet_arn = f"arn:aws:ec2:{self.region}:{self.account}:subnet/{subnet_id}"
        CfnOutput(self, "subnet_arn", value=subnet_arn)

        self.role = iam.Role(
            self,
            "CodeBuildRole",
            assumed_by=iam.CompositePrincipal(
                iam.ServicePrincipal("codebuild.amazonaws.com"),
                iam.ServicePrincipal("lambda.amazonaws.com"),
            ),
        )
        self.role.apply_removal_policy(removal_policy)

        CfnOutput(self, "CodeBuildRoleArn", value=self.role.role_arn)

        policy = iam.Policy(
            self,
            "codebuild-fleet-policy",
            statements=[
                iam.PolicyStatement(
                    actions=[
                        "ec2:CreateNetworkInterface",
                        "ec2:DescribeDhcpOptions",
                        "ec2:DescribeNetworkInterfaces",
                        "ec2:DeleteNetworkInterface",
                        "ec2:DescribeSubnets",
                        "ec2:DescribeSecurityGroups",
                        "ec2:DescribeVpcs",
                    ],
                    effect=iam.Effect.ALLOW,
                    resources=["*"],
                ),
                iam.PolicyStatement(
                    actions=[
                        "ec2:CreateNetworkInterfacePermission",
                        "ec2:ModifyNetworkInterfaceAttribute",
                    ],
                    effect=iam.Effect.ALLOW,
                    resources=[
                        f"arn:aws:ec2:{self.region}:{self.account}:network-interface/*"
                    ],
                    conditions={
                        # Doesn't work for some reason
                        # "StringEquals": {
                        #     "ec2:AuthorizedService": "codebuild.amazonaws.com"
                        # },
                        # "ArnEquals": {"ec2:Subnet": [subnet_arn]},
                    },
                ),
            ],
        )
        policy.attach_to_role(self.role)

codebuild_updater.py

from aws_cdk import (
    # Duration,
    NestedStack,
    CfnTag,
    CfnOutput,
    aws_iam as iam,
    aws_codebuild as codebuild,
    aws_ec2 as ec2,
    custom_resources as cr,
    Duration,
    aws_lambda as lambda_,
    aws_logs as logs,
    Lazy,
    aws_iam as iam,
    RemovalPolicy,
)
from constructs import Construct
from datetime import datetime

class CodeBuildUpdater(Construct):

    def __init__(
        self,
        scope: Construct,
        construct_id: str,
        parameters: dict,
        service_role: iam.IRole,
        removal_policy: RemovalPolicy = RemovalPolicy.DESTROY,
        **kwargs,
    ) -> None:
        super().__init__(scope, construct_id, **kwargs)

        self.log_group = logs.LogGroup(
            self,
            "LogGroup",
            retention=logs.RetentionDays.ONE_WEEK,
            removal_policy=removal_policy,
        )
        CfnOutput(
            self,
            "LogGroupArn",
            description="The ARN of this log group.",
            value=self.log_group.log_group_arn,
        )
        CfnOutput(
            self,
            "LogGroupName",
            description="The name of this log group.",
            value=self.log_group.log_group_name,
        )

        custom_resource = cr.AwsCustomResource(
            self,
            "UpdateFleet",
            log_group=self.log_group,
            role=service_role,
            on_update=cr.AwsSdkCall(  # will also be called for a CREATE event
                service="codebuild",
                action="UpdateFleet",
                parameters=parameters,
                physical_resource_id=cr.PhysicalResourceId.of(f"{datetime.now()}"),
            ),
            policy=cr.AwsCustomResourcePolicy.from_sdk_calls(
                resources=cr.AwsCustomResourcePolicy.ANY_RESOURCE
            ),
            install_latest_aws_sdk=False,
            timeout=Duration.seconds(5),
            removal_policy=removal_policy,
        )