0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Terraform : 第2回 RDS(PostgreSQL)モジュールを強化する ― バックアップ・監視・セキュリティの積み残しを埋める

0
Posted at

はじめに

前回の記事では、TerraformでRDS(PostgreSQL)モジュールを構築する際の基本的な設計判断を、構築順に沿って解説しました。パスワード管理、暗号化、Enhanced Monitoring、パラメータグループによるログ出力、メンテナンスウィンドウまでを扱いましたが、実際に運用を見据えると、まだいくつかの積み残しがあります。

  • 自動バックアップの詳細な挙動を制御できているか
  • パスワードは自動生成されるが、定期的に更新される仕組みはあるか
  • 障害の予兆をアラームとして検知できるか
  • どのSQLが遅いのかまで追跡できるか
  • DBインスタンス自体からの通信は必要な範囲に絞れているか

本記事では、これらを前回のモジュールに追記する形で進めます。


1. バックアップウィンドウを明示する

前回、backup_retention_periodでバックアップの保持日数は設定しましたが、いつ自動バックアップを取得するかは指定していませんでした。未指定の場合、AWSが任意の時間帯を自動的に割り当てます。

  backup_window = "17:00-17:30"

UTCの17:00〜17:30、JSTでは深夜2:00〜2:30にあたります。ポイントは、前回設定したmaintenance_window(sun:18:00-sun:19:00)と時間帯が重複しないようにすることです。バックアップ処理とメンテナンス(再起動を伴う可能性がある処理)が同時に走ると、想定外の遅延や失敗につながる可能性があります。

2. 自動スナップショットにタグを引き継ぐ

デフォルトでは、自動バックアップから作成されるスナップショットに、インスタンスのタグは引き継がれません。コスト按分やリソースの棚卸しをタグベースで行っている場合、スナップショットだけがタグなしで漏れてしまいます。

  copy_tags_to_snapshot = true

設定自体は1行ですが、タグ運用の一貫性を保つ上では見落としやすい項目です。


3. マスターパスワードのローテーションを設定する

前回、manage_master_user_password = trueによってパスワードをSecrets Managerで自動管理する設定を入れました。ただし、これは「安全に保管される」ことを保証するだけで、定期的に値が更新されるわけではありません。作成時に生成されたパスワードは、明示的にローテーションを設定しない限りそのまま使われ続けます。

ローテーションには、RDS用にAWSが提供するマネージドローテーション機能を使うのが簡単です。

resource "aws_secretsmanager_secret_rotation" "rds_master" {
  secret_id           = aws_db_instance.this.master_user_secret[0].secret_arn
  rotation_lambda_arn = aws_lambda_function.rds_rotation.arn

  rotation_rules {
    automatically_after_days = 90
  }
}

rotation_lambda_arnには、AWSがSecrets Manager用に提供するローテーション用Lambda(SAR: Serverless Application Repositoryで配布されているテンプレート)をデプロイしたものを指定します。ローテーション実行時、Lambdaが新しいパスワードを生成し、RDS側のマスターパスワードを変更した上でSecrets Managerに反映します。

90日という周期は一例です。監査要件や社内のパスワードポリシーに合わせて調整します。ローテーションの本体(Lambda関数のデプロイ)は構成がやや大きくなるため、別記事で扱う想定です。


4. CloudWatchアラームで異常を検知できるようにする

ログとメトリクスの転送先は前回までに用意しましたが、閾値を超えたときに通知する仕組みがまだありません。最低限、以下の4項目を監視します。

resource "aws_cloudwatch_metric_alarm" "rds_cpu" {
  alarm_name          = "${var.project}-${var.env}-rds-cpu-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods   = 3
  metric_name          = "CPUUtilization"
  namespace            = "AWS/RDS"
  period               = 300
  statistic            = "Average"
  threshold            = 80
  alarm_description    = "RDS CPU使用率が80%を3回連続で超過"
  dimensions = {
    DBInstanceIdentifier = aws_db_instance.this.id
  }
  alarm_actions = [var.alarm_sns_topic_arn]
}

resource "aws_cloudwatch_metric_alarm" "rds_free_storage" {
  alarm_name          = "${var.project}-${var.env}-rds-storage-low"
  comparison_operator = "LessThanThreshold"
  evaluation_periods   = 1
  metric_name          = "FreeStorageSpace"
  namespace            = "AWS/RDS"
  period               = 300
  statistic            = "Average"
  threshold            = 2147483648  # 2GiB
  alarm_description    = "RDS空きストレージが2GiBを下回った"
  dimensions = {
    DBInstanceIdentifier = aws_db_instance.this.id
  }
  alarm_actions = [var.alarm_sns_topic_arn]
}

resource "aws_cloudwatch_metric_alarm" "rds_free_memory" {
  alarm_name          = "${var.project}-${var.env}-rds-memory-low"
  comparison_operator = "LessThanThreshold"
  evaluation_periods   = 3
  metric_name          = "FreeableMemory"
  namespace            = "AWS/RDS"
  period               = 300
  statistic            = "Average"
  threshold            = 268435456  # 256MiB
  alarm_description    = "RDS空きメモリが256MiBを下回る状態が続いている"
  dimensions = {
    DBInstanceIdentifier = aws_db_instance.this.id
  }
  alarm_actions = [var.alarm_sns_topic_arn]
}

resource "aws_cloudwatch_metric_alarm" "rds_connections" {
  alarm_name          = "${var.project}-${var.env}-rds-connections-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods   = 3
  metric_name          = "DatabaseConnections"
  namespace            = "AWS/RDS"
  period               = 300
  statistic            = "Average"
  threshold            = var.max_connections_threshold
  alarm_description    = "RDS接続数が閾値を超過。コネクションリークやスケールアウトの過多を疑う"
  dimensions = {
    DBInstanceIdentifier = aws_db_instance.this.id
  }
  alarm_actions = [var.alarm_sns_topic_arn]
}
  • CPUUtilization: クエリのボトルネックやインスタンスサイズ不足の兆候。3回連続超過を条件にして、一時的なスパイクでの誤検知を避けています。
  • FreeStorageSpace: max_allocated_storageによる自動拡張はありますが、拡張が追いつかない・上限に達した場合の最終防衛ラインとして必要です。
  • FreeableMemory: メモリ不足はcrash(OOM)に直結するため、早期検知の価値が高い指標です。
  • DatabaseConnections: ECS側のタスク数が想定以上に増えた場合や、コネクションプールの設定ミスによる枯渇を検知します。max_connections_thresholdはインスタンスクラスごとの上限を踏まえて変数化しておきます。

alarm_actionsに渡すvar.alarm_sns_topic_arnは、ALB側で既に使っているアラーム通知の仕組み(SNSトピック)を再利用する想定です。

5. Performance Insightsで「重いSQL」を可視化する

Enhanced MonitoringはOS・プロセスレベルの負荷は見えますが、どのSQLが原因かまでは分かりません。Performance Insightsを有効にすると、待機イベントや実行中のSQLを可視化できます。

  performance_insights_enabled          = true
  performance_insights_retention_period = 7  # 無料枠は7日

無料枠では7日分のデータが保持されます。より長期の傾向分析が必要な場合は、有料の長期保持(最大2年)に設定を変更できますが、まずは無料枠で運用を始め、必要性を見極めてから拡張するのが妥当です。

6. RDSイベント通知を設定する

フェイルオーバー、ストレージのオートスケーリング発生、パラメータグループの変更適用など、RDSが発するイベントを能動的に受け取る仕組みも未設定でした。

resource "aws_db_event_subscription" "this" {
  name      = "${var.project}-${var.env}-rds-events"
  sns_topic = var.alarm_sns_topic_arn

  source_type = "db-instance"
  source_ids  = [aws_db_instance.this.id]

  event_categories = [
    "failover",
    "failure",
    "low storage",
    "maintenance",
    "notification",
  ]
}

CloudWatchアラームがメトリクスの閾値超過を検知するのに対し、イベント通知はRDSサービス自体が発する「何かが起きた」という事実を伝えます。両者は補完関係にあり、片方だけでは検知できない障害があります。

7. セキュリティグループのegressを絞る

前回作成したセキュリティグループは、ingressをECSからのみに絞る一方、egressは全開放(0.0.0.0/0)のままでした。RDSインスタンスは通常、外向き通信をほとんど必要としません。

resource "aws_security_group" "rds" {
  # ingress設定は前回の通り

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = []
    # 必要な場合のみ、拡張機能の取得先などを個別に許可する
  }
}

cidr_blocks = []で実質的に外向き通信を遮断し、将来的にVPCエンドポイント経由でのS3アクセス(拡張機能のインポート/エクスポート等)が必要になった時点で、必要な範囲だけ個別に許可を追加する方針です。最初から全開放にしておくよりも、必要になった通信を都度追加する方が、意図しない通信経路を残さずに済みます。

8. 変更の反映タイミングを制御する(apply_immediately)

aws_db_instanceに対する変更の多くは、デフォルトでは次のメンテナンスウィンドウまで適用が保留されます。検証環境で変更をすぐに反映して確認したい場合は、明示的に即時適用を指定します。

  apply_immediately = var.apply_immediately

検証環境ではtrueにして開発イテレーションを速くし、本番環境ではfalse(デフォルト)のままにして、意図しないタイミングでの変更適用を避ける、という環境ごとの切り替えが基本方針になります。特にパラメータグループのstaticパラメータ変更や、インスタンスクラスの変更は接続断や再起動を伴うため、本番では計画的なタイミングでの適用が前提です。

9. 運用上の注意点(再作成時の落とし穴)

検証環境のようにterraform destroyapplyを繰り返す運用では、final_snapshot_identifierに固定値を指定していると、同名のスナップショットが既に存在してエラーになることがあります。

  final_snapshot_identifier = "${var.identifier}-final-snapshot"

skip_final_snapshot = trueにしている間はこの値自体が使われないため実害はありませんが、本番運用に向けてskip_final_snapshot = falseに切り替える際は、タイムスタンプを含めるなどして一意性を持たせる必要があります。

  final_snapshot_identifier = "${var.identifier}-final-${formatdate("YYYYMMDDhhmmss", timestamp())}"

ただしtimestamp()を使うと、Terraformの実行のたびに差分が発生する点に注意が必要です。破棄のタイミングでのみ意味を持つ値であるため、通常のapplyでは無視できる差分ではありますが、CIのプラン確認などで「差分あり」と表示される点は事前に共有しておくとよいでしょう。

今後の選択肢として残るもの

以下は今回のスコープには含めていませんが、要件次第で検討対象になる項目です。

  • リードレプリカ: 読み取り負荷の分散が必要になった場合。
  • RDS Proxy: ECS Fargateのスケールアウト時にコネクション数が急増し、DB側の上限に達しやすい場合の緩衝材として。

まとめ

前回のモジュールに対して、以下の観点を追加しました。

  1. バックアップウィンドウをメンテナンスウィンドウと重複しないよう明示する
  2. 自動スナップショットにタグを引き継ぐ
  3. マスターパスワードのローテーションを設定する
  4. CPU・ストレージ・メモリ・接続数のCloudWatchアラームを追加する
  5. Performance Insightsで重いSQLを可視化する
  6. RDSイベント通知でフェイルオーバー等を能動的に検知する
  7. セキュリティグループのegressを必要最小限に絞る
  8. 環境ごとに変更の即時反映を制御する(apply_immediately)
  9. 再作成時のfinal_snapshot_identifier衝突を避ける

「転送先(ログ・メトリクス)を用意すること」と「実際に検知・通知される状態にすること」は別の作業であり、後者を見落としたまま運用に入ってしまうケースは少なくありません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?