【问题标题】:How to make GCE instance stop when its deployed container finishes?如何让 GCE 实例在其部署的容器完成时停止?
【发布时间】:2018-10-10 20:29:55
【问题描述】:

我有一个执行单个大型计算的 Docker 容器。此计算需要大量内存,运行大约需要 12 小时。

我可以创建一个适当大小的 Google Compute Engine VM,并使用“将容器映像部署到此 VM 实例”选项来完美运行此作业。但是,一旦作业完成,容器就会退出,但 VM 仍在运行(并充电)。

容器退出时如何让虚拟机退出/停止/删除?

当虚拟机处于僵尸模式时,只有 stackdriver 容器仍在运行:

$ docker ps
CONTAINER ID        IMAGE                                                                COMMAND                  CREATED             STATUS              PORTS               NAMES
bfa2feb03180        gcr.io/stackdriver-agents/stackdriver-logging-agent:0.2-1.5.33-1-1   "/entrypoint.sh /u..."   17 hours ago        Up 17 hours                             stackdriver-logging-agent
161439a487c2        gcr.io/stackdriver-agents/stackdriver-metadata-agent:0.2-0.0.17-2    "/bin/sh -c /opt/s..."   17 hours ago        Up 17 hours         8000/tcp            stackdriver-metadata-agent

我这样创建虚拟机:

gcloud beta compute --project=abc instances create-with-container vm-name \
                    --zone=us-central1-c --machine-type=custom-1-65536-ext \
                    --network=default --network-tier=PREMIUM --metadata=google-logging-enabled=true \
                    --maintenance-policy=MIGRATE \
                    --service-account=xyz \
                    --scopes=https://www.googleapis.com/auth/cloud-platform \
                    --image=cos-stable-69-10895-71-0 --image-project=cos-cloud --boot-disk-size=10GB \
                    --boot-disk-type=pd-standard --boot-disk-device-name=vm-name \
                    --container-image=gcr.io/abc/my-image --container-restart-policy=on-failure \
                    --container-command=python3 \
                    --container-arg="a" --container-arg="b" --container-arg="c" \
                    --labels=container-vm=cos-stable-69-10895-71-0

【问题讨论】:

    标签: containers google-compute-engine


    【解决方案1】:

    创建虚拟机时,您需要为其授予计算写入权限,以便从内部删除实例。此时您还应该设置容器环境变量,如gce_zonegce_project_id。您需要它们来删除实例。

    gcloud beta compute instances create-with-container {NAME} \
        --container-env=gce_zone={ZONE},gce_project_id={PROJECT_ID} \
        --service-account={SERVICE_ACCOUNT} \
        --scopes=https://www.googleapis.com/auth/compute,...
        ...
    

    然后在容器内,每当您确定您的任务完成时:

    1. 请求一个 api 令牌(为了简单起见,我使用 curl 和 DEFAULT gce 服务帐户)
    curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" -H "Metadata-Flavor: Google"
    

    这将使用看起来像这样的 json 响应

    {
      "access_token": "foobarbaz...",
      "expires_in": 1234,
      "token_type": "Bearer"
    }
    
    1. 获取该访问令牌并点击instances.delete api endpoint(注意环境变量)
    curl -XDELETE -H 'Authorization: Bearer {TOKEN}' https://www.googleapis.com/compute/v1/projects/$gce_project_id/zones/$gce_zone/instances/$HOSTNAME
    

    【讨论】:

    • 你知道--scopes=https://www.googleapis.com/auth/compute的交易吗?我的方案只有在我省略时才有效。当我包含它时,VM 的控制台出现此错误Error: Failed to start container: Error response from daemon: {"message":"unauthorized: You don't have the needed permissions to perform this operation, and you may have invalid credentials. To authenticate your request, follow the steps in: https://cloud.google.com/container-registry/docs/advanced-authentication"}
    • @Adam 这很奇怪。好吧,我想只要你让它工作,那就是要走的路:P。
    • 如何设置 gce_zone 和 gce_project_id ?
    • @mcmillab 使用 --container-env 参数,就像答案一样
    • 其他人在尝试此操作时遇到Request had insufficient authentication scopes 错误?
    【解决方案2】:

    在解决这个问题一段时间后,这里有一个运行良好的完整解决方案。

    此解决方案不使用“使用容器映像启动机器”选项。相反,它使用更灵活的启动脚本。您仍然使用 Container-Optimized OS 实例。

    1. 创建启动脚本:
    #!/usr/bin/env bash
    
    # get image name and container parameters from the metadata
    IMAGE_NAME=$(curl http://metadata.google.internal/computeMetadata/v1/instance/attributes/image_name -H "Metadata-Flavor: Google")
    
    CONTAINER_PARAM=$(curl http://metadata.google.internal/computeMetadata/v1/instance/attributes/container_param -H "Metadata-Flavor: Google")
    
    # This is needed if you are using a private images in GCP Container Registry
    # (possibly also for the gcp log driver?)
    sudo HOME=/home/root /usr/bin/docker-credential-gcr configure-docker
    
    # Run! The logs will go to stack driver 
    sudo HOME=/home/root  docker run --log-driver=gcplogs ${IMAGE_NAME} ${CONTAINER_PARAM}
    
    # Get the zone
    zoneMetadata=$(curl "http://metadata.google.internal/computeMetadata/v1/instance/zone" -H "Metadata-Flavor:Google")
    # Split on / and get the 4th element to get the actual zone name
    IFS=$'/'
    zoneMetadataSplit=($zoneMetadata)
    ZONE="${zoneMetadataSplit[3]}"
    
    # Run compute delete on the current instance. Need to run in a container 
    # because COS machines don't come with gcloud installed 
    docker run --entrypoint "gcloud" google/cloud-sdk:alpine compute instances delete ${HOSTNAME}  --delete-disks=all --zone=${ZONE}
    
    1. 将脚本放在公开的地方。例如,将其放在 Cloud Storage 上并创建一个公共 URL。 COS 启动脚本不能使用gs:// URI。

    2. 使用startup-script-url 启动实例,并传递图像名称和参数,例如:

    gcloud compute --project=PROJECT_NAME instances create INSTANCE_NAME  \
    --zone=ZONE --machine-type=TYPE \
    --metadata=image_name=IMAGE_NAME,\
    container_param="PARAM1 PARAM2 PARAM3",\
    startup-script-url=PUBLIC_SCRIPT_URL \
    --maintenance-policy=MIGRATE --service-account=SERVICE_ACCUNT \
    --scopes=https://www.googleapis.com/auth/cloud-platform --image-family=cos-stable \
    --image-project=cos-cloud --boot-disk-size=10GB --boot-disk-device-name=DISK_NAME
    

    (您可能想限制scopes,为简单起见,示例使用完全访问权限)

    【讨论】:

    • 这对我有用!与其他解决方案相比,它是完全自动化的。顺便说一下,步骤 2 不是必需的:您可以指定脚本的本地路径,而不是在 metadata 参数中指定 startup-script-url--metadata-from-file "startup-script=path/to/startup-script.sh" Google Cloud docs: using a local startup script
    • 在计时器上启动此功能的最佳方法是什么?使用云调度程序在云运行的容器中运行该脚本?
    【解决方案3】:

    我根据文森特的回答写了一个独立的 Python 函数。

    def kill_vm():
        """
        If we are running inside a GCE VM, kill it.
        """
        # based on https://stackoverflow.com/q/52748332/321772
        import json
        import logging
        import requests
    
        # get the token
        r = json.loads(
            requests.get("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token",
                         headers={"Metadata-Flavor": "Google"})
                .text)
    
        token = r["access_token"]
    
        # get instance metadata
        # based on https://cloud.google.com/compute/docs/storing-retrieving-metadata
        project_id = requests.get("http://metadata.google.internal/computeMetadata/v1/project/project-id",
                                  headers={"Metadata-Flavor": "Google"}).text
    
        name = requests.get("http://metadata.google.internal/computeMetadata/v1/instance/name",
                            headers={"Metadata-Flavor": "Google"}).text
    
        zone_long = requests.get("http://metadata.google.internal/computeMetadata/v1/instance/zone",
                                 headers={"Metadata-Flavor": "Google"}).text
        zone = zone_long.split("/")[-1]
    
        # shut ourselves down
        logging.info("Calling API to delete this VM, {zone}/{name}".format(zone=zone, name=name))
    
        requests.delete("https://www.googleapis.com/compute/v1/projects/{project_id}/zones/{zone}/instances/{name}"
                        .format(project_id=project_id, zone=zone, name=name),
                        headers={"Authorization": "Bearer {token}".format(token=token)})
    

    一个简单的atexit 钩子让我得到了我想要的行为:

    import atexit
    atexit.register(kill_vm)
    

    【讨论】:

    • 这很好,但我认为最好尽可能避免使用元数据服务器。 $HOSTNAME 应该已经在实例上设置,正如我在回答中所说,在创建 VM 时将区域/项目 ID 设置为容器环境变量可能是谨慎的,这样您就不必从元数据服务器中获取。无论如何,这是我的两分钱。很好的实现。
    • 出于好奇,您为什么宁愿避开元数据服务器?
    • 嗯,我认为尽可能避免越过电线总是更好。就是这样。
    • 可以理解。我怀疑这些调用是由本地主机服务的,或者非常接近它的东西,因为返回值会立即返回。此外,这只是 VM 关闭时的 3 次调用,另外 2 次无论如何都是不可避免的。所以我可以接受 :) 好处是该方法是独立的,在部署过程中不需要小心。
    • 新的 URL 是什么?
    【解决方案4】:

    只需使用curl 和本地元数据服务器(无需 Python 脚本或 gcloud)。将以下内容添加到 Docker 入口点脚本的末尾,以便在容器完成时运行它:

    # Note: inside the container the name is exposed as $HOSTNAME
    INSTANCE_NAME=$(curl -sq "http://metadata.google.internal/computeMetadata/v1/instance/name" -H "Metadata-Flavor: Google")
    INSTANCE_ZONE=$(curl -sq "http://metadata.google.internal/computeMetadata/v1/instance/zone" -H "Metadata-Flavor: Google")
    
    echo "Terminating instance [${INSTANCE_NAME}] in zone [${INSTANCE_ZONE}}"
    TOKEN=$(curl -sq "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" -H "Metadata-Flavor: Google" | jq -r '.access_token')
    curl -X DELETE -H "Authorization: Bearer ${TOKEN}" https://www.googleapis.com/compute/v1/$INSTANCE_ZONE/instances/$INSTANCE_NAME
    

    为了安全起见,Principle of Least Privilege,您可以使用 custom service account 运行 VM,并为该服务帐户赋予角色,并使用此权限(最好使用 custom role)。

    compute.instances.delete
    

    【讨论】:

      【解决方案5】:

      另一种解决方案是不使用 GCE,而是使用 AI Platform 的自定义作业服务,该服务会在 Docker 容器退出后自动关闭 VM。

      gcloud ai-platform jobs submit training $JOB_NAME \
        --region $REGION \
        --master-image-uri $IMAGE_URI
      

      您可以指定--master-machine-type

      请参阅GCP documentation on custom containers

      【讨论】:

        【解决方案6】:

        最简单的方法,在容器内完成后:

        ZONE=`gcloud compute instances list --filter="name=($HOSTNAME)" --format 'csv[no-heading](zone)'`
        
        gcloud compute instances delete $HOSTNAME --zone=$ZONE -q
        

        -q 跳过交互确认

        $HOSTNAME 已经导出

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-12-17
          • 2020-09-18
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多