使用 SSH 將應用程式連線至執行個體

如果您在執行個體上透過服務帳戶執行自動化工作,以及與其他 Google Cloud Platform API 互動,則該服務帳戶可能也需要其他 Compute Engine 執行個體的 SSH 存取權。本教學課程說明如何將應用程式設為透過 SSH 連線存取您的執行個體。本教學課程的範例應用程式使用服務帳戶和 OS 登入進行安全殼層 (SSH) 金鑰組管理。

如要略過練習及檢視完整的程式碼範例,請造訪 GoogleCloudPlatform/python-docs-samples GitHub 頁面。

目標

本教學課程會說明如何達成以下目標:

  • 建立並設定服務帳戶,針對與您的執行個體連線的應用程式提供 OS 登入的 SSH 存取權。
  • 建立與您的服務帳戶相關聯的執行個體。
  • 在執行個體上設定範例應用程式,以使用服務帳戶來管理自己的安全殼層 (SSH) 金鑰組及建立 SSH 連線。
  • 在與服務帳戶相關聯的執行個體上執行應用程式。
  • 在 Compute Engine 以外的位置執行應用程式,其中您必須手動提供服務帳戶金鑰並指定其他 SSH 參數。

費用

本教學課程使用包括 Compute Engine 在內的 Cloud Platform 可計費元件。

初次使用 Cloud Platform 的使用者可能符合申請免費試用的資格。

事前準備

  1. 登入您的 Google 帳戶。

    如果您沒有帳戶,請申請新帳戶

  2. 選取或建立 Google Cloud Platform 專案。

    前往「Manage resources」(管理資源) 頁面

  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-example 網路上任何連至執行個體的 SSH 連線:

    gcloud compute firewall-rules create ssh-all --project $PROJECT_ID \
       --network ssh-example --allow tcp:22
    
  6. us-central1-f 中建立名為 target 的執行個體,服務帳戶將會透過 SSH 連線至這個遠端執行個體。此執行個體必須已在專案層級或執行個體層級啟用 OS 登入功能。本範例說明如何使用 --metadata 標記來啟用此特定執行個體的 OS 登入功能,其中包含 --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. 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 用戶端程式庫能夠以服務帳戶的身分使用應用程式預設憑證進行驗證,並使用您先前授予該服務帳戶的角色。

下一步為設定並執行可從某一執行個體透過 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 登入會在公開金鑰檔案到期後自動將檔案移除。
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 登入指示金鑰的到期時間。每個帳戶最多只能有 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 說明文件