【Oracle Cloud】インスタンスの起動と停止をスケジュールしたい

カバー

[!] この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

Oracle Cloud Infrastructure (以下 OCI と表記) の Compute サービスを利用して、Windows インスタンスを月に数回、不定期に利用しています。 これらインスタンスは常時稼働し続ける必要はないため、必要な時に起動し、作業が終わると手動で停止する運用をしています。 手動での運用のため、以下の懸念点があります。

  • 停止忘れによる不要なコストの発生
  • 誤操作による他のインスタンスの停止

これらを防ぐために、毎日自動で停止することができないかと思い、手段を検討し、なるべくコストをかけないように導入したものがこの記事の内容になります。

構成と概要

毎日決まった時間に停止する場合、Amazon Web Services (AWS) の Amazon EventBridge や Google Cloud Platform (GCP) の Cloud Scheduler のような定期的にイベントを発行するサービスが必要になります。 OCI で検討しましたが、2023 年 2 月末時点では該当のサービスを見つけることはできませんでした。 そこで、常時稼働のインスタンスを 1 台用意し、Cron を利用することで定期実行を実現しました。

構成は以下のとおりです。

概要図

構築

ユーザーやグループ、ポリシーの作成

まず、OCI 上にインスタンスを起動 / 停止できる権限を持つユーザーを作成します。 権限設定のため、ユーザーが所属するグループと権限を設定するポリシーも作成します。

  • ユーザーの作成
    • ユーザー名: instance-power-action-user
  • グループの作成
    • グループ名: instance-power-action-group
    • グループメンバーに instance-power-action-user を追加
  • ポリシーの作成
    • ポリシー名: instance-power-action-policy
    • コンパートメント: インスタンスの起動 / 停止を制御する予定のコンパートメント
    • ポリシービルダー: 手動エディターから以下を設定 (<対象コンパートメント名> は修正する)
      • Allow group instance-power-action-group to use instances in compartment <対象コンパートメント名> where any {request.permission = 'INSTANCE_READ', request.permission = 'INSTANCE_POWER_ACTIONS'}
      • 内容は instance-power-action-group グループに所属するユーザーに <対象コンパートメント名> 上のインスタンスの一覧取得と電源管理 (起動 / 停止等) の権限を付与する

権限の付与にはインスタンスプリンシパル等別の方法を用いてもよいです。

Cron 実行用インスタンスの作成

次に、Cron を実行するインスタンスを作成します。 このインスタンスは稼働したままになるため、シェイプは無料枠 (Always Free) の VM.Standard.E2.1.Micro または VM.Standard.A1.Flex (CPU: 1 OCPU / Mem: 1 GB) を利用します。 無料枠がない場合は Cron でスクリプトを実行するだけのため、最小設定の VM.Standard.A1.Flex (CPU: 1 OCPU / Mem: 1 GB) を用意します (その場合、Instance: ($0.01 + $0.0015) * 24h * 30d = $8.28/月 + BootVolume: $0.0255 * 50GB = $1.275/月 = $9.555 ≒ \1,300/月 かかります)。 OCI ユーザーとの紐づけができて、oci コマンドが実行できればよいため、ローカルやオンプレミスの環境で Cron 実行も可能です。 しかし、ネットワークが異なるため疎通ができなくなる、といった、不要なリスクや依存も発生するため、できるだけ同じネットワークのインスタンス上で実行するのがよいかと思います。

oci コマンドのインストールと利用設定

インスタンス作成後、インスタンスの自動起動 / 停止に oci コマンドを利用するため、コマンドのインストールと利用設定をします。

# oci コマンドインストール
# https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/cliinstall.htm#InstallingCLI__oraclelinux8
$ sudo dnf update
$ sudo dnf install -y oraclelinux-developer-release-el8 python36-oci-cli

# バージョン表示でコマンドが利用できるか確認
$ oci --version
3.23.0

OCI Documentation - Setting up the Configuration File を参照し、 OCI の instance-power-action-user ユーザーをインスタンスの opc ユーザーに紐づけます。

# oci コマンドの実行ユーザーの設定
$ oci setup config

# 設定ファイルの配置先 (デフォルトのまま)
Enter a location for your config [/home/opc/.oci/config]: <そのまま Enter>
# OCI ユーザーの OCID
Enter a user OCID: <instance-power-action-user ユーザーの OCID (ocid1.user.oc1..xxxxxxxxxx)>
# OCI のテナンシーの OCID
Enter a tenancy OCID: <テナンシーの OCID (ocid1.tenancy.oc1..xxxxxxxxxx)>
# リージョンを選択 (東京リージョンを選択)
Enter a region by index or name(e.g. ... 13: ap-tokyo-1, ... ): 13
# RSA キーの生成の有無を問われるため、y (生成) を選択
Do you want to generate a new API Signing RSA key pair? (If you decline you will be asked to supply the path to an existing key.) [Y/n]: y
# 出力先ディレクトリーの選択 (デフォルトのまま)
Enter a directory for your keys to be created [/home/opc/.oci]: <そのまま Enter>
# 出力する API キーの名前 (デフォルトのまま)
Enter a name for your key [oci_api_key]: <そのまま Enter>
# 出力された API キーの公開鍵のパス
Public key written to: /home/opc/.oci/oci_api_key_public.pem
# パスフレーズの設定 (デフォルトの空のまま)
Enter a passphrase for your private key (empty for no passphrase): <そのまま Enter>
# 出力された API キーの秘密鍵のパス
Private key written to: /home/opc/.oci/oci_api_key.pem
# フィンガープリント
Fingerprint: xx:xx:xx:xx: ...
# 出力された API の設定ファイル
Config written to /home/opc/.oci/config

# 出力を確認
$ ls /home/opc/.oci/
config   oci_api_key.pem   oci_api_key_public.pem

# 公開鍵の表示 (内容を控える)
$ cat /home/opc/.oci/oci_api_key_public.pem

-----BEGIN PUBLIC KEY-----
xxxxxxxxxx ...
-----END PUBLIC KEY-----

OCI コンソール上で実行ユーザー (instance-power-action-user) の API キーに追加します。 実行ユーザーでログインし、「API キーの追加」から「公開キーの貼り付け」を選択し、前手順で控えた公開鍵を貼り付けます (詳細は OCI Documentation - IAM - Managing User Credentials - Using the Console - To add an API signing key - To upload or paste an API key を参照してください)。

インスタンスの自動起動 / 停止スクリプトの作成

次に、インスタンスの自動起動 / 停止を操作するスクリプトを作成します。 このスクリプトでは以下内容を実装しています (正常系のみ記載し、エラールートは省略しています)。

  1. スクリプト実行時の時刻を取得
  2. スクリプトを実行しているインスタンスと、同じリージョン、かつ、同じコンパートメントを対象にインスタンスリストを取得
  3. それぞれのインスタンスのフリーフォームタグから指定したタグ名と値を取得し、値と 1. で取得した時刻が一致したインスタンスのみを抽出
  4. 抽出したインスタンスに対して、起動 / 停止コマンドを実行
## ディレクトリーを作成
$ sudo mkdir -p /opt/auto-startstop-instance/bin/

## スクリプトを作成
$ sudo vim /opt/auto-startstop-instance/bin/auto-startstop-instance.sh
#!/bin/bash

# 自動起動したいインスタンスに付与するタグの名前
readonly AUTOSTART_TAG_NAME="Auto-Start"
# 自動停止したいインスタンスに付与するタグの名前
readonly AUTOSTOP_TAG_NAME="Auto-Stop"

# タグが付与されたインスタンスを起動する
# Globals:
#   AUTOSTART_TAG_NAME: 検索するフリーフォームタグの名前
#   HOUR: 起動対象時刻 (時)
# Arguments:
#   $1: Json 形式のインスタンスのリスト
# Returns:
#   0: 正常終了
function start_instance () {
  local instance_list=$1

  # 起動時刻が現時刻と一致したもののみ対象とする
  local targets=$( echo ${instance_list} | jq '[.data[] | select(."freeform-tags"."'${AUTOSTART_TAG_NAME}'" == "'${HOUR}'")]' )
  local length=$( echo ${targets} | jq length )

  # タグが付与されたインスタンスの起動
  for i in $( seq 0 $(( ${length} - 1 )) ) ; do
    local target_id=$( echo ${targets} | jq -r '.['${i}'].id' )
    echo "start: ${target_id}"
    # oci コマンドの実行結果は不要なため、/dev/null で破棄
    oci compute instance action --action start --instance-id ${target_id} >/dev/null
  done

  return 0
}

# タグが付与されたインスタンスを停止する
# Globals:
#   AUTOSTOP_TAG_NAME: 検索するフリーフォームタグの名前
#   HOUR: 停止対象時刻 (時)
# Arguments:
#   $1: Json 形式のインスタンスのリスト
# Returns:
#   0: 正常終了
function stop_instance () {
  local instance_list=$1

  # 停止時刻が現時刻と一致したもののみ対象とする
  local targets=$( echo ${instance_list} | jq '[.data[] | select(."freeform-tags"."'${AUTOSTOP_TAG_NAME}'" == "'${HOUR}'")]' )
  local length=$( echo ${targets} | jq length )

  # タグが付与されたインスタンスの停止
  for i in $( seq 0 $(( ${length} - 1 )) ) ; do
    local target_id=$( echo ${targets} | jq -r '.['${i}'].id' )
    echo "stop: ${target_id}"
    # oci コマンドの実行結果は不要なため、/dev/null で破棄
    oci compute instance action --action softstop --instance-id ${target_id} >/dev/null
  done

  return 0
}

# --- main --- #

# 現在の JST 時刻を取得 (0 詰めはしない)
readonly HOUR=$( TZ=Asia/Tokyo date "+%-H" )

# インスタンスと同じコンパートメント OCID を取得
readonly COMPARTMENT_ID=$( oci-compartmentid )

# インスタンスのリストを取得
INSTANCE_LIST=$( oci compute instance list --compartment-id ${COMPARTMENT_ID} )

while :
do
  # インスタンスの起動
  start_instance "${INSTANCE_LIST}"

  # インスタンスの停止
  stop_instance "${INSTANCE_LIST}"

  # インスタンスのリストに次のページがあるか確認
  NEXT_PAGE=$( echo ${INSTANCE_LIST} | jq '."opc-next-page"?' )

  # 次のページがない (=最後のページの) 場合、while を抜ける
  ## 最後のページの場合は null を返す
  ## 最後のページが limit の倍数ちょうどで終わる場合は空文字列が入る (例: limit = 5 の場合に 10 インスタンスがある場合)
  if [[ ${NEXT_PAGE} == null || ${NEXT_PAGE} == "" ]]; then
    break
  fi

  # 次のページ取得
  INSTANCE_LIST=$( oci compute instance list --compartment-id ${COMPARTMENT_ID} --page ${NEXT_PAGE} )
done

echo "finish"
# 所有者は opc ユーザーに変更、実行権限を付与
$ sudo chown -R opc:opc /opt/auto-startstop-instance/
$ sudo chmod 755 /opt/auto-startstop-instance/bin/auto-startstop-instance.sh

# 権限と所有者情報を確認
$ ls -l /opt/
drwxr-xr-x. 3 opc opc 17 Feb 14 06:23 auto-startstop-instance
$ ls -l /opt/auto-startstop-instance/
drwxr-xr-x. 2 opc opc 40 Feb 14 06:24 bin
$ ls -l /opt/auto-startstop-instance/bin/
-rwxr-xr-x. 1 opc opc 0 Feb 14 06:24 auto-startstop-instance.sh

Cron の定期実行設定

定期的に実行するため、Cron の設定を登録します。

# Cron の設定
$ sudo vim /etc/cron.d/auto-startstop-instance
# 毎時 0 分に opc ユーザーの権限で実行
0 * * * * opc bash /opt/auto-startstop-instance/bin/auto-startstop-instance.sh
# cron 設定ファイルの権限を変更 (cron はサービスのため、root ユーザーが read できればよい)
$ sudo chown root:root /etc/cron.d/auto-startstop-instance
$ sudo chmod 644 /etc/cron.d/auto-startstop-instance

# 確認
$ ls -l /etc/cron.d/auto-startstop-instance
-rw-r--r--. 1 root root 128 Feb 14 06:32 /etc/cron.d/auto-startstop-instance

# Cron を再起動して設定適用
$ sudo systemctl restart crond

以上でスクリプトの配置と自動実行設定は完了です。 これで、毎時 0 分にタグの付いたインスタンスを検出し、自動でインスタンスを起動 / 停止する準備ができました。

自動起動 / 停止の設定方法

インスタンスのフリーフォームタグに、スクリプトで指定したタグ名 (上記のスクリプトの場合は、Auto-Start, Auto-Stop)、 および、値に時刻が追加されている場合に、自動起動 / 停止を実行します (時刻は、日本時間で 0 詰めなしで 0 ~ 23 で記載)。

例: 9 時に自動起動、18 時に自動停止の場合
9 時に自動起動、18 時に自動停止の場合

自動起動 / 停止 が不要な場合は、タグを削除、または、値を時刻以外の値にすると、対象から外れます。

例: 20 時に自動停止のみの場合 (自動起動の値は時刻で無効な -- を入力)
20 時に自動停止のみの場合

これにより、インスタンスの稼働時間を 9 ~ 18 時に設定した場合、24 - 9 = 15 時間分、インスタンスの稼働時間を減らすことができ、コストをおよそ 半額 ~ 3/8 程度に減らすことができます。

おわりに

OCI の定期実行サービスは 2023 年 2 月末時点で存在しなかったため、インスタンスの無料枠を利用して Cron で定期的にスクリプトを実行し、インスタンスの自動起動 / 停止を実装しました。 結果として、インスタンスの消し忘れは減り、コストの削減につなげることができました。 ゆくゆくは AWS の EventBridge や GCP の Cloud Scheduler のような定期的なイベントの発行サービスができるかもしれませんが、 それまではこの手法で定期実行を実現するしかなさそうです。

また、コマンドをインスタンスの自動起動 / 停止だけではなく、他のサービスのコマンドの実行に変えることで、定期的なイベントの呼び出しに利用ができます。 無料枠のインスタンスのパフォーマンスがあまりよくないため、スクリプトやタスクはできるだけ他のサービスを呼び出すだけにして、 実行は OCI Functions (旧 Oracle Functions) のような他サービスに任せるとよいかもしれません。

OCI ではインスタンスを OS 側でシャットダウンした場合に、ステータスが稼働状態のままとなり、コストがかかります。 これを防ぐよい方法がないか、今後も継続して検討する予定です。 (定期実行ではありませんが、Oracle Health Checks サービスを利用して、OS の稼働状態を確認し、イベントを発行することで、インスタンスの停止が実現できるのではないかと思っています)


TOP
アルファロゴ 株式会社アルファシステムズは、ITサービス事業を展開しています。このブログでは、技術的な取り組みを紹介しています。X(旧Twitter)で更新通知をしています。