使用 SSH 将应用连接到实例

您的应用可以使用服务帐号运行自动任务,并与其他 Google Cloud API 交互。允许应用管理自己的 SSH 密钥并连接到实例对自动执行系统管理流程非常有用。本教程介绍了如何配置应用,使其通过 SSH 连接访问您的实例。本教程中的示例应用使用服务帐号和 OS Login 进行 SSH 密钥管理。

本教程中使用的所有代码都托管在 GoogleCloudPlatform/python-docs-samples GitHub 页面上。

目标

本教程介绍了以下任务:

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

费用

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

Google Cloud 新用户可能有资格申请免费试用

准备工作

  1. 登录您的 Google 帐号。

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

  2. 在 GCP Console 的项目选择器页面上,选择或创建 GCP 项目。

    转到项目选择器页面

  3. 确保您的 Google Cloud Platform 项目已启用结算功能。 了解如何确认您的项目已启用结算功能

  4. 在您的个人用户帐号中,为您的项目获取以下 IAM 角色
    • compute.instanceAdmin.v1
    • compute.networkAdmin
    • compute.osAdminLogin
    • iam.serviceAccountAdmin
    • iam.serviceAccountKeyAdmin
    • iam.serviceAccountUser
  5. 了解如何使用 Cloud Shell 运行 gcloud 命令行工具命令。

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

创建一个服务帐号和两个实例供本教程使用。您可以使用服务帐号向应用授予 SSH 访问权限,该应用将通过 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. us-central1-f 中创建一个名为 target 的实例。此实例用作您的服务帐号将通过 SSH 连接的远程实例。使用 --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 角色还会授予该服务帐号对该实例的超级用户权限。虽然您可以在项目级层授予此角色,使其可应用于项目中的所有实例,但在实例级层授予此角色可以更精细地控制 SSH 访问权限。以后,如果您发现应用需要访问您的项目中的其他资源,则可以向服务帐号授予更多权限:

    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. us-central1-f 中创建一个名为 source 的实例。将该实例与 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 版 Cloud 客户端库可使用应用默认凭据以服务帐号的身份进行身份验证,并使用您之前授予该服务帐号的角色。

接下来,配置并运行应用,以便可以通过 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 版 Cloud 客户端库可通过应用默认凭据使用与 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 版 Cloud 客户端库:

      $ 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 执行其他任务。
  • 开始创建您自己的应用。