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

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

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

目標

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

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

料金

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

Cloud Platform を初めて使用する方は、無料トライアルをご利用いただけます。

始める前に

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

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

  2. GCP プロジェクトを選択または作成します。

    [リソースの管理] ページに移動

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

    課金を有効にする方法について

  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-f 内に target という名前のインスタンスを作成します。このインスタンスは、サービス アカウントが 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 役割は、サービス アカウントにインスタンスのスーパーユーザー権限も付与します。この役割をプロジェクト レベルで付与してプロジェクト内のすべてのインスタンスに適用することもできますが、この例では権限の制限を保持するために、役割を特定のインスタンス レベルで付与します。

    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 サービス アカウントに関連付けます。また、このインスタンスでの 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 クライアント ライブラリはアプリケーションのデフォルト認証情報を使用してサービス アカウントとして認証し、そのサービス アカウントに前述の手順で付与した役割を使用できます。

次に、あるインスタンスから別のインスタンスに 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 アドレスを取得します。このアドレスは、Console の [インスタンス] ページで見つけるか、gcloud コマンドライン ツールから次のコマンドを実行して見つけることができます。

    gcloud compute instances describe target \
       --project $PROJECT_ID --zone us-central1-f
    
  2. 前の例で使用した ssh-account サービス アカウント用のサービス アカウント キーを作成し、そのキーファイルをローカル ワークステーションにダウンロードします。

  3. この例を実行するシステムにサービス アカウント キーをコピーします。

  4. この例を実行するシステム上のデバイスを開きます。

  5. サービス アカウント キーの .json ファイルがあるパスを指示する GOOGLE_APPLICATION_CREDENTIALS 環境変数を設定します。キーが 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 ログイン 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 ドキュメント