【Terraform】RDSにLambdaからRDS Proxyを経由して接続

# はじめに
Japan AWS Jr. Champions Advent Calendar 2024 8日目の記事です!

今回は、AWSのRDSにLambdaから接続するために、RDS Proxyを使って接続するという構成をTerraformで実装した話をシェアしたいと思います。

ソースコードは[こちら](https://github.com/tiga-ga/terraform-aws/tree/main/rds_proxy_handson)になります。

この記事では、Terraformでの設定に苦労した点と、その際に工夫した部分を中心に紹介します。


# 構成とそれぞれの特徴

![スクリーンショット 2024-12-08 23.54.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/d79e1c30-3594-a51e-01ef-8dae0989b8e3.png)

- Lambda、RDS Proxy、RDSを同じVPC内で管理する理由
    - RDS ProxyはRDSへの接続を効率化し、接続プールを管理することでパフォーマンスを向上させます。しかし、RDS Proxyは同じVPC内でのみ動作する制約がある
    - Lambdaが同じVPC内にあることで、RDS Proxyを経由して効率的にRDSに接続できる
    - セキュリティグループやネットワークACLを活用し、安全性の高いネットワーク環境を構築できる
- VPCで管理しているLambdaのSecret Managerアクセスについて
    - AWS Secret Managerは、データベースの認証情報やAPIキーなどの秘密情報を安全に管理
    - LambdaがVPC内で動作している場合、インターネットアクセスがデフォルトで無効になっている。そのため、VPCエンドポイントを利用してSecret Managerにアクセスする必要がある
- VPCで管理しているLambdaに必要な権限
    - "secretsmanager:GetSecretValue"
    - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
    - "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"

# 実装を通じて学んだAWSの裏技と小話

Japan AWS Jr. Champions Advent Calendarの記事なので、最初に実装を通じて学んだAWSの裏技と小話をしてからTerraformについて話していきます！

## プライベートサブネットに作成されたRDSに対してCloudShellで接続できる

2024/06/26のアップデートでVPC内のサブネット上でCloudShellを動かせるようになりました。
これにより、今回のような踏み台サーバーを作るまでもないような接続確認をする際には、CloudShellで接続して中身を確認することができます！

[AWS CloudShell now supports Amazon Virtual Private Cloud (VPC)](https://aws.amazon.com/jp/about-aws/whats-new/2024/06/aws-cloudshell-amazon-virtual-private-cloud/)

![スクリーンショット 2024-12-08 20.53.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/14fc91b0-df4e-bbd5-aaa7-dcfae53d85f1.png)

接続エラーが起こった際に、RDS Proxy側にあるのか、RDSインスタンス側にあるのかを切り分けるため、以下のコマンドを実行して確かめました。

- RDS Proxy経由での接続テスト:`mysql -h <RDS Proxyエンドポイント> -u <ユーザー名> -p`
- 直接RDSインスタンスへの接続テスト:`mysql -h <RDSインスタンスエンドポイント> -u <ユーザー名> -p`

## RDSのマスター認証情報はAWS Secrets Managerで管理

RDS作成時、RDSのマスター認証情報を「AWS Secrets Managerで管理する」で選択するのが一番安全！さらに、RDSがこのシークレットのローテーションを管理するため、Lambda ローテーション関数を作成する必要はありません。

参考）[Secrets Manager でマスターユーザーパスワードを管理する利点](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-secrets-manager.html#rds-secrets-manager-benefits)

![スクリーンショット 2024-12-08 21.10.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/06450843-892b-7c99-c0ea-956f141bfde8.png)

:::note info
RDSのマスター認証情報はSecrets Managerで管理して、それ以外のDBユーザー認証情報は**IAM認証**で管理した方が良さそう！
まだ試したことないですが、IAM DB認証は、パスワード管理の手間を省き、セキュリティと管理を一元化するため、AWS環境でのデータベースアクセスを効率的かつ安全に行うための重要な手段です。特に、セキュリティ面での強化やアクセス管理の一元化が大きなメリットがあります。
参考）[ユーザーが IAM の認証情報を使用して Amazon RDS for MySQL の DB インスタンスを認証できるようにするにはどうすればよいですか？](https://repost.aws/ja/knowledge-center/users-connect-rds-iam)
:::

# Terraformでの実装について

今回使用したフォルダ構造が以下のようになります。

```
.
├── README.md
├── modules
│   ├── db_proxy
│   │   ├── README.md
│   │   ├── main.tf
│   │   ├── output.tf
│   │   └── variables.tf
│   ├── iam_role
│   │   ├── main.tf
│   │   ├── output.tf
│   │   └── variables.tf
│   ├── lambda_function
│   │   ├── main.tf
│   │   ├── output.tf
│   │   └── variables.tf
│   ├── lambda_layer_python
│   │   ├── create_lambda_layer.sh
│   │   ├── main.tf
│   │   ├── output.tf
│   │   └── variables.tf
│   └── rds
│       ├── README.md
│       ├── main.tf
│       ├── output.tf
│       └── variables.tf
├── rds_proxy_handson
│   ├── Makefile
│   ├── backend.tf
│   ├── data.tf
│   ├── iam.tf
│   ├── lambda
│   │   └── lambda_proxy_access
│   │       ├── index.py
│   │       ├── module
│   │       │   └── test.py
│   │       └── requirements.txt
│   ├── lambda.tf
│   ├── locals.tf
│   ├── output.tf
│   ├── provider.tf
│   ├── rds.tf
│   ├── rds_proxy.tf
│   ├── security_groups.tf
│   ├── subnet.tf
│   ├── variables.tf
│   ├── vpc.tf
│   └── vpc_endpoint.tf
```

---

## 工夫1：Terraformを使ってLambda Layer(Python)の作成を完全自動化する
Terraformを使用して、Lambda Layer（Python）を完全に自動化する方法について紹介します。Lambda Layerの作成には、複数の手順が必要ですが、Terraformのコードを活用することで、その手順を簡略化し、より効率的に管理できます。

以下ブログに記載されているスクリプトを使用することで、Lambda Layerの作成を自動化できます。PythonのLambda Layerを作成する際には、ぜひ参考にしてください！

参考）[TerraformでLambda Layerの作成を完全自動化する](https://zenn.dev/sre_holdings/articles/35694018e71b99)

### Terraformコード例：


```./modules/lambda_layer_python/main.tf
resource "aws_lambda_layer_version" "main" {
  layer_name          = var.layer_name
  filename            = data.archive_file.lambda_layer.output_path
  compatible_runtimes = [var.runtime]
  source_code_hash    = data.archive_file.lambda_layer.output_base64sha256
}

data "external" "lambda_layer" {
  program = ["../modules/lambda_layer_python/create_lambda_layer.sh", var.runtime, var.source_file]
}

data "archive_file" "lambda_layer" {
  type             = "zip"
  output_path      = ".terraform/tmp/lambda/${var.layer_name}.zip"
  source_dir       = data.external.lambda_layer.result.path
  output_file_mode = "0644"
}
```

```./rds_proxy_handson/lambda.tf
module "lambda_layer" {
  source = "../modules/lambda_layer_python"

  layer_name  = "${local.prefix}-layer"
  runtime     = "python3.10"
  source_file = "lambda/lambda_proxy_access/requirements.txt"
}
```

これにより、Lambda Layerの作成をTerraformで完全に自動化できます。外部スクリプトを呼び出して、必要なライブラリを含むLambda Layerを生成し、その結果をLambda LayerのバージョンとしてAWSにデプロイします。

---

## 工夫2：TerraformのModule機能を活用してコードを整理
Terraformでは、Moduleを使用することでリソース定義を再利用可能で簡潔に管理できます。例えば、IAMロールを作成する際にもModuleを使うことで、ポリシーの適用や依存関係の管理を効率的に行えます。

### IAMロール作成のModule使用例：


```./modules/iam_role/main.tf
resource "aws_iam_role" "main" {
  name = var.iam_role_name

  assume_role_policy = jsonencode({
    Version   = "2012-10-17"
    Statement = [var.iam_assume_role_policy]
  })
  depends_on = [var.dependency_resources]
}

resource "aws_iam_policy" "custom_policy" {
  count = length(var.iam_policy_statements) > 0 ? 1 : 0

  name = var.iam_policy_name
  policy = jsonencode({
    Version   = "2012-10-17"
    Statement = var.iam_policy_statements
  })
}

resource "aws_iam_role_policy_attachment" "custom" {
  count = length(var.iam_policy_statements) > 0 ? 1 : 0

  role       = aws_iam_role.main.name
  policy_arn = aws_iam_policy.custom_policy[0].arn
}

resource "aws_iam_role_policy_attachment" "managed" {
  count = length(var.managed_policy_arns)

  role       = aws_iam_role.main.name
  policy_arn = var.managed_policy_arns[count.index]
```

このようにModule機能を活用することで、IAMロールの作成に関連するリソースを整理して管理でき、コードの可読性が向上します。

### IAMロール作成のModuleを使用した実装例：

```./rds_proxy_handson/iam.tf
# IAM Role for Lambda Function
module "lambda_role" {
  source = "../modules/iam_role"

  iam_role_name = "${local.prefix}-lambda-role"
  iam_assume_role_policy = {
    Effect = "Allow"
    Principal = {
      Service = "lambda.amazonaws.com"
    }
    Action = "sts:AssumeRole"
  }
  iam_policy_name = "${local.prefix}-lambda-policy"
  iam_policy_statements = [
    {
      Effect = "Allow"
      Action = [
        "secretsmanager:GetSecretValue"
      ]
      Resource = [
        module.rds.master_user_secret
      ]
    }
  ]
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
    "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
  ]

  dependency_resources = [module.rds]
}
```

このように、Moduleを利用することで、再利用可能なコードのブロックとしてIAMロールやポリシーの設定を行い、依存関係を管理できます。

---

これらの工夫を取り入れることで、Terraformでのインフラ構成管理がより効率的に、かつ可読性が高くなることが実感できるでしょう。TerraformのModule機能を駆使することで、プロジェクト全体のコードの保守性が向上し、必要なリソースを簡潔に管理することができます。


## 苦戦中：Lambdaのアーカイブ作成時にフォルダごとアーカイブできない問題

AWS Lambda関数を作成する際、ソースコードをZIPアーカイブにまとめてアップロードする必要があります。しかし、現状のModuleだとLambdaのアーカイブを作成する際、特定のフォルダ内にあるファイルが正しくフォルダごとアーカイブされず、直接Lambdaにアップロードされるという問題が発生されます。。。

```plaintext
├── lambda
│   └── lambda_proxy_access
│       ├── index.py
│       ├── module
│       │   └── test.py
│       └── requirements.txt
```

上記の構造では、module/test.pyがLambdaにアップロードされる際に、moduleというフォルダ内に格納されず、ルートディレクトリ直下に配置されてしまいます。このため、Lambda内でフォルダ構造を保持したままコードを利用することができません。


Lambda作成のModule：

```
resource "aws_lambda_function" "main" {
  function_name = var.function_name

  role     = var.role_arn
  filename = data.archive_file.main-lambda.output_path
  handler  = var.handler
  runtime  = var.runtime
  timeout  = var.timeout
  layers   = var.lambda_layer_arn
  environment {
    variables = var.environment_variables
  }
  vpc_config {
    security_group_ids = var.security_group_ids
    subnet_ids         = var.subnet_ids
  }
  source_code_hash = data.archive_file.main-lambda.output_base64sha256
  depends_on       = [var.dependency_resources]
}

locals {
  # 指定されたディレクトリ内のファイルをリスト化（セットをリストに変換）
  source_files = tolist(fileset("./${var.source_file}", "**"))
}

output "source_files" {
  value = local.source_files
}

# Lambda関数のソースコードをテンプレートとして処理
data "template_file" "t_file" {
  for_each = toset(local.source_files) # source_files をセットに変換して反復処理
  template = file("./${var.source_file}/${each.value}")
}

# Lambdaのソースコードをzip化
data "archive_file" "main-lambda" {
  type        = "zip"
  output_path = ".terraform/tmp/lambda/${var.function_name}-code.zip"

  # 動的にsourceブロックを生成
  dynamic "source" {
    for_each = local.source_files
    content {
      filename = basename(source.value)                           # source.value はファイルパス
      content  = data.template_file.t_file[source.value].rendered # 各テンプレートのcontent
    }
  }
}
```

解決策が見つかればブログで追記予定


# まとめ

後日、各サービスや機能について詳細に掘り下げた記事を書く予定です！
現状では、Lambdaのアーカイブ作成時にフォルダごとアーカイブできない問題に対する完全な解決策は見つかっていませんが、zipコマンド等打つような処理を挟むなどして解決していこうと思っています！