Firestore でのセッション処理

このチュートリアルでは、Cloud Run でセッションを処理する方法を示します。

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

目標

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

費用

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

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

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

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

始める前に

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  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 (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 (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. Google Cloud コンソールで、Cloud Shell でアプリを開きます。

    Cloud Shell に移動

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

  9. Cloud Shell で、新しい Google Cloud プロジェクトを使用するように gcloud CLI を構成します。
    # 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 関数は、projectIDcollectionID の値を設定して app インスタンスを作成し、HTML テンプレートを解析します。

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

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

クリーンアップ

プロジェクトの削除

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Cloud Run インスタンスの削除

  1. In the Google Cloud console, go to the Versions page for App Engine.

    Go to Versions

  2. Select the checkbox for the non-default app version that you want to delete.
  3. アプリのバージョンを削除するには、[削除] をクリックします。

次のステップ