Firestore でのセッション処理

このチュートリアルでは、Cloud Run でセッションを取り扱う方法を示します。

多くのアプリには、認証設定とユーザー設定用のセッション処理が必要です。 Gorilla ウェブ ツールキット sessions パッケージには、この機能を実行するためのファイル システムベースの実装が付属しています。ただし、記録されるセッションがインスタンス間で異なる場合があるため、この実装は複数のインスタンスから提供できるアプリには適していません。gorilla/sessions パッケージには、Cookie ベースの実装も付属しています。ただし、この実装では、セッション ID だけでなく、Cookie を暗号化してセッション全体をクライアントに保存する必要があるため、一部のアプリでは大きすぎる場合があります。

目標

  • アプリを作成する。
  • アプリをローカルで実行する。
  • アプリを Cloud Run にデプロイする。

費用

このドキュメントでは、課金対象である次のコンポーネントを使用します。 Google Cloud

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。

新規の Google Cloud ユーザーは無料トライアルをご利用いただける場合があります。

このドキュメントに記載されているタスクの完了後、作成したリソースを削除すると、それ以上の請求は発生しません。詳細については、クリーンアップをご覧ください。

始める前に

  1. アカウントにログインします Google Cloud を初めて使用する場合は、 アカウントを作成して、実際のシナリオで Google プロダクトのパフォーマンスを評価してください。 Google Cloud新規のお客様には、ワークロードの実行、テスト、デプロイができる無料クレジット $300 分を差し上げます。
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. Enable the Firestore API.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the API

  5. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  6. Verify that billing is enabled for your Google Cloud project.

  7. Enable the Firestore API.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the API

  8. コンソールで、Cloud Shellでアプリを開きます。 Google Cloud

    Cloud Shell に移動

    Cloud Shell を使用すると、ブラウザからコマンドラインで直接クラウド リソースにアクセスできます。ブラウザで Cloud Shell を開き、 [**確認**] をクリックしてサンプルコードをダウンロードし、アプリ ディレクトリに移動します。

  9. Cloud Shell で、gcloud CLI を構成して 新しい Google Cloud プロジェクトを使用します:
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID
    

プロジェクトの設定

  1. ターミナル ウィンドウで、サンプルアプリ リポジトリのクローンをローカル マシンに作成します。

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git
  2. サンプルコードが入っているディレクトリに移動します。

    cd golang-samples/getting-started/sessions

ウェブアプリについて理解する

このアプリは、ユーザーごとに異なる言語で挨拶を表示します。リピーターは常に同じ言語で挨拶されます。

さまざまな言語で挨拶が表示されている複数のアプリ ウィンドウ。

アプリでユーザーの設定を保存するには、現在のユーザーに関する情報をセッションに保存する方法が必要です。このサンプルアプリは、Firestore を使用してセッション データを保存します。

  1. アプリは、依存関係をインポートし、sessions.Store と HTML テンプレートを保持するための app 関数を定義して、挨拶の一覧を定義することにより起動します。

    import (
    	"context"
    	"html/template"
    	"log"
    	"math/rand"
    	"net/http"
    	"os"
    
    	"cloud.google.com/go/firestore"
    )
    
    // app stores a sessions.Store. Create a new app with newApp.
    type app struct {
    	tmpl         *template.Template
    	collectionID string
    	projectID    string
    }
    
    // session stores the client's session information.
    // This type is also used for executing the template.
    type session struct {
    	Greetings string `json:"greeting"`
    	Views     int    `json:"views"`
    }
    
    // greetings are the random greetings that will be assigned to sessions.
    var greetings = []string{
    	"Hello World",
    	"Hallo Welt",
    	"Ciao Mondo",
    	"Salut le Monde",
    	"Hola Mundo",
    }
    
  2. 次に、アプリが main 関数を定義します。この関数は、新しい app インスタンスを作成し、インデックス ハンドラを登録して、HTTP サーバーを起動します。newApp 関数は、projectID 値と collectionID 値を設定して HTML テンプレートを解析することで、app インスタンスを作成します。

    func main() {
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    	}
    
    	projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
    	if projectID == "" {
    		log.Fatal("GOOGLE_CLOUD_PROJECT must be set")
    	}
    
    	// collectionID is a non-empty identifier for this app, it is used as the Firestore
    	// collection name that stores the sessions.
    	//
    	// Set it to something more descriptive for your app.
    	collectionID := "hello-views"
    
    	a, err := newApp(projectID, collectionID)
    	if err != nil {
    		log.Fatalf("newApp: %v", err)
    	}
    
    	http.HandleFunc("/", a.index)
    
    	log.Printf("Listening on port %s", port)
    	if err := http.ListenAndServe(":"+port, nil); err != nil {
    		log.Fatal(err)
    	}
    }
    
    // newApp creates a new app.
    func newApp(projectID, collectionID string) (app, error) {
    	tmpl, err := template.New("Index").Parse(`<body>{{.Views}} {{if eq .Views 1}}view{{else}}views{{end}} for "{{.Greetings}}"</body>`)
    	if err != nil {
    		log.Fatalf("template.New: %v", err)
    	}
    
    	return app{
    		tmpl:         tmpl,
    		collectionID: collectionID,
    		projectID:    projectID,
    	}, nil
    }
    
  3. インデックス ハンドラは、ユーザーのセッションを取得し、必要に応じてセッションを作成します。新しいセッションには、ランダムな言語とビューカウント「0」が割り当てられます。次に、ビューカウントが 1 増加し、セッションが保存され、HTML テンプレートにより応答が書き込まれます。

    
    // index uses sessions to assign users a random greeting and keep track of
    // views.
    func (a *app) index(w http.ResponseWriter, r *http.Request) {
    	// Allows requests only for the root path ("/") to prevent duplicate calls.
    	if r.RequestURI != "/" {
    		http.NotFound(w, r)
    		return
    	}
    
    	var session session
    	var doc *firestore.DocumentRef
    
    	isNewSession := false
    
    	ctx := context.Background()
    
    	client, err := firestore.NewClient(ctx, a.projectID)
    	if err != nil {
    		log.Fatalf("firestore.NewClient: %v", err)
    	}
    	defer client.Close()
    
    	// cookieName is a non-empty identifier for this app, it is used as the key name
    	// that contains the session's id value.
    	//
    	// Set it to something more descriptive for your app.
    	cookieName := "session_id"
    
    	// If err is different to nil, it means the cookie has not been set, so it will be created.
    	cookie, err := r.Cookie(cookieName)
    	if err != nil {
    		// isNewSession flag is set to true
    		isNewSession = true
    	}
    
    	// If isNewSession flag is true, the session will be created
    	if isNewSession {
    		// Get unique id for new document
    		doc = client.Collection(a.collectionID).NewDoc()
    
    		session.Greetings = greetings[rand.Intn(len(greetings))]
    		session.Views = 1
    
    		// Cookie is set
    		cookie = &http.Cookie{
    			Name:  cookieName,
    			Value: doc.ID,
    		}
    		http.SetCookie(w, cookie)
    	} else {
    		// The session exists
    
    		// Retrieve document from collection by ID
    		docSnapshot, err := client.Collection(a.collectionID).Doc(cookie.Value).Get(ctx)
    		if err != nil {
    			log.Printf("doc.Get error: %v", err)
    			http.Error(w, "Error getting session", http.StatusInternalServerError)
    			return
    		}
    
    		// Unmarshal documents's content to local type
    		err = docSnapshot.DataTo(&session)
    		if err != nil {
    			log.Printf("doc.DataTo error: %v", err)
    			http.Error(w, "Error parsing session", http.StatusInternalServerError)
    			return
    		}
    
    		doc = docSnapshot.Ref
    
    		// Add 1 to current views value
    		session.Views++
    	}
    
    	// The document is created/updated
    	_, err = doc.Set(ctx, session)
    	if err != nil {
    		log.Printf("doc.Set error: %v", err)
    		http.Error(w, "Error creating session", http.StatusInternalServerError)
    		return
    	}
    
    	if err := a.tmpl.Execute(w, session); err != nil {
    		log.Printf("Execute: %v", err)
    	}
    }
    

    次の図は、Firestore が Cloud Run アプリのセッションを取り扱う方法を示しています。

    アーキテクチャの図: ユーザー、Cloud Run、Firestore。

セッションの削除

コンソールでセッション データを削除するか、自動削除戦略を実装できます。Google Cloud セッションに、Memcache や Redis などのストレージ ソリューションを使用すると、期限切れのセッションが自動的に削除されます。

ローカルでの実行

  1. ターミナル ウィンドウで、sessions バイナリをビルドします。

    go build
    
  2. HTTP サーバーを起動します。

    ./sessions
    
  3. ウェブブラウザでアプリを表示します。

    Cloud Shell

    Cloud Shell ツールバーの [ウェブでプレビュー] ウェブでプレビュー アイコンをクリックし、[ポート 8080 でプレビュー] を選択します。

    ローカルマシン

    ブラウザで、http://localhost:8080 にアクセスします。

    「Hello World」、「Hallo Welt」、「Hola mundo」、「Salut le Monde」、「Ciao Mondo」の 5 つの挨拶のいずれかが表示されます。別のブラウザまたはシークレット モードでページを開くと、別の言語で表示されます。 セッション データは Google Cloud コンソールで表示して編集できます。

     Google Cloud コンソールの Firestore セッション。

  4. HTTP サーバーを停止するには、ターミナル ウィンドウで Control+C を押します。

Cloud Run でのデプロイと実行

Cloud Run を使用すると、高負荷の元で大量の データを使用して正常に動作するアプリをビルドしてデプロイできます。

  1. アプリを Cloud Run にデプロイします。
        gcloud run deploy firestore-tutorial-go 
    --source . --allow-unauthenticated --port=8080
    --set-env-vars=GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID
  2. このコマンドにより返された URL にアクセスして、ページ移動のときセッションデータがどのように保持されるかを確認してください。

これで、Cloud Run インスタンスで実行しているウェブサーバーから挨拶が配信されます。

アプリのデバッグ

Cloud Run アプリに接続できない場合は、次の点を確認してください。

  1. gcloud デプロイ コマンドが正常に終了して、エラーを出力しなかったことを確認します。エラー( message=Build failed など)がある場合は、修正し、 Cloud Run アプリのデプロイを再度行ってください。
  2. コンソールで、[ログ エクスプローラ] ページに移動します。 Google Cloud

    [ログ エクスプローラ] ページに移動

    1. [最近選択したリソース] プルダウン リストで [Cloud Run アプリケーション] をクリックし、[すべての module_id] をクリックします。アプリにアクセスした以降のリクエストのリストが表示されます。リクエストのリストが表示されない場合は、プルダウン リストで [すべての module_id] が選択されていることを確認します。 エラー メッセージが Google Cloud コンソールに出力された場合は、アプリのコードが ウェブアプリの作成に関するセクション内のコードと一致することを確認します

    2. Firestore API が有効になっていることを確認します。

クリーンアップ

プロジェクトを削除する

  1. コンソールで [**リソースの管理**] ページに移動します。 Google Cloud

    [リソースの管理] に移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、 [Shut down] をクリックしてプロジェクトを削除します。

Cloud Run インスタンスの削除

  1. コンソールで、App Engine の [**バージョン**] ページに移動します。 Google Cloud

    [バージョン] に移動

  2. デフォルト以外で削除するアプリのバージョンのチェックボックスをオンにします。 削除します。
  3. アプリのバージョンを削除するには、 [Delete] をクリックします。

次のステップ