使用 SSH 将应用连接到实例

如果您在实例上使用服务帐号来运行自动化任务并与其他 Google Cloud Platform API 进行交互,则该服务帐号可能还需要对其他 Compute Engine 实例的 SSH 访问权限。本教程演示了如何配置应用以通过 SSH 连接访问实例。本教程中的示例应用使用服务帐号和 OS Login 进行 SSH 密钥管理。

要跳过练习并查看完整的代码示例,请访问 GoogleCloudPlatform/python-docs-samples GitHub 页面。

目标

本教程将教您如何实现以下目标:

  • 创建服务帐号并对其进行配置,以便为连接到您的实例的应用提供 OS Login SSH 访问权限。
  • 创建与您的服务帐号关联的实例。
  • 在实例上配置示例应用,以使用服务帐号管理其专属的 SSH 密钥并建立 SSH 连接。
  • 在服务帐号关联的实例上运行应用。
  • 在 Compute Engine 之外运行应用。在此情况下,您必须手动提供服务帐号密钥并指定其他 SSH 参数。

费用

本教程使用 Cloud Platform 的计费组件,包括 Compute Engine。

Cloud Platform 新用户可能有资格免费试用

准备工作

  1. 登录您的 Google 帐号。

    如果您还没有 Google 帐号,请注册新帐号

  2. 选择或创建 Google Cloud Platform 项目。

    转到“管理资源”页面

  3. 确保您的 Google Cloud Platform 项目已启用结算功能。

    了解如何启用结算功能

  4. 您的用户帐号必须具有创建、删除和修改多个 Compute Engine 资源的权限。本教程假定您拥有项目的以下 IAM 角色
    • compute.instanceAdmin.v1
    • compute.networkAdmin
    • compute.osAdminLogin
    • iam.serviceAccountAdmin
    • iam.serviceAccountKeyAdmin
    • iam.serviceAccountUser
  5. 本教程假定您使用 Cloud Shell 运行 gcloud 命令。

创建和配置服务帐号及示例实例

本教程使用一个服务帐号和两个实例来演示应用如何在远程实例上运行 SSH 命令。

使用以下命令配置测试环境:

  1. 在控制台中打开 Cloud Shell:

    打开 Cloud Shell

  2. 导出环境变量以为将来的命令设置项目 ID:

    export PROJECT_ID='[PROJECT_ID]'
    
  3. 在项目中创建新的服务帐号。对于此示例,创建一个名为 ssh-account 的服务帐号:

    gcloud iam service-accounts create ssh-account --project $PROJECT_ID \
       --display-name "ssh-account"
    
  4. 创建一个名为 ssh-example 的临时网络,仅将其用于此示例:

    gcloud compute networks create ssh-example --project $PROJECT_ID
    
  5. 创建一条防火墙规则,允许通过 SSH 连接到 ssh-example 网络上的实例:

    gcloud compute firewall-rules create ssh-all --project $PROJECT_ID \
       --network ssh-example --allow tcp:22
    
  6. 在名为 targetus-central1-f 中创建一个实例。此实例用作您的服务帐号将通过 SSH 连接的远程实例。必须在项目级层或实例级层为该实例启用 OS Login。此示例演示了如何使用 --metadata 标志在此特定实例上启用 OS Login。请添加 --no-service-account--no-scopes 标志,因为此实例不需要为此特定示例运行任何 API 请求:

    gcloud compute instances create target --project $PROJECT_ID \
       --zone us-central1-f --network ssh-example \
       --no-service-account --no-scopes \
       --machine-type f1-micro --metadata=enable-oslogin=TRUE
    
  7. compute.osAdminLogin IAM 角色授予服务帐号,使其能够专门为名为 target 的实例建立 SSH 连接。compute.osAdminLogin 角色还会授予服务帐号对实例的超级用户权限。虽然您可以在项目级层授予此角色,使其应用于项目中的所有实例,但还是请在实例级层专门授予该角色,使权限仅限于此示例:

    gcloud compute instances add-iam-policy-binding target \
       --project $PROJECT_ID --zone us-central1-f \
       --member serviceAccount:ssh-account@$PROJECT_ID.iam.gserviceaccount.com \
       --role roles/compute.osAdminLogin
    
  8. 在名为 sourceus-central1-f 中创建一个实例。将该实例与 ssh-account 服务帐号关联。另外,指定 cloud-platform 范围,服务帐号在此实例上执行 API 请求需要使用该范围:

    gcloud compute instances create source \
       --project $PROJECT_ID --zone us-central1-f \
       --service-account ssh-account@$PROJECT_ID.iam.gserviceaccount.com  \
       --scopes https://www.googleapis.com/auth/cloud-platform \
       --network ssh-example --machine-type f1-micro
    

服务帐号现在可以管理其专属的 SSH 密钥对,并且可以使用 SSH 专门连接到 target 实例。由于 source 实例与您创建的 ssh-account 服务帐号关联,因此 Python 客户端库可以使用应用默认凭据以服务帐号身份进行身份验证,并使用您之前授予该服务帐号的角色。

接下来,配置并运行应用,以便可以通过 SSH 从一个实例连接到另一个实例。

在实例上运行 SSH 应用

当您的实例上运行的应用需要对其他实例的 SSH 访问权限时,您可以管理服务帐号的 SSH 密钥对并以编程方式执行 SSH 命令。对于此示例,请按照以下步骤运行示例应用:

  1. 使用 gcloud 命令行工具连接到 source 实例:

    gcloud compute ssh source --project $PROJECT_ID --zone us-central1-f
    
  2. source 实例上,安装 pip 和 Python 客户端库:

    my-username@source:~$ sudo apt update && sudo apt install python-pip -y && pip install --upgrade google-api-python-client
    
  3. GoogleCloudPlatform/python-docs-samples 下载 service_account_ssh.py 示例应用:

    my-username@source:~$ curl -O https://raw.githubusercontent.com/GoogleCloudPlatform/python-docs-samples/master/compute/oslogin/service_account_ssh.py
    
  4. 运行示例应用,该应用使用 argparse 从命令行接受变量。在此示例中,指示应用在 target 实例上安装和运行 cowsay。对于此命令,请手动添加项目 ID:

    my-username@source:~$ python service_account_ssh.py \
        --cmd 'sudo apt install cowsay -y && cowsay "It works!"' \
        --project [PROJECT_ID] --zone us-central1-f --instance target
    
    ⋮
    ___________
      It works!
    -----------
           \   ^__^
            \  (oo)\_______
               (__)\       )\/\
                   ||----w |
                   ||     ||
    
    

如果应用正确运行,您将收到来自 cowsay 应用的输出。您可以修改 --cmd 标志以包含所需的任何命令。或者,您可以编写自己的应用来导入 service_account_ssh.py 并直接调用它。

运行 exit 以断开与 source 实例的连接并返回到 Cloud Shell。

在 Compute Engine 之外运行 SSH 应用

在前面的示例中,您在 Compute Engine 实例上运行应用,这种情况下 Python 客户端库可以通过应用默认凭据来使用与 source 实例关联的服务帐号。如果您在 Compute Engine 实例之外运行此应用,则除非您手动提供服务帐号密钥,否则客户端库无法访问服务帐号及其权限。

  1. 获取您在本教程前面创建的 target 实例的外部 IP 地址。您可以在控制台中的实例页面上或通过从 gcloud 命令行工具运行以下命令来查找此地址:

    gcloud compute instances describe target \
       --project $PROJECT_ID --zone us-central1-f
    
  2. 为您在前面的示例中使用的 ssh-account 服务帐号创建服务帐号密钥,并将密钥文件下载到本地工作站。

  3. 将该服务帐号密钥复制到您要运行此示例的系统。

  4. 在您要运行此示例的系统上打开终端。

  5. 设置 GOOGLE_APPLICATION_CREDENTIALS 环境变量,使其指向服务帐号密钥 .json 文件所在的路径。如果密钥位于 Downloads 文件夹中,则您可以如以下示例所示设置环境变量:

    $ export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/key.json"
    
  6. 在该系统上安装必备组件:

    1. 安装 Pythonpip。在基于 Debian 的系统上,您可以使用 apt 来完成此步骤:

      $ sudo apt update && sudo apt install python python-pip -y
      
    2. 使用 pip 安装 Python 客户端库:

      $ pip install --upgrade google-api-python-client
      
  7. 下载示例应用:

    $ curl -O https://raw.githubusercontent.com/GoogleCloudPlatform/python-docs-samples/master/compute/oslogin/service_account_ssh.py
    
  8. 运行示例应用。在 Compute Engine 之外运行应用时,元数据服务器不可用,因此您必须手动指定服务帐号电子邮件。您还必须为前面获取的 target 实例指定外部 IP 地址。

    $ python service_account_ssh.py \
        --cmd 'sudo apt install cowsay -y && cowsay "It works!"' \
        --account ssh-account@[PROJECT_ID].iam.gserviceaccount.com \
        --project [PROJECT_ID] --hostname [TARGET_EXTERNAL_IP]
    
    ⋮
    ___________
      It works!
    -----------
           \   ^__^
            \  (oo)\_______
               (__)\       )\/\
                   ||----w |
                   ||     ||
    
    

如果应用正确运行,您将收到来自 cowsay 应用的输出。

示例应用的工作原理

service_account_ssh.py 示例应用按照以下步骤运行:

  1. 初始化 OS Login API 对象。
  2. 如果您未手动提供服务帐号电子邮件地址,则应用将读取实例元数据以确定与实例关联的服务帐号。如果您在 Compute Engine 之外运行此应用,则必须手动提供服务帐号地址。
  3. 调用 create_ssh_key() 方法为运行此示例的实例上的服务帐号生成一个临时 SSH 密钥,并将公钥添加到服务帐号,同时使用您可以指定的到期计时器。
  4. 从 OS Login API 调用 getLoginProfile() 方法,以获取服务帐号使用的 POSIX 用户名。
  5. 调用 run_ssh() 方法,以服务帐号身份执行远程 SSH 命令。
  6. 从远程 SSH 命令输出响应。
  7. 移除临时 SSH 密钥文件。
  8. OS Login 会在公钥文件到期后自动将其移除。
def main(cmd, project, instance=None, zone=None,
         oslogin=None, account=None, hostname=None):
    """Run a command on a remote system."""

    # Create the OS Login API object.
    oslogin = oslogin or googleapiclient.discovery.build('oslogin', 'v1')

    # Identify the service account ID if it is not already provided.
    account = account or requests.get(
        SERVICE_ACCOUNT_METADATA_URL, headers=HEADERS).text
    if not account.startswith('users/'):
        account = 'users/' + account

    # Create a new SSH key pair and associate it with the service account.
    private_key_file = create_ssh_key(oslogin, account)

    # Using the OS Login API, get the POSIX user name from the login profile
    # for the service account.
    profile = oslogin.users().getLoginProfile(name=account).execute()
    username = profile.get('posixAccounts')[0].get('username')

    # Create the hostname of the target instance using the instance name,
    # the zone where the instance is located, and the project that owns the
    # instance.
    hostname = hostname or '{instance}.{zone}.c.{project}.internal'.format(
        instance=instance, zone=zone, project=project)

    # Run a command on the remote instance over SSH.
    result = run_ssh(cmd, private_key_file, username, hostname)

    # Print the command line output from the remote instance.
    # Use .rstrip() rather than end='' for Python 2 compatability.
    for line in result:
        print(line.decode('utf-8').rstrip('\n\r'))

    # Shred the private key and delete the pair.
    execute(['shred', private_key_file])
    execute(['rm', private_key_file])
    execute(['rm', private_key_file + '.pub'])

if __name__ == '__main__':

    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument(
        '--cmd', default='uname -a',
        help='The command to run on the remote instance.')
    parser.add_argument(
        '--project',
        help='Your Google Cloud project ID.')
    parser.add_argument(
        '--zone',
        help='The zone where the target instance is locted.')
    parser.add_argument(
        '--instance',
        help='The target instance for the ssh command.')
    parser.add_argument(
        '--account',
        help='The service account email.')
    parser.add_argument(
        '--hostname',
        help='The external IP address or hostname for the target instance.')
    args = parser.parse_args()

    main(args.cmd, args.project, instance=args.instance, zone=args.zone,
         account=args.account, hostname=args.hostname)

create_ssh_key() 方法会生成新的 SSH 密钥对。然后,该方法从 OS Login API 调用 users().importSshPublicKey() 以将公钥与服务帐号相关联。users().importSshPublicKey() 方法也接受到期值,该值指示公钥保持有效的时间。

def create_ssh_key(oslogin, account, private_key_file=None, expire_time=300):
    """Generate an SSH key pair and apply it to the specified account."""
    private_key_file = private_key_file or '/tmp/key-' + str(uuid.uuid4())
    execute(['ssh-keygen', '-t', 'rsa', '-N', '', '-f', private_key_file])

    with open(private_key_file + '.pub', 'r') as original:
        public_key = original.read().strip()

    # Expiration time is in microseconds.
    expiration = int((time.time() + expire_time) * 1000000)

    body = {
        'key': public_key,
        'expirationTimeUsec': expiration,
    }
    oslogin.users().importSshPublicKey(parent=account, body=body).execute()
    return private_key_file

最佳做法是配置您的服务帐号,使其定期为自己生成新的密钥对。在此示例中,服务帐号为其建立的每个 SSH 连接创建一个新密钥对,但您可以将其修改为按计划运行以更好地满足应用的需求。

users().importSshPublicKey() 的请求正文包含 expirationTimeUsec 值,该值会告知 OS Login 密钥应何时到期。每个帐号最多只能有 32 KB 的 SSH 密钥数据,因此最好将公共 SSH 密钥配置为在服务帐号完成其操作后不久到期。

在服务帐号配置其 SSH 密钥后,它便可以执行远程命令。在此示例中,应用使用 run_ssh() 方法在远程实例上执行命令并返回命令输出。

def run_ssh(cmd, private_key_file, username, hostname):
    """Run a command on a remote system."""
    ssh_command = [
        'ssh', '-i', private_key_file, '-o', 'StrictHostKeyChecking=no',
        '{username}@{hostname}'.format(username=username, hostname=hostname),
        cmd,
    ]
    ssh = subprocess.Popen(
        ssh_command, shell=False, stdout=subprocess.PIPE,
        stderr=subprocess.PIPE)
    result = ssh.stdout.readlines()
    return result if result else ssh.stderr.readlines()

清理

为避免系统因本教程中使用的资源向您的 Google Cloud Platform 帐号收取费用,请执行以下操作:

使用以下命令清理测试环境中的资源:

  1. 在控制台中打开 Cloud Shell:

    打开 Cloud Shell

  2. 删除名为 source 的实例:

    gcloud compute instances delete source \
       --project $PROJECT_ID --zone us-central1-f
    
  3. 删除名为 target 的实例:

    gcloud compute instances delete target \
       --project $PROJECT_ID --zone us-central1-f
    
  4. 删除 ssh-account 服务帐号:

    gcloud iam service-accounts delete ssh-account --project $PROJECT_ID
    
  5. 删除名为 ssh-example 的网络:

    gcloud compute networks delete ssh-example --project $PROJECT_ID
    

后续步骤

  • 下载并查看完整的代码示例。完整的示例中包含了一个同时用到所有这些方法的小例子。您可以根据自己的需求随意下载、更改并运行该示例。
  • 查看 Compute Engine API 参考OS Login API 参考,了解如何使用这些 API 执行其他任务。
  • 开始创建自己的应用!
此页内容是否有用?请给出您的反馈和评价:

发送以下问题的反馈:

此网页
Compute Engine 文档