Oracle FunctionsでOCIインスタンスの起動・停止・再起動


 Oracle Functions (Fn)を使いOCIインスタンスを起動・停止・再起動させる関数を紹介します。
実行方法やコードの解説について記載します。

 Fn,OracleFunctionsについて、どのようなものかメモ書きを掲載したのでよろしければご覧下さい。
・Oracle Functionsに関するメモ

実行方法

 インスタンスのOCIDを指定して、対象を起動・停止・再起動させます。Goでやってます。

 OCI SDKを実行するのに本記事ではRSA秘密鍵をコンテナイメージに格納する方法を
とっていますが、こちらに記載のあるInterfaceを実装する方法のほうがセキュアなためおススメです。

※追記
 OCI SDKの実行についてリソースプリンシパルを使う方法を教えて頂きました。
・[Oracle Cloud] リソースプリンシパル を Oracle Functions で使ってみた
こちらの方法で上記セキュリティの懸念を解決できるため、是非ご参考に。

前提

Oracle Functionsセットアップ済みであること。
・対象インスタンスを停止させておく。

ソースコード用意

実物はGitに格納しております。
インスタンス起動: https://github.com/y-araki-git/fn-oci-compute/tree/master/start
インスタンス停止: https://github.com/y-araki-git/fn-oci-compute/tree/master/stop
 再起動についてはコードをこのように編集すれば実装できますという説明を、
最後に記載しています。

Functions開発環境で上記のソースコードをダウンロードします。

├Dockerfile (デプロイ時の設定を記載)
├func.go (メインの処理を記載)
├func.yaml (name: に関数名を記載)
└Gopkg.toml

Dockerfileにクレデンシャル情報記載

Dockerfileのクレデンシャル情報を修正します。最初はインスタンス起動のほうをやってみます。

ENV TENANT_OCID=Fnセットアップ時のテナンシーOCID
ENV USER_OCID=Fnセットアップ時のユーザOCID
ENV REGION=Fnセットアップ時のリージョン
ENV FINGERPRINT=Fnセットアップ時のフィンガープリント
# Target INSTANCE OCIDE
ENV INSTANCE_OCID=対象インスタンスのOCID

Fnのアプリケーション作成

インスタンス制御用にfn-compute-appというアプリケーションを作成します。

fn create app --annotation oracle.com/oci/subnetIds='["OracleFunctions用に作成したSubnetのOCID"] fn-compute-app

関数デプロイ(秘密鍵も同時に渡す)

 用意したソースコードをデプロイします。インスタンス起動用ソースコードのディレクトリに移動して実行です。

# Fnセットアップ時に作成した鍵をコピー
cp  ~/.oci/oci_api_key.pem .

# 権限変更(デプロイ後は削除してよい)
chmod 644 oci_api_key.pem

# デプロイ
fn -v deploy --app fn-compute-app --build-arg PRIVATE_KEY_NAME=oci_api_key.pem

実行

fn invokeコマンドで関数を実行し、OCIコンソール上で停止されていた対象インスタンスが起動していることを確認できればOKです。

fn invoke fn-compute-app start

停止の場合も同様にコードをデプロイし、 fn invoke fn-compute-app stop で実行できます。

コード解説 (func.go)

以下インスタンス起動のほうのメイン処理を記載する、func.goです。

package main

import (
        // Goの各パッケージインポート
        "context"
        "encoding/json"
        "io"
        "io/ioutil"
        "log"
        "os"

        // fdk(GoでFnのコードを書くための開発キット)をインポート
        fdk "github.com/fnproject/fdk-go"

        // oci go sdkで今回使うcommonとcoreパッケージをインポート
        "github.com/oracle/oci-go-sdk/common"
        "github.com/oracle/oci-go-sdk/core"
)

// main関数の実行。Goはmain関数でリターンされた処理が実行される。
// 下記のociComputeEventHandler(インスタンス起動関数)を呼び出している。
func main() {
        fdk.Handle(fdk.HandlerFunc(ociComputeEventHandler))
}

// dockerイメージデプロイ時の鍵格納パス
const privateKeyFolder string = "/function"

//ファンクション成功時メッセージ
const successMsg string = "Started Compute Instance information successfully"

//インスタンス起動処理の内容
func ociComputeEventHandler(ctx context.Context, in io.Reader, out io.Writer) {

        // クレデンシャルを変数に格納
        tenancy := os.Getenv("TENANT_OCID")
        user := os.Getenv("USER_OCID")
        region := os.Getenv("REGION")
        fingerprint := os.Getenv("FINGERPRINT")
        passphrase := os.Getenv("PASSPHRASE")
        instance := os.Getenv("INSTANCE_OCID")
        log.Println("INSTANCE_OCID ", instance)

        // クレデンシャル情報をログに出力
        log.Println("TENANT_OCID ", tenancy)
        log.Println("USER_OCID ", user)
        log.Println("REGION ", region)
        log.Println("FINGERPRINT ", fingerprint)
        log.Println("OCI_PRIVATE_KEY_FILE_NAME ", privateKeyName)
        log.Println("PRIVATE_KEY_LOCATION ", privateKeyLocation)
        log.Println("INSTANCE_OCID ", instance)

        // 秘密鍵を読み込み、変数格納
        privateKey, err := ioutil.ReadFile(privateKeyLocation)

        // 秘密鍵の読み込み失敗時
        if err == nil {
                log.Println("read private key from ", privateKeyLocation)
        } else {
                resp := FailedResponse{Message: "Unable to read private Key", Error: err.Error()}
                log.Println(resp.toString())
                json.NewEncoder(out).Encode(resp)
                return
        }

        // クレデンシャル情報設定 (OCI SDKのcommonパッケージにある関数を使って最初設定する。)
        rawConfigProvider := common.NewRawConfigurationProvider(tenancy, user, region, fingerprint, string(privateKey), common.String(passphrase))
        // クレデンシャル情報設定 (この後coreパッケージの関数を使うので、coreの関数で再度設定する。)
        cc, err := core.NewComputeClientWithConfigurationProvider(rawConfigProvider)

        // クレデンシャル設定エラー時の処理
        if err != nil {
                resp := FailedResponse{Message: "Problem getting Compute Client handle", Error: err.Error()}
                log.Println(resp.toString())
                json.NewEncoder(out).Encode(resp)
                return
        }

        //// インスタンス起動処理
        // 一番右のstartを変更すればその他インスタンス操作も可能
        // start:起動、stop:停止、reset:再起動、softstop:OSのシャットダウンコマンドを使い停止、softreset:OSのシャットダウンコマンドで再起動
        _, updateErr := cc.InstanceAction(context.Background(), core.InstanceActionRequest{InstanceId: common.String(instance), Action: core.InstanceActionActionEnum("start") })

        // エラー処理
        if updateErr != nil {
                resp := FailedResponse{Message: "Problem starting instance", Error: updateErr.Error()}
                log.Println(resp.toString())
                json.NewEncoder(out).Encode(resp)
                return
        }

        // 成功時ログ出力
        log.Println(successMsg)

        out.Write([]byte(successMsg))
}

// エラーハンドリング用の構造体
type FailedResponse struct {
        Message string
        Error   string
}

// エラーメッセージ関数
func (response FailedResponse) toString() string {
        return response.Message + " due to " + response.Error
}

OCI Go SDKの使い方

クレデンシャルを設定すればOCI SDKが使えます。そして公式ドキュメントを参照しOCI Go SDKの関数を使います。

コードのインスタンス起動部分を細かく見ます。

_, updateErr := cc.InstanceAction(context.Background(), core.InstanceActionRequest{InstanceId: common.String(instance), Action: core.InstanceActionActionEnum("start") })

まず今回OCI Go SDKのcoreパッケージに入っている関数、InstanceAction を使いました↓

InstanceActionで呼び出している、InstanceActionRequestという構造体の仕様を見ると、
InstanceId と Action という項目が必須(mandatory)だとわかります↓

Action項目 は さらにInstanceActionActionEnum という構造体を使うよう指定されていて、StartやStopなど定義されています↓

 そのためソースコード内の InstanceActionActionEnum("start") を InstanceActionActionEnum("reset") など変更すれば再起動も同様に作成できます。
func.yamlのname: startもname: resetとすれば関数名も変えられます。

最後に

 このソースコードでは対象インスタンスのOCIDを指定する必要がありますが、編集すれば複数台一気に処理することもできます。
 24/365で起動する必要のないインスタンスに適用したり、イベント機能や他の関数と連携してもいいかもしれません。