Go SDKを使用したOCIオブジェクトストレージとの対話(最初のステップ) (2021/12/28)
Go SDKを使用したOCIオブジェクトストレージとの対話(最初のステップ) (2021/12/28)
投稿者:Lucas Jellema
私はまだGoを扱うのにかなり慣れていません。私は、実装言語としてすべてGoを使用するいくつかのオープン ソース プロジェクトで作業し、おそらくそのプロジェクトにも参加したいという野望を持っています。Go を使用するオープン ソース プロジェクトの数は、[急速に] 増加しているように思われます。Goにもっと精通する必要がありそうですし、GoからOracle Cloud Infrastructure(OCI)と対話する方法を見つける必要がありそうです。(オープンソース製品にOCIサービスを活用した機能を追加するつもりです)。
この記事では、OCI Go SDK を使用した簡単な Go プログラムから、バケットとその中のオブジェクトを作成および削除するための、OCI Object Storageサービスとのやり取りについて簡単に紹介します。私の環境は、Windows10上のWSL2上のUbuntu20.4ですが、それはあまり重要ではないはずです。
私がこの記事で行っている手順
- Go環境をインストール
- OCI SDKを使用してOCIに接続するためのGoアプリケーションを作成
- OCI Object Storageと対話
Goのインストール
この記事を使って Install Go on Ubuntu - https://buildvirtual.net/how-to-upgrade-go-on-ubuntu/ - さっそく、現在のバージョンのGoを削除して、最新バージョンのGoをインストールしました。
最新リリースのURLを取得: https://go.dev/dl/
wgetでダウンロードし、untar
そして、解凍したディレクトリを/usr/localディレクトリに移動
この時点でgoを実行しようとしたのですが、早すぎました。まず、.profileの小編集です。環境変数GOPATHの定義を追加し、PATH環境変数に$GOPATHを追加します。
.profileに変更を加え、source ~/,profileで変更を適用すると、再び最新版のgoを実行できる環境が整います。
OCI SDK を使って OCI に接続するための Go アプリケーションを作成
Linuxのコマンドラインで、oci-clientという新しいディレクトリを作り、"code ." とします。このディレクトリでVS Codeを起動します。
VS Codeでコードを編集し、GoランタイムがセットアップされたUbuntu環境のコンテキストでコードを実行することができるんだ。最もシンプルなGoアプリケーションを作成してみましょう。
そして、実行します。
まだOCIとのインタラクションはありませんが、Goアプリケーションが動作するのは良いスタートです。
さて、go.modを作成して、この内容を追加します。
module oci-client
go 1.16
require github.com/oracle/oci-go-sdk/v54 v54.0.0
そして、 "go mod tidy" を実行します(ヘルプはドキュメントを参照してください)。"go mod tidy"は、go.modファイルがモジュール内のソースコードと一致することを保証します。これは、現在のモジュールのパッケージと依存関係を構築するために必要なモジュールの要件を追加し、関連するパッケージを提供しないモジュールの要件を削除します。また、go.sum に不足しているエントリを追加し、不要なエントリを削除します。"
テナント OCID、ユーザー OCID、リージョン、フィンガープリントの定数をアプリケーションで定義します(oci 設定ファイルの通常のエントリです)。
これらのパッケージをインポートします。
アプリケーションを次のリストに拡張します - initializeConfigurationProvider関数(OCIにコンタクトするため)とlistDomains関数(IDサービスAPIと対話するため)を持ちます。
最後に、OCIアカウントの秘密鍵をPEM形式で格納したファイル(例えばppk(putty private key)と呼ばれる)を作成します。
この状態で、アプリケーションを実行することができます。
最初のアクションは、ファイルppkの内容を値として環境変数をエクスポートすることです。
export pem=$(cat ./ppk)
これにより、os.Getenv("pem")を使って、Goアプリケーションから秘密鍵にアクセスできるようになります。
そして、私のローカルで実行されているGoアプリケーションとOCIとの間に接触があります。
GoアプリケーションとOCI Object Storageとの連動
ここで、SDKのObject Storage APIを紹介しましょう(ドキュメントはこちら)。これによって、Go アプリケーションからバケットとそのバケット内のオブジェクトを作成することができます。そして、この機能は、私の野望であるOCI Object Storageを活用したカスタムDaprステート・コンポーネントを実装する際に必要となるものです。
私が作ったGoアプリケーションを実行してみましょう。
このコードを実行した結果、Bucketが作成されます。
と、このBucketに指定された内容のファイルを新規に作成します。
私のローカルファイルシステムにオブジェクトをダウンロードして表示することで、内容を点検します。
そこで、私のGoアプリケーションは、OCI Object Storageと連携しています。
アプリケーションのメイン関数が移動をオーケストレートします。
ConfigurationProviderには、OCI接続の詳細(テナントとユーザーのOCID、指紋、秘密鍵など)が含まれています。このプロバイダは、Object Storage Client の作成に使用され、その後の OCI Object Storage サービスとのすべての対話に使用されます。
続いて行われる手順
- テナンシー用のネームスペースを決定
- 指定された名前のバケットが存在するかどうかを確認し、存在しない場合は作成
- 指定された名前と内容のオブジェクトをバケットに入れる(すでにその名前で存在する可能性のあるオブジェクトは上書きする)。
- オブジェクトを削除するステートメントを再び延期する
- 作成されたばかりのオブジェクトを取得し、その内容を出力に書き出す。
- 1分間スリープし、その後延期したステートメントをすべて実行する(つまり、オブジェクトが削除される)。
このコードは堅牢ではなく、エラーチェックやエラー処理もほとんどしていません。間違いなく、本番環境では使用できません。しかし、このコードは Go SDK を通じて Object Storage サービスがどのように操作されるかを示しており、適切な状況下では、バケット とオブジェクトの作成、オブジェクトの読み込み、削除を確実に行うことができます。
OCI Configuration Provider を初期化するこのコードは、OCI サービスと対話するすべての Go アプリケーションに共通です。それは、一般的な OCI 構成の詳細 - OCI CLI を使用する場合、ディレクトリ .oci の設定ファイルに一般的に見られる - を使用して、OCI REST API に署名された HTTP リクエストを作成します。
バケット がすでに存在するかどうかを確認するコード(Bucket を取得し、ステータスコード 404 (not found) を確認し、見つからなければ Bucket を作成する)は、以下のとおりです。getBucketRequest 構造体は、namespacename と bucketname の文字列へのポインタを想定していることに注意してください(ドキュメント 参照)。したがって、&namespace と &name は、これらの文字列値に対するアドレスを生成するものです(私がまだ慣れていない Go の複雑な仕組みの一つです)。
バケットが確立された状態で、オブジェクトの作成は以下のように行われます。
オブジェクトの内容を含む文字列をリーダーオブジェクトに変換する方法(少なくとも私には少し特殊に見える)を除けば、これは非常に単純なコードです。
OCI Object Storageサービスからオブジェクトを取得するのは、putObjectとほぼ逆の操作になります。
この関数は、オブジェクトの内容を文字列として返す。このオブジェクトを文字列に変換することに全く意味があると仮定してのことである。レスポンスオブジェクトは ContentLength、ContentType、Content へのポインタを返すことに注意してください。アスタリスク (*response.Content:Length) は、実際の値 (ポインタの位置) を取得するために使用されます。
最後にオブジェクトを削除 - 最も単純な操作です。
私のOCI ObjectStorageサンプルクライアントの全コードは以下の通りです。
package main |
import ( |
"context" |
"fmt" |
"io" |
"io/ioutil" |
"log" |
"net/http" |
"os" |
"strings" |
"time" |
"github.com/oracle/oci-go-sdk/v54/common" |
"github.com/oracle/oci-go-sdk/v54/objectstorage" |
) |
const tenancyOCID string = "ocid1.tenancy.oc1..aokq" |
const userOCID string = "ocid1.user.oc1..aaaaaaa" |
const region string = "us-ashburn-1" |
const fingerprint string = "02:91:6c:49:d8:04:56" |
const compartmentOCID = "ocid1.compartment.oc1..aaaaazrq" |
func initializeConfigurationProvider() common.ConfigurationProvider { |
privateKey := os.Getenv("pem") // set content of PEM in environment variable pem using export pem=$(cat ./ppk) with ppk a text file that contains the PEM private key00 |
configurationProvider := common.NewRawConfigurationProvider(tenancyOCID, userOCID, region, fingerprint, string(privateKey), nil) |
return configurationProvider |
} |
// fatalIfError is equivalent to Println() followed by a call to os.Exit(1) if error is not nil |
func fatalIfError(err error) { |
if err != nil { |
log.Fatalln(err.Error()) |
} |
} |
func getResponseStatusCode(response *http.Response) int { |
return response.StatusCode |
} |
// bucketname needs to be unique within compartment. there is no concept of "child" buckets. |
func ensureBucketExists(ctx context.Context, client objectstorage.ObjectStorageClient, namespace, name string, compartmentOCID string) { |
req := objectstorage.GetBucketRequest{ |
NamespaceName: &namespace, |
BucketName: &name, |
} |
// verify if bucket exists |
response, err := client.GetBucket(context.Background(), req) |
if err != nil { |
fatalIfError(err) |
if 404 == response.RawResponse.StatusCode { |
createBucket(ctx, client, namespace, name, compartmentOCID) |
} |
} |
} |
// bucketname needs to be unique within compartment. there is no concept of "child" buckets. |
func createBucket(ctx context.Context, client objectstorage.ObjectStorageClient, namespace string, name string, compartmentOCID string) { |
request := objectstorage.CreateBucketRequest{ |
NamespaceName: &namespace, |
} |
request.CompartmentId = &compartmentOCID |
request.Name = &name |
request.Metadata = make(map[string]string) |
request.PublicAccessType = objectstorage.CreateBucketDetailsPublicAccessTypeNopublicaccess |
_, err := client.CreateBucket(ctx, request) |
fatalIfError(err) |
fmt.Println("Created bucket ", name) |
} |
func getNamespace(ctx context.Context, client objectstorage.ObjectStorageClient) string { |
request := objectstorage.GetNamespaceRequest{} |
r, err := client.GetNamespace(ctx, request) |
fatalIfError(err) |
return *r.Value |
} |
func putObject(ctx context.Context, c objectstorage.ObjectStorageClient, namespace, bucketname, objectname string, objectContent, metadata map[string]string) error { |
request := objectstorage.PutObjectRequest{ |
NamespaceName: &namespace, |
BucketName: &bucketname, |
ObjectName: &objectname, |
ContentLength: &int64(len(objectContent)), |
PutObjectBody: ioutil.NopCloser(strings.NewReader(objectContent)), |
OpcMeta: metadata, |
} |
_, err := c.PutObject(ctx, request) |
fmt.Println("Put object ", objectname, " in bucket ", bucketname) |
return err |
} |
func getObject(ctx context.Context, c objectstorage.ObjectStorageClient, namespace string, bucketname string, objectname string) (string, error) { |
fmt.Println("get object ", objectname) |
request := objectstorage.GetObjectRequest{ |
NamespaceName: &namespace, |
BucketName: &bucketname, |
ObjectName: &objectname, |
} |
response, err := c.GetObject(ctx, request) |
// fmt.Println("get object, status code ", response.RawResponse.StatusCode) |
// fmt.Println("content length ", *response.ContentLength) |
// fmt.Println("content type ", *response.ContentType) |
buf := new(strings.Builder) |
_, err = io.Copy(buf, response.Content) |
return buf.String(), err |
} |
func deleteObject(ctx context.Context, c objectstorage.ObjectStorageClient, namespace, bucketname, objectname string) (err error) { |
request := objectstorage.DeleteObjectRequest{ |
NamespaceName: &namespace, |
BucketName: &bucketname, |
ObjectName: &objectname, |
} |
_, err = c.DeleteObject(ctx, request) |
fatalIfError(err) |
fmt.Println("Deleted object ", objectname) |
return |
} |
func main() { |
configurationProvider := initializeConfigurationProvider() |
objectStorageClient, clerr := objectstorage.NewObjectStorageClientWithConfigurationProvider(configurationProvider) |
fatalIfError(clerr) |
ctx := context.Background() |
namespace := getNamespace(ctx, objectStorageClient) |
bucketName := "GoBucket" |
ensureBucketExists(ctx, objectStorageClient, namespace, bucketName, compartmentOCID) |
objectName := "FreshObject" |
objectContent := "My Content in plain text" |
err := putObject(ctx, objectStorageClient, namespace, bucketName, objectName, objectContent, nil) |
fatalIfError(err) |
// remove the object after 60 seconds - by first deferring the removal and then waiting for 8 seconds |
defer deleteObject(ctx, objectStorageClient, namespace, bucketName, objectName) |
fmt.Println("go get object ", objectName) |
contents, error := getObject(ctx, objectStorageClient, namespace, bucketName, objectName) |
fatalIfError(error) |
fmt.Println("Object contents: ", contents) |
fmt.Println("Go Sleep") |
time.Sleep(60 * time.Second) |
// when sleep is over, deferred statements are executed prior to exiting the application |
}
リソース
パッケージインデックス: https://docs.oracle.com/en-us/iaas/tools/go/54.0.0/index.html#pkg-index
Go SDK の設定: https://github.com/oracle/oci-go-sdk/blob/master/README.md#configuring
Article DEVELOPING MICROSERVICES WITH OCI SDKS AVOIDING TO INCLUDE THE PRIVATE KEY FILE IN THE CONTAINER IMAGE | EXAMPLE IN GO by Javier Mugueta - https://javiermugueta.blog/2021/04/26/developing-microservices-with-oci-sdks-avoiding-to-include-the-private-key-file-in-the-container-image-example-in-go/
OCI Object Storage REST API - https://docs.oracle.com/en-us/iaas/api/#/en/objectstorage/20160918/Bucket/CreateBucket
OCI Go SDK - GetObjectの生成されたクライアント - https://docs.oracle.com/en-us/iaas/tools/go-sdk-examples/54.0.0/objectstorage/GetObject.go.html
OCI Go SDK - ObjectStorageパッケージの索引 - https://docs.oracle.com/en-us/iaas/tools/go/54.0.0/objectstorage/index.htm
コメント
コメントを投稿