Aurora ServerlessはオートスケールするAuroraで、 使ったAurora Capacity Unit (ACU)によって料金が発生するため、 使用頻度が少なかったり変動するアプリケーションにおいて安くRDBを使うことができる。 インスタンスを立てると最低でも月3000円くらいかかるが、Serverlessだとほとんどストレージ分から運用することができて趣味でも使いやすい。 ただしLambdaと同様に常に同等のリソースを使っている状態だとインスタンスと比べて割高になる。
今回はLambdaで使う。 Serverlessと名前には付いているが用途としてはLambdaに限らず、 むしろコンテナの数が容易に増え得るLambdaは同時接続数が問題になるRDBと一般に相性が良くない。 現在Betaの、コネクションを張らずにHTTPSでクエリを投げられるData APIはこの問題を解消すると思われるが、トランザクションが張れなかったり、レスポンスサイズに制限があるようだ。今回はコンソール上から初期クエリを流すためにData APIを有効にしている。
他の選択肢として、DynamoDBは現状最有力で最近トランザクションもサポートされたがSQLのように複雑なクエリは投げられない。 Athenaはクエリは投げられるがそこそこ時間がかかるし、INSERT/UPDATEはできずクエリごとに料金が発生する。
Serverless Frameworkを使ってリソースを作成しデプロイする。リポジトリはここ。
Serverless FrameworkでLambdaをデプロイする - sambaiz-net
VPCの作成
Aurora Serverlessの制限の一つとしてVPC内からしか接続しかできないというものがある。ということでVPCから作成していく。以前Terraformで作ったのと同じリソースをCloudFormationで作る。
TerraformでVPCを管理するmoduleを作る - sambaiz-net
LambdaをVPC内で動かすとコンテナ起動時にENIも作成するため立ち上がりの際時間がかかる。必要なら定期的に呼び出して削除されないようにする。
また、今回はテストのため/24
でVPCを切っているが、小さいとENIのIPアドレスが枯渇する可能性がある。
TestVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 172.32.0.0/24
Tags:
- Key: Name
Value: test-vpc
Aurora Serverlessのために少なくとも2つのサブネットが必要。
TestPublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref TestVPC
CidrBlock: 172.32.0.0/25
AvailabilityZone: us-east-1d
Tags:
- Key: Name
Value: test-public-subnet1
TestPrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref TestVPC
CidrBlock: 172.32.0.128/26
AvailabilityZone: us-east-1a
Tags:
- Key: Name
Value: test-private-subnet1
TestPrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref TestVPC
CidrBlock: 172.32.0.192/26
AvailabilityZone: us-east-1b
Tags:
- Key: Name
Value: test-private-subnet2
TestInternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: test-igw
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref TestVPC
InternetGatewayId: !Ref TestInternetGateway
TestPublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref TestVPC
Tags:
- Key: Name
Value: test-public-route-table
TestPublicRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref TestPublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref TestInternetGateway
TestPublicSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref TestPublicSubnet
RouteTableId: !Ref TestPublicRouteTable
TestEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
TestNatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt TestEIP.AllocationId
SubnetId: !Ref TestPublicSubnet
Tags:
- Key: Name
Value: TestNatGateway
TestPrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref TestVPC
Tags:
- Key: Name
Value: test-private-route-table
TestPrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref TestPrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref TestNatGateway
TestPrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref TestPrivateSubnet1
RouteTableId: !Ref TestPrivateRouteTable
TestPrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref TestPrivateSubnet2
RouteTableId: !Ref TestPrivateRouteTable
TestSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: !Ref TestVPC
GroupName: test-sg
GroupDescription: test security group
Tags:
- Key: Name
Value: test-sg
TestSecurityGroupIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref TestSecurityGroup
SourceSecurityGroupId: !Ref TestSecurityGroup
IpProtocol: -1
FromPort: -1
ToPort: -1
Aurora Serverlessの作成
認証情報はSecretsManagerに置いてパスワードも自動生成させる。
AWS Systems Manager (SSM)のParameter Storeに認証情報を置き参照する - sambaiz-net
ACUが0の状態で確認しやすくするためSecondsUntilAutoPauseを最短の300にしている。
AuroraSecret:
Type: AWS::SecretsManager::Secret
Properties:
GenerateSecretString:
SecretStringTemplate: '{"username": "test"}'
GenerateStringKey: 'password'
PasswordLength: 16
ExcludeCharacters: '"@/\'
SecretRDSInstanceAttachment:
Type: AWS::SecretsManager::SecretTargetAttachment
Properties:
SecretId: !Ref AuroraSecret
TargetId: !Ref AuroraServerless
TargetType: AWS::RDS::DBCluster
TestSubnetGroup:
Type: "AWS::RDS::DBSubnetGroup"
Properties:
DBSubnetGroupDescription: test subnet group
SubnetIds:
- !Ref TestPrivateSubnet1
- !Ref TestPrivateSubnet2
Tags:
- Key: Name
Value: test-subnet-group
AuroraServerless:
Type: AWS::RDS::DBCluster
Properties:
Engine: aurora
EngineMode: serverless
DeletionProtection: true
Port: 3306
DatabaseName: test
MasterUsername: !Join ['', ['{{resolve:secretsmanager:', !Ref AuroraSecret, ':SecretString:username}}' ]]
MasterUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref AuroraSecret, ':SecretString:password}}' ]]
DBSubnetGroupName: !Ref TestSubnetGroup
VpcSecurityGroupIds:
- !Ref TestSecurityGroup
ScalingConfiguration:
AutoPause: true
MaxCapacity: 2
SecondsUntilAutoPause: 300
環境変数とVPCまわりの設定はこんな感じ。 ServerlessFrameworkではデプロイ時にSecretを解決することもできるが、ローテーションすることも考えて実行時に取りに行っている。
environment:
TZ: Asia/Tokyo
DB_SECRET: !Ref AuroraSecret
vpc:
securityGroupIds:
- !Ref TestSecurityGroup
subnetIds:
- !Ref TestPrivateSubnet1
- !Ref TestPrivateSubnet2
iamRoleStatements:
- Effect: "Allow"
Action:
- "ec2:CreateNetworkInterface"
- "ec2:DescribeNetworkInterfaces"
- "ec2:DeleteNetworkInterface"
Resource:
- "*"
アプリケーション
Secretsにはusernameやpasswordだけではなくhostやport、dbNameまで含まれているのでこれだけで接続情報が作れる。
type dbConfig struct {
Password string `json:"password"`
DbName string `json:"dbname"`
Engine string `json:"engine"`
Port int `json:"port"`
Host string `json:"host"`
UserName string `json:"username"`
}
func dbSrc() (string, error) {
secret, err := secret.GetSecretString(os.Getenv("DB_SECRET"))
if err != nil {
return "", err
}
var config dbConfig
if err := json.Unmarshal([]byte(secret), &config); err != nil {
return "", err
}
return fmt.Sprintf(
"%s:%[email protected](%s:%d)/%s?parseTime=true",
config.UserName,
config.Password,
config.Host,
config.Port,
config.DbName,
), nil
}
トランザクションを張ってINSERTしSELECTする。
...
tx, err := db.Begin()
if err != nil {
return Response{StatusCode: http.StatusInternalServerError}, err
}
_, err = tx.Exec("INSERT INTO b (a_id) VALUES (1)")
if err != nil {
tx.Rollback()
return Response{StatusCode: http.StatusInternalServerError}, err
}
if err := tx.Commit(); err != nil {
return Response{StatusCode: http.StatusInternalServerError}, err
}
rows, err := db.Query("SELECT b.id FROM a JOIN b ON (a.id = b.a_id)")
if err != nil {
return Response{StatusCode: http.StatusInternalServerError}, err
}
...
初期クエリの実行
デプロイ後、DBの設定からData APIを有効にしてコンソールのQuery Editorから初期クエリを流す。
CREATE TABLE a (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(128) NOT NULL
);
CREATE TABLE b (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
a_id BIGINT UNSIGNED NOT NULL,
FOREIGN KEY (a_id) REFERENCES a (id)
);
INSERT INTO a (id, name) VALUES (1, '1');
レスポンスタイム
ENIなし、ACUが0の状態で呼んだところAPI Gatewayの上限30秒でタイムアウトしてしまった。 ENIがある状態でも25秒前後ほどかかって、いずれも立ち上がっている状態では800ms前後。 API Gatewayの後ろでAutoPauseなAurora Serverlessを使う場合は、時々レスポンスに時間がかかることを許容した上で、 タイムアウトしたときにクライアントでリトライさせるなりする必要がある。 Auroraへのアクセスはバッチで行い、API呼び出し時は別のデータソースを参照するといった使い方が良いかもしれない。