「サーバーレス LAMP スタック – Part 2」をやってみる

18 min read読了の目安(約16500字 1

Summary

  • サーバーレス LAMP スタック – Part 2をベースにlambda -> rds proxy -> rds の挙動確認をしてみる
  • Blogとの主な違い
    • Lambda以外のインフラ部分はTerraformで構築
    • IAM認証は使用しない(理由は後述)
    • samでのデプロイは使用しない
  • Blogの手順を実施したい場合は、Part1のPHPコンパイル手順時に--with-mysqliを入れるようにすれば他は適宜読み替えることで実施可能かと

Detail

今回の構成はこんな感じに
※RouteTable等諸々のものは端折ってる、特にLambda

VPCの構築

BlogではVPC等の構築についてはさらっと記載されているだけなので
Blogの内容をとりあえず実施したい場合はデフォルトVPCを使うことが最速だとは思う。

Aurora DB クラスターを作成する前に、VPC や RDS DB サブネットグループの作成などの前提条件を満たしている必要があります。これをセットアップする方法の詳細については、DBクラスターの前提条件を参照してください。

Part1にも記載したようにインフラの管理部分はどこらへんまでがいいのかを理解するためなので
イチからTerraformで構築してみる。

terraformのバージョンは現行最新の0.14.5
ファイル構成については特別なことはなく、フラットな構造でファイルごとに役割を分けてある。
強いていうと、各サービスで利用するIAM、Security Groupなどはそのサービスの中に含めている。
IAMのポリシーについてはJSONファイルを外だしし、あとから読み込む形とした。

Security Groupはモジュールなどで作成すると、SG自身の許可設定がデフォルト含まれているが
そこらへんを考えるに、バンバンつくってしまってSG単位で通信許可設定をしてしまうほうが
いろいろ考えないですむのでは?と思ってるが今の所まだ結論は出ていない。

最終構成
.
├── file
│   ├── rds-proxy-policy.json.tpl
│   └── rds-proxy-role.json
└── stage
    ├── output.tf
    ├── provider.tf
    ├── rds_aurora.tf
    ├── rds_proxy.tf
    ├── variables.tf
    └── vpc.tf

構成図に合わせてHCLファイルを書き出してみる
今回はtfstateをローカルに置いてるが、実運用時はS3にアップすることが多いかと思う

provider.tf
terraform {
  required_version = "0.14.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "ap-northeast-1"
}
vpc.tf
# https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest
module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "${local.environment}-${local.project_name}"
  cidr = "10.0.0.0/16"

  # Lambda用とDatabase用のSubnetを分けて作成する
  azs              = ["ap-northeast-1a", "ap-northeast-1c"]
  private_subnets  = ["10.0.10.0/24", "10.0.20.0/24"]
  database_subnets = ["10.0.100.0/24", "10.0.200.0/24"]

  # 今回はInternet Gatewayも不要
  create_igw = false

  # natgatewayも不要なので無効
  enable_nat_gateway = false

  # 名前解決系
  enable_dns_hostnames = true
  enable_dns_support   = true

  # common tags
  tags = local.common_tags
}
variables.tf
locals {
  project_name = "sample"
  manage_tool  = "terraform"
  environment  = "stage"
}

locals {
  common_tags = {
    Project   = local.project_name
    ManagedBy = local.manage_tool
    Env       = local.environment
  }
}

まずはVPCを構築

terraform init
terraform plan
terraform apply

Aurora Mysqlの構築

tfファイルのコメントに記載してしまっているが、IAM認証はインスタンスのサイズであったり
推奨事項を確認するに、場合によっては本番環境等では利用しないほうが良さそうであったので
今回検証時にも無効とした。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.html
rds_aurora.tf
#############
# RDS Aurora
# (https://registry.terraform.io/modules/terraform-aws-modules/rds-aurora/aws/latest)
#############

# iam_database_authentication_enabledを有効にするためにはt2.small及びt3.small以上
# 利用する際に1秒間に200以上のアクセスがある場合は非推奨とのことなので導入はしない方向
# https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.html

resource "aws_db_parameter_group" "aurora" {
  name        = "${local.environment}-${local.project_name}-aurora-db-57-param-group"
  family      = "aurora-mysql5.7"
  description = "${local.environment}-${local.project_name}-aurora-db-57-param-group"
}

resource "aws_rds_cluster_parameter_group" "aurora" {
  name        = "${local.environment}-${local.project_name}-aurora-57-cl-param-group"
  family      = "aurora-mysql5.7"
  description = "${local.environment}-${local.project_name}-aurora-57-cl-param-group"
}

module "aurora" {
  source                              = "terraform-aws-modules/rds-aurora/aws"
  version                             = "~> 3.0"
  name                                = "${local.environment}-${local.project_name}-db"
  engine                              = "aurora-mysql"
  engine_version                      = "5.7.mysql_aurora.2.07.2"
  vpc_id                              = module.vpc.vpc_id
  subnets                             = module.vpc.database_subnets
  replica_count                       = 1
  instance_type                       = "db.t3.small"
  username                            = local.project_name
  password                            = random_password.aurora.result
  create_random_password              = false
  apply_immediately                   = true
  skip_final_snapshot                 = true
  db_parameter_group_name             = aws_db_parameter_group.aurora.id
  db_cluster_parameter_group_name     = aws_rds_cluster_parameter_group.aurora.id
  enabled_cloudwatch_logs_exports     = ["audit", "error", "general", "slowquery"]
  allowed_cidr_blocks                 = concat(module.vpc.private_subnets_cidr_blocks, module.vpc.database_subnets_cidr_blocks)
  allowed_security_groups             = [module.aurora.this_security_group_id]
  create_security_group               = true
  iam_database_authentication_enabled = false
  tags                                = local.common_tags
}


# https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password
resource "random_password" "aurora" {
  length           = 16
  special          = true
  override_special = "_%<>"
}

新規のmoduleが入ったときは再度initしないと怒られる
エラーとしてinitしてねって出るので、特に迷うことはないかと

RDSは構築に時間がかかるのでのんびり待つことにする。

terraform init
terraform plan
terraform apply

RDS Proxyを構築する

Secrets Managerへの登録及びIAMの作成

AWS Secrets Managerに登録するAuroraの認証情報はProxyで使うので
rds_proxyのファイルに記載する
CLIだと1コマンドですんでいるが、Terraform的にはシークレットの作成と記載の中身が分離している。

このシークレットは、データベースへの接続プールを維持するためにRDS Proxy によって使用されます。
シークレットにアクセスするには、RDS Proxy サービスにアクセス許可を明示的に付与する必要があります。

作成したSecretsManagerにアクセス許可を提供するIAMポリシーも必要になるのでこちらもrds_proxyのファイルに記載する。ポリシーはJSON形式となっており、ファイルに直接記載すると可視性が悪くなるので別途ファイルからの読み出す形に。

こちらの流れはCLIとTerraformの記載内容がマッチしてるのでとくに違和感はなかった。
jsonファイルはfileディレクトリでの管理とした。

file/rds-proxy-role.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Service": "rds.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
file/rds-proxy-policy.json.tpl
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetResourcePolicy",
                "secretsmanager:GetSecretValue",
                "secretsmanager:DescribeSecret",
                "secretsmanager:ListSecretVersionIds"
            ],
            "Resource": [
                "${secretmanager_aurora_arn}"
            ]
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetRandomPassword",
                "secretsmanager:ListSecrets"
            ],
            "Resource": "*"
        }
    ]
}
rds_proxy.tf
############
# RDS Proxy
############

# Getting Started の手順3に相当
# Auroraのユーザ、パスワードをSecretManagerに格納
# Secretに必要な情報はlocalsにまとめる形に
# 即時削除を可能とするようにrecovery_window_in_daysは0
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret#recovery_window_in_days

# Connect to Aurora Mysql from RDS Proxy 
# https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.html#rds-proxy-secrets-arns
locals {
  aurora_secret_data = {
    username            = module.aurora.this_rds_cluster_master_username
    password            = module.aurora.this_rds_cluster_master_password
    engine              = "mysql"
    host                = module.aurora.this_rds_cluster_endpoint
    port                = module.aurora.this_rds_cluster_port
    dbClusterIdentifier = module.aurora.this_rds_cluster_id
  }
}

resource "aws_secretsmanager_secret" "aurora" {
  name                    = "${local.environment}-${local.project_name}-aurora"
  description             = "My test database secret created"
  recovery_window_in_days = 0
}

resource "aws_secretsmanager_secret_version" "aurora" {
  secret_id     = aws_secretsmanager_secret.aurora.id
  secret_string = jsonencode(local.aurora_secret_data)
}

# Getting Startedの手順4に相当
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment
# RDS ProxyからSecret Mangerへのアクセス可能にするためのIAMを作る
resource "aws_iam_role" "rds-proxy" {
  name               = "${local.environment}-${local.project_name}-rds-proxy-role"
  assume_role_policy = file("../file/rds-proxy-role.json")
}

# 手順5に相当
resource "aws_iam_policy" "rds-proxy" {
  name   = "${local.environment}-${local.project_name}-rds-proxy-policy"
  policy = templatefile("../file/rds-proxy-policy.json.tpl", { secretmanager_aurora_arn = aws_secretsmanager_secret.aurora.arn })
}

# 手順6に相当
resource "aws_iam_role_policy_attachment" "rds-proxy" {
  role       = aws_iam_role.rds-proxy.name
  policy_arn = aws_iam_policy.rds-proxy.arn
}

RDS Proxyの構築

Blogの手順ではIAM認証を適用するため、IAMAuth値(tfファイル内ではauth内のiam_auth)をREQUIREDとしているが、本手順では使わないのでtf内ではDISABLEとしている。

rds_proxy.tf
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_proxy
# AuroraのIAM認証をfalseにしてるので iam_authはDISABLE
# TLS認証も使用しないのでrequire_tlsはfalse
# https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.html#rds-proxy-creating
# AuroraとRDS Proxyを一度に作ろうとするとProxy作成時にRDS Instanceができてないエラーが出るのでそれを回避するためにSleepを入れる

resource "time_sleep" "wait_60_seconds" {
  depends_on      = [module.aurora]
  create_duration = "60s"
}

# AuroraのSGではEgressがないためRDS ProxyからAuroraへの接続ができない
# なのでProxy用にSGを作成する

resource "aws_security_group" "rds-proxy" {
  name_prefix = "${local.environment}-${local.project_name}-rds-proxy-"
  description = "For rds-proxy"
  vpc_id      = module.vpc.vpc_id

  tags = merge(local.common_tags,
    {
      Name = "${local.environment}-${local.project_name}-rds-proxy"
    },
  )

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_security_group_rule" "ingress_rds-proxy" {
  type              = "ingress"
  from_port         = module.aurora.this_rds_cluster_port
  to_port           = module.aurora.this_rds_cluster_port
  protocol          = "tcp"
  cidr_blocks       = module.vpc.private_subnets_cidr_blocks
  security_group_id = aws_security_group.rds-proxy.id
}

resource "aws_security_group_rule" "egress_rds-proxy" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.rds-proxy.id
}

resource "aws_db_proxy" "rds_proxy" {
  name                   = "${local.environment}-${local.project_name}-rds-proxy"
  debug_logging          = false
  engine_family          = "MYSQL"
  idle_client_timeout    = 1800
  require_tls            = false
  role_arn               = aws_iam_role.rds-proxy.arn
  vpc_security_group_ids = [aws_security_group.rds-proxy.id]
  vpc_subnet_ids         = module.vpc.database_subnets

  auth {
    auth_scheme = "SECRETS"
    iam_auth    = "DISABLED"
    secret_arn  = aws_secretsmanager_secret.aurora.arn
  }

  tags = local.common_tags

  depends_on = [time_sleep.wait_60_seconds]
}

resource "aws_db_proxy_default_target_group" "rds_proxy" {
  db_proxy_name = aws_db_proxy.rds_proxy.name

  connection_pool_config {
    connection_borrow_timeout    = 120
    max_connections_percent      = 100
    max_idle_connections_percent = 50
  }
}

resource "aws_db_proxy_target" "rds_proxy" {
  db_cluster_identifier = module.aurora.this_rds_cluster_id
  db_proxy_name         = aws_db_proxy.rds_proxy.name
  target_group_name     = aws_db_proxy_default_target_group.rds_proxy.name
}


# LambdaからRDS Proxyへ接続するための情報はパラメータストアに格納
# IAM認証を使わないのと、Secret Parameterは呼び出しに費用が発生するため。
# 確認するのも楽になる。
# Lambdaからの接続となるのでEndpointはRDS Proxyのものとなる
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter
# https://qiita.com/NaokiIshimura/items/3d5b1a6db906338ae103

locals {
  rds_proxy_ssm_data = {
    "/${local.environment}/${local.project_name}/rds_proxy/username" = module.aurora.this_rds_cluster_master_username
    "/${local.environment}/${local.project_name}/rds_proxy/password" = module.aurora.this_rds_cluster_master_password
    "/${local.environment}/${local.project_name}/rds_proxy/host"     = aws_db_proxy.rds_proxy.endpoint
  }
}

resource "aws_ssm_parameter" "rds-proxy" {
  for_each = local.rds_proxy_ssm_data

  name  = each.key
  value = each.value
  type  = "SecureString"

  tags = local.common_tags
}

VPC 構成での PHP Lambda 関数のデプロイ

SAMを使ったデプロイはしない(いろいろTemplate.ymlを修正する必要があるし)
IAM認証を使わないので、githubのindex.phpも使わないため

Part1のREADMEの手順とほぼ同一となるが、configure時のオプションに--with-mysqliを追加する

./configure --prefix=/home/ec2-user/environment/php-7-bin/ --with-openssl=/usr/local/ssl --with-curl --with-zlib --with-mysqli
aws lambda publish-layer-version \
    --layer-name PHP-example-runtime \
    --zip-file fileb://runtime.zip \
    --region ap-northeast-1
aws lambda publish-layer-version \
    --layer-name PHP-example-vendor \
    --zip-file fileb://vendor.zip \
    --region ap-northeast-1

レイヤーのアップロードまではPart1と同じ
ブラウザからLambdaの関数を作成する際に「詳細設定」にてVPC等の設定も行うこと

LambdaはVPCに所属する際、ENIを指定したサブネットに作成する。そのためにENIの作成権限が必要となるが、詳細設定をしない場合は作成されたロールに後からEC2のENI作成権限を付与する必要があるためLambda関数作成の際にまとめて行うことをおすすめする。

サブネットはPrivate
セキュリティグループは今回についていえば何でも良いので「default」に

作成後 Part1と同様に以下を実施

  • layerの追加(runtime,vendor)
  • bootstrap.sampleを削除し、src/下にindex.phpを作成
  • 作成後「デプロイ」を押してセーブ
  • ハンドラをindexに変更
index.php
<?php

function index($data){

$proxyHost=getenv('proxyHost');
$username = getenv('username');
$pass = getenv('password');
$db=getenv('db');

//Connect to Proxy
$mysqli = new mysqli($proxyHost, $username, $pass, $db);

if ($mysqli->connect_errno) {
    echo "Error: Failed to make a MySQL connection, here is why: <br />";
    echo "Errno: " . $mysqli->connect_errno . "<br />";
    echo "Error: " . $mysqli->connect_error . "<br />";
    exit;
}

/***** Example code to perform a query and return all tables in the DB *****/
$res = mysqli_query($mysqli,"SHOW TABLES");
while($cRow = mysqli_fetch_array($res))
{
    $tables[] = $cRow;
}
echo '<pre>';
print_r($tables);
echo '</pre>';
$mysqli -> close();

return json_encode($tables);

}

php内でgetenvで値をとってきてるので、Lambdaの「環境変数」に必要な情報を入れていく

username,passはSecretsManagerにて確認可能(tfstateとかからでも確認は可能)
dbはmysqlで(※Aurora作成時にDBを作成していないので今回はmysqlとしている)
proxyHostはブラウザにてAWS管理画面よりRDS -> Proxies
対象のProxy識別子をクリックし、プロキシエンドポイントを入れる。
今回はパラメータストアに入れたのでそちらで確認をしてみる。

aws ssm get-parameters-by-path --path /stage/sample/rds_proxy --with-decryption |jq '.[][]|{"Path": .Name, "Value": .Value}'
{
  "Path": "/stage/sample/rds_proxy/host",
  "Value": "stage-sample-rds-proxy.proxy-cjsmtzknhogt.ap-northeast-1.rds.amazonaws.com"
}
{
  "Path": "/stage/sample/rds_proxy/password",
  "Value": "1iP9B9kQs6odIa%j"
}
{
  "Path": "/stage/sample/rds_proxy/username",
  "Value": "sample"
}

最後にBlogの手順書「Lambda 関数への RDS Proxy 設定の追加」と同じように
Lambdaの一番の下にあるデータベースプロキシの追加を実施する。

その後、Part1と似たような感じでテストを作成するが
今回は受け取る値は何もないので、空とする。

テスト作成後、テストを実施

お疲れ様でした。

さて、不要になったリソースはキレイに片付けたいが今回気をつけたいことが1つある。

terraform destroy実施前に必ずLambdaを先に削除すること
Lambdaが作成したENIはLambdaでしか削除できないため面倒になるから。

https://aws.amazon.com/jp/premiumsupport/knowledge-center/lambda-eni-find-delete/

Summary

  • 多分あとから追記することになるかもしれないけど、もう疲れたので一旦はこれぎり
    • 備忘録のはずなのにまる一日かかった・・・
  • lambdaに関してはインフラ側ではなく、開発側で管理してもらったほうがよさそう
  • ただlambdaのアップ時に必要な情報については確認できるようにしておく必要がある(VPCとか)
  • 今回はLambdaの環境変数にいれたが実運用はパラメータストアから参照してもらうほうがおそらくいいだろう
    • SecretsMangerは費用がかかるので、特別な理由がない場合はパラメータストアがいいと思う

その他参照した記事等

https://qiita.com/minamijoyo/items/3a7467f70d145ac03324

https://qiita.com/keiichi-hikita/items/09ea25fab094986f4950