サーバーレス アーキテクチャ:AWS Lambda、Upstash Redis、Go によるスケーラブルでコスト効率の高いアプリ
イントロ
サーバーレス コンピューティング プラットフォームは素晴らしいものですが、サーバーレス データベースがなければ制限が多すぎます。
次のコース「CI/CD の要素」のプラットフォームを構築しているときに、特定の目的でサーバーとして AWS Lambda を使用することに決めたため、サーバーレス データベースが必要になりました。私が求めていた要件は次のとおりです。
<オル>eu-west-1) ).Upstash Redis は上記の要件をすべて満たしており、その点で素晴らしい仕事をします。
- 従量課金制ですか?
- ✅ 非常に手頃な価格で開始でき、規模も拡大可能です!
- 低遅延?
- ✅
<AWS Lambda 内からのクエリによるレイテンシは 1 ミリ秒!
- ✅
- 素晴らしい DevX?
- ✅ 標準の Redis です。そうですね。
この記事では、AWS Lambda 内から Upstash Redis を使用する方法を見ていき、必要に応じて十分な速度を確保しながら、同時にローカルでテストしたり、必要に応じて別のプラットフォームにデプロイしたりできるようにコードを保守可能に保つ方法を説明します。
何を実装するのでしょうか?
簡単にするために、3 つの API エンドポイントのみを実装します。
<オル>GET|POST /login userId を受け入れるエンドポイント GET のクエリ パラメータとして 、または POST で送信されたフォーム値内 リクエスト。このエンドポイントはセッション ID を生成して Redis に保存し、その後のアクセスのために Cookie も設定します。 GET テストが簡単になるだけです。 🙃GET /lessons/completed エンドポイントは、ユーザーがログインしていること(つまり、セッション ID を含む Cookie を持つこと)を必要とし、ユーザーが完了したすべてのレッスンとその日時を含む JSON レスポンスを返します。POST /lessons/{lessonSlug}/mark-complete エンドポイントはログインしているユーザーを必要とし(つまり、セッション ID を持つ Cookie を持っている)、lessonSlug で示されるレッスンをマークします。 現在の時刻で完了したもの。
注:以下のコードにはいくつかの欠落があるため、これは運用環境でコピー&ペーストできるコードではありません。たとえば、指定された lessonSlug が 更新前から存在します。ログインエンドポイントはパスワードを受け入れ、セッション ID などを作成する前に適切なソルト/ハッシュ検証を行う必要もあります。
1.セットアップ
- 以下に詳述する完全なコードは、私の
aws-playgroundにも存在します。 すべてがどのように連携しているかを確認したい場合は、リポジトリを参照してください。
以下でわかるように、2 つのエントリポイント、つまり 2 つの実行可能コマンドを作成しています。 1 つは通常のローカル サーバー用で、もう 1 つは AWS Lambda 用です。こうすることで、ロジック全体をローカルでテストできるようになり、必要に応じて標準の単体テスト/統合テストを使用できるようになります。
それらの唯一の違いは、以下のセクション 1.2 と 1.3 に示されています。
1.1 ワークスペース
コードに入る前に、Go の作業ディレクトリをセットアップしましょう。
<オル>eu-west-1 を使用します。 (ヨーロッパ、アイルランド) この記事では

上記を完了したら、ワークスペースを作成できるようになります。この記事の残りの部分では、コードが ~/dev/aws-lambda-upstash-redis の下にあると仮定します。 .
mkdir -p ~/dev/aws-lambda-upstash-redis
cd ~/dev/aws-lambda-upstash-redis 次に、Go パッケージを作成します。
go mod init com.upstash/example/aws-lambda-upstash-redis 1.2 ローカルサーバーのエントリポイント
- 次のコードを
~/dev/aws-lambda-upstash-redis/cmd/server/main.goに貼り付けます。 .
package main
import (
"log"
"net/http"
"os"
"com.upstash/example/aws-lambda-upstash-redis/core"
)
func main() {
mux := core.NewMux()
port := os.Getenv("PORT")
if len(port) == 0 {
port = "5000"
}
if err := http.ListenAndServe(":"+port, mux); err != nil {
log.Fatal(err)
}
} 1.3 AWS Lambda エントリポイント
- 次のコードを
~/dev/aws-lambda-upstash-redis/cmd/lambda/main.goに貼り付けます。 .
package main
import (
"com.upstash/example/aws-lambda-upstash-redis/core"
"github.com/aws/aws-lambda-go/lambda"
"github.com/awslabs/aws-lambda-go-api-proxy/httpadapter"
)
func main() {
mux := core.NewMux()
lambda.Start(httpadapter.NewV2(mux).ProxyWithContext)
} 1.4 コア ロジック
メインコアロジックは core に入ります。 パッケージは、上記の両方のエントリ ポイントで共有されます。
- 次のコードを
~/dev/aws-lambda-upstash-redis/core/lib.goに貼り付けます。 .
package core
import (
"github.com/go-chi/chi/v5"
)
func NewMux() *chi.Mux {
r := chi.NewRouter()
return r
} 1.5 ビルド/コンパイル
私は通常、小さな makefile を書きます。 コンパイルするたびに長いコマンドを入力するのを避けるため、以下を ~/dev/aws-lambda-upstash-redis/makefile にコピーします。 :
default: build
clean:
rm -rf build/
build: build-lambda build-server
build-lambda: clean
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o build/handler cmd/lambda/main.go
cd build/ && zip handler.zip ./handler
build-server: clean
CGO_ENABLED=0 go build -o build/server cmd/server/main.go 現時点では詳細についてあまり心配する必要はありませんが、これにより次のことが可能になります。
make build-server:サーバーをローカルで実行するためのバイナリをビルドします (実行可能ファイル./build/server) ).make build-lambda:AWS Lambda でサーバーを実行するためのバイナリをビルドします (実行可能ファイル./build/handler) および./build/handler.zip).makeまたはmake build両方を行います。
CGO_ENABLED=0 このオプションは、実行可能バイナリが自己完結型 (つまり、静的にコンパイルされる) であることを確認します。 GOOS=linux GOARCH=amd64 Mac または Windows システムをローカルで使用している場合、AWS Lambda の Linux 環境をクロスコンパイルして一致させるには、オプションが必要です。
次に、go mod tidy を実行します。 すべてのコードの依存関係を取得します。 Go の依存関係を追加または削除するたびに、これを忘れずに実行してください。
最後に、make を実行します。 コードをさらに深く掘り下げる前に、すべてをビルドしてワークスペースがセットアップされていることを確認するために、一度だけ実行します。
2. API の実装
このセクションでは、常に ~/dev/aws-lambda-upstash-redis/core/lib.go 内で作業します。 ファイルです。
次の数行は、驚くべき go-chi を使用して、前に説明した API エンドポイントを定義します。 ライブラリ。
import (
//...
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func NewMux() *chi.Mux {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/login", login)
r.Post("/login", login)
r.Group(func(r chi.Router) {
r.Use(UsersWithSessionOnly)
r.Get("/lessons/completed", listLessonsCompleted)
r.Post("/lessons/{lessonSlug}/mark-complete", markLessonComplete)
})
return r
}
上のスニペットでは、r.Group(...) 共有レイヤーを作成し、その内部で定義されたルートに共通のミドルウェアを適用できます。この場合、独自のミドルウェア UsersWithSessionOnly を追加します。 これにより、後ほど説明しますが、リクエストにアクティブなセッション ID を持つ Cookie が含まれることが保証されます。
2.1 UsersWithSessionOnly ミドルウェア
このミドルウェアでは、以下を実装したいと考えています。
<オル>context.Context に保存します。 ダウンストリームのミドルウェアまたはハンドラーで利用できるようにするため。まず、あらゆる場所で使用されるいくつかのインポートと定義用の定型コードが必要です。
import (
//...
"log"
"os"
"strings"
"github.com/go-redis/redis/v8"
)
type contextKey struct {
name string
}
const (
COOKIE_AUTH_NAME = "xxx_session_id"
)
var (
CTX_USER_ID = &contextKey{"LoggedInUserId"}
redisDb = NewClient()
)
func NewClient() *redis.Client {
redisUrl := strings.TrimSpace(os.Getenv("UPSTASH_REDIS_URL"))
if redisUrl == "" {
log.Fatalln("Required env UPSTASH_REDIS_URL not set!")
}
opt, _ := redis.ParseURL(redisUrl)
redisDb := redis.NewClient(opt)
return redisDb
} 次に、認証ミドルウェアのメイン ロジックです。
// UsersWithSessionOnly middleware restricts access to just logged-in users.
// If validation passes, then the context will contain the user id (CTX_USER_ID).
func UsersWithSessionOnly(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie(COOKIE_AUTH_NAME)
if err != nil {
render.Status(r, http.StatusForbidden)
render.JSON(w, r, struct{}{})
return
}
ctx := r.Context()
userId, err := redisDb.Get(ctx, "session:"+c.Value).Result()
if err == redis.Nil {
// If session is not found then user is forbidden from accessing the API!
render.Status(r, http.StatusForbidden)
render.JSON(w, r, struct{}{})
return
} else if err != nil {
// Something went wrong querying Redis!
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, struct{ Message string }{Message: "We could not validate the provided session ID"})
return
}
// Set it for downstream middleware and handlers.
next.ServeHTTP(w, r.WithContext(context.WithValue(ctx, CTX_USER_ID, userId)))
})
} 2.2 markLessonComplete(...)
これは、lessonSlug で示されるレッスンを Redis に保存する簡単な操作です。 path パラメータはリクエストの現時点で完了しています。
Redis では、マップ内の各キーと値のペアがキーとしてレッスン、値として完了日となるマップをユーザーごとに保持したいと考えています。したがって、HSET を使用します。 Redisコマンド。レッスンごとに個別のキーを保存することもできますが、これにより、後でユーザーのすべてのレッスンを一度に取得することが容易になります。
func markLessonComplete(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
lessonSlug := chi.URLParam(r, "lessonSlug")
userId := r.Context().Value(CTX_USER_ID).(string)
timeNow := time.Now().Format(time.RFC3339)
err := redisDb.HSet(ctx, "lessons:"+userId, lessonSlug, timeNow).Err()
if err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, struct{ Message string }{Message: "We could not save your progression..."})
return
}
render.JSON(w, r, struct {
LessonSlug string
LastCompleted string
}{
lessonSlug,
timeNow,
})
} 2.3 listLessonsCompleted(...)
前のセクションと同様の方法で、ここではレッスン完了のマップ全体を返し、それを JSON 応答でユーザーに返すだけです。 HGETALL を使用します。 このコマンド。
func listLessonsCompleted(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
userId := r.Context().Value(CTX_USER_ID).(string)
lessons, err := redisDb.HGetAll(ctx, "lessons:"+userId).Result()
if err == redis.Nil {
lessons = map[string]string{}
} else if err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, struct{ Message string }{Message: "We could not load your lessons..."})
return
}
render.JSON(w, r, struct {
Lessons map[string]string
}{
lessons,
})
} 2.4 ログイン(...)
最後にログインエンドポイントです。繰り返しになりますが、次のコードはいかなる検証も行わないため、運用環境にコピーしないでください。この記事の目的として、Redis がどのようにクエリを実行するか、およびセッション ID の Cookie を設定する方法のみに関心があります。
セッション ID は ksuid によって生成されます。 このライブラリには通常の UUID に比べていくつかの利点があり、アクティブになるのは 1 時間だけであると考えられます。 Redis SET の有効期限機能を使用します。 1 時間後にデータベースから自動的に削除するコマンド。
import (
// ...
"github.com/segmentio/ksuid"
)
func login(w http.ResponseWriter, r *http.Request) {
// Check credentials and update redis session and return Set-Cookie
// WARNING: You should do an actual validation in production for credentials!
// ...
// For now we always assume correctness and automatically create a session token
// by saving it to Redis, and also setting it as a cookie.
userId := strings.TrimSpace(r.FormValue("userId"))
if userId == "" {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, struct{ Message string }{Message: "Missing required userId"})
return
}
sessionId := ksuid.New()
redisDb.Set(r.Context(), "session:"+sessionId.String(), userId, time.Hour*1)
http.SetCookie(w, &http.Cookie{
Name: COOKIE_AUTH_NAME, Value: sessionId.String(),
Path: "/", MaxAge: int((time.Hour * 1).Seconds()),
// This should be true when deploying in production (https), but locally we need it false (http).
Secure: false,
})
http.Redirect(w, r, "/lessons/completed", http.StatusTemporaryRedirect)
} 3.デモ - ローカル
ふーん、コード量が多かったですね 😅
簡単なデモを行って、すべてが期待どおりに動作することを確認しましょう。
- まず、
UPSTASH_REDIS_URLを設定します。 環境変数を上記のセクション 1 で作成したデータベースの URL に設定します。それは詳細で見つけることができます。 データベース ページのタブ (上記のセクション 1.1 を参照)。
export UPSTASH_REDIS_URL="<your-url-here>" - 次に、ローカル サーバーを構築して実行します。
make build-server && ./build/server ブラウザのテスト
次に、http://localhost:5000/lessons/completed にアクセスして、ブラウザでテストを行ってみましょう。

403 - Forbidden を取得します http://localhost:5000/login?userId=lambros にアクセスしてログインしましょう。

現在ログインしていますが、自動的に /lessons/completed にリダイレクトされます。 、しかし、それらは空です。それでは、レッスンを完了としてマークしましょう。 console 内 ブラウザの devtools 内のタブで、次を実行します。
await (
await fetch("http://localhost:5000/lessons/123/mark-complete", {
method: "POST",
credentials: "same-origin",
})
).json();
// Should output something like:
// {LessonSlug: '123', LastCompleted: '2022-10-12T02:01:14+03:00'} http://localhost:5000/lessons/completed にアクセスすると、このレッスンがマーク付きで表示されるはずです。
{ "Lessons": { "123": "2022-10-12T02:01:14+03:00" } } さあ、出来上がりです。すべて正常に動作します!
最近リリースされたオンライン データ ブラウザを使用して Redis データベース自体を調べると、期待したデータがそこに存在することも証明されます。

4. AWS ラムダ
AWS Lambda をテストしてデプロイするには、sam を使用します。 クリ。
-
まず、SAM cli をセットアップし、ユーザー/ロールに適切な権限があることを確認します。
-
samcli が機能するには Cloudformation テンプレートが必要なので、以下をaws-iac/sam-template.ymlにコピーします。 :
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Defines all the AWS resources we need for our Upstash Redis API.
Resources:
# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html
GoUpstashRedis:
Type: AWS::Serverless::Function
Properties:
CodeUri: ../build/handler.zip
Handler: handler
Runtime: go1.x
MemorySize: 512
FunctionUrlConfig:
AuthType: NONE
Cors:
AllowCredentials: false
AllowMethods: ["*"]
AllowOrigins: ["*"]
Outputs:
GoUpstashRedisApi:
Description: "Endpoint URL"
Value: !GetAtt GoUpstashRedisUrl.FunctionUrl
GoUpstashRedis:
Description: "Lambda Function ARN"
Value: !GetAtt GoUpstashRedis.Arn
GoUpstashRedisIamRole:
Description: "Implicit IAM Role created for GoUpstashRedis"
Value: !GetAtt GoUpstashRedisRole.Arn - AWS Lambda のハンドラー バンドルを構築します。
make build-lambda - 以下を
makefileに追加します コード変更後のデプロイを容易にするため:
sam-deploy: build-lambda
sam deploy -t aws-iac/sam-template.yml --stack-name "UpstashRedisGoArticleStackDemo" --region eu-west-1 --resolve-s3 --no-confirm-changeset --no-fail-on-empty-changeset --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND
- 指定されたリージョンにデプロイします (前のコマンドを参照)。
make sam-deploy - 以下のような出力が得られるはずです。
CloudFormation outputs from deployed stack
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Key GoUpstashRedis
Description Lambda Function ARN
Value arn:aws:lambda:eu-west-1:<redacted>:function:UpstashRedisGoArticleStackDem-GoUpstashRedis-baB8dQPkTfg0
Key GoUpstashRedisIamRole
Description Implicit IAM Role created for GoUpstashRedis
Value arn:aws:iam::<redacted>:role/UpstashRedisGoArticleStac-GoUpstashRedisRole-16UWC7HR6KII8
Key GoUpstashRedisApi
Description Endpoint URL
Value https://6pmmwqmg5vec3bcsldabckaf5i0nlgje.lambda-url.eu-west-1.on.aws/
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Successfully created/updated stack - UpstashRedisGoArticleStackDemo in eu-west-1 - デプロイされた AWS Lambda の URL が印刷出力に表示されます。この場合は
https://6pmmwqmg5vec3bcsldabckaf5i0nlgje.lambda-url.eu-west-1.on.aws/。したがって、localhostを使用する前にブラウザで行ったデモ手順を自由に繰り返してください。 実際のドメインを使用してください。- または、CloudFormation スタック
UpstashRedisGoArticleStackDemoの出力で新しく作成した関数の URL を見つけることもできます。 Cloudformation コンソール内。 - 注:
UPSTASH_REDIS_URLを必ず設定してください。 AWS Lambda 設定の環境変数も同様に変更します。そうしないと、クラッシュするだけです。 AWS Lambda コンソールにアクセスし、新しくデプロイした Lambda をクリックし、設定をクリックします。 タブをクリックし、左側のメニューで環境変数をクリックします。 。 「UPSTASH_REDIS_URL」と入力します。 をキーとして、Upstash Redis URL を値として指定します。 [保存] をクリックします。 これで、Lambda の準備が整いました。
- または、CloudFormation スタック
4.1 SAM ローカル テスト
sample-event.json を指定することで、Lambda をローカルでテストできます。 正しいパス/Cookie/クエリパラメータなどを使用してください。このような JSON の例は、aws-lambda-upstash-redis-article/sample-event.json にあります。 .
- 次に、有効な JSON イベント ファイルを取得したら、次のコマンドを実行して、AWS Lambda で実行する場合と同様にサーバー ロジックを呼び出します。
sam local invoke -t aws-iac/sam-template.yml -e sample-event.json 4.2 Upstash Redis URL のセキュリティ
この記事では、わかりやすくするために、パスワードを含む Upstash Redis URL を環境変数を通じて提供しました。コードとともにバージョン管理される SAM Cloudformation テンプレートにこれをハードコーディングしたくありません。そのため、AWS Lambda コンソールを通じて手動で設定する必要がありました。
Lambda 設定を毎回変更せずにこれを自動的に実行し、コンソールにアクセスできる人にとって Redis 認証情報/URL が目に見えないようにする、より良い方法があります。
AWS Systems Manager パラメータ ストアと、対応する Cloudformation リソース AWS::SSM::Parameter を使用できます。 URL を保持し (一度設定してデプロイ中に保持できます)、実行時にパラメーターの値をフェッチするように Lambda コードを変更します。 sam-template.yml 内の環境変数として自動的に挿入することもできます。 ただし、この場合でもコンソールにはプレーン テキストで表示されます。
SSM パラメータ ストアからフェッチするコードの変更は、エントリポイントが分離されているため簡単です。そのため、AWS Lambda (~/dev/aws-lambda-upstash-redis/cmd/lambda/main.go 内で実行されている場合にのみパラメータをフェッチできます) ) を NewClient() に渡します。 Redis クライアントを作成する関数。
5.どれくらいの速さですか?
最初のコールド スタート呼び出しは別として、およそ 100-120 ms かかります。 、その後のすべての呼び出しは超高速で、常に 4 ms の下にあります。 .
以下は、/login?userId=lambros のホット呼び出しの例です。 上記で実装されたエンドポイント:

ご覧のとおり、リクエストの合計時間は 2.06 ms でした。 。はい、 それは2 ミリ秒です。 、セッション ID を生成するには、それをリモートで Upstash Redis に書き込み、リダイレクト応答を返します。
ログ出力をさらに注意深く見てみましょう。 セクションでは、リクエストが 789.435μs 続いたことがわかります。 少なくともコードの観点からは。これは、ロジックが 1 ms の下で十分に完了したことを意味します。 (およそ 0.790 ms )。リモート データベースを使用していることを考えると驚くべきことです。🤯
結論
特に、AWS Lambda のようなプラットフォームに適したサーバーレス データベースでこのようなパフォーマンスを見つけるのは難しいため、Upstash Redis のパフォーマンスの良さには本当に驚かされます。
Redis API は非常に便利で、Upstash Redis には一流の価格設定モデルと素晴らしい開発者エクスペリエンスがあり、高速です。この組み合わせが大好きです!
AWS Lambda + Upstash Redis + Go =🚀❤️
-
RedisTimeSeries 1.6がリリースされました!
本日、RedisTimeSeries1.6の一般提供を発表できることをうれしく思います。このブログ投稿では、現在利用可能な主な新機能について詳しく説明しています。 RedisTimeSeriesについて RedisTimeSeriesは、Redis用の高性能でメモリファーストの時系列データ構造です。 RedisTimeSeriesは、時系列マルチテナンシー(多数の時系列を同時に保持できます)をサポートし、これらの時系列に同時にアクセスする複数のクライアントにサービスを提供できます。 Redisスタックの一部としても利用できるようになりました。 RedisTimeSeries1.6の主な新機能
-
Redis Pub Sub(メッセージブローカーシステム)–Redisチュートリアル
このチュートリアルでは、Redisデータストアをパブリッシュ/サブスクライブメッセージングシステムとして使用する方法について学習します。 Redis Pub / Sub System Redisは、パブリッシュ/サブスクライブメッセージングパラダイムを実装します。このメッセージングパラダイムによれば、メッセージの送信者(発行者)は、メッセージを特定の受信者(サブスクライバー)に直接送信するようにプログラムされていません。彼らは、どの受信者(サブスクライバー)がメッセージを消費するかを知らずに、特定のチャネルにメッセージを送信(公開)します。メッセージを消費したい受信者(サブスクライバ