Пример #1
0
def describe_fargate_cluster(
        cluster_name: str) -> DescribeClustersResponseTypeDef:
    """
    Fetches the status for a given cluster
    """
    client: ECSClient = fetch_boto3_client("ecs")
    return client.describe_clusters(clusters=[cluster_name])
Пример #2
0
def load_running_task_info(
    cluster_name: str,
    instance_name: Optional[str] = None,
    bastion_id: Optional[str] = None,
) -> List[TaskTypeDef]:
    """
    Loads and returns all running bastion tasks in the given cluster with the
    selected instance name
    """
    client: ECSClient = fetch_boto3_client("ecs")

    task_list = client.list_tasks(cluster=cluster_name, family=DEFAULT_NAME)
    task_response = describe_task(cluster_name, task_list["taskArns"])

    if not task_response:
        return []

    response = task_response["tasks"]

    if instance_name:
        response = [
            t for t in response
            if f"{DEFAULT_NAME}/{instance_name}" == get_tag_value(
                "ecs", t["tags"], "Name")
        ]

    if bastion_id:
        response = [
            t for t in response
            if bastion_id == get_tag_value("ecs", t["tags"], "BastionId")
        ]

    return response
Пример #3
0
def attach_policies_to_role(role_name: str, policy_arns: List[str]) -> None:
    """
    Attaches a list of IAM policies to a given IAM role
    """
    client: IAMClient = fetch_boto3_client("iam")
    for policy_arn in policy_arns:
        client.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
Пример #4
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)
Пример #5
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
Пример #6
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
Пример #7
0
def detach_policies_from_role(role_name: str) -> None:
    """
    Detaches all policies from a role
    """
    client: IAMClient = fetch_boto3_client("iam")

    try:
        policies = client.list_attached_role_policies(RoleName=role_name)
        policy_arns = [p["PolicyArn"] for p in policies["AttachedPolicies"]]
    except client.exceptions.NoSuchEntityException:
        return None

    for policy_arn in policy_arns:
        client.detach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
Пример #8
0
def fetch_role_arn(role_name: str) -> Optional[str]:
    """
    Checks if the given role name exists

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

    try:
        response = client.get_role(RoleName=role_name)
    except client.exceptions.NoSuchEntityException:
        return None

    return response["Role"]["Arn"]
Пример #9
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
Пример #10
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)
Пример #11
0
def describe_task(
    cluster_name: str,
    task_arns: List[str],
) -> Optional[DescribeTasksResponseTypeDef]:
    """
    Fetches the statuses for a group of tasks
    """
    client: ECSClient = fetch_boto3_client("ecs")

    if len(task_arns) == 0:
        return None

    return client.describe_tasks(
        cluster=cluster_name,
        tasks=task_arns,
        include=["TAGS"],
    )
Пример #12
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"]
Пример #13
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"),
    )
Пример #14
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"]
Пример #15
0
def load_public_ips_for_network_interfaces(
    interface_ids: List[str], ) -> Dict[str, str]:
    """
    Loads the public ip addresses for a list of network
    interface ids
    """
    client: EC2Client = fetch_boto3_client("ec2")
    interfaces = client.describe_network_interfaces(
        NetworkInterfaceIds=interface_ids, )

    public_ips = {}
    for interface in interfaces["NetworkInterfaces"]:
        try:
            public_ips[get_tag_value(
                "ec2", interface["TagSet"],
                "BastionId")] = interface["Association"]["PublicIp"]
        except KeyError:
            pass

    return public_ips
Пример #16
0
def create_activation(
    iam_role_name: str,
    instance_name: str,
    bastion_id: str,
) -> CreateActivationResultTypeDef:
    """
    Creates an SSM activation code that is used to connect the agent
    back to SSM
    """
    instance_name = f"{DEFAULT_NAME}/{instance_name}"

    client: SSMClient = fetch_boto3_client("ssm")
    response = client.create_activation(
        Description=f"Used to activate ssm agent in {DEFAULT_NAME}",
        DefaultInstanceName=instance_name,
        IamRole=iam_role_name,
        RegistrationLimit=1,
        ExpirationDate=datetime.utcnow() + timedelta(minutes=5),
        Tags=build_tags("ssm", {"Name": instance_name, "BastionId": bastion_id}),
    )
    return response
Пример #17
0
def load_instance_ids(
    instance_name: str = None,
    bastion_ids: List[str] = None,
) -> Dict[str, str]:
    """
    Loads all of the ssm instance ids for instances that were
    created by this cli. If the instance name is passed in, then
    instances are also filtered by name.
    """
    client: SSMClient = fetch_boto3_client("ssm")
    filters: List[InstanceInformationStringFilterTypeDef] = [
        {
            "Key": "tag:CreatedBy",
            "Values": ["serverless-aws-bastion:cli"],
        },
    ]

    if instance_name:
        filters.append(
            {
                "Key": "tag:Name",
                "Values": [f"{DEFAULT_NAME}/{instance_name}"],
            },
        )

    if bastion_ids:
        filters.append(
            {
                "Key": "tag:BastionId",
                "Values": bastion_ids,
            },
        )

    response = client.describe_instance_information(Filters=filters)
    return {
        i["ActivationId"]: i["InstanceId"] for i in response["InstanceInformationList"]
    }
Пример #18
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}"
Пример #19
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
Пример #20
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"])