はじめに
Reactで作成したWebアプリケーションのユーザー認証部分をCognito + Amplifyフレームワークで構築してみました。構築の基本部分については「【React】ユーザー認証をCognito + Amplifyで構築してみた」の構築準備編と構築完成編をご覧ください。
本記事は、アプリケーションからユーザーを登録、削除する方法についてまとめています。
完成画面
今回は、アプリケーションからユーザーを登録すると、ユーザープールとDBそれぞれにユーザーが登録されて、画面には「ユーザーを登録しました。」というアラートが出力されるようにします。
[Submit]をクリックすると↓↓↓
DB(userテーブル)※一部抜粋
+----+-----------+------------------+
| id | user_name | email |
+----+-----------+------------------+
| 2 | test | test@example.com |
+----+-----------+------------------+
方法検討
要件
構築方法を考えるにあたり、条件は以下の通りです。
- 静的コンテンツをS3に置いている
- アプリケーション部分はLambda + RDS Proxy + RDSで実装している
- ユーザーデータはCognitoユーザープール以外に、RDSに保存している
- NATゲートウェイはコストが高いので使いたくない
現在の構成図(ユーザー認証付加前)
ユーザー認証付加前のアプリケーション部分の構成図は下記の通りです。
VPC Lambdaによる弊害
ここで、LambdaをVPC内に設置していることで、Cognitoにアクセスできないことに気付きました。パブリックサブネットに置いているんだから、アクセスできると勝手に思っていました。
AWS開発者ガイドによると、次のように説明されています。
プライベートリソースにアクセスするには、関数をプライベートサブネットに接続します。関数にインターネットアクセスが必要な場合は、ネットワークアドレス変換 (NAT) を使用します。関数をパブリックサブネットに接続しても、インターネットアクセスやパブリック IP アドレスは提供されません。
Lambda関数をパブリックサブネットに接続しても、インターネットアクセスやパブリック IP アドレスは提供されないんです。NATゲートウェイを使用する場合にもLambda関数はプライベートサブネットに置くべきだそうです。パブリックサブネットにLambdaを置いておくメリットはなさそうなので、VPC Lambdaはプライベートサブネットに置きましょう!!!
結論
この条件に沿ってアプリケーションの登録、削除処理を考えた結果、VPC Lambdaをプライベートサブネットに移動させ、NATゲートウェイは使いたくないので、強引にLambdaからLambdaを呼び出すことにしました。
シーケンス図
シーケンス図を書くと次のようになります。
構成図(ユーザー認証付加後)
構成は下図の通りになりました。
手順
下記の流れで進めていきます。
- RDSを更新するLambda関数:Lambda(VPC) の作成
- Cognitoを更新するLambda関数:Lambda(非VPC) の作成
- API Gatewayの作成
- フロントの実装
ユーザーを登録する
1. DBを更新するLambda関数:Lambda(VPC) の作成
Lambda(非VPC)の作成時につけるIAMロールにLambda(VPC)のarnが必要なので、先にLambda(VPC)から作成します。
VPC内に設置してRDSに情報を書き込むLambdaを作成していきます。このLambdaに関しては、RDSにデータが保存できれば良く、特に既存のLambdaと変わりないので割愛します。
祝GA‼︎【Go】Lambda + RDS 接続にRDS Proxyを使ってみたの「8. Lambda関数の作成」を参考に作成しました。
ソースコード
ソースコードはこのような感じです。
※↓クリックするとソースコードが見れます。
ソースコード
package main
import (
"database/sql"
"fmt"
"github.com/aws/aws-lambda-go/lambda"
_ "github.com/go-sql-driver/mysql"
"os"
)
type MyEvent struct {
UserName string `json:"userName"`
Email string `json:"email"`
}
// os.Getenv()でLambdaの環境変数を取得
var dbEndpoint = os.Getenv("dbEndpoint")
var dbUser = os.Getenv("dbUser")
var dbPass = os.Getenv("dbPass")
var dbName = os.Getenv("dbName")
func RDSConnect() (*sql.DB, error) {
connectStr := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=%s",
dbUser,
dbPass,
dbEndpoint,
"3306",
dbName,
"utf8",
)
db, err := sql.Open("mysql", connectStr)
if err != nil {
return nil, err
}
return db, nil
}
func RDSProcessing(event MyEvent, db *sql.DB) (interface{}, error) {
tx, err := db.Begin()
if err != nil {
return nil, err
}
defer tx.Rollback()
// ユーザーテーブルに情報を登録
stmt, err := tx.Prepare("INSERT INTO user(user_name, email) VALUES (?, ?) ")
if err != nil {
return nil, err
}
defer stmt.Close()
if _, err := stmt.Exec(event.UserName, event.Email); err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
response := "正常に処理が完了しました。"
return response, nil
}
func run(event MyEvent) (interface{}, error) {
fmt.Println("RDS接続 start!")
db, err := RDSConnect()
if err != nil {
fmt.Println("DBの接続に失敗しました。")
panic(err.Error())
}
fmt.Println("RDS接続 end!")
fmt.Println("RDS処理 start!")
response, err := RDSProcessing(event, db)
if err != nil {
fmt.Println("DB処理に失敗しました。")
panic(err.Error())
}
fmt.Println("RDS処理 end!")
return response, nil
}
/**************************
メイン
**************************/
func main() {
lambda.Start(run)
}
2. Cognitoを更新するLambda関数:Lambda(非VPC) の作成
VPCの外に置いて、Cognitoユーザープールへの登録とRDSを更新するLambda(VPC)を実行するLambdaを作成していきます。
IAMロール
下記の2つの権限をつけたポリシーを作成してアタッチします。
- Cognitoユーザープールにユーザーを登録/削除する権限
- Lambda(VPC)を実行する権限
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"cognito-idp:AdminDeleteUser",
"cognito-idp:AdminCreateUser"
],
"Resource": "<Cognitoのarn>"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": "<Lambda(VPC)のarn>"
}
]
}
ソースコード
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
"github.com/aws/aws-sdk-go/service/cognitoidentityprovider/cognitoidentityprovideriface"
l "github.com/aws/aws-sdk-go/service/lambda"
"github.com/aws/aws-sdk-go/service/lambda/lambdaiface"
)
type MyEvent struct {
UserName string `json:"userName"`
Email string `json:"email"`
}
func AddCognitoUser(svc cognitoidentityprovideriface.CognitoIdentityProviderAPI, event MyEvent) error {
// 登録時にユーザーにメール送信
desiredDeliveryMediums := []*string{aws.String("EMAIL")}
// メールアドレスとメールアドレス検証済みを設定
userAttributes := []*cognitoidentityprovider.AttributeType{
{
Name: aws.String("email"),
Value: aws.String(event.Email),
},
{
Name: aws.String("email_verified"),
Value: aws.String("true"),
},
}
// ユーザープールの設定
// os.Getenv()でLambdaの環境変数を取得
userPoolId := aws.String(os.Getenv("userPoolId"))
// ユーザー名の設定
username := aws.String(event.UserName)
// Inputの作成
input := &cognitoidentityprovider.AdminCreateUserInput{}
input.DesiredDeliveryMediums = desiredDeliveryMediums
input.UserAttributes = userAttributes
input.UserPoolId = userPoolId
input.Username = username
// 処理実行
result, err := svc.AdminCreateUser(input)
if err != nil {
fmt.Println(err.Error())
return err
}
fmt.Println(result)
return nil
}
func AddDbUser(svc lambdaiface.LambdaAPI, event MyEvent) error {
// ValidateLambdaに送る情報の作成
jsonBytes, _ := json.Marshal(event)
// Inputの作成
input := &l.InvokeInput{}
input.FunctionName = aws.String(os.Getenv("arn"))
input.Payload = jsonBytes
input.InvocationType = aws.String("RequestResponse") // 同期実行
// 処理実行
result, err := svc.Invoke(input)
if err != nil {
fmt.Println(err.Error())
return err
}
fmt.Println(result)
fmt.Println(string(result.Payload))
return nil
}
func run(event MyEvent) (interface{}, error) {
fmt.Println("Cognito登録 start!")
// セッション作成
csvc := cognitoidentityprovider.New(session.Must(session.NewSession()))
if err := AddCognitoUser(csvc, event); err != nil {
fmt.Println("ユーザー登録に失敗しました。")
panic(err.Error())
}
fmt.Println("Cognito登録 end!")
fmt.Println("db登録 start!")
// セッションの作成
lsvc := l.New(session.Must(session.NewSession()))
if err := AddDbUser(lsvc, event); err != nil {
fmt.Println("ユーザー登録に失敗しました。")
panic(err.Error())
}
fmt.Println("db登録 end!")
fmt.Println("end!")
response := "正常に処理が完了しました。"
return response, nil
}
/**************************
メイン
**************************/
func main() {
lambda.Start(run)
}
3. API Gatewayの作成
REST APIでPOSTメソッドを作成し、Lambda(非VPC)を紐付けます。
特に特別な設定は不要なので省略します。
4. フロントの実装
登録画面を作成します。今回は、ユーザー名とメールアドレスが必須項目なので、その2つを登録できる入力欄と登録ボタンを簡単に作成しています。登録が完了すると「ユーザーを登録しました。」というアラートが出ます。
axiosのインストール
API Gatewayを叩くのにaxios
を使うために、プロジェクトにaxios
を追加します。
$ yarn add axios
ソースコード
axios
の使い方はaxiosライブラリを使ってリクエストするを参考にしました。
import React from "react";
import axios from "axios";
function RegistrationForm() {
const API_ADD_URL = "<API Gatewayで取得したURL>"
const [userName, setUserName] = React.useState("");
const [email, setEmail] = React.useState("");
const handleNameChange = event => {
setUserName(event.target.value);
};
const handleEmailChange = event => {
setEmail(event.target.value);
}
const handleSubmit = event => {
axios.post(API_ADD_URL, {userName: userName, email: email})
.then((response) => {
if(response.data === "正常に処理が完了しました。"){
alert("ユーザーを登録しました。")
console.log(response);
} else {
throw Error(response.data.errorMessage)
}
}).catch((response) => {
alert("登録に失敗しました。もう一度登録してください。");
console.log(response);
});
event.preventDefault();
}
return (
<div>
<h2>ユーザー登録</h2>
<form onSubmit={handleSubmit} >
<label >
ユーザー名:
<input type="text" value={userName} onChange={handleNameChange} /><br/>
</label>
<label >
Eメール:
<input type="text" value={email} onChange={handleEmailChange} /><br/>
</label>
<input type="submit" value="Submit" />
</form>
</div>
);
}
export default RegistrationForm;
ユーザーを削除する
ユーザーから削除する場合も、基本的に登録するのと同じです。ユーザープールから削除するにはSDKのAdminDeleteUser
を使用します。
実行結果
無事冒頭の完成画面のように動くようになりました!
おわりに
LambdaからLambdaを実行することで、NATゲートウェイを使わずにCognitoとDBの両方にユーザーを登録することができました!今考えると、Cognitoユーザープールに登録するのはAmplifyでAdmin Queries APIを使うようにして、DBに保存するのは既存のようにLambdaを呼び出すようにするのでも良かったかなとも思います!
次回は、サインインページにある、使用しないアカウント作成ボタンを消したいと思います!