はじめに
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を呼び出すようにするのでも良かったかなとも思います!
次回は、サインインページにある、使用しないアカウント作成ボタンを消したいと思います!








