SSH を使用したアプリのインスタンスへの接続

自動タスクを実行するためにインスタンスでサービス アカウントを使用して、他の Google Cloud API とやり取りする場合、そのサービス アカウントには他の Compute Engine インスタンスへの SSH アクセスも必要になることがあります。このチュートリアルでは、アプリが SSH 接続経由でインスタンスにアクセスするよう構成する方法を説明します。このチュートリアルのサンプルアプリでは、SSH 認証鍵の管理にサービス アカウントと OS ログインを使用します。

演習をスキップして完全なコードサンプルを確認する場合は、GitHub ページの GoogleCloudPlatform/python-docs-samples をご覧ください。

目標

このチュートリアルでは、次の目的を達成する方法について説明します。

  • サービス アカウントを作成し、インスタンスに接続するアプリのための OS ログイン、SSH アクセスを提供するようにサービス アカウントを構成する。
  • サービス アカウントに関連付けるインスタンスを作成する。
  • 独自の SSH 認証鍵の管理と SSH 接続の確立にサービス アカウントを使用するようにインスタンス上のサンプルアプリを構成する。
  • サービス アカウントが関連付けられたインスタンスでアプリを実行する。
  • アプリを Compute Engine の外部で実行する。これにはサービス アカウント キーを手動で指定し、追加の SSH パラメータを指定する必要があります。

料金

このチュートリアルでは、Google Cloud の課金対象となるコンポーネント(Compute Engine を含む)を使用しています。

新しい Google Cloud ユーザーは無料トライアルをご利用いただける場合があります。

始める前に

  1. Google アカウントにログインします。

    Google アカウントをまだお持ちでない場合は、新しいアカウントを登録します。

  2. GCP Console のプロジェクト セレクタのページで、GCP プロジェクトを選択または作成します。

    プロジェクト セレクタのページに移動

  3. Google Cloud Platform プロジェクトに対して課金が有効になっていることを確認します。 プロジェクトに対して課金が有効になっていることを確認する方法を学習する

  4. ユーザー アカウントには、複数の Compute Engine リソースを作成、削除、および変更する権限が必要です。このチュートリアルでは、自分のプロジェクトに対する次の IAM 役割がユーザーにあることを前提としています。
    • compute.instanceAdmin.v1
    • compute.networkAdmin
    • compute.osAdminLogin
    • iam.serviceAccountAdmin
    • iam.serviceAccountKeyAdmin
    • iam.serviceAccountUser
  5. このチュートリアルでは、gcloud コマンドライン ツール コマンドの実行に Cloud Shell を使用することを前提としています。

サービス アカウントとサンプル インスタンスを作成および構成する

このチュートリアルでは、1 つのサービス アカウントと 2 つのインスタンスを使用して、アプリがリモート インスタンスで 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-ftarget というインスタンスを作成します。このインスタンスは、サービス アカウントが SSH 経由で接続するリモート インスタンスとして機能します。このインスタンスでは、プロジェクト レベルまたはインスタンス レベルで OS ログインが有効化されている必要があります。この例は、--metadata フラグを使用してこの特定のインスタンスで OS ログインを有効にする方法を示しています。このインスタンスでは API リクエストを実行する必要がないため、--no-service-account フラグと --no-scopes フラグを含めます。

    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. target という名前のインスタンス専用の SSH 接続を確立できるようにするため、compute.osAdminLogin の IAM 役割をサービス アカウントに付与します。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-fsource というインスタンスを作成します。インスタンスを ssh-account サービス アカウントに関連付けます。また、このインスタンスでの API リクエストを実行するためにサービス アカウントで必要になる cloud-platform スコープを指定します。

    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 アドレスを取得します。このアドレスは、Console の [インスタンス] ページで見つけるか、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 ログイン 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 ログイン 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 ログインに伝えます。各アカウントで保持可能な SSH 認証鍵データは最大 32 KB です。そのため、サービス アカウントが操作を完了したらあまり時間をおかずに有効期限が切れるように公開 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 ドキュメント