個人で複数台のVPSを運用していたが、いろいろなものがサポート切れしてOSの入れ直しが必要になったのを機にVPSの見直しをすることになった。元々3台のVPSを運用していたが、1台減らした。
続きを読むLightsailをIPv6オンリーにしたらLet's Encryptで証明書の更新ができなくなってた
先日、LambdaからIPv6でアクセスできるようにする で書いたとおり、IPv4アドレス課金を避けるためにLightsailをIPv6 Onlyのインスタンスにしていました。 LightsailをIPv6/IPv4デュアルスタックからIPv6 Onlyにするには、一度停止した上でスナップショットを作成し、スナップショットから新しいインスタンスを作るだけで簡単にできました。
が、数日後Let's Encryptで証明書が更新できていないことに気がつきます。
確認してみると、Let's Encrypt自体はIPv6で使えるものの、Route53のAPIの呼び出しに失敗していることがわかりました。(実際に route53.amazonaws.com
をnslookupで AAAA レコードを引くと No Answerになります)
対応としては、いくつか考えられます。
- どこかで証明書を別途発行してダウンロードしてくる
- 例えば Go言語製Let's Encryptクライアントlegoをライブラリとして使う のような方法でダウンロードしておく
- IPv6オンリーのLightsailから、IPv4に疎通できる仕組みを作る
うちは代々グローバルIPを持たない環境が続いていたため、リモートアクセスの中継拠点としてさくらのVPS上にVPNを構築していました。 LightsailからのIPv4トラフィックは、このVPNを経由してアクセスするように設定することでRoute53 APIを直接呼び出せるようにしました。
IPv6に進んでいくのは良いことなのですが、まだ課題が多そうです。
LambdaからIPv6でアクセスできるようにする
AWSでは各種サービスのグローバルIPv4アドレスが有料化されることになり、EC2などで一部無料枠もあるものの、基本的にはIPアドレスあたり概ね500〜600円程度の課金額がかかるようになりました。
無償版G Suiteが廃止されるのでAWS SESとDockerでメール送受信システムを作ったで作成したシステムでは最終的にLightsailで中継用のIMAP/POP3サーバーを作ることになりましたが、こちらもIPv6オンリーのインスタンスに変更しなければ値上げとなりました。
LightsailをIPv6インスタンスに変更するにあたって、LambdaのコードからIPv6にアクセスが必要となりましたが、そのためにはLambdaにVPCアウトバウンド接続の設定が必要となったので、上記のメール受信システムのアップデートに合わせて行った設定の要点をAWS CDKのコードベースで説明します。
要点
続きを読むFirebase AuthenticationでLogin with Discordを実現する
Firebase Authenticationでは様々な認証を簡単に導入することができ、Google、Apple、Faccebookといったよく利用されるサービスを統合した認証のほかに、カスタムの認証システムと統合することもできます。
これを利用して、Discordのアカウントを使ってログインするWebサービスを、Firebase HostingとFirebase Functionsを統合して作ってみたいと思います。Discordでは一般的なOAuth 2.0ののフローでサービスにログインすることができますので、これを使ってFirebase Authenticationにユーザーを登録してログインできるようにします。
なお「Identity Platform を使用する Firebase Authentication」というものもあり、こちらはSAMLやOpenID Connectなども簡単に設定でき様々な機能を利用でき、SAMLやOpenID Connectも簡単に実装できますが、課金額が結構高い[^1]ので、無料枠超えない範囲とわかってる場合やコストが見合うかどうか、急にトラフィックが増大するリスクがあるかどうか等で検討すると良いでしょう。
続きを読むmkcertを使用してローカルホストにマルチドメインの環境を作る
mkcertを使用するとローカルの開発環境であるlocalhostをhttpsにすることができます。[^1] 簡単にやろうとすると、localhostをhttpsにすることになりますが、これだと複数のサービスを扱う場合にCookieやLocalStorageが共有され不具合を起こす場合があります。
実際はポート番号をサービスごとにばらしても問題を解決することができますが、覚えるのが面倒。サービスごとにわかりやすいドメインにしたい。
そこで、開発用のローカルを指し示すドメインを用意することで、ローカルで開発中のサービスごとにドメインを付与しつつ、そのサービス用の開発用のhttps証明書をmkcertで作ります。
手順
必要なのは2つです。
1.は最初の1回だけやってしまえばあとは作業不要です。
続きを読むM2 MacBook Airに買い換えてIntelから乗り換えたので、最初から環境を再設定した
M2 MacBook Air(M2, 13インチ)に買い換えました。
前使っていたのは2018年モデルのMacBook Pro 15インチでした。 個人的に5年買い換えサイクルにしていたのと、最近ファンが回る場面が多くなりそろそろintelは限界かと思った次第です。
元々アプリエンジニアなので前回はMacBook Pro 15インチにしていたのに、今回そうしなかったのは、M2は強力なので個人でやる開発ならProでなくても十分な性能ではなかろうかと考えたこと、外でStoryboardを操作する機会はなくなったので13インチで十分(必要なら家で4Kモニターつないでやればいい)、あと見た目が好きだったので13インチのMacBook Airとなりました。
IntelからApple Siliconへの乗り換えなので、一度全部設定をやり直そうと、今回はゼロから設定することにしました。 このエントリは設定手順のメモです。
旧端末での準備
続きを読むアクセスキーを使わずにAWS SESを使って、Firebaseからメール送信できるようにする
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 でプログラムを書いていきましょう。
必要となる情報
できるだけ実行環境から設定値を取って、定数を定義せずに済むようにしようと思いますが、定数化しておかなければならない情報は以下の通りです。
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: "from@example.com", to: "to@example.com", subject: "test mail", text: "this is a test mail", }); };