SSH를 사용하도록 앱 구성


이 문서에서는 SSH 및 OS 로그인을 사용하여 두 가상 머신(VM) 인스턴스 간에 프로그래매틱 방식으로 연결하도록 앱을 구성하는 방법을 설명합니다. 앱에서 SSH를 사용하도록 설정하면 시스템 관리 프로세스를 자동화하는 데 유용할 수 있습니다.

이 가이드에서 사용하는 모든 코드 샘플은 GoogleCloudPlatform/python-docs-samples GitHub 페이지에서 호스팅됩니다.

시작하기 전에

  • 서비스 계정의 SSH를 설정합니다.
  • 프로젝트 또는 서비스 계정으로 실행되는 VM에 OS 로그인을 설정합니다.
  • 아직 인증을 설정하지 않았다면 설정합니다. 인증은 Google Cloud 서비스 및 API에 액세스하기 위해 ID를 확인하는 프로세스입니다. 로컬 개발 환경에서 코드 또는 샘플을 실행하려면 다음과 같이 Compute Engine에 인증하면 됩니다.

    이 페이지의 샘플 사용 방법에 대한 탭을 선택하세요.

    콘솔

    Google Cloud 콘솔을 사용하여 Google Cloud 서비스 및 API에 액세스할 때는 인증을 설정할 필요가 없습니다.

    gcloud

    1. Google Cloud CLI를 설치한 후 다음 명령어를 실행하여 초기화합니다.

      gcloud init
    2. 기본 리전 및 영역을 설정합니다.

SSH 앱 설정

앱에서 SSH 키를 관리하고 Compute Engine VM에 대한 SSH 연결을 시작하도록 설정합니다. 대략적으로 앱에서 다음을 수행해야 합니다.

  1. Google OS 로그인 라이브러리를 가져와 OS 로그인 API로 인증할 수 있게 해주는 클라이언트 라이브러리를 빌드합니다.
  2. 앱에서 OS 로그인을 사용할 수 있도록 OS Login 클라이언트 객체를 초기화합니다.
  3. VM 서비스 계정의 SSH 키를 생성하고 공개 키를 서비스 계정에 추가하는 create_ssh_key() 메서드를 구현합니다.
  4. OS Login 라이브러리에서 get_login_profile() 메서드를 호출하여 서비스 계정이 사용하는 POSIX 사용자 이름을 가져옵니다.
  5. run_ssh() 메서드를 구현하여 원격 SSH 명령어를 실행합니다.
  6. 임시 SSH 키 파일을 삭제합니다.

샘플 SSH 앱

oslogin_service_account_ssh.py 샘플 앱에서는 가능한 SSH 앱 구현을 보여줍니다. 이 예시에서는 앱이 run_ssh() 메서드를 사용하여 원격 인스턴스에서 명령어를 실행하고 명령어 결과를 반환합니다.

"""
Example of using the OS Login API to apply public SSH keys for a service
account, and use that service account to run commands on a remote
instance over SSH. This example uses zonal DNS names to address instances
on the same internal VPC network.
"""
from __future__ import annotations

import argparse
import subprocess
import time
from typing import Optional
import uuid

from google.cloud import oslogin_v1
import requests

SERVICE_ACCOUNT_METADATA_URL = (
    "http://metadata.google.internal/computeMetadata/v1/instance/"
    "service-accounts/default/email"
)
HEADERS = {"Metadata-Flavor": "Google"}

def execute(
    cmd: list[str],
    cwd: Optional[str] = None,
    capture_output: bool = False,
    env: Optional[dict] = None,
    raise_errors: bool = True,
) -> tuple[int, str]:
    """
    Run an external command (wrapper for Python subprocess).

    Args:
        cmd: The command to be run.
        cwd: Directory in which to run the command.
        capture_output: Should the command output be captured and returned or just ignored.
        env: Environmental variables passed to the child process.
        raise_errors: Should errors in run command raise exceptions.

    Returns:
        Return code and captured output.
    """
    print(f"Running command: {cmd}")
    process = subprocess.run(
        cmd,
        cwd=cwd,
        stdout=subprocess.PIPE if capture_output else subprocess.DEVNULL,
        stderr=subprocess.STDOUT,
        text=True,
        env=env,
        check=raise_errors,
    )
    output = process.stdout
    returncode = process.returncode

    if returncode:
        print(f"Command returned error status {returncode}")
        if capture_output:
            print(f"With output: {output}")

    return returncode, output

def create_ssh_key(
    oslogin_client: oslogin_v1.OsLoginServiceClient,
    account: str,
    expire_time: int = 300,
) -> str:
    """
    Generates a temporary SSH key pair and apply it to the specified account.

    Args:
        oslogin_client: OS Login client object.
        account: Name of the service account this key will be assigned to.
            This should be in form of `user/<service_account_username>`.
        expire_time: How many seconds from now should this key be valid.

    Returns:
        The path to private SSH key. Public key can be found by appending `.pub`
        to the file name.

    """
    private_key_file = f"/tmp/key-{uuid.uuid4()}"
    execute(["ssh-keygen", "-t", "rsa", "-N", "", "-f", private_key_file])

    with open(f"{private_key_file}.pub") as original:
        public_key = original.read().strip()

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

    request = oslogin_v1.ImportSshPublicKeyRequest()
    request.parent = account
    request.ssh_public_key.key = public_key
    request.ssh_public_key.expiration_time_usec = expiration

    print(f"Setting key for {account}...")
    oslogin_client.import_ssh_public_key(request)

    # Let the key properly propagate
    time.sleep(5)

    return private_key_file

def run_ssh(cmd: str, private_key_file: str, username: str, hostname: str) -> str:
    """
    Runs a command on a remote system.

    Args:
        cmd: command to be run.
        private_key_file: private SSH key to be used for authentication.
        username: username to be used for authentication.
        hostname: hostname of the machine you want to run the command on.

    Returns:
        Output of the executed command.
    """
    ssh_command = [
        "ssh",
        "-i",
        private_key_file,
        "-o",
        "StrictHostKeyChecking=no",
        "-o",
        "UserKnownHostsFile=/dev/null",
        f"{username}@{hostname}",
        cmd,
    ]
    print(f"Running ssh command: {' '.join(ssh_command)}")
    tries = 0
    while tries < 3:
        try:
            ssh = subprocess.run(
                ssh_command,
                shell=False,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                check=True,
                env={"SSH_AUTH_SOCK": ""},
                timeout=10,
            )
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as err:
            time.sleep(10)
            tries += 1
            if tries == 3:
                if isinstance(err, subprocess.CalledProcessError):
                    print(
                        f"Failed to run SSH command (return code: {err.returncode}. Output received: {err.output}"
                    )
                else:
                    print("Failed to run SSH - timed out.")
                raise err
        else:
            return ssh.stdout

def main(
    cmd: str,
    project: str,
    instance: Optional[str] = None,
    zone: Optional[str] = None,
    account: Optional[str] = None,
    hostname: Optional[str] = None,
    oslogin: Optional[oslogin_v1.OsLoginServiceClient] = None,
) -> str:
    """
    Runs a command on a remote system.

    Args:
        cmd: command to be executed on the remote host.
        project: name of the project in which te remote instance is hosted.
        instance: name of the remote system instance.
        zone: zone in which the remote system resides. I.e. us-west3-a
        account: account to be used for authentication.
        hostname: hostname of the remote system.
        oslogin: OSLogin service client object. If not provided, a new client will be created.

    Returns:
        The commands output.
    """
    # Create the OS Login API object.
    if oslogin is None:
        oslogin = oslogin_v1.OsLoginServiceClient()

    # 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 = f"users/{account}"

    # Create a new SSH key pair and associate it with the service account.
    private_key_file = create_ssh_key(oslogin, account)
    try:
        # Using the OS Login API, get the POSIX username from the login profile
        # for the service account.
        profile = oslogin.get_login_profile(name=account)
        username = profile.posix_accounts[0].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 f"{instance}.{zone}.c.{project}.internal"

        # 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.
        print(result)
        return result
    finally:
        # Shred the private key and delete the pair.
        execute(["shred", private_key_file])
        execute(["rm", private_key_file])
        execute(["rm", f"{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 located.")
    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,
    )

SSH 앱 실행

SSH를 사용하는 앱을 만든 후 oslogin_service_account_ssh.py 샘플 앱을 설치하고 실행하는 다음 예시와 비슷한 프로세스를 수행하여 앱을 실행할 수 있습니다. 설치하는 라이브러리는 앱에서 사용하는 프로그래밍 언어에 따라 다를 수 있습니다.

또는 oslogin_service_account_ssh.py를 가져오고 직접 실행하는 앱을 작성할 수 있습니다.

  1. SSH 앱을 호스팅하는 VM에 연결합니다.

  2. VM에서 pip 및 Python 3 클라이언트 라이브러리를 설치합니다.

    sudo apt update && sudo apt install python3-pip -y && pip install --upgrade google-cloud-os-login requests
    
  3. 선택사항: oslogin_service_account_ssh.py 샘플 앱을 사용하는 경우 GoogleCloudPlatform/python-docs-samples에서 다운로드합니다.

    curl -O https://raw.githubusercontent.com/GoogleCloudPlatform/python-docs-samples/master/compute/oslogin/oslogin_service_account_ssh.py
    
  4. SSH 앱을 실행합니다. 샘플 앱에서 argparse를 사용하여 명령줄의 변수를 수락합니다. 이 예시에서는 프로젝트의 다른 VM에 cowsay를 설치하고 실행하도록 앱에 지시합니다.

    python3 service_account_ssh.py \
       --cmd 'sudo apt install cowsay -y && cowsay "It works!"' \
       --project=PROJECT_ID --instance=VM_NAME --zone=ZONE
    

    다음을 바꿉니다.

    • PROJECT_ID: 앱이 연결되는 VM의 프로젝트 ID
    • VM_NAME: 앱이 연결되는 VM의 이름
    • ZONE: 앱이 연결되는 VM의 영역

    출력은 다음과 비슷합니다.

    ⋮
    ___________
     It works!
    -----------
          \   ^__^
           \  (oo)\_______
              (__)\       )\/\
                  ||----w |
                  ||     ||
    

다음 단계

  • 전체 코드 샘플을 다운로드하고 확인합니다. 전체 샘플에는 이러한 모든 메서드를 함께 사용하는 방법에 대한 작은 예시가 포함되어 있습니다. 다운로드한 후 필요에 맞게 변경하여 실행하세요.
  • SSH 키 구성과 스토리지를 포함하여 Compute Engine에서 SSH 연결이 작동하는 방식에 대해 자세히 알아보기