Пример #1
0
def describe(
    config: Config,
    ami: Optional[str] = None,
    owner: Optional[str] = None,
    name_match: Optional[str] = None,
    show_snapshot_id: bool = False,
) -> List[Image]:
    """List AMIs."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    if ami:
        response = ec2_client.describe_images(ImageIds=[ami])
    else:
        if owner:
            owners_filter = [owner]
        else:
            describe_images_owners = config.get("describe_images_owners", None)

            if not describe_images_owners:
                owners_filter = ["self"]
            elif isinstance(describe_images_owners, str):
                owners_filter = [describe_images_owners]
            else:
                owners_filter: List[str] = describe_images_owners

        if name_match is None:
            name_match = config.get("describe_images_name_match", None)

        filters: List[FilterTypeDef] = [] if name_match is None else [{"Name": "name", "Values": [f"*{name_match}*"]}]

        print(f'Describing images owned by {owners_filter} with name matching {name_match or "*"}')

        response = ec2_client.describe_images(Owners=owners_filter, Filters=filters)

    images = []
    for i in response["Images"]:
        image: Image = {
            "Name": i.get("Name", None),
            "ImageId": i["ImageId"],
            "CreationDate": i["CreationDate"],
            "RootDeviceName": i["RootDeviceName"] if "RootDeviceName" in i else None,
            "Size": i["BlockDeviceMappings"][0]["Ebs"]["VolumeSize"] if i["BlockDeviceMappings"] else None,
        }
        if show_snapshot_id:
            image["SnapshotId"] = i["BlockDeviceMappings"][0]["Ebs"]["SnapshotId"]
        images.append(image)

    return sorted(images, key=lambda i: i["CreationDate"], reverse=True)
Пример #2
0
def tag(
    config: Config,
    tags: List[str],
    name: Optional[str] = None,
    name_match: Optional[str] = None,
) -> List[Dict[str, Any]]:
    """Tag EC2 instance(s)."""
    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    tagdefs: List[TagTypeDef] = []
    for t in tags:
        parts = t.split("=")
        tagdefs.append({"Key": parts[0], "Value": parts[1]})

    instances = describe(config, name, name_match)

    ids = [i["InstanceId"] for i in instances]

    if not ids:
        raise NoInstancesError(name=name, name_match=name_match)

    ec2_client.create_tags(Resources=ids, Tags=tagdefs)

    return describe_tags(config,
                         name,
                         name_match,
                         keys=[d["Key"] for d in tagdefs])
Пример #3
0
def instance_tags(config: Config,
                  name: Optional[str] = None,
                  name_match: Optional[str] = None,
                  keys: List[str] = []) -> List[Dict[str, Any]]:
    """List EC2 instances with their tags."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    response = ec2_client.describe_instances(Filters=filters(name, name_match))

    instances: List[Dict[str, Any]] = []
    for r in response["Reservations"]:
        for i in r["Instances"]:
            if i["State"]["Name"] != "terminated":
                inst = {
                    "InstanceId": i["InstanceId"],
                    "Name": util_tags.get_value(i, "Name")
                }
                if not keys:
                    inst["Tags"] = ", ".join(f"{tag['Key']}={tag['Value']}"
                                             for tag in i.get("Tags", []))
                else:
                    for key in keys:
                        inst[f"Tag: {key}"] = util_tags.get_value(i, key)

                instances.append(inst)

    return sorted(instances, key=lambda i: str(i["Name"]))
Пример #4
0
def volume_tags(config: Config,
                name: Optional[str] = None,
                name_match: Optional[str] = None,
                keys: List[str] = []) -> List[Dict[str, Any]]:
    """List EC2 volumes with their tags."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    response = ec2_client.describe_volumes(Filters=filters(name, name_match))

    volumes: List[Dict[str, Any]] = []
    for v in response["Volumes"]:
        vol = {
            "VolumeId": v["VolumeId"],
            "Name": util_tags.get_value(v, "Name")
        }
        if not keys:
            vol["Tags"] = ", ".join(f"{tag['Key']}={tag['Value']}"
                                    for tag in v.get("Tags", []))
        else:
            for key in keys:
                vol[f"Tag: {key}"] = util_tags.get_value(v, key)

        volumes.append(vol)

    return sorted(volumes, key=lambda i: str(i["Name"]))
Пример #5
0
def fetch_instance_ids(config: Config, ids_or_names: List[str]) -> List[str]:

    if ids_or_names == ["all"]:
        return [i["ID"] for i in describe(config)]

    ids: List[str] = []
    names: List[str] = []

    for i in ids_or_names:
        if i.startswith("i-"):
            ids.append(i)
        else:
            names.append(i)

    if names:
        ec2_client = boto3.client("ec2", region_name=config.get("region", None))
        response = ec2_client.describe_instances(Filters=[{"Name": "tag:Name", "Values": names}])

        try:
            for r in response["Reservations"]:
                for i in r["Instances"]:
                    ids.append(i["InstanceId"])
        except IndexError:
            raise ValueError(f"No instances with names {','.join(names)}")
    return ids
Пример #6
0
def describe(config: Config) -> Iterator[Agent]:
    """List running instances with the SSM agent."""

    instances_names = describe_instances_names(config)

    kwargs = {"MaxResults": 50}
    client = boto3.client("ssm", region_name=config.get("region", None))
    while True:
        response = client.describe_instance_information(**kwargs)

        for i in response["InstanceInformationList"]:
            a: Agent = {
                "ID": i["InstanceId"],
                "Name": instances_names.get(i["InstanceId"], None),
                "PingStatus": i["PingStatus"],
                "Platform": f'{i["PlatformName"]} {i["PlatformVersion"]}',
                "AgentVersion": i["AgentVersion"],
            }
            yield a

        next_token = response.get("NextToken", None)
        if next_token:
            kwargs = {"NextToken": next_token}
        else:
            break
Пример #7
0
def over_provisioned(config: Config) -> List[Dict[str, Any]]:
    """Show recommendations for over-provisioned EC2 instances."""
    def util(metric: UtilizationMetricTypeDef) -> str:
        return f'{metric["name"]} {metric["statistic"][:3]} {metric["value"]}'

    instances_uptime = describe_instances_uptime(config)

    client = boto3.client("compute-optimizer",
                          region_name=config.get("region", None))

    response = client.get_ec2_instance_recommendations(
        filters=[{
            "name": "Finding",
            "values": ["Overprovisioned"]
        }])

    recs = [{
        "ID": i["instanceArn"].split("/")[1],
        "Name": i.get("instanceName", None),
        "Instance Type": i["currentInstanceType"],
        "Recommendation": i["recommendationOptions"][0]["instanceType"],
        "Utilization": util(i["utilizationMetrics"][0]),
        "Uptime": instances_uptime[i["instanceArn"].split("/")[1]],
    } for i in response["instanceRecommendations"]]

    return recs
Пример #8
0
def describe_instances_names(config: Config) -> Dict[str, Optional[str]]:
    """List EC2 instance names in the region."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    response = ec2_client.describe_instances()

    return {i["InstanceId"]: util_tags.get_value(i, "Name") for r in response["Reservations"] for i in r["Instances"]}
Пример #9
0
def delete(config: Config, ami: str) -> None:
    """Deregister an AMI and delete its snapshot."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    response = describe(config, ami, show_snapshot_id=True)

    ec2_client.deregister_image(ImageId=ami)

    ec2_client.delete_snapshot(SnapshotId=response[0]["SnapshotId"])
Пример #10
0
def fetch_instance_id(config: Config, name: str) -> str:
    if name.startswith("i-"):
        return name

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))
    response = ec2_client.describe_instances(Filters=[{"Name": "tag:Name", "Values": [name]}])

    try:
        return response["Reservations"][0]["Instances"][0]["InstanceId"]
    except IndexError:
        raise ValueError(f"No instance named {name}")
Пример #11
0
def templates(config: Config) -> List[Dict[str, Any]]:
    """Describe launch templates."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    response = ec2_client.describe_launch_templates()

    return [{
        "Name": t["LaunchTemplateName"],
        "Default Version": t["DefaultVersionNumber"]
    } for t in response["LaunchTemplates"]]
Пример #12
0
def create_key_pair(config: Config, key_name: str, file_path: str) -> str:
    """Create a key pair."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    path = os.path.expanduser(file_path)
    with open(path, "x") as file:
        key = ec2_client.create_key_pair(KeyName=key_name)
        file.write(key["KeyMaterial"])
        os.chmod(path, 0o600)

    return f"Created key pair {key_name} and saved private key to {path}"
Пример #13
0
def describe(
    config: Config,
    name: Optional[str] = None,
    name_match: Optional[str] = None,
    include_terminated: bool = False,
    show_running_only: bool = False,
    sort_by: str = "State,Name",
    columns: Optional[str] = None,
) -> List[Instance]:
    """List EC2 instances in the region."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    response = ec2_client.describe_instances(Filters=filters(name, name_match))

    # print(response["Reservations"][0]["Instances"][0])

    cols = columns.split(",") if columns else [
        "InstanceId", "State", "Name", "Type", "DnsName"
    ]

    # don't sort by cols we aren't showing
    sort_cols = [sc for sc in sort_by.split(",") if sc in cols]

    instances: List[Instance] = []
    for r in response["Reservations"]:
        for i in r["Instances"]:
            if (include_terminated or i["State"]["Name"] != "terminated") and (
                    not show_running_only
                    or i["State"]["Name"] in ["pending", "running"]):
                desc: Instance = {}

                for col in cols:
                    if col == "State":
                        desc[col] = i["State"]["Name"]
                    elif col == "Name":
                        desc[col] = util_tags.get_value(i, "Name")
                    elif col == "Type":
                        desc[col] = i["InstanceType"]
                    elif col == "DnsName":
                        desc[col] = i["PublicDnsName"] if i.get(
                            "PublicDnsName",
                            None) != "" else i["PrivateDnsName"]
                    else:
                        desc[col] = i[col]

                instances.append(desc)

    return sorted(
        instances,
        key=lambda i: "".join(str(i[field]) for field in sort_cols),
    )
Пример #14
0
def share(config: Config, ami: str, account: str) -> None:
    """Share an AMI with another account."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    ec2_client.modify_image_attribute(
        ImageId=ami,
        LaunchPermission={"Add": [{"UserId": account}]},
        OperationType="add",
        UserIds=[account],
        Value="string",
        DryRun=False,
    )
Пример #15
0
def describe_instances_uptime(config: Config) -> Dict[str, str]:
    """List EC2 instance uptimes in the region."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    response = ec2_client.describe_instances()

    instances = {
        i["InstanceId"]: difference_in_words(datetime.now(pytz.utc),
                                             i["LaunchTime"])
        for r in response["Reservations"] for i in r["Instances"]
    }

    return instances
Пример #16
0
def logs(config: Config, name: str) -> str:
    """Show the system logs."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    instances = describe(config, name)

    if not instances:
        raise NoInstancesError(name=name)

    instance_id = instances[0]["InstanceId"]
    response = ec2_client.get_console_output(InstanceId=instance_id)

    return response.get("Output", "No logs yet 😔")
Пример #17
0
def output(config: Config, command_id: str, instance_id: str, stderr: bool) -> None:
    """Fetch output of a command from S3."""
    ssm_client = boto3.client("ssm", region_name=config.get("region", None))

    command = ssm_client.list_commands(CommandId=command_id)["Commands"][0]

    if not command.get("OutputS3BucketName", None):
        raise ValueError("No OutputS3BucketName")

    bucket = command["OutputS3BucketName"]

    try:
        doc_path = DOC_PATHS[command["DocumentName"]]
    except KeyError:
        raise NotImplementedError(
            f"for {command['DocumentName']}. Run aws s3 ls {command['OutputS3KeyPrefix']}/{command_id}/{instance_id}/awsrunShellScript/"
        )

    std = "stderr" if stderr else "stdout"
    key = f"{command['OutputS3KeyPrefix']}/{command_id}/{instance_id}/awsrunShellScript/{doc_path}/{std}"

    s3_client = boto3.client("s3", region_name=config.get("region", None))

    try:
        response = s3_client.get_object(Bucket=bucket, Key=key)
    except ClientError as e:
        if e.response["Error"]["Code"] == "NoSuchKey":
            raise KeyError(f"s3://{bucket}/{key} does not exist")
        else:
            raise e

    # converts body bytes to string lines
    streaming_body = cast(IO[bytes], response["Body"])
    for line in codecs.getreader("utf-8")(streaming_body):
        print(line, end="")

    return None
Пример #18
0
def terminate(config: Config, name: str) -> List[Dict[str, Any]]:
    """Terminate EC2 instance."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    instances = describe(config, name)

    if not instances:
        raise NoInstancesError(name=name)

    response = ec2_client.terminate_instances(
        InstanceIds=[instance["InstanceId"] for instance in instances])

    return [{
        "State": i["CurrentState"]["Name"],
        "InstanceId": i["InstanceId"]
    } for i in response["TerminatingInstances"]]
Пример #19
0
def modify(config: Config, name: str, type: str) -> List[Instance]:
    """Change an instance's type."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    instances = describe(config, name)

    if not instances:
        raise NoInstancesError(name=name)

    instance_id = instances[0]["InstanceId"]
    ec2_client.modify_instance_attribute(InstanceId=instance_id,
                                         InstanceType={"Value": type})
    ec2_client.modify_instance_attribute(
        InstanceId=instance_id,
        EbsOptimized={"Value": is_ebs_optimizable(type)})

    return describe(config, name)
Пример #20
0
def start(config: Config, name: str) -> List[Instance]:
    """Start EC2 instance."""

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    print(f"Starting instances with the name {name} ... ")

    instances = describe(config, name)

    if not instances:
        raise NoInstancesError(name=name)

    instance_ids = [instance["InstanceId"] for instance in instances]
    ec2_client.start_instances(InstanceIds=instance_ids)

    waiter = ec2_client.get_waiter("instance_running")
    waiter.wait(InstanceIds=instance_ids)

    return describe(config, name)
Пример #21
0
def patch(
    config: Config, operation: Literal["scan", "install"], names: List[str], no_reboot: bool
) -> List[Dict[str, Optional[str]]]:
    """Scan or install AWS patch baseline."""

    instance_ids = fetch_instance_ids(config, names)

    client = boto3.client("ssm", region_name=config.get("region", None))

    kwargs: Dict[str, Any] = {
        "DocumentName": "AWS-RunPatchBaseline",
        "InstanceIds": instance_ids,
    }

    if operation == "scan":
        kwargs["Parameters"] = {"Operation": ["Scan"], "SnapshotId": [str(uuid.uuid4())]}
    else:
        kwargs["Parameters"] = {
            "Operation": ["Install"],
            "RebootOption": ["NoReboot" if no_reboot else "RebootIfNeeded"],
            "SnapshotId": [str(uuid.uuid4())],
        }

    try:
        kwargs["OutputS3BucketName"] = config["ssm"]["s3bucket"]
        kwargs["OutputS3KeyPrefix"] = config["ssm"]["s3prefix"]
    except KeyError:
        pass

    response = client.send_command(**kwargs)

    return [
        {
            "CommandId": response["Command"]["CommandId"],
            "InstanceId": i,
            "Status": response["Command"]["Status"],
            "Document": response["Command"]["DocumentName"],
            "Output": f"s3://{response['Command']['OutputS3BucketName']}/{response['Command']['OutputS3KeyPrefix']}"
            if response["Command"].get("OutputS3BucketName", None)
            else None,
        }
        for i in response["Command"]["InstanceIds"]
    ]
Пример #22
0
def compliance_summary(config: Config) -> List[Dict[str, Any]]:
    """Compliance summary for running instances that have run the patch baseline."""
    instances_names = describe_instances_names(config)

    client = boto3.client("ssm", region_name=config.get("region", None))

    response = client.list_resource_compliance_summaries(
        Filters=[{"Key": "ComplianceType", "Values": ["Patch"], "Type": "EQUAL"}]
    )

    return [
        {
            "InstanceId": i["ResourceId"],
            "Name": instances_names.get(i["ResourceId"], None),
            "Status": i["Status"],
            "NonCompliantCount": i["NonCompliantSummary"]["NonCompliantCount"],
            "Last operation time": i["ExecutionSummary"]["ExecutionTime"],
        }
        for i in response["ResourceComplianceSummaryItems"]
    ]
Пример #23
0
def run(config: Config, names: List[str]) -> List[Dict[str, Optional[str]]]:
    """
    Run a shell script on instance(s).

    Script is read from stdin.
    """

    instance_ids = fetch_instance_ids(config, names)

    client = boto3.client("ssm", region_name=config.get("region", None))

    script = sys.stdin.readlines()

    kwargs: Dict[str, Any] = {
        "DocumentName": "AWS-RunShellScript",
        "InstanceIds": instance_ids,
        "Parameters": {"commands": script},
    }

    try:
        kwargs["OutputS3BucketName"] = config["ssm"]["s3bucket"]
        kwargs["OutputS3KeyPrefix"] = config["ssm"]["s3prefix"]
    except KeyError:
        pass

    response = client.send_command(**kwargs)

    return [
        {
            "CommandId": response["Command"]["CommandId"],
            "InstanceId": i,
            "Status": response["Command"]["Status"],
            "Document": response["Command"]["DocumentName"],
            "Output": f"s3://{response['Command']['OutputS3BucketName']}/{response['Command']['OutputS3KeyPrefix']}"
            if response["Command"].get("OutputS3BucketName", None)
            else None,
        }
        for i in response["Command"]["InstanceIds"]
    ]
Пример #24
0
def invocations(config: Config, command_id: str) -> List[Dict[str, Any]]:
    """List invocations of a command across instances."""

    client = boto3.client("ssm", region_name=config.get("region", None))

    command = client.list_commands(CommandId=command_id)["Commands"][0]
    invocations = client.list_command_invocations(CommandId=command_id)
    instances_names = describe_instances_names(config)

    return [
        {
            "RequestedDateTime": i["RequestedDateTime"].strftime("%Y-%m-%d %H:%M"),
            "InstanceId": i["InstanceId"],
            "Name": instances_names.get(i["InstanceId"], None),
            "Status": i["Status"],
            "DocumentName": i["DocumentName"],
            "Parameters": json.dumps(command["Parameters"]),
            "Output": f"s3://{command['OutputS3BucketName']}/{command['OutputS3KeyPrefix']}"
            if command.get("OutputS3BucketName", None)
            else None,
        }
        for i in invocations["CommandInvocations"]
    ]
Пример #25
0
def patch_summary(config: Config) -> Iterator[Dict[str, Any]]:
    """Patch summary for all instances that have run the patch baseline."""
    instances_names = describe_instances_names(config)
    instance_ids = list(instances_names.keys())

    client = boto3.client("ssm", region_name=config.get("region", None))

    max_at_a_time = 50

    for i in range(0, len(instance_ids), max_at_a_time):
        chunk = instance_ids[i : i + max_at_a_time]
        response = client.describe_instance_patch_states(InstanceIds=chunk)
        for i in response["InstancePatchStates"]:
            yield {
                "InstanceId": i["InstanceId"],
                "Name": instances_names.get(i["InstanceId"], None),
                "Needed": i["MissingCount"],
                "Pending Reboot": i["InstalledPendingRebootCount"],
                "Errored": i["FailedCount"],
                "Rejected": i["InstalledRejectedCount"],
                "Last operation time": i["OperationEndTime"],
                "Last operation": i["Operation"],
            }
Пример #26
0
def commands(config: Config, name: Optional[str]) -> List[Dict[str, Optional[str]]]:
    """List commands by instance."""

    client = boto3.client("ssm", region_name=config.get("region", None))
    instances_names = describe_instances_names(config)

    if name:
        instance_id = fetch_instance_id(config, name)
        response = client.list_commands(InstanceId=instance_id)
    else:
        response = client.list_commands()

    return [
        {
            "RequestedDateTime": c["RequestedDateTime"].strftime("%Y-%m-%d %H:%M"),
            "CommandId": c["CommandId"],
            "InstanceIds": ",".join(c["InstanceIds"]),
            "Names": ",".join([instances_names.get(i, None) or "" for i in c["InstanceIds"]]),
            "Status": c["Status"],
            "DocumentName": c["DocumentName"],
            "Operation": first(c["Parameters"].get("Operation", None)),
        }
        for c in response["Commands"]
    ]
Пример #27
0
def launch(
    config: Config,
    name: str,
    ami: Optional[str] = None,
    template: Optional[str] = None,
    volume_size: Optional[int] = None,
    encrypted: bool = True,
    instance_type: Optional[str] = None,
    key_name: Optional[str] = None,
    userdata: Optional[str] = None,
) -> List[Instance]:
    """Launch a tagged EC2 instance with an EBS volume."""

    if not (template or ami):
        raise ValueError("Please specify either an ami or a launch template")

    if not template and not instance_type:
        # if no instance type is provided set one
        instance_type = "t3.small"

    ec2_client = boto3.client("ec2", region_name=config.get("region", None))

    runargs: RunArgs = {
        "MaxCount": 1,
        "MinCount": 1,
    }

    desc = ""
    if template:
        runargs["LaunchTemplate"] = {"LaunchTemplateName": template}
        desc = f"template {template} "

    if ami:
        image = ami_cmd.fetch(config, ami)
        if not image["RootDeviceName"]:
            raise ValueError(f"{image['ImageId']} is missing RootDeviceName")

        volume_size = volume_size or config.get("volume_size",
                                                None) or image["Size"]
        if not volume_size:
            raise ValueError("No volume size")

        device: BlockDeviceMappingTypeDef = {
            "DeviceName": image["RootDeviceName"],
            "Ebs": {
                "VolumeSize": volume_size,
                "DeleteOnTermination": True,
                "VolumeType": "gp3",
                "Encrypted": encrypted,
            },
        }

        kms_key_id = config.get("kms_key_id", None)
        if kms_key_id:
            device["Ebs"]["KmsKeyId"] = kms_key_id

        runargs["ImageId"] = image["ImageId"]
        runargs["BlockDeviceMappings"] = [device]

        desc = desc + image["Name"] if image[
            "Name"] else desc + image["ImageId"]

    key = key_name or config.get("key_name", None)
    if key:
        runargs["KeyName"] = key
    elif not template:
        raise HandledError("Please provide a key name")

    if instance_type:
        runargs["InstanceType"] = cast("InstanceTypeType", instance_type)
        runargs["EbsOptimized"] = is_ebs_optimizable(instance_type)

    tags: List[TagTypeDef] = [{"Key": "Name", "Value": name}]
    additional_tags = config.get("additional_tags", {})
    if additional_tags:
        tags.extend([{
            "Key": k,
            "Value": v
        } for k, v in additional_tags.items()])
    runargs["TagSpecifications"] = [
        {
            "ResourceType": "instance",
            "Tags": tags
        },
        {
            "ResourceType": "volume",
            "Tags": tags
        },
    ]

    if config.get("vpc", None):
        # TODO: support multiple subnets
        security_group = config["vpc"]["security_group"]
        runargs["NetworkInterfaces"] = [{
            "DeviceIndex":
            0,
            "Description":
            "Primary network interface",
            "DeleteOnTermination":
            True,
            "SubnetId":
            config["vpc"]["subnet"],
            "Ipv6AddressCount":
            0,
            "Groups":
            security_group
            if isinstance(security_group, list) else [security_group],
        }]
        associate_public_ip_address = config["vpc"].get(
            "associate_public_ip_address", None)
        if associate_public_ip_address is not None:
            runargs["NetworkInterfaces"][0][
                "AssociatePublicIpAddress"] = associate_public_ip_address

        vpc_name = f" vpc {config['vpc']['name']}"
    else:
        vpc_name = ""

    iam_instance_profile_arn = config.get("iam_instance_profile_arn", None)
    if iam_instance_profile_arn:
        runargs["IamInstanceProfile"] = {"Arn": iam_instance_profile_arn}

    if userdata:
        runargs["UserData"] = read_file(userdata)

    # use IMDSv2 to prevent SSRF
    runargs["MetadataOptions"] = {"HttpTokens": "required"}

    region_name = ec2_client.meta.region_name

    print(
        f"Launching a {instance_type} in {region_name}{vpc_name} named {name} using {desc} ... "
    )
    response = ec2_client.run_instances(**runargs)

    instance = response["Instances"][0]

    waiter = ec2_client.get_waiter("instance_running")
    waiter.wait(InstanceIds=[instance["InstanceId"]])

    # TODO: wait until instance checks passed (as they do in the console)

    # the response from run_instances above always contains an empty string
    # for PublicDnsName, so we call describe to get it
    return describe(config=config, name=name)