【Google Compute Engine】SSH接続できるDebian VMを構築するシェル関数を作る

Code

はじまり

リサちゃん
リサちゃん

う~ん、GUIメンドクサイ!

135ml
135ml

それではCLIで完結するようにしましょう。

Google Compute Engineとは?

Google Compute Engine(GCE)は、Googleが提供するIaaSサービスで、仮想マシンをクラウド上で実行できるサービスです。様々なOSやマシンタイプを選択でき、必要に応じてスケールアップ・ダウンが可能な柔軟なサービスとなっています。

Compute Engine
Easily create and run online VMs on Google Cloud. Choose from preset or custom machine types for web servers, databases,...

今回は、このGCE上にDebian VMを構築し、SSHですぐに接続できるようにするシェル関数を作成します。この作成したシェル関数が常用の関数やスニペット的な存在となり、開発環境やテスト環境を素早く立ち上げることができるようになります。

前提条件

このシェル関数を使用するには、以下の準備が必要です。

  1. Google Cloud SDKがインストールされていること。
  2. CLI上で、gcloud auth loginでGCPアカウントにログインしていること。
  3. 対象のプロジェクトが作成済みであること。
  4. Compute Engine APIが有効化されていること。

また、Google Chrome以外のブラウザでもChrome Remote Desktopを使える・・・?かもしれないので、その場合はChrome Remote Desktopの拡張機能を入れれば使える・・・?(Chrome以外で使ったことがないのでちょっと分かりません。)

とりあえずまあ、構築の方をやっていきましょう。

シェル関数の実装

主要な関数の紹介

今回のDebian VM構築の流れは以下のような感じです。以下のシェル関数で行っていきます。

  1. VMインスタンスを作成して設定する – create_gce_instance_and_configure
  2. ルーターとNATを作成する – create_gce_router_and_nat
  3. ファイアウォール設定とSSH接続の設定を行う – setup_gce_firewall_and_ssh
  4. SSH鍵をコピーする – copy_gce_ssh_key
  5. インスタンスにSSH接続する – connect_gce_instance

それでは、各関数の詳細を見ていきましょう。

1. create_gce_instance_and_configure

この関数は、GCEインスタンスを作成し、メタデータとスタートアップスクリプトを設定します。

function create_gce_instance_and_configure() {
  local FUNC_NAME="create_gce_instance_and_configure"

  # --help オプションのチェック
  for arg in "$@"; do
    if [ "$arg" = "--help" ]; then
      echo "[INFO] Usage: ${FUNC_NAME} INSTANCE_NAME [ZONE] [MACHINE_TYPE] [YAML_FILE] [STARTUP_SCRIPT_FILE]"
      echo "  INSTANCE_NAME       : 作成するインスタンスの名前"
      echo "  ZONE                : インスタンスを作成するゾーン (デフォルト: us-central1-a)"
      echo "  MACHINE_TYPE        : マシンタイプ (デフォルト: e2-medium)"
      echo "  STARTUP_SCRIPT_FILE : スタートアップスクリプトのファイルパス (デフォルト: ./iac/gcloud/setup_scripts/startup-script.sh)"
      echo "  YAML_FILE           : metadata設定用のYAMLファイル (デフォルト: env.yml)"
      return 0
    fi
  done

  # 引数チェック:最低1つは INSTANCE_NAME を指定する必要があります
  if [ "$#" -lt 1 ]; then
    echo "[ERROR] ${FUNC_NAME}: Usage: ${FUNC_NAME} INSTANCE_NAME [ZONE] [MACHINE_TYPE] [STARTUP_SCRIPT_FILE] [YAML_FILE]" >&2
    return 1
  fi

  # パラメータの初期化(デフォルト値付き)
  local INSTANCE_NAME="$1"
  local ZONE="${2:-us-central1-a}"
  local MACHINE_TYPE="${3:-e2-medium}"
  local YAML_FILE="${4:-env.yml}"
  local STARTUP_FILE="${5:-./iac/gcloud/setup_scripts/startup-script.sh}"

  # 1. インスタンス作成
  if ! create_gce_instance "$INSTANCE_NAME" "$ZONE" "$MACHINE_TYPE"; then
    echo "[ERROR] ${funcName}: Failed to create instance '$INSTANCE_NAME'." >&2
    return 1
  fi

  # 2. YAMLファイルからmetadataの設定
  if ! set_gce_instance_metadata_from_yaml "$INSTANCE_NAME" "$ZONE" "$YAML_FILE"; then
    echo "[ERROR] ${FUNC_NAME}: インスタンス '$INSTANCE_NAME' のmetadata設定に失敗しました。" >&2
    return 1
  fi

  # 3. とスタートアップスクリプトの登録
  if ! add_startup_script_to_gce_instance "$INSTANCE_NAME" "$ZONE" "$FILE_PATH"; then
    echo "[ERROR] ${funcName}: Failed to add startup script to instance '$INSTANCE_NAME'." >&2
    return 1
  fi

  echo "[INFO] ${FUNC_NAME}: インスタンス '$INSTANCE_NAME' の作成、スタートアップスクリプト登録、metadata設定が正常に完了しました。"
  return 0
}

以下のコマンドは、以下のパラメータを使用してVMインスタンスを作成します:

create_gce_instance_and_configure instance-test us-central1-a e2-medium
  • インスタンス名: instance-test
  • ゾーン: us-central1-a
  • マシンタイプ: e2-medium

内部的には、create_gce_instance関数を呼び出してインスタンスを作成し、set_gce_instance_metadata_from_yaml関数でメタデータを設定し、add_startup_script_to_gce_instance関数でスタートアップスクリプトを登録します。

function set_gce_instance_metadata_from_yaml() {
  local vm_name="$1"
  local zone="$2"
  local yaml_file="${3:-env.yml}"
  local fn_name="set_gce_instance_metadata_from_yaml"

  # ヘルプオプション
  if [[ "$1" == "--help" ]]; then
    echo "Usage: set_instance_metadata_from_yaml <VM_NAME> <ZONE> [YAML_FILE]"
    echo "Example: set_instance_metadata_from_yaml my-vm us-central1-a env.yml"
    echo "If YAML_FILE is not specified, 'env.yml' will be used by default."
    return 0
  fi

  # YAMLファイルの存在チェック
  if [[ ! -f "$yaml_file" ]]; then
    echo "Error: YAML file '$yaml_file' not found!"
    return 1
  fi

  # YAMLを key=value 形式に変換
  local metadata_args=""
  while IFS=":" read -r key value; do
    # 空白を除去
    key=$(echo "$key" | xargs)
    value=$(echo "$value" | xargs)

    # 空行やコメント行を無視
    if [[ -n "$key" && -n "$value" && "$key" != \#* ]]; then
      metadata_args+="$key=$value,"
    fi
  done < "$yaml_file"

  # 最後のカンマを削除
  metadata_args="${metadata_args%,}"

  if [[ -z "$metadata_args" ]]; then
    echo "[INFO] No valid metadata found in '$yaml_file'"
    return 0
  fi

  # gcloud compute instances add-metadata を実行
  gcloud compute instances add-metadata "$vm_name" --zone "$zone" --metadata "$metadata_args"

  local ret_code=$?

  if [ $ret_code -eq 0 ]; then
    echo "[INFO] ${fn_name}: Env variables of instance '${vm_name}' have set successfully."
  else
    echo "[ERROR] ${fn_name}: Failed to set env variables for instance '${vm_name}'."
    return $ret_code
  fi
}

function add_startup_script_to_gce_instance() {
  local funcName="add_startup_script_to_gce_instance"
  local DEFAULT_ZONE="us-central1-a"
  local DEFAULT_FILE_PATH="./shell/setup_scripts/startup-script.sh"

  # --help が指定された場合は利用方法を表示
  if [ "$1" = "--help" ]; then
    echo "[INFO] ${funcName}: Usage: ${funcName} VM_NAME ZONE [FILE_PATH]"
    echo "[INFO] ${funcName}: Example: ${funcName} my-vm ${DEFAULT_ZONE} ${DEFAULT_FILE_PATH}"
    echo "[INFO] ${funcName}: If ZONE is not provided, default value '${DEFAULT_ZONE}' is used."
    echo "[INFO] ${funcName}: If FILE_PATH is not provided, default value '${DEFAULT_FILE_PATH}' is used."
    return 0
  fi

  # 必須パラメータの数チェック (1~3個)
  if [ "$#" -lt 1 ] || [ "$#" -gt 3 ]; then
    echo "[ERROR] ${funcName}: Invalid number of parameters." >&2
    echo "[INFO] ${funcName}: Usage: ${funcName} VM_NAME [ZONE] [FILE_PATH]" >&2
    return 1
  fi

  local VM_NAME="$1"
  local ZONE="${2:-$DEFAULT_ZONE}"
  # 3番目の引数が未指定の場合はデフォルト値を使用
  local FILE_PATH="${3:-$DEFAULT_FILE_PATH}"

  # gcloud コマンドの実行
  if ! gcloud compute instances add-metadata "${VM_NAME}" --zone="${ZONE}" --metadata-from-file startup-script="${FILE_PATH}"; then
    echo "[ERROR] ${funcName}: Failed to add metadata from file '${FILE_PATH}' to instance '${VM_NAME}' in zone '${ZONE}'." >&2
    return 1
  fi

  echo "[INFO] ${funcName}: Successfully added metadata from file '${FILE_PATH}' to instance '${VM_NAME}' in zone '${ZONE}'."
  return 0
}

VMの起動時に特定のスクリプトを実行したい場合は、カスタムのスタートアップスクリプトを作成し、create_gce_instance_and_configure関数の第5引数として指定できます。

create_gce_instance_and_configure instance-test us-central1-a e2-medium env.yml /path/to/custom-startup-script.sh

スタートアップスクリプトに関しては後述します!

2. create_gce_router_and_nat

この関数は、Cloud RouterとCloud NATを作成します。これにより、外部IPアドレスを持たないインスタンスからインターネットにアクセスできるようになります。

function create_gce_router_and_nat() {
  local FUNC_NAME="create_gce_router_and_nat"

  # --help オプションのチェック
  for arg in "$@"; do
    if [ "$arg" = "--help" ]; then
      echo "[INFO] Usage: ${FUNC_NAME} ROUTER_NAME [REGION] [NETWORK] [NAT_NAME]"
      echo "  ROUTER_NAME : 作成するルーターの名前"
      echo "  REGION      : ルーターとNATを作成するリージョン (デフォルト: us-central1)"
      echo "  NETWORK     : ルーターを作成するネットワーク (デフォルト: default)"
      echo "  NAT_NAME    : 作成するNATの名前 (デフォルト: nat1)"
      return 0
    fi
  done

  # 引数チェック:最低1つはルーター名を指定する必要があります
  if [ $# -lt 1 ]; then
    echo "[ERROR] Usage: ${FUNC_NAME} ROUTER_NAME [REGION] [NETWORK] [NAT_NAME]" >&2
    return 1
  fi

  # パラメータの初期化(デフォルト値付き)
  local router_name="$1"
  local region="${2:-us-central1}"
  local network="${3:-default}"
  local nat_name="${4:-nat1}"

  # gcloud コマンドの存在確認
  if ! command -v gcloud >/dev/null 2>&1; then
    echo "[ERROR] Error: gcloud コマンドが見つかりません。Google Cloud SDKがインストールされているか確認してください。" >&2
    return 1
  fi

  # ルーター作成処理
  if ! gcloud compute routers create "$router_name" \\\\
    --region="$region" \\\\
    --network="$network"; then
    echo "[ERROR] Error: ルーター '$router_name' の作成に失敗しました。" >&2
    return 1
  fi

  # NAT作成処理
  if ! gcloud compute routers nats create "$nat_name" \\\\
    --router="$router_name" \\\\
    --region="$region" \\\\
    --auto-allocate-nat-external-ips \\\\
    --nat-all-subnet-ip-ranges; then
    echo "[ERROR] Error: NAT '$nat_name' の作成に失敗しました。" >&2
    return 1
  fi

  echo "ルーター '$router_name' と NAT '$nat_name' が正常に作成されました。"
}

以下のコマンドは、以下のパラメータを使用してCloud RouterとCloud NATを作成します。

create_gce_router_and_nat router1 us-central1 default nat1
  • ルーター名: router1
  • リージョン: us-central1
  • ネットワーク: default
  • NAT名: nat1

これにより、外部IPアドレスを持たないインスタンスからインターネットにアクセスできるようになります。

3. setup_gce_firewall_and_ssh

この関数は、SSH接続に必要なファイアウォールルールを設定し、SSH鍵をコピーして、インスタンスにSSH接続します。

function setup_gce_firewall_and_ssh() {
  local FUNC_NAME="setup_gce_firewall_and_ssh"

  # --help オプションのチェック
  for arg in "$@"; do
    if [ "$arg" = "--help" ]; then
      echo "[INFO] Usage: ${FUNC_NAME} INSTANCE_NAME [ZONE] [SSH_KEY_PATH]"
      echo "  INSTANCE_NAME : SSH 接続対象のインスタンス名 (例: crd1)"
      echo "  ZONE         : インスタンスのゾーン (デフォルト: us-central1-a)"
      echo "  SSH_KEY_PATH : SSH 秘密鍵ファイルのパス (デフォルト: \\\\$HOME/.ssh/google_compute_engine)"
      return 0
    fi
  done

  # 引数チェック:最低1つはインスタンス名を指定する必要があります
  if [ $# -lt 1 ]; then
    echo "[ERROR] Usage: ${FUNC_NAME} INSTANCE_NAME [ZONE] [SSH_KEY_PATH]" >&2
    return 1
  fi

  # パラメータの初期化(デフォルト値付き)
  local instance_name="$1"
  local zone="${2:-us-central1-a}"
  local ssh_key_path="${3:-$HOME/.ssh/google_compute_engine}"

  # 現在の日付 (YYYYMMDD) をサフィックスとして生成
  local today
  today=$(date +%Y%m%d)

  # gcloud コマンドの存在確認
  if ! command -v gcloud >/dev/null 2>&1; then
    echo "[ERROR] Error: gcloud コマンドが見つかりません。Google Cloud SDK がインストールされているか確認してください。" >&2
    return 1
  fi

  echo "【STEP 1】 IAP TCP 転送用の SSH ファイアウォールルールを作成中..."
  # 第一引数にルール名+実行日付を渡す
  if ! create_gce_iap_ssh_firewall_rule "allow-ssh-ingress-from-iap-${today}"; then
    echo "[ERROR] Error: IAP 用ファイアウォールルールの作成に失敗しました。" >&2
    return 1
  fi

  echo "【STEP 2】 VPC 内 SSH 用のファイアウォールルールを作成中..."
  if ! create_gce_ingress_ssh_firewall_rule "allow-ingress-ssh-${today}"; then
    echo "[ERROR] Error: VPC 内 SSH 用ファイアウォールルールの作成に失敗しました。" >&2
    return 1
  fi

  echo "【STEP 3】 SSH 秘密鍵のコピーを実行中..."
  if ! copy_gce_ssh_key "$instance_name" "$zone"; then
    echo "[ERROR] Error: インスタンス '$instance_name' への SSH 秘密鍵のコピーに失敗しました。" >&2
    return 1
  fi

  echo "【STEP 4】 インスタンスへの SSH 接続を実行中..."
  if ! connect_gce_instance "$instance_name" "$zone"; then
    echo "[ERROR] Error: インスタンス '$instance_name' への SSH 接続に失敗しました。" >&2
    return 1
  fi

  echo "[INFO] ファイアウォール設定、SSH 秘密鍵のコピー、SSH 接続が正常に完了しました。"
}

以下のコマンドは、以下の処理を行います。

setup_gce_firewall_and_ssh instance-test
  1. IAP TCP転送用のSSHファイアウォールルールを作成
  2. VPC内SSH用のファイアウォールルールを作成
  3. SSH秘密鍵のコピーを実行
  4. インスタンスへのSSH接続を実行

ファイアウォールルール名に日付を追加することで、同じ名前のルールが既に存在する場合でも、新しいルールを作成できます。

local today=$(date +%Y%m%d)
create_gce_iap_ssh_firewall_rule "allow-ssh-ingress-from-iap-${today}"

4. copy_gce_ssh_key

この関数は、ローカルのSSH鍵をGCEインスタンスにコピーします。

function copy_gce_ssh_key() {
  local FUNC_NAME="copy_gce_ssh_key"

  # --help オプションのチェック
  for arg in "$@"; do
    if [ "$arg" = "--help" ]; then
      echo "[INFO] Usage: ${FUNC_NAME} INSTANCE_NAME [ZONE] [SSH_KEY_PATH]"
      echo "  INSTANCE_NAME : SSH 秘密鍵をコピーする対象のインスタンス名"
      echo "  ZONE         : インスタンスのゾーン (デフォルト: us-central1-a)"
      echo "  SSH_KEY_PATH : SSH 秘密鍵ファイルのパス (デフォルト: \\\\$HOME/.ssh/google_compute_engine)"
      return 0
    fi
  done

  # 引数チェック: インスタンス名は必須
  if [ $# -lt 1 ]; then
    echo "[ERROR]  Usage: ${FUNC_NAME} INSTANCE_NAME [ZONE] [SSH_KEY_PATH]" >&2
    return 1
  fi

  local instance_name="$1"
  local zone="${2:-us-central1-a}"
  local ssh_key_path="${3:-$HOME/.ssh/google_compute_engine}"

  # gcloud コマンドの存在確認
  if ! command -v gcloud >/dev/null 2>&1; then
    echo "[ERROR] Error: gcloud コマンドが見つかりません。Google Cloud SDK がインストールされているか確認してください。" >&2
    return 1
  fi

  # SSH 秘密鍵のコピー処理
  if ! gcloud compute scp "$ssh_key_path" "${instance_name}:/tmp" \\\\
      --zone="$zone" \\\\
      --tunnel-through-iap; then
    echo "Error: インスタンス '$instance_name' への SSH 秘密鍵のコピーに失敗しました。" >&2
    return 1
  fi

  echo "[INFO] SSH 秘密鍵 '$ssh_key_path' がインスタンス '$instance_name' の /tmp に正常にコピーされました。"
}

以下のコマンドは、ローカルのSSH鍵をローカルからGCEインスタンスにコピーします。コピーする際に、キーフレーズを適当に長く入力する鍵ファイルを作成する必要がありますね。

setup_gce_firewall_and_ssh関数内で既に実行されているため、通常は個別に実行する必要はありませんが、必要に応じて使用できます。

copy_gce_ssh_key instance-test

5. connect_gce_instance

この関数は、GCEインスタンスにSSH接続します。

function connect_gce_instance() {
  local FUNC_NAME="connect_gce_instance"

  # --help オプションのチェック
  for arg in "$@"; do
    if [ "$arg" = "--help" ]; then
      echo "[INFO] Usage: ${FUNC_NAME} INSTANCE_NAME [ZONE]"
      echo "  INSTANCE_NAME : SSH 接続するインスタンスの名前"
      echo "  ZONE         : インスタンスが存在するゾーン (デフォルト: us-central1-a)"
      return 0
    fi
  done

  # 引数チェック: 少なくともインスタンス名は必須
  if [ $# -lt 1 ]; then
    echo "[ERROR] Usage: ${FUNC_NAME} INSTANCE_NAME [ZONE]" >&2
    return 1
  fi

  local instance_name="$1"
  local zone="${2:-us-central1-a}"

  # gcloud コマンドの存在確認
  if ! command -v gcloud >/dev/null 2>&1; then
    echo "[ERROR] Error: gcloud コマンドが見つかりません。Google Cloud SDK がインストールされているか確認してください。" >&2
    return 1
  fi

  echo ""
  echo "[INFO] **Notice**: Setup Chrome Remote Desktop on your ssh connection and [Remote Desktop Service](<https://remotedesktop.google.com/headless>) if you want."
  echo ""

  # インスタンスへの SSH 接続処理
  if ! gcloud compute ssh "$instance_name" \\\\
      --zone="$zone" \\\\
      --tunnel-through-iap; then
    echo "[ERROR] Error: インスタンス '$instance_name' への SSH 接続に失敗しました。" >&2
    return 1
  fi

  echo "[INFO] インスタンス '$instance_name' への SSH 接続が正常に完了しました。"
}

このコマンドは、GCEインスタンスにSSH接続します。setup_gce_firewall_and_ssh関数内で既に実行されているため、通常は個別に実行する必要はありませんが、既存のインスタンスに接続する場合などに使用できます。

以下のコマンドは、以下の処理を行います。

connect_gce_instance instance-test

IAP(Identity-Aware Proxy)の設定により、Google Cloud Identity-Aware Proxy(IAP)を使用してSSH接続します。IAPを使用すると、外部IPアドレスを持たないインスタンスにも安全にアクセスできます。

gcloud compute scp "$ssh_key_path" "${instance_name}:/tmp" \
  --zone="$zone" \
  --tunnel-through-iap

そんなこんなで、先程適当に長く入力したキーフレーズを再び入力すれば、GCEインスタンスにSSH接続が出来ます。このリンクからChromeリモートデスクトップにアクセスして、その「SSH経由でセットアップする」からパソコンを設定します。その過程でPINを設定すれば、その後はリモデでアクセス出来ます。

実際の使用方法

これらの関数を組み合わせて、SSH接続できるDebian VMを構築する手順は以下の通りです。

# 1. VMインスタンスを作成して設定する
create_gce_instance_and_configure instance-test us-central1-a e2-medium

# 2. ルーターとNATを作成する
create_gce_router_and_nat router1 us-central1 default nat1

# 3. ファイアウォール設定とSSH接続の設定を行う
setup_gce_firewall_and_ssh instance-test

# 4. SSH鍵をコピーする(必要に応じて)
copy_gce_ssh_key instance-test

# 5. インスタンスにSSH接続する
connect_gce_instance instance-test

その他の詳細

1. スタートアップスクリプトをカスタマイズする。

VMの起動時に特定のスクリプトを実行したい場合は、カスタムのスタートアップスクリプトを作成してVMに反映します。今回は、create_gce_instance_and_configure関数の第5引数として指定していたスタートアップスクリプトを、以下のような内容で反映しています。

#!/bin/bash -x
#
# Startup script to install Chrome remote desktop and a desktop environment.
#
# See environmental variables at then end of the script for configuration
#

function send_discord_notification() {
  # --helpオプションの確認
  if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
    cat <<EOF
Usage: ${FUNCNAME[0]} <通知テキスト> <Embedのテキスト> <Embedのフッターテキスト> <EmbedのアイコンのURL> <Embedの色> [DISCORD_WEBHOOK_URL]

Parameters:
  通知テキスト           : 通知内容のテキスト
  Embedのテキスト        : Embedメッセージの内容
  Embedのフッターテキスト : Embedフッターに表示するテキスト
  EmbedのアイコンのURL   : Embedフッターに表示するアイコンのURL
  Embedの色             : Embedの色(10進数の数値または以下の文字列指定が可能)
  DISCORD_WEBHOOK_URL    : (任意) DiscordのWebhook URL。省略した場合は環境変数DISCORD_WEBHOOK_URLを使用します。

Color Samples:
  green     : 4569935   (0x45BB4F)
  red       : 16711680  (0xFF0000)
  sky_blue  : 52479     (0x00CCFF)
  orange    : 14177041  (0xD85311)
  white     : 16777215  (0xFFFFFF)
  blue      : 39423     (0x0099FF)
  yellow    : 16770560  (0xFFE600)
  pink      : 16711833  (0xFF0099)
  purple    : 10494192  (0xA020F0)
  gray_blue : 9212588   (0x8C92AC)
  black     : 3355443   (0x333333)

Examples:
  # Using environment variable DISCORD_WEBHOOK_URL (5 arguments)
  ${FUNCNAME[0]} "通知テキスト" "Embedのテキスト" "Embedのフッターテキスト" "https://example.com/footer_icon.png" green

  # Explicitly specifying DISCORD_WEBHOOK_URL as the last argument (6 arguments)
  ${FUNCNAME[0]} "通知テキスト" "Embedのテキスト" "Embedのフッターテキスト" "https://example.com/footer_icon.png" red "https://discord.com/api/webhooks/your_webhook_id/your_webhook_token"
EOF
    return 0
  fi

  local message embed_text embed_footer_text embed_icon_url embed_color webhook_url

  # Embedなし
  # 引数の個数チェック
  if [ "$#" -eq 1 ]; then
    message="$1"
    webhook_url="${DISCORD_WEBHOOK_URL}"
    # JSONペイロードの作成
    local payload=$(cat <<EOF
{
  "content": "$message"
}
EOF
    )
    # curlでDiscordのWebhookにPOSTリクエストを送信
    curl -H "Content-Type: application/json" \
      -X POST \
      -d "$payload" \
      "$webhook_url"
    return 0
  fi

  # Embedあり
  # 引数の個数チェック
  if [ "$#" -eq 5 ]; then
    message="$1"
    embed_text="$2"
    embed_footer_text="$3"
    embed_icon_url="$4"
    embed_color="$5"
    webhook_url="${DISCORD_WEBHOOK_URL}"
  elif [ "$#" -eq 6 ]; then
    message="$1"
    embed_text="$2"
    embed_footer_text="$3"
    embed_icon_url="$4"
    embed_color="$5"
    webhook_url="$6"
  else
    echo "Error: 引数の数が正しくありません。"
    cat <<EOF
Usage: ${FUNCNAME[0]} <通知テキスト> <Embedのテキスト> <Embedのフッターテキスト> <EmbedのアイコンのURL> <Embedの色> [DISCORD_WEBHOOK_URL];
EOF
    return 1
  fi

  # いずれかの引数が空文字だった場合のエラーハンドリング
  if [ -z "$message" ] || [ -z "$embed_text" ] || [ -z "$embed_icon_url" ] || [ -z "$embed_color" ] || [ -z "$webhook_url" ]; then
    echo "Error: 引数に空文字が含まれています。"
    cat <<EOF
Usage: ${FUNCNAME[0]} <通知テキスト> <Embedのテキスト> <Embedのフッターテキスト> <EmbedのアイコンのURL> <Embedの色> [DISCORD_WEBHOOK_URL];
EOF
    return 1
  fi

  # Embedの色が文字列の場合、対応する10進数の値に変換
  case "$embed_color" in
    green)
      embed_color=4569935
      ;;
    red)
      embed_color=16711680
      ;;
    sky_blue)
      embed_color=52479
      ;;
    orange)
      embed_color=14177041
      ;;
    white)
      embed_color=16777215
      ;;
    blue)
      embed_color=39423
      ;;
    yellow)
      embed_color=16770560
      ;;
    pink)
      embed_color=16711833
      ;;
    purple)
      embed_color=10494192
      ;;
    gray_blue)
      embed_color=9212588
      ;;
    black)
      embed_color=3355443
      ;;
    *)
      # 数値が直接入力されているものとみなす
      ;;
  esac

  # JSONペイロードの作成
  local payload=$(cat <<EOF
{
  "content": "$message",
  "embeds": [
    {
      "description": "$embed_text",
      "color": $embed_color,
      "timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
      "footer": {
          "text": "$embed_footer_text",
          "icon_url": "$embed_icon_url"
      }
    }
  ]
}
EOF
  )

  # curlでDiscordのWebhookにPOSTリクエストを送信
  curl -H "Content-Type: application/json" \
    -X POST \
    -d "$payload" \
    "$webhook_url"
}

function send_discord_notification_about_gce() {
  # 第一引数以降のパラメータは send_discord_notification と同じ順序
  # 通知テキストに [GCE] プレフィックスを追加する例
  local message="$1"
  local embed_text="$2"
  local embed_color="$3"
  local embed_footer_text="GoogleComputeEngine"
  local embed_icon_url=$GCE_ICON_URL
  local webhook_url=$DISCORD_WEBHOOK_URL

  # webhook_url が空でないかチェックし、あれば最後の引数として渡す
  if [ -n "$webhook_url" ]; then
    send_discord_notification "$message" "$embed_text" "$embed_footer_text" "$embed_icon_url" "$embed_color" "$webhook_url"
  else
    send_discord_notification "$message" "$embed_text" "$embed_footer_text" "$embed_icon_url" "$embed_color"
  fi
}

function set_env_var_from_custom_metadata() {
  local key=$1
  local MY_CUSTOM_VALUE=$(curl -s -H "Metadata-Flavor: Google" \
    http://metadata.google.internal/computeMetadata/v1/instance/attributes/$key)
  export $key="$MY_CUSTOM_VALUE"
}

function install_chrome_remote_desktop() {
  curl https://dl.google.com/linux/linux_signing_key.pub \
    | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/chrome-remote-desktop.gpg
  echo "deb [arch=amd64] https://dl.google.com/linux/chrome-remote-desktop/deb stable main" \
    | sudo tee /etc/apt/sources.list.d/chrome-remote-desktop.list
  sudo apt-get update
  sudo DEBIAN_FRONTEND=noninteractive \
    apt-get install --assume-yes chrome-remote-desktop
  echo "[INFO] Setup for Chrome Remote Desktop completed successfully."
}

function install_xfce_desktop_env() {
  # Xfce デスクトップ環境の導入
  sudo DEBIAN_FRONTEND=noninteractive \
    apt install --assume-yes xfce4 desktop-base dbus-x11 xscreensaver
  # Xfce デスクトップ環境をデフォルトへ設定
  sudo bash -c 'echo "exec /etc/X11/Xsession /usr/bin/xfce4-session" > /etc/chrome-remote-desktop-session'
  # Chrome ブラウザをインストール
  curl -L -o google-chrome-stable_current_amd64.deb \
  https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
  sudo apt install --assume-yes --fix-broken ./google-chrome-stable_current_amd64.deb
  # Color Manager を停止
  sudo systemctl stop colord
  sudo systemctl disable colord
  echo "[INFO] Setup for Xfce (X Windows System Desktop Environment) completed successfully."
}

function install_desktop_env_with_xfce() {
  install_chrome_remote_desktop
  install_xfce_desktop_env
}

function install_lxqt_desktop_env() {
  # LXQt デスクトップ環境の導入
  sudo DEBIAN_FRONTEND=noninteractive \
    apt install --assume-yes lxqt dbus-x11
  # LXQt デスクトップ環境をデフォルトへ設定
  echo "exec startlxqt" > /etc/chrome-remote-desktop-session
  # Chrome ブラウザをインストール
  curl -L -o google-chrome-stable_current_amd64.deb \
    https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
  sudo apt install --assume-yes --fix-broken ./google-chrome-stable_current_amd64.deb
  # Color Manager を停止
  sudo systemctl stop colord 2>/dev/null
  sudo systemctl disable colord 2>/dev/null
  echo "[INFO] Setup for LXQt (X Windows System Desktop Environment) completed successfully."
}

function install_desktop_env_with_lxqt() {
  install_chrome_remote_desktop
  install_lxqt_desktop_env
}

function download_and_install { # args URL FILENAME
  if [[ -e "$2" ]] ; then
    echo "cannot download $1 to $2 - file exists"
    return 1;
  fi
  curl -L -o "$2" "$1" && \
    apt-get install --assume-yes --fix-broken "$2" && \
    rm "$2"
}

function is_installed {  # args PACKAGE_NAME
  dpkg-query --list "$1" | grep -q "^ii" 2>/dev/null
  return $?
}

function install_japanese_locale() {
  # install japanese locale
  sudo apt -y install locales
  sudo localectl set-locale LANG=ja_JP.UTF-8 LANGUAGE="ja_JP:ja"
  source /etc/default/locale
  echo $LANG

  # install japanese locale for desktop
  sudo apt -y install task-japanese-desktop
}

function setup_japanese_timezone() {
  sudo chmod 777 /etc/timezone
  sudo rm /etc/localtime
  echo Asia/Tokyo > /etc/timezone
  sudo chmod 644 /etc/timezone
  sudo dpkg-reconfigure -f noninteractive tzdata
}

function install_japanese_input_method() {
  # install input methods
  sudo apt update
  sudo apt install fcitx5-mozc -y

  touch ~/.profile
  inserting=$(cat <<EOF
while true; do
  dbus-update-activation-environment --systemd DBUS_SESSION_BUS_ADDRESS DISPLAY XAUTHORITY 2> /dev/null && break
done

export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS="@im=fcitx"
if [ $SHLVL = 1 ] ; then
  (fcitx5 --disable=wayland -d --verbose '*'=0 &)
  xset -r 49  > /dev/null 2>&1
fi
EOF
  )
  echo $inserting >> ~/.profile

  im-config -n fcitx5
}

function setup_dev_resources() {
  # Setup development resources
  # local home_dir="$DEV_HOME"
  sudo apt update
  sudo apt install git -y
}

function setup_vscode() {
  # Install VSCode
  wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
  sudo install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg
  sudo sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list'
  rm -f packages.microsoft.gpg
  sudo apt install apt-transport-https
  sudo apt update
  sudo apt install code
}

# Configure the following environmental variables as required:
INSTALL_XFCE=yes
INSTALL_CINNAMON=yes
INSTALL_CHROME=yes
INSTALL_FULL_DESKTOP=yes

# Any additional packages that should be installed on startup can be added here
EXTRA_PACKAGES="less bzip2 zip unzip tasksel wget"

set_env_var_from_custom_metadata VSC_PROFILE_URL
set_env_var_from_custom_metadata DISCORD_WEBHOOK_URL
set_env_var_from_custom_metadata GCE_ICON_URL
send_discord_notification "VMのカスタムメタデータを環境変数に反映したよ!"

send_discord_notification "VMのスタートアップスクリプトを実行するよ!"

apt-get update

# Install X Windows desktop system
if ! is_installed chrome-remote-desktop; then
  # install_desktop_env_with_xfce
  install_desktop_env_with_lxqt
fi

# install_desktop_env_with_xfce
# install_desktop_env_with_lxqt
send_discord_notification "VMのデスクトップ環境の設定が完了したよ!"

# [[ "$INSTALL_CHROME" = "yes" ]] && ! is_installed google-chrome-stable && \
#   download_and_install \
#     https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \
#     /tmp/google-chrome-stable_current_amd64.deb

echo "[INFO] Chrome remote desktop installation completed"
send_discord_notification "VMのChromeリモデの設定が完了したよ!"

install_japanese_locale
echo "[INFO] Install japanese locale completed"
send_discord_notification "VMのロケールの設定が完了したよ!"

setup_japanese_timezone
echo "[INFO] Setup timezone completed"
send_discord_notification "VMのタイムゾーンの設定が完了したよ!"

install_japanese_input_method
echo "[INFO] Pre-installation of japanese input methods completed"
send_discord_notification "VMのIMEの設定が完了したよ!"

setup_dev_resources
echo "[INFO] Setup development resources completed"
send_discord_notification "VMの開発リソースの設定が完了したよ!"

setup_vscode
echo "[INFO] Setup for VSCode completed"
send_discord_notification "VMのVSCodeの設定が完了したよ!"
send_discord_notification_about_gce "終わった!" "VMのスタートアップスクリプトが完了したよ!" "green"

まだ、細かいところが整備途中ですが、これでLXQtデスクトップ環境が立ち上がります。スタートアップスクリプトの実行が完了すると、自分のDiscordチャンネルにWebhookを介して通知が飛ぶようにもなっています。

2. メタデータを設定する。

そのDiscordのWebhookのURLを設定するために、VMのメタデータを設定する必要があります。

VMのメタデータを設定したい場合は、YAMLファイルを作成し、create_gce_instance_and_configure関数の第4引数として指定できます。

create_gce_instance_and_configure instance-test us-central1-a e2-medium /path/to/custom-metadata.yml

3. Chrome Remote Desktopを設定する。

connect_gce_instance関数は、SSH接続時にChrome Remote Desktopの設定を促すメッセージを表示します。これを利用して、GUIが必要な作業も行えるようになります。

echo "[INFO] **Notice**: Setup Chrome Remote Desktop on your ssh connection and [Remote Desktop Service](<https://remotedesktop.google.com/headless>) if you want."

4. リモデで接続した後。

リモデしてVM内のデスクトップから実行しないと設定できない事柄があったりもします。例えば、日本語キーボード配列に変更することです。

function update_japanese_input_method() {
  # Set keyboard layout
  setxkbmap -model jp109a -layout jp

  fcitx5 &
  fcitx5-configtool
}

トラブルシューティング

1. SSH接続ができない場合。

IAPを使用したSSH接続ができない場合は、以下を確認してください。

  • ファイアウォールルールが正しく設定されているか。
  • インスタンスが実行中であるか。
  • 適切なIAM権限が付与されているか。

2. 権限エラーが発生する場合。

必要なIAM権限が付与されているか確認してください。最低限、以下の権限が必要です。

  • compute.instances.create
  • compute.instances.get
  • compute.instances.list
  • compute.zones.get
  • compute.routers.create
  • compute.routers.update
  • compute.networks.updatePolicy

3. クォータ制限に達した場合

GCPプロジェクトには、リージョンごとにVMインスタンスの数やCPUコアの数などのクォータ制限があります。クォータ制限に達した場合は、Google Cloudのコンソールからクォータの引き上げをリクエストしてください。

まとめ

今回は、Google Compute Engine上にSSH接続できるDebian VMを簡単に構築するためのシェル関数を作成しました。この関数を使えば、コマンド一つでVMを作成し、SSH上でChromeリモートデスクトップのPINコードを作成すれば、すぐにSSH接続することができます。

以下の処理を紹介しました。

  • GCEインスタンスを立ち上げる。
  • メタデータを設定する。
  • スタートアップスクリプトを配置する。
  • ルーターを設定する。
  • NATを設定する。
  • ファイアウォールルールを設定する。
  • SSH 秘密鍵を設定して、GCEインスタンス内にコピーする。
  • GCEインスタンスにSSH接続する。

開発環境やテスト環境の素早い構築に役立つだけでなく、このシェル関数をベースにして、より複雑な環境構築のための関数を作成することもできます。例えば、特定のソフトウェアが自動的にインストールされるVMや、特定の設定が適用されたVMなど、用途に応じてカスタマイズしてみてください。

おしまい

リサちゃん
リサちゃん

よーし、Enterキーで立ち上がるぞぉ

135ml
135ml

すぐにDebianを立ち上げられます。

以上になります!

コメント

タイトルとURLをコピーしました