SSH2でJump Server経由のRemoteトンネルを作成

# はじめに
SSHでJumpサーバーを経由してRemoteサーバーに接続し、さらにそれを安定化させるために「自動復活」する仕組みをTypeScript + SSH2ライブラリを使って実装しました。

今回作ったシステムは、以下の流れで動きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3760374/ae9a1696-85d3-4ddf-a6fe-8a0bc5c95613.png)

# Remoteクラス：リモート通信の管理
Remoteクラスは、Jumpサーバー経由でRemoteサーバーに接続し、コマンド実行やファイル送受信を管理するメインクラスです。

### 🔹 インポート
```typescript:Remote.ts
import Logger from "../common/Logger";
import child_process from "child_process";
import util from "util";
import { Client, ClientChannel, ConnectConfig } from "ssh2";
import fs from "fs";
import Robin from "./Robin";
import SFTP from "./SFTP";
import ports from "@utils/constants/ports";
import credentials from "@utils/constants/credentials";
import environment from "@utils/constants/envirnment";
import CommandLine from "@class/manager/CommandLine";
import SessionDB from "@class/db/SessionDB";
import hosts from "@utils/constants/hosts";
import paths from "@utils/constants/paths";
```
- 使う外部ライブラリ (`ssh2`, `child_process`, `fs`) と、自作モジュールを読み込み。
- 接続情報、認証情報、環境情報をここで持ってくる。

次に…

```typescript:Remote.ts
const exec = util.promisify(child_process.exec);
const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf-8"));
```
- `exec`をPromiseベースにする
- `package.json`を読み込んで、設定情報（例: pubKeyEnabled）を取得

### 🔹 クラス定義
```typescript:Remote.ts
cclass Remote {
  private jumpTunnel?: Client;
  private remoteTunnel?: Client;
  private jumpSFTP?: SFTP;
  private remoteSFTP?: SFTP;
```
- `jumpTunnel`: JumpサーバーとのSSHクライアントインスタンス
- `remoteTunnel`: リモートサーバーへのSSHクライアントインスタンス
- `jumpSFTP`, `remoteSFTP`: それぞれのSFTP用オブジェクト

### 🔹 接続情報取得 (環境に応じて切り替え)
- Pomeriumポート番号取得
```typescript:Remote.ts
private pomeriumPort(): number {
  const env = SessionDB.getEnv();
  return env === "prod" ? ports.POMERIUM_PORT_PROD : ports.POMERIUM_PORT_STG;
}
```
- リモートホスト名取得

```typescript:Remote.ts
private remoteHost(): string {
  const env = SessionDB.getEnv();
  return env === "prod" ? hosts.REMOTE_HOST_PROD : hosts.REMOTE_HOST_STG;
}
```
- SSHユーザー名取得

```typescript:Remote.ts
private remoteUsername(): string {
  const env = SessionDB.getEnv();
  return env === "prod" ? credentials.SSH_USERNAME_PROD : credentials.SSH_USERNAME_STG;
}
```

- namespace取得

```typescript:Remote.ts
public static namespace(): string {
  const env = SessionDB.getEnv();
  switch (env) {
    case "stg":
      return names.NAMESPACE_STG1;
    case "stg2":
      return names.NAMESPACE_STG2;
    case "stg3":
      return names.NAMESPACE_STG3;
    case "prod":
      return names.NAMESPACE_PROD;
    default:
      return "";
  }
}
```

- SSHプロファイル取得

```typescript:Remote.ts
public static getSSHProfile(env: Env): ConnectConfig {
  const env = SessionDB.getEnv();
  switch (env) {
    case "prod":
      return {
        host: "127.0.0.1",
        port: ports.POMERIUM_PORT_PROD,
        username: credentials.SSH_USERNAME_PROD,
        password: credentials.JUMP_SERVER_PASSWORD_PROD,
        readyTimeout: 60000,
      };
    default:
      return {
        host: "127.0.0.1",
        port: ports.POMERIUM_PORT_STG,
        username: credentials.SSH_USERNAME_STG,
        password: credentials.JUMP_SERVER_PASSWORD_STG,
        readyTimeout: 60000,
      };
  }
}
```


### 🔹 トンネル初期化
- Jumpトンネル初期化

```typescript:Remote.ts
private async initJumpTunnel() {
  await this.connectJumpServer();
  await this.checkPublicKey();
}
```
**Jumpサーバー接続**と**公開鍵登録チェック**を実施

- Remoteトンネル初期化

```typescript:Remote.ts
private async initRemoteTunnel() {
  await this.fowardToRemote();
  await Robin.start();
}
```
Jumpサーバー経由でリモート接続＋Robin起動

### 🔹 開鍵認証セットアップ

```typescript:Remote.ts
private async checkPublicKey() {
  if (!packageJson.pubKeyEnabled) {
    await this.getPrivateKey();
    await this.checkAuthorizedKeys();
  }
}
```
まだ鍵認証が有効でないなら、秘密鍵作成＋公開鍵登録をする。

### 🔹 Jumpサーバー接続処理

```typescript:Remote.ts
public async connectJumpServer(): Promise<void> {
  await CommandLine.initPomerium();
  return new Promise((resolve, reject) => {
    const jumpTunnel = new Client();
    jumpTunnel
      .on("ready", () => {
        Logger.success(`Connected to jump server on port ${this.pomeriumPort()}`);
        this.jumpTunnel = jumpTunnel;
        resolve();
      })
      .on("error", (err) => {
        Logger.error(`Failed to connect to port ${this.pomeriumPort()} locally.`);
        reject(err);
      })
      .connect(this.sshProfile());
  });
}
```
Pomeriumポートに接続 → 成功したらjumpTunnel保存

### 🔹 リモートサーバーへのポートフォワード接続

```typescript:Remote.ts
public async fowardToRemote(): Promise<void> {
  if (!this.jumpTunnel) await this.initJumpTunnel();
  return new Promise((resolve, reject) => {
    if (!this.jumpTunnel) throw new Error();
    this.jumpTunnel.forwardOut(
      "127.0.0.1",
      8000,
      this.remoteHost(),
      22,
      (err, stream) => {
        if (err) reject(err);
        const remoteTunnel = new Client();
        remoteTunnel
          .on("ready", () => {
            Logger.success(`forwarded out to ${this.remoteHost()}`);
            this.remoteTunnel = remoteTunnel;
            resolve();
          })
          .on("error", (err) => reject(err))
          .connect({
            sock: stream,
            ...this.sshProfile(),
          } as ConnectConfig);
      }
    );
  });
}
```
Jumpサーバーを**中継**して**リモートサーバー22番ポートにforwardOut**する！

### 🔹 SFTP初期化

- Jump用

```typescript:Remote.ts
private async initJumpSFTP() {
  if (!this.jumpTunnel) await this.initJumpTunnel();
  if (!this.jumpTunnel) throw new Error();
  this.jumpSFTP = new SFTP(this.jumpTunnel);
}
```

- Remote用

```typescript:Remote.ts
private async initRemoteSFTP() {
  if (!this.remoteTunnel) await this.initRemoteTunnel();
  if (!this.remoteTunnel) throw new Error();
  this.remoteSFTP = new SFTP(this.remoteTunnel);
}
```
SSH接続セッションを渡して、SFTP通信できるように初期化


### 🔹 ファイルアップロード

```typescript
public async uploadFileToRemote(fileName: string): Promise<void> {
  if (!this.remoteSFTP) await this.initRemoteSFTP();
  const localPath = `${paths.LOCAL_File_DIR}/${fileName}`;
  const remotePath = `${paths.REMOTE_File_DIR}/${fileName}`;
  await this.remoteSFTP?.fastput(localPath, remotePath);
}
```
ローカルからリモートへ**ファイル転送**します。

### 🔹 SSH秘密鍵の取得・生成 `getPrivateKey`

```typescript:Remote.ts
public static async getPrivateKey(): Promise<string | undefined> {
  let privateKey;
  let isPrivateKeyExisted;
  const sshDir = `C:\\Users\\${environment.OS_USERNAME}\\.ssh`;

  if (!fs.existsSync(sshDir)) {
    Logger.warning(`.ssh folder does not exist for user ${environment.OS_USERNAME}`);
    Logger.task(`Creating ${sshDir}...`);
    fs.mkdirSync(sshDir, { recursive: true });
  }

  try {
    privateKey = fs.readFileSync(`${sshDir}\\id_rsa`, "utf8");
    Logger.success("found private key locally.");
    isPrivateKeyExisted = true;
  } catch (err) {
    Logger.warning("Private key not found.");
    Logger.task(`Generating new SSH key pair for user ${environment.OS_USERNAME}...`);
    isPrivateKeyExisted = false;
  }

  if (!isPrivateKeyExisted) {
    packageJson.pubKeyEnabled = false;
    const updatedPackageJson = JSON.stringify(packageJson, null, 2);
    fs.writeFileSync("./package.json", updatedPackageJson, "utf-8");

    try {
      await exec(
        `ssh-keygen -t rsa -b 4096 -f C:\\Users\\${environment.OS_USERNAME}\\.ssh\\id_rsa -q -N ""`
      );
      Logger.success("SSH key pair generated successfully.");
    } catch (error) {
      Logger.error(`exec error: ${error}`);
    }
  }

  return privateKey;
}
```
- `.ssh`ディレクトリがなければ作成
- 秘密鍵がなければ `ssh-keygen` で作成
- 作った場合は `package.json`を更新してフラグを保存

### 🔹 リモートコマンド実行
- jump用

### ジャンプサーバーコマンド実行 `execJump`

```typescript:Remote.ts
public async execJump(cmd: string, debug: boolean = false): Promise<string> {
  if (!this.jumpTunnel) await this.initJumpTunnel();
  
  return new Promise((resolve, reject) => {
    if (!this.jumpTunnel) throw new Error();
    this.jumpTunnel.exec(cmd, (err, stream) => {
      if (err) reject(err);

      let data = "";
      let meta = "";
      stream.on("data", (chunk: Buffer) => (data += chunk.toString()));
      stream.stderr.on("data", (chunk: Buffer) => (meta += chunk.toString()));
      stream.on("end", () => {
        if (debug) console.log(meta);
        resolve(data);
      });
    });
  });
}
```
- ジャンプサーバーでコマンドを実行して、その結果 (`stdout`) を返す。
- `stderr`はデバッグオプションで表示できる。

**remote用**

```typescript:Remote.ts
public async execRemote(cmd: string, debug: boolean = false): Promise<string> {
  if (!this.remoteTunnel) await this.initRemoteTunnel();
  
  return new Promise((resolve, reject) => {
    if (!this.remoteTunnel) throw new Error();
    this.remoteTunnel.exec(cmd, (err, stream) => {
      if (err) reject(err);

      let data = "";
      let meta = "";
      stream.on("data", (chunk: Buffer) => (data += chunk.toString()));
      stream.stderr.on("data", (chunk: Buffer) => (meta += chunk.toString()));
      stream.on("end", () => {
        if (debug) console.log(meta);
        resolve(data);
      });
    });
  });
}
```
- リモートサーバーでコマンドを実行して、その結果 (`stdout`) を返す。
- `stderr`はデバッグオプションで表示できる。



### 🔹 シェルセッションを開く `openShell`
```typescript:Remote.ts
public openShell(): Promise<ClientChannel> {
  if (!this.remoteTunnel) await this.initRemoteTunnel();
  
  return new Promise((resolve, reject) => {
    if (!this.remoteTunnel) throw new Error();
    this.remoteTunnel.shell((err, stream) => {
      if (err) reject(err);
      else resolve(stream);
    });
  });
}
```
- 対話式のシェルストリーム (`ClientChannel`) を開く。
- ログイン後にコマンドを打てるようにするため。

### ストリームから特定文字列を待つ `waitForString`
```typescript:Remote.ts
public async waitForString(
  stream: ClientChannel,
  expectedString: string
): Promise<string> {
  return new Promise((resolve, reject) => {
    let data = "";
    stream
      .on("data", (chunk: Buffer) => {
        Logger.info(chunk.toString());
        data += chunk.toString();
        if (data.includes(expectedString)) {
          resolve(data);
        }
      })
      .stderr.on("data", (data: Buffer) => {
        reject(new Error(`STDERR: ${data}`));
      })
      .on("close", () => {
        reject(new Error("unexpected stream close"));
      });
  });
}
```
- シェル通信中、特定のキーワードが現れるのを待つ。
- 例えば「ログイン成功」の文字列を待つ用途など。

### 🔹 最後：エクスポート

```typescript
export default new Remote();
```
- Remoteクラスを**インスタンス化して即エクスポート**
- どこでも `import Remote from './Remote'` で使える！

# Robinクラス：Kubernetesコンテキスト設定
Robinクラスでは、Kubernetesの認証・コンテキスト設定を自動で行います。
### 🔹 インポート
```typescript:Robin.ts
import Logger from "../common/Logger";
import SessionDB from "@class/db/SessionDB";
import { Client } from "ssh2";
import { Env } from "@utils/types/helper";
import names from "@utils/constants/names";
import hosts from "@utils/constants/hosts";
import ports from "@utils/constants/ports";
import credentials from "@utils/constants/credentials";
```
- `Logger`：ログ出力ツール
- `SessionDB`：現在の環境やNamespace情報を持っているクラス
- `ssh2`の`Client`：SSH接続クラス
- `names`, `hosts`, `ports`, `credentials`：環境ごとの定数設定（接続先、認証情報など）

### 🔹 クラス定義 `Robin`
ステートレースクラスのため、フィールドやコンストラクタを持たない

```typescript:Robin.ts
class Robin {
```

### 🔹 接続情報取得 (環境に応じて切り替え)
- Namespace取得

```typescript:Robin.ts
private namespace(): string {
  const env = SessionDB.getEnv();
  switch(env){
    case"stg1":
    return names.NAMESPACE_STG1;

    case"stg2":
    retur nnames.NAMESPACE_STG2;

    case "stg3":
    return names.NAMESPACE_STG3;

    case"prod":
    return names.NAMESPACE_PROD;

    default:
    return"";
  }
}
```

- tenant取得
```typescript:Robin.ts
private tenant(): string {
  const env = SessionDB.getEnv();
  switch (env) {
    case "stg1":
      return names.TENANT_STG1;
    case "stg2":
      return names.TENANT_STG2;
    case "stg3":
      return names.TENANT_STG3;
    case "prod":
      return "";
  }
}
```

- cluster vip取得
```typescript:Robin.ts
private clusterVIP(): string {
  const env = SessionDB.getEnv();
  switch (env) {
    case "prod":
      return hosts.CLUSTER_VIP_PROD;
    case "stg1":
      return hosts.CLUSTER_VIP_STG1;
    case "stg2":
      return hosts.CLUSTER_VIP_STG2;
    case "stg3":
      return hosts.CLUSTER_VIP_STG3;
  }
}
```

### 🔹 処理まとめて実行 `start`
```typescript:Robin.ts
public async start(): Promise<void> {
  await this.removeOldContext();
  await this.addNewContext();
  await this.login();
  await this.saveKubeConfig();
  await this.setCurrentContext();
}
```
- Kubernetesとやり取りするための準備を一括で実行
  1. 古いコンテキスト削除
  2. 新しいコンテキスト追加
  3. ログイン
  4. kubeconfigファイル保存
  5. コンテキスト切り替え

### 🔹 古いRobinコンテキスト削除 `removeOldContext`

```typescript:Robin.ts
private async removeOldContext(): Promise<void> {
  await this.execRemote("rm -rf ~/.robin");
}
```
- リモートサーバー上の`~/.robin`ディレクトリを削除する。

### 🔹 新しいRobinコンテキスト追加 `addNewContext`

```typescript:Robin.ts
private async addNewContext(): Promise<void> {
  const cmd = [
    `robin client`,
    `add-context ${this.clusterVIP}`,
    `--port ${ports.ROBIN_PORT}`,
    `--event-port ${ports.ROBIN_PORT}`,
    `--file-port ${ports.ROBIN_PORT}`,
    `--set-current --product platform`,
  ].join("");

  await this.execRemote(cmd).then((result) => {
    if (typeof result === "string") Logger.info(result);
  });
}
```
- Robinクライアントで新しいコンテキストを作成
- Cluster VIPやポート番号を使って設定
- 成功したらログ出力

### 🔹 ログインコマンドを作る `getLoginCommand`
```typescript:Robin.ts
private getLoginCommand(): string {
  switch (this.env) {
    case "prod":
      return [
        `robin login ${credentials.ROBIN_USERNAME_PROD}`,
        `--password ${credentials.ROBIN_PASSWORD_PROD}`,
      ].join("");
    default:
      return [
        `robin login ${credentials.ROBIN_USERNAME_STG}`,
        `--password ${credentials.ROBIN_PASSWORD_STG}`,
        `--tenant ${this.tenant}`,
      ].join("");
  }
}
```
- 環境によってログインコマンドを作成する
- 本番はテナントなし、ステージングはテナント付き

### 🔹 Robinログイン `login`
```typescript:Robin.ts
private async login(): Promise<void> {
  const cmd = this.getLoginCommand();
  const robinLoginResult = await this.execRemote(cmd);

  if (typeof robinLoginResult === "string") {
    if (robinLoginResult.includes("is logged into")) {
      Logger.success(
        this.tenant
          ? `Logged into Robin with ${this.tenant}`
          : "Logged into Robin"
      );
      Logger.info(robinLoginResult);
    } else {
      Logger.error("user can't login to robin");
      Logger.info(robinLoginResult);
      process.exit(1);
    }
  }
}
```
- `robin login`コマンドを実行
- 成功したらログ、失敗したらエラーログ＋プロセス終了

### 🔹 kubeconfigを保存 `saveKubeConfig`
```typescript:Robin.ts
private async saveKubeConfig(): Promise<void> {
  const cmd = [
    `mkdir -p ~/.kube`,
    `&&`,
    `robin k8s get-kube-config`,
    `--save-as-file`,
    `--dest-dir ~/.kube`,
  ].join("");

  await this.execRemote(cmd);
}
```
- リモート上に`.kube`フォルダを作成
- Robinからk8s設定ファイルを保存

### 🔹 現在のk8sコンテキストをセット `setCurrentContext`
```typescript:Robin.ts
private async setCurrentContext(): Promise<void> {
  const cmd = [
    `kubectl config`,
    `set-context`,
    `--current`,
    `--namespace=${this.namespace}`,
  ].join("");

  await this.execRemote(cmd).then((result) => {
    if (typeof result === "string") Logger.info(result);
  });
}
```
- `kubectl config set-context`で、今使うnamespaceを指定する。

### 🔹 コマンドをリモート実行 `execRemote`
```typescript:Robin.ts
private async execRemote(cmd: string): Promise<string> {
  return new Promise((resolve, reject) => {
    this.client.exec(cmd, (err, stream) => {
      if (err) reject(err);

      let data = "";
      stream.on("data", (chunk: Buffer) => (data += chunk.toString()));
      stream.on("end", () => resolve(data));
    });
  });
}
```
- SSH経由でコマンドを実行して、標準出力を返す
- エラー時はrejectする
- 
# SFTPクラス：ファイル操作
SFTPクラスでは、SSH経由でリモートにファイルをアップロード・ダウンロードします。

### 🔹 インポート

```typescript:SFTP.ts
import { Client, FileEntry, SFTPWrapper } from "ssh2";
import Logger from "../common/Logger";
import csv from "csv-parser";
import credentials from "@utils/constants/credentials";
import path from "path";
```
- **ssh2**からSSHクライアント (`Client`)、ファイルエントリ情報 (`FileEntry`)、SFTP操作用 (`SFTPWrapper`) をインポート
- **Logger**でログ出力
- **csv-parser**でCSVファイルのストリームパース
- **credentials**からユーザー情報
- **path**でファイルパス操作

# クラス定義 `SFTP`

```typescript:SFTP.ts
class SFTP {
  private tunnel: Client;
  private sftpTunnel?: SFTPWrapper;
```
- `tunnel`：SSH接続しているクライアント
- `sftpTunnel`：SFTP用セッション（未初期化なら接続する）

### 🔹  コンストラクタ

```typescript:SFTP.ts
constructor(tunnel: Client) {
  this.tunnel = tunnel;
}
```
- 外から渡されたSSHクライアント (`tunnel`) を内部に保持する

### 🔹 SFTPセッション初期化 `initSftp`

```typescript:SFTP.ts
private initSftp(): Promise<void> {
  return new Promise((resolve, reject) => {
    this.tunnel.sftp((err, sftp) => {
      if (err) reject(err);
      this.sftpTunnel = sftp;
      resolve();
    });
  });
}
```
- SSHトンネル (`tunnel`) を使って新たにSFTPセッションを開始する
- 失敗したらエラー

### 🔹 ファイルアップロード `fastput`

```typescript:SFTP.ts
public async fastput(localPath: string, remotePath: string): Promise<boolean> {
  if (!this.sftpTunnel) await this.initSftp();

  await this.createIfNotExisted(path.dirname(remotePath));
  return new Promise((resolve) => {
    if (!this.sftpTunnel) throw new Error();

    this.sftpTunnel.fastPut(localPath, remotePath, {}, (err) => {
      if (err) resolve(false);
      else resolve(true);
    });
  });
}
```
- SFTPセッションがなければ初期化
- 先にアップロード先のディレクトリが存在するか確認・作成
- `fastPut`でローカルファイルをリモートにアップロード

### 🔹 ファイルダウンロード `fastget`

```typescript:SFTP.ts
public async fastget(localPath: string, remotePath: string): Promise<boolean> {
  if (!this.sftpTunnel) await this.initSftp();

  return new Promise((resolve) => {
    if (!this.sftpTunnel) throw new Error();

    this.sftpTunnel.fastGet(remotePath, localPath, (err) => {
      if (err) resolve(false);
      else resolve(true);
    });
  });
}
```
- リモートのファイルをローカルにダウンロードする

### 🔹 ディレクトリ存在チェック `checkDir`

```typescript:SFTP.ts
public async checkDir(remotePath: string): Promise<boolean> {
  if (!this.sftpTunnel) await this.initSftp();

  return new Promise((resolve) => {
    if (!this.sftpTunnel) throw new Error();

    this.sftpTunnel.stat(remotePath, (err) => {
      if (err) resolve(false);
      else resolve(true);
    });
  });
}
```
- 指定されたパスがリモートに存在するかチェックする

### 🔹 ディレクトリ作成 `makeDir`

```typescript:SFTP.ts
public async makeDir(remotePath: string): Promise<boolean> {
  if (!this.sftpTunnel) await this.initSftp();

  return new Promise((resolve) => {
    if (!this.sftpTunnel) throw new Error();

    this.sftpTunnel.mkdir(remotePath, (err) => {
      if (err) {
        console.log(err);
        resolve(false);
      } else {
        resolve(true);
      }
    });
  });
}
```
- 指定されたパスにリモートでディレクトリを作成する

### 🔹 ファイル追記 `appendFile`

```typescript:SFTP.ts
public async appendFile(remotePath: string, text: string): Promise<void> {
  if (!this.sftpTunnel) await this.initSftp();

  return new Promise((resolve, reject) => {
    if (!this.sftpTunnel) throw new Error();

    this.sftpTunnel.appendFile(remotePath, text, (err) => {
      if (err) reject(err);
      else resolve();
    });
  });
}
```
- 指定ファイルにテキストを追記する（なければエラー）

### 🔹 CSVからBAを取得 `findBA`

```typescript:SFTP.ts
public async findBA(path: string): Promise<string> {
  if (!this.sftpTunnel) await this.initSftp();
  let isFirstRowProcessed = false;

  return new Promise((resolve) => {
    if (!this.sftpTunnel) throw new Error();

    let detectedBA = "";
    const stream = this.sftpTunnel.createReadStream(path).pipe(csv());
    stream.on("data", (data) => {
      if (!isFirstRowProcessed) {
        detectedBA = data[Object.keys(data)[0]!];
        isFirstRowProcessed = true;
      }
    });
    stream.on("end", () => resolve(detectedBA));
  });
}
```
- リモートのCSVファイルをストリームで読み込み
- 最初の行の最初のカラムを抽出して返す

### 🔹 ディレクトリ一覧取得 `readDir`

```typescript:SFTP.ts
public async readDir(path: string): Promise<FileEntry[]> {
  if (!this.sftpTunnel) await this.initSftp();

  return new Promise((resolve, reject) => {
    if (!this.sftpTunnel) throw new Error();

    this.sftpTunnel.readdir(path, (err, list) => {
      if (err) reject(err);
      else resolve(list);
    });
  });
}
```
- 指定パスのディレクトリの中身一覧を取得する

### 🔹 パス存在チェック＆作成 `createIfNotExisted`

```typescript:SFTP.ts
public async createIfNotExisted(pathName: string): Promise<void> {
  const isPathExisted = await this.checkDir(pathName);
  if (!isPathExisted) {
    Logger.warning(`Cannot found ${pathName} for user ${credentials.SSH_USERNAME_STG}`);
    Logger.task(`Trying to create ${pathName} for user ${credentials.SSH_USERNAME_STG}`);
    const pathCreated = await this.makeDir(pathName);

    if (!pathCreated) {
      Logger.error(`Failed to create folder`);
      return;
    }

    Logger.success(`Successfully created remote folder for user ${credentials.SSH_USERNAME_STG}`);
  }
}
```
- パスがなければmkdirする
- 成功・失敗ログを出す

# SessionDBクラス：セッション管理
SessionDBは、アプリ全体で使うセッション情報を保持します。

### 🔹 クラス定義 `SessionDb`

```typescript:SessionDb.ts
class SessionDb {
```
- `SessionDb`は**シングルトンパターン**（インスタンスは1個だけ）
- 各種リソースをまとめて管理するクラス

### 🔹 プライベートプロパティ

```typescript
private env: Env = "stg3";
```
- 環境名

### 🔹 環境をセットする

```typescript:SessionDb.ts
public setEnv(env: Env): void {
  this.env = env;
}
```
- 環境を設定する（例えば `prod` とか `stg3`）

### 🔹 環境を取得する

```typescript:SessionDb.ts
public getEnv(): Env {
  return this.env;
}
```
- 現在の環境を返す

# まとめ

今回の記事では、  
- Jumpサーバー経由でRemoteサーバーに接続する仕組み
- リモート操作とファイル操作の管理
- そして接続断時の自動復活まで  

フルカバーしました。

業務自動化・バッチ運用・データ収集基盤など、  
幅広い用途に応用できるので、ぜひ活用してみてください！

応用は次の記事をご参照ください！

https://qiita.com/oharu121/items/2cf1f650a89d52fa5abc