PythonによるSSHリモート制御とデータベース操作の実装手法

Paramikoライブラリによるセキュアな接続基盤

インフラの自動化において、リモートサーバーへの安全なコマンド実行とファイル転送は不可欠な要素です。PythonのparamikoモジュールはSSHプロトコルをネイティブにサポートしており、Transportオブジェクトを中核とするアーキテクチャを採用しています。このトランスポートレイヤーは暗号化された通信チャネルを維持するため、同じセッションでコマンド実行(SSHClient)とファイル転送(SFTPClient)を効率的に統合できます。

以下は、パスワード認証と公開鍵認証の両方をサポートし、接続の確立からコマンド実行・ファイル転送までを一元的に管理するマネージャークラスの実装例です。頻繁なコネクションの作成・破棄を避けるため、単一セッションを保持する設計にしています。

import paramiko
import uuid
import os

class RemoteInfrastructureClient:
    def __init__(self, host, port, user, auth_mode="password", password=None, key_path=None):
        self.host = host
        self.port = port
        self.user = user
        self.auth_mode = auth_mode
        self.password = password
        self.key_path = key_path
        self.channel = None

    def establish_connection(self):
        self.channel = paramiko.Transport((self.host, self.port))
        if self.auth_mode == "key" and self.key_path:
            private_key = paramiko.RSAKey.from_private_key_file(self.key_path)
            self.channel.connect(username=self.user, pkey=private_key)
        else:
            self.channel.connect(username=self.user, password=self.password)

    def execute_remote_command(self, command):
        session = paramiko.SSHClient()
        session._transport = self.channel
        stdin, stdout, stderr = session.exec_command(command)
        result_out = stdout.read().decode("utf-8")
        result_err = stderr.read().decode("utf-8")
        return result_out, result_err

    def transfer_asset(self, local_src, remote_dst, direction="upload"):
        file_client = paramiko.SFTPClient.from_transport(self.channel)
        if direction == "upload":
            file_client.put(local_src, remote_dst)
        else:
            file_client.get(remote_dst, local_src)
        file_client.close()

    def terminate(self):
        if self.channel and self.channel.is_active():
            self.channel.close()

インタラクティブ端末(バスタミオン機)の構築

バスタミオン機(Jump Server)では、管理者が一元化された入口から複数のサーバーに対して対話型でコマンドを入力できる環境を提供します。この動作を実現するには、ローカル端末の入力をリアルタイムでリモートセッションへストリーミングし、同時にサーバーからの出力を取得して表示する必要があります。また、監査目的で入力されたコマンドをログに記録する機能も実装が求められます。

標準入力のデフォルト動作(行単位でのバッファリングやCtrl+Cによる中断など)を解除し、生データ(raw mode)として1バイト単位で処理することで、端末エミュレーションに近い挙動を実現できます。さらに、タブキーによる補完入力をログに記録しないようフィルタリングするロジックを組み込みます。

import paramiko
import sys
import select
import termios
import tty
import socket

class InteractiveTerminalSession:
    def __init__(self, host, port, user, password):
        self.host = host
        self.port = port
        self.user = user
        self.password = password
        self.transport = None
        self.shell_channel = None
        self.original_tty_settings = None

    def start_terminal(self):
        self.transport = paramiko.Transport((self.host, self.port))
        self.transport.start_client()
        self.transport.auth_password(self.user, self.password)
        
        self.shell_channel = self.transport.open_session()
        self.shell_channel.get_pty()
        self.shell_channel.invoke_shell()

        self.original_tty_settings = termios.tcgetattr(sys.stdin)
        tty.setraw(sys.stdin.fileno())
        self.shell_channel.settimeout(0.0)

        audit_log = open("remote_session_audit.log", "ab")
        try:
            while True:
                ready_read, _, _ = select.select([self.shell_channel, sys.stdin], [], [], 1)
                
                if self.shell_channel in ready_read:
                    try:
                        server_output = self.shell_channel.recv(1024)
                        if len(server_output) == 0:
                            break
                        sys.stdout.buffer.write(server_output)
                        sys.stdout.flush()
                    except socket.timeout:
                        pass

                if sys.stdin in ready_read:
                    local_input = sys.stdin.read(1)
                    if len(local_input) == 0:
                        break
                    
                    if local_input != "\t":
                        audit_log.write(local_input)
                    self.shell_channel.send(local_input)
        finally:
            audit_log.close()
            termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.original_tty_settings)
            self.shell_channel.close()
            self.transport.close()

Windows環境ではtermiosおよびptyが存在しないため、標準入力の監視とリモート出力の受信を別スレッドで非同期に実行するアーキテクチャに変更する必要があります。スレッド間通信にはソケットチャネルを直接利用し、メインスレッドでキーボード入力を待ち受け、受信スレッドでサーバー応答を標準出力へ流す構成が一般的です。

Python連携によるデータベース操作

自動化スクリプトからリレーショナルデータベース(MySQLなど)に対してCRUD操作を実行する際、DB-API 2.0準拠のドライバーを使用します。セキュリティの観点から、ユーザー入力や変数値を直接SQL文字列に連結するのではなく、パラメータ化されたクエリを実装することが必須です。これによりSQLインジェクションのリスクを完全に排除できます。

以下は、接続のライフサイクル管理、パラメータ化クエリによる一貫したデータ操作、およびバッチ処理をサポートするデータアクセス層の実装例です。

import MySQLdb
from MySQLdb import cursors

class DatabaseOperationLayer:
    def __init__(self, host, user, password, database):
        self.connection_params = {
            "host": host,
            "user": user,
            "passwd": password,
            "db": database,
            "charset": "utf8mb4",
            "autocommit": False
        }

    def _get_connection(self):
        return MySQLdb.connect(**self.connection_params)

    def bulk_insert(self, table_name, columns, data_rows):
        conn = self._get_connection()
        try:
            with conn.cursor() as cursor:
                placeholders = ", ".join(["%s"] * len(columns))
                column_str = ", ".join(columns)
                insert_query = f"INSERT INTO {table_name} ({column_str}) VALUES ({placeholders})"
                affected_count = cursor.executemany(insert_query, data_rows)
            conn.commit()
            return affected_count
        finally:
            conn.close()

    def execute_dml(self, query, params=None):
        conn = self._get_connection()
        try:
            with conn.cursor() as cursor:
                updated_rows = cursor.execute(query, params or ())
            conn.commit()
            return updated_rows
        finally:
            conn.close()

    def query_records(self, query, params=None):
        conn = self._get_connection()
        try:
            with conn.cursor(cursors.DictCursor) as cursor:
                cursor.execute(query, params or ())
                return cursor.fetchall()
        finally:
            conn.close()

上記のレイヤーを用いることで、データの挿入、更新、削除、および検索操作を一貫したインターフェース経由で実行できます。DictCursorを使用すると、取得結果がカラム名をキーとした辞書型リストとして返されるため、インデックス番号依存の脆弱なコードを排除し、可読性と保守性を向上させることができます。トランザクションのコミットとリソースの解放はtry...finallyブロックまたはコンテキスト管理により確実に行う設計が推奨されます。

タグ: paramiko Python SSH sftp MySQL

8月2日 03:17 投稿