Example #1
0
def wait_for_fargate_cluster_status(
    cluster_name: str,
    cluster_stats: ClusterStatus,
    timeout_seconds: int = CLUSTER_PROVISION_TIMEOUT,
) -> None:
    """
    Waits for a cluster to to reach a desired status by polling the current
    state of the cluster
    """
    cluster_provisioned = False
    wait_time = 0

    log_info(f"Waiting for cluster to reach {cluster_stats.value} state...")
    while not cluster_provisioned and wait_time < timeout_seconds:
        cluster_info = describe_fargate_cluster(cluster_name)

        if len(cluster_info["failures"]) > 0:
            break

        cluster_provisioned = all([
            c["status"] == cluster_stats.value
            for c in cluster_info["clusters"]
        ], )

        sleep(2)
        wait_time += 2

    if not cluster_provisioned:
        log_error("Cluster failed to provision")
        raise Abort()
Example #2
0
def wait_for_tasks_to_start(
    cluster_name: str,
    tasks: List[TaskTypeDef],
    timeout_seconds: int = TASK_BOOT_TIMEOUT,
) -> None:
    """
    Waits for all of the tasks to reach their desired state by polling
    the current state of the tasks
    """
    task_arns = [t["taskArn"] for t in tasks]

    tasks_started = False
    wait_time = 0

    log_info("Waiting for bastion task to start...")
    while not tasks_started and wait_time < timeout_seconds:
        task_info = describe_task(cluster_name, task_arns)

        if not task_info or len(task_info["failures"]) > 0:
            break

        tasks_started = all([
            t["lastStatus"] == t["desiredStatus"] for t in task_info["tasks"]
        ], )

        sleep(2)
        wait_time += 2

    if not tasks_started:
        log_error("Bastion task failed to start")
        raise Abort()
Example #3
0
def delete_task_definition() -> None:
    """
    Inactivates all serverless-aws-bastion task definitions
    """
    client: ECSClient = fetch_boto3_client("ecs")
    task_definitions = client.list_task_definitions(familyPrefix=DEFAULT_NAME)

    log_info("Deregistering task definitions")
    for td in task_definitions["taskDefinitionArns"]:
        client.deregister_task_definition(taskDefinition=td)
Example #4
0
def delete_role(role_name: str) -> None:
    """
    Safely deletes a given role by first detaching any policies
    and then deleting the role and handling any exceptions
    """
    client: IAMClient = fetch_boto3_client("iam")

    try:
        log_info(f"Deleting {role_name} role")
        detach_policies_from_role(role_name)
        client.delete_role(RoleName=role_name)
    except client.exceptions.NoSuchEntityException:
        return None
Example #5
0
def create_fargate_cluster(cluster_name: str) -> CreateClusterResponseTypeDef:
    """
    Creates a Fargate cluster to launch the bastion task into
    """
    client: ECSClient = fetch_boto3_client("ecs")

    log_info("Creating Fargate cluster")
    response = client.create_cluster(
        clusterName=cluster_name,
        capacityProviders=["FARGATE"],
        tags=build_tags("ecs"),
    )
    wait_for_fargate_cluster_status(cluster_name, ClusterStatus.ACTIVE)
    return response
Example #6
0
def delete_deregister_ssm_policy() -> None:
    """
    Deletes the IAM policy that allows the bastion ECS task to
    deregister itself from SSM.
    """
    client: IAMClient = fetch_boto3_client("iam")

    try:
        log_info(f"Deleting {SSM_DEREGISTER_POLICY_NAME} policy")
        account_id = load_aws_account_id()
        client.delete_policy(
            PolicyArn=
            f"arn:aws:iam::{account_id}:policy/{SSM_DEREGISTER_POLICY_NAME}", )
    except client.exceptions.NoSuchEntityException:
        return None
Example #7
0
def delete_fargate_cluster(cluster_name: str) -> None:
    """
    Deletes a given Fargate cluster
    """
    client: ECSClient = fetch_boto3_client("ecs")

    log_info("Deleting Fargate cluster")

    try:
        client.delete_cluster(cluster=cluster_name)
    except client.exceptions.ClusterNotFoundException:
        log_error(f"Failed to find {cluster_name} Fargate cluster")
        raise Abort()

    wait_for_fargate_cluster_status(cluster_name, ClusterStatus.INACTIVE)
Example #8
0
def create_bastion_task_role() -> str:
    """
    Creates the role that will be used by the bastion ECS task.
    Skips creation if the role already exists.

    Returns role arn
    """
    client: IAMClient = fetch_boto3_client("iam")

    current_role_arn = fetch_role_arn(TASK_ROLE_NAME)
    if current_role_arn:
        return current_role_arn

    log_info(f"Creating {TASK_ROLE_NAME} role")
    response = client.create_role(
        RoleName=TASK_ROLE_NAME,
        Description="Used by serverless-aws-bastion ECS tasks",
        AssumeRolePolicyDocument=json.dumps(
            {
                "Version":
                "2012-10-17",
                "Statement": [
                    {
                        "Sid": "",
                        "Effect": "Allow",
                        "Principal": {
                            "Service":
                            ["ecs-tasks.amazonaws.com", "ssm.amazonaws.com"],
                        },
                        "Action": "sts:AssumeRole",
                    },
                ],
            }, ),
        Tags=build_tags("iam"),
    )

    deregister_ssm_arn = create_deregister_ssm_policy()
    attach_policies_to_role(
        TASK_ROLE_NAME,
        [
            deregister_ssm_arn,
            "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
        ],
    )

    return response["Role"]["Arn"]
Example #9
0
def create_bastion_task_execution_role() -> str:
    """
    Creates the role that will be used by ECS to launch the bastion ECS task.
    Skips creation if the role already exists.

    Returns role arn
    """
    client: IAMClient = fetch_boto3_client("iam")

    current_role_arn = fetch_role_arn(TASK_EXECUTION_ROLE_NAME)
    if current_role_arn:
        return current_role_arn

    log_info(f"Creating {TASK_EXECUTION_ROLE_NAME} role")
    response = client.create_role(
        RoleName=TASK_EXECUTION_ROLE_NAME,
        Description=
        "Used by Fargate to launch serverless-aws-bastion ECS tasks",
        AssumeRolePolicyDocument=json.dumps(
            {
                "Version":
                "2012-10-17",
                "Statement": [
                    {
                        "Sid": "",
                        "Effect": "Allow",
                        "Principal": {
                            "Service": ["ecs-tasks.amazonaws.com"]
                        },
                        "Action": "sts:AssumeRole",
                    },
                ],
            }, ),
        Tags=build_tags("iam"),
    )
    attach_policies_to_role(
        TASK_EXECUTION_ROLE_NAME,
        [
            "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
        ],
    )

    return response["Role"]["Arn"]
Example #10
0
def create_task_definition(task_role_arn: str,
                           execution_role_arn: str) -> None:
    """
    Creates the task definition that will be used to launch the
    serverless bastion container
    """
    client: ECSClient = fetch_boto3_client("ecs")

    log_info("Creating bastion ECS task")
    client.register_task_definition(
        family=DEFAULT_NAME,
        networkMode="awsvpc",
        cpu=TASK_CPU,
        memory=TASK_MEMORY,
        taskRoleArn=task_role_arn,
        executionRoleArn=execution_role_arn,
        containerDefinitions=[
            {
                "image":
                f"nplutt/{DEFAULT_NAME}",
                "name":
                DEFAULT_NAME,
                "essential":
                True,
                "portMappings": [
                    {
                        "hostPort": 22,
                        "protocol": "tcp",
                        "containerPort": 22,
                    },
                ],
                "logConfiguration": {
                    "logDriver": "awslogs",
                    "options": {
                        "awslogs-group": "/ecs/ssh-bastion",
                        "awslogs-region": load_aws_region_name(),
                        "awslogs-stream-prefix": "ecs",
                    },
                },
            },
        ],
        tags=build_tags("ecs"),
    )
Example #11
0
def create_deregister_ssm_policy() -> str:
    """
    Creates an IAM policy that allows the bastion ECS task to
    deregister itself from SSM.

    Returns the policy arn
    """
    client: IAMClient = fetch_boto3_client("iam")
    try:
        log_info(f"Creating {SSM_DEREGISTER_POLICY_NAME} policy")
        response = client.create_policy(
            Description="Used by serverless-aws-bastion ECS task to "
            "deregister itself from SSM",
            PolicyName=SSM_DEREGISTER_POLICY_NAME,
            PolicyDocument=json.dumps(
                {
                    "Version":
                    "2012-10-17",
                    "Statement": [
                        {
                            "Action": [
                                "ssm:DeregisterManagedInstance",
                                "ssm:DescribeInstanceInformation",
                            ],
                            "Effect":
                            "Allow",
                            "Resource":
                            "*",
                        },
                    ],
                }, ),
        )
        return response["Policy"]["Arn"]
    except client.exceptions.EntityAlreadyExistsException:
        account_id = load_aws_account_id()
        return f"arn:aws:iam::{account_id}:policy/{SSM_DEREGISTER_POLICY_NAME}"
Example #12
0
def stop_fargate_tasks(cluster: str, tasks: List[TaskTypeDef]) -> None:
    client: ECSClient = fetch_boto3_client("ecs")

    log_info(f"Stopping {len(tasks)} tasks...")
    for t in tasks:
        client.stop_task(cluster=cluster, task=t["taskArn"])
Example #13
0
def launch_fargate_task(
    cluster_name: str,
    subnet_ids: str,
    security_group_ids: str,
    authorized_keys: str,
    instance_name: str,
    timeout_minutes: int,
    bastion_type: BastionType,
) -> RunTaskResponseTypeDef:
    """
    Launches the ssh bastion Fargate task into the proper subnets & security groups,
    also sends in the authorized keys.
    """
    client: ECSClient = fetch_boto3_client("ecs")

    bastion_id = str(uuid4())

    activation: Dict[str, str] = {}
    if bastion_type == BastionType.ssm:
        activation = create_activation(TASK_ROLE_NAME, instance_name,
                                       bastion_id)  # type: ignore

    log_info("Starting bastion task")
    try:
        response = client.run_task(
            cluster=cluster_name,
            taskDefinition=DEFAULT_NAME,
            overrides={
                "containerOverrides": [
                    {
                        "name":
                        DEFAULT_NAME,
                        "environment": [
                            {
                                "name": "AUTHORIZED_SSH_KEYS",
                                "value": authorized_keys
                            },
                            {
                                "name": "ACTIVATION_ID",
                                "value": activation.get("ActivationId", ""),
                            },
                            {
                                "name": "ACTIVATION_CODE",
                                "value": activation.get("ActivationCode", ""),
                            },
                            {
                                "name": "AWS_REGION",
                                "value": load_aws_region_name()
                            },
                            {
                                "name": "TIMEOUT",
                                "value": str(timeout_minutes * 60)
                            },
                            {
                                "name": "BASTION_TYPE",
                                "value": bastion_type.value
                            },
                        ],
                    },
                ],
            },
            count=1,
            launchType="FARGATE",
            networkConfiguration={
                "awsvpcConfiguration": {
                    "subnets": subnet_ids.split(","),
                    "securityGroups": security_group_ids.split(","),
                    "assignPublicIp": "ENABLED",
                },
            },
            tags=build_tags(
                "ecs",
                {
                    "Name": f"{DEFAULT_NAME}/{instance_name}",
                    "BastionId": bastion_id,
                    "ActivationId": activation.get("ActivationId", ""),
                },
            ),
        )

    except client.exceptions.ClusterNotFoundException:
        log_error(
            "Specified cluster to launch bastion task into doesn't exist")
        raise Abort()

    except (
            client.exceptions.ClientException,
            client.exceptions.InvalidParameterException,
    ) as e:
        log_error(e.response["Error"]["Message"])
        raise Abort()

    wait_for_tasks_to_start(cluster_name, response["tasks"])
    return response