GCPãFirebaseã«ã¯ã¡ã¼ã«é¢é£ã®ç¾è¡ãµã¼ãã¹ããªããå¤é¨ã®ã¡ã¼ã«ãµã¼ãã¹ã使ç¨ããå¿ è¦ãããã¾ãã ãã®ãããSendGridãAWS SESãªã©ãçµã¿åãããå¿ è¦ãããã¾ãã
ä»åã¯AWS SESã使ããã¨ã«ãªã£ãã®ã§ããããã®ããã«ã¢ã¯ã»ã¹ãã¼ãSMTPãã¹ã¯ã¼ãã管çããã®ã¯ãã¼ã®ç®¡çæ¹æ³ãèããå¿ è¦ããããæéã§ãã ã§ãã®ã§ãGCPã®ãµã¼ãã¹ã¢ã«ã¦ã³ãã§AWSã®IAMãã¼ã«ã«èªè¨¼ãã¦ã¡ã¼ã«ãéä¿¡ããã«ãã¦ã¿ã¾ããã
ä»åã®æ§æ
ã¡ã¼ã«ãéãããGCPå´ã®ãµã¼ãã¹ã¯Firebase Functionsã§å®è£ ããã¦ãã¾ããFirebase Functionsã¯App Engineããã©ã«ããµã¼ãã¹ã¢ã«ã¦ã³ãã§åä½ãã¾ãã
App Engineããã©ã«ããµã¼ãã¹ã¢ã«ã¦ã³ãã®IDãã¼ã¯ã³ã§ãAWS STS(Security Token Service)ãå¼ã³åºããAWSå´ã®IAMãã¼ã«ã«å¯¾å¿ããä¸æçãªèªè¨¼æ å ±ãåå¾ãã¾ãã
ã¡ã¼ã«éä¿¡ã¯nodemailerã®SESãã©ã³ã¹ãã¼ãã使ç¨ãã¾ãã®ã§ãããã°ã©ã ããã¯é常ã®SMTPã¨ããå¤ãããªã使ç¨æã§éä¿¡ã§ãã¾ãã
GCPãµã¼ãã¹ã¢ã«ã¦ã³ãã®ç¢ºèªã¨è¨å®
ã¾ãã¯ãGCPã®ãµã¼ãã¹ã¢ã«ã¦ã³ãã®æ å ±ã確èªãã¾ãã GCPã®IAMã³ã³ã½ã¼ã«ã§ãApp Engine ããã©ã«ããµã¼ãã¹ã¢ã«ã¦ã³ãã®è©³ç´°æ å ±ãéããä¸æã®IDãã調ã¹ã¾ãã
次ã«ããµã¼ãã¹ã¢ã«ã¦ã³ãã®è©³ç´°æ å ±ã®ã権éãã¿ããéããããµã¼ãã¹ã¢ã«ã¦ã³ããã¼ã¯ã³ä½æè ã権éãä»ä¸ãã¦ããã¾ãã*1
AWS IAMã®è¨å®
次ã«ãã¿ã¼ã²ããã¨ãªãAWSã¢ã«ã¦ã³ãã§ãIAMã®ç®¡çç»é¢ãéããè¨å®ãå ãã¦ããã¾ãã
ã¡ã¼ã«éä¿¡ã®ããªã·ã¼ãä½æ
ã¡ã¼ã«éä¿¡ã ãã®æ¨©éãããããªã·ã¼ãä½æãã¾ããGCPSendEmailãªã©ã¨ãã£ãããªã·ã¼åã§ä»¥ä¸ã®å 容ã§ä½æãã¾ããã
{ "Version": "2012-10-17", "Statement": [ { "Sid": "SendEmail", "Effect": "Allow", "Action": [ "ses:SendEmail", "ses:SendRawEmail" ], "Resource": "arn:aws:ses:*:000000000000:identity/*" } ] }
ãã¼ã«ã®è¿½å
次ã«ãGCPã®ãµã¼ãã¹ã¢ã«ã¦ã³ãããã¼ã«ã¨ãã¦è¿½å ãã¾ãã
ãã¼ã«ãä½æç»é¢ã§ãã¦ã§ãã¢ã¤ãã³ãã£ãã£ããæå®ããã¢ã¤ãã³ãã£ãã£ãããã¤ãã§Googleãæå®ãAudienceã«ãµã¼ãã¹ã¢ã«ã¦ã³ãã®ãä¸æã®IDããæå®ãã¦ããã¾ãã
許å¯ã追å ã§ãå ã»ã©ä½ã£ã¦ãããããªã·ã¼(GCPSendEmail)ãæå®ãã¾ãã
æå¾ã®ãã¼ã«ã®è©³ç´°ã§ã¯ãGCPSA_(ãµã¼ãã¹ã¢ã«ã¦ã³ãã¢ãã¬ã¹) ã¨ãã¦ããã¨ããã§ãããããã®ååã¯STSã®APIãå¼ã³åºãã¨ãã«å¿ è¦ã«ãªãã¾ãã
AWSã®èªè¨¼æ å ±ãå¾ãããã°ã©ã ãè¨è¿°ãã
ããã§ã¯ãFirebase Function ã§ããã°ã©ã ãæ¸ãã¦ããã¾ãããã
å¿ è¦ã¨ãªãæ å ±
ã§ããã ãå®è¡ç°å¢ããè¨å®å¤ãåã£ã¦ãå®æ°ãå®ç¾©ããã«æ¸ãããã«ãããã¨æãã¾ãããå®æ°åãã¦ãããªããã°ãªããªãæ å ±ã¯ä»¥ä¸ã®éãã§ãã
- AWSã®ã¢ã«ã¦ã³ãID
- GCPã®ãµã¼ãã¹ã¢ã«ã¦ã³ãå (ã¨ãã¥ã¬ã¼ã¿ã¼åä½æç¨)
Cloud Functionsã®ç°å¢å¤æ°ã使ã£ã¦æ³¨å ¥ãã¦ãè¯ãã§ãããã
ãµã¼ãã¹ã¢ã«ã¦ã³ãã¡ã¼ã«ã¢ãã¬ã¹ã®åå¾
gcp-metadata ã©ã¤ãã©ãªã使ããã¨ã§ãå®è¡ä¸ã®ãµã¼ãã¹ã¢ã«ã¦ã³ãã¡ã¼ã«ã¢ãã¬ã¹ãåå¾ã§ãã¾ãã ãã¼ã«ã«ã§å®è¡ãã¦ããã¨ãã¯åå¾ã§ããªããããprocess.env.FUNCTIONS_EMULATOR ã true ã®å ´åã¯å®æ°ãããµã¼ãã¹ã¢ã«ã¦ã³ãã¡ã¼ã«ã¢ãã¬ã¹ãå¾ãããã«ãã¾ãã
import * as gcpMetadata from "gcp-metadata"; const isEmulator = Boolean(process.env.FUNCTIONS_EMULATOR); const emulatorServiceAccount = process.env.EMULATOR_SA; const getServiceAccountEmailAsync = async (): Promise<string> => { if (isEmulator) { return emulatorServiceAccount; } else { return await gcpMetadata.instance( "service-accounts/default/email", ); } };
ãµã¼ãã¹ã¢ã«ã¦ã³ãã®IDãã¼ã¯ã³åå¾
ãµã¼ãã¹ã¢ã«ã¦ã³ãã®IDãã¼ã¯ã³ãåå¾ãã¾ãã
gcp-metadataã©ã¤ãã©ãªã使ã£ã¦åå¾ãããã¨ãã§ãã¾ããããã¼ã«ã«åä½ãèæ ®ãã¦GCPã®IAM Credential APIã使ç¨ãã¦åå¾ãã¾ãã(GCPã®APIã¨ãµã¼ãã¹ã³ã³ã½ã¼ã«ã§ãIAM Credentials APIã®æå¹åãå¿ è¦ã§ã)
import { IAMCredentialsClient } from "@google-cloud/iam-credentials"; const audience = "https://www.googleapis.com/"; const getServiceAccountIdTokenAsync = async ( sa: string, ): Promise<string | undefined> => { const client = new IAMCredentialsClient(); const [response] = await client.generateIdToken({ name: `projects/-/serviceAccounts/${sa}`, audience, }); if (!response.token) { return undefined; } return response.token; };
ãµã¼ãã¹ã¢ã«ã¦ã³ãã®IDãã¼ã¯ã³ã§AWSã®èªè¨¼æ å ±ãåå¾ãã
ãµã¼ãã¹ã¢ã«ã¦ã³ãã®IDãã¼ã¯ã³ã使ã£ã¦AWSã®STS APIãå¼ã³åºããAWSã®èªè¨¼æ å ±ãåå¾ãã¾ãã
import { AssumeRoleWithWebIdentityCommand, STSClient, } from "@aws-sdk/client-sts"; const awsAccountId = "000000000000"; type AWSCredentials = { accessKeyId: string; secretAccessKey: string; sessionToken: string; expiration?: Date; }; const getAWSCredentialsByWebIdentityAsync = async ( sa: string, gcpToken: string ): Promise<AWSCredential | undefined> => { const roleArn = `arn:aws:iam::${awsAccountId}:role/GCPSA_${sa}`; const sts = new STSClient({}); const result = await sts.send( new AssumeRoleWithWebIdentityCommand({ RoleArn: roleArn, WebIdentityToken: gcpToken, RoleSessionName: new Date().getTime().toString(), }), ); if (!result.Credentials) { return undefined; } const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = result.Credentials; if (!AccessKeyId || !SecretAccessKey || !SessionToken) { return undefined; } return { accessKeyId: AccessKeyId, secretAccessKey: SecretAccessKey, sessionToken: SessionToken, expiration: Expiration, }; };
åå¾ããAWSèªè¨¼æ å ±ã使ã£ã¦ã¡ã¼ã«ãéä¿¡ãã
ããã§ã¯ãåå¾ããèªè¨¼æ å ±ã§ã¡ã¼ã«ãéä¿¡ãã¦ã¿ã¾ãããã
import * as aws from "@aws-sdk/client-ses"; import { SES } from "@aws-sdk/client-ses"; import * as nodemailer from "nodemailer"; const sendMailAsync = async (): Promiose<void> => { const sa = await getServiceAccountEmailAsync(); const token = await getServiceAccountIdTokenAsync(sa); if (!token) { return; } const awsCredentials = await getAWSCredentialsByWebIdentityAsync(sa); if (!awsCredentials) { return; } const ses = new SES({ credentials }); const transporter = nodemailer.createTransport({ SES: { ses, aws }, }); await transporter.sendMail({ from: "[email protected]", to: "[email protected]", subject: "test mail", text: "this is a test mail", }); };