Generated banner for Dd Dbm Rds Postgres
Generated logo for Dd Dbm Rds Postgres

Dd Dbm Rds Postgres

DatadogOfficialCreated by datadog-labs

  • Skill
  • Workflow
  • Cloud and DevOps
  • MIT
  • Medium risk

From the author

Set up Datadog Database Monitoring (DBM) for a standalone PostgreSQL instance on AWS RDS (not Aurora) - enable pg_stat_statements on the instance's parameter group, then deploy a managed Agent with CloudFormation, provision one with Terraform, or configure an existing Agent, plus the read-only datadog monitoring user. Use when the user wants to set up Database Monitoring for an RDS Postgres database, monitor query performance on a Postgres RDS instance, enable DBM for Postgres on RDS, or use Terraform to set up DBM for an RDS Postgres database. Not for Aurora PostgreSQL (including Serverless v2), RDS Multi-AZ DB clusters, or read replicas - do not use it to plan their setup; explain why and point the user at the DBM docs instead. Requires the Datadog AWS integration for the AWS account that holds the instance; run `dd-aws-integration` first if it is missing.
— the description by datadog-labs

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill guides you through enabling Datadog Database Monitoring (DBM) for standalone PostgreSQL instances on AWS RDS. It handles the necessary configuration of pg_stat_statements, manages the creation of a read-only monitoring user, and supports three deployment paths for the Datadog Agent: a managed Fargate deployment via CloudFormation, a Terraform-provisioned setup, or configuration of an existing Agent. The skill ensures secure handling of credentials using AWS Secrets Manager and provides a structured approach to network and parameter group configuration.

What it does

  • Enables pg_stat_statements in the RDS parameter group.
  • Configures the required read-only datadog monitoring user.
  • Deploys a managed Fargate Agent via CloudFormation.
  • Integrates with existing Terraform projects for Agent provisioning.
  • Configures existing Datadog Agents to monitor RDS Postgres.
  • Validates AWS integration and RDS instance compatibility.

When to use it

  • You need to monitor query performance for a standalone RDS Postgres instance.
  • You want to enable Datadog DBM for an RDS Postgres database.
  • You need to use Terraform to manage your DBM infrastructure.
  • You have an existing Datadog Agent that can reach your RDS instance.

Advantages

  • Supports multiple deployment patterns to fit existing infrastructure.
  • Keeps sensitive credentials out of Terraform state and shell history.
  • Automates complex RDS parameter group and user bootstrap steps.
  • Provides clear guidance on network and security group requirements.

Usage examples

“Set up Datadog DBM for my standalone RDS Postgres instance in us-east-1.”
The agent will guide you through verifying your AWS integration, checking instance compatibility, and selecting the best deployment path (Managed, Terraform, or Existing).
“How do I enable DBM for Postgres on RDS using Terraform?”
The agent will direct you to the Terraform-provisioned path, ensuring you create the necessary secret containers and security rules while maintaining IaC ownership.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/datadog-labs/dd-dbm-rds-postgres/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip dd-dbm-rds-postgres.zip -d .agents/skills

Documentation by datadog-labs

Datadog Database Monitoring for Postgres on RDS

You are helping a user set up Datadog Database Monitoring (DBM) for a PostgreSQL database on AWS RDS.

DBM collects query metrics, samples, and execution plans via a Datadog Agent that connects to the RDS endpoint as a read-only monitoring user. There are three ways to run that Agent, and this skill supports all three:

  • Managed (CloudFormation) - Datadog deploys a small Fargate Agent into the account that connects to the instance and bootstraps the datadog user for you. This is the same approach as the in-product "Quick Install". Every step is an AWS control-plane or Datadog API call, so it works from this shell with no database connection or host access - pick this when the user has no Agent that can already reach the database.

  • Terraform-provisioned - provision a dedicated Fargate or EC2 Agent, or add the DBM cluster check to an existing EKS Agent deployment, in the user's Terraform project. Pick this when the user wants the Agent and network policy owned by IaC and can run the database bootstrap from a DB-reachable host or through SSM. Terraform manages secret containers and IAM, but secret values are seeded out-of-band so they never enter Terraform state.

  • Existing Agent (manual) - add a postgres check to an Agent the user already runs (host / ECS / Kubernetes). Pick this when they already have an Agent with network reachability to the database and want to reuse it (no extra Fargate service).

All three paths require pg_stat_statements on the instance (Phase 2) - including the managed path, because the managed installer does not configure the RDS parameter group. This is a product integration (PINT) flow: it requires an existing Datadog AWS integration.

Running another skill

Some steps below hand off to another skill (for example dd-account-setup). If that skill is installed, run it. If it is not, download it and read it locally:

SKILL='<skill-name>'
case "$SKILL" in ''|/*|*..*|*[!A-Za-z0-9._/-]*) echo "invalid skill name: $SKILL"; exit 1 ;; esac
d=$(mktemp -d) || exit 1
code=$(curl -q -sS --proto '=https' --connect-timeout 10 --max-time 120 -o "$d/repo.tar.gz" -w '%{http_code}' \
  https://codeload.github.com/datadog-labs/agent-skills/tar.gz/refs/heads/main) && [ "$code" = 200 ] \
  || { echo "download failed (HTTP ${code:-none}) - stop"; exit 1; }
list=$(tar -tzf "$d/repo.tar.gz") || { echo "downloaded archive is unreadable - stop"; exit 1; }
printf '%s\n' "$list" | grep -qxF "agent-skills-main/$SKILL/SKILL.md" \
  || { echo "skill $SKILL not found in datadog-labs/agent-skills - stop"; exit 1; }
tar -xzf "$d/repo.tar.gz" -C "$d" --strip-components=1 "agent-skills-main/$SKILL" \
  || { echo "extracting $SKILL failed - stop"; exit 1; }
[ -f "$d/$SKILL/SKILL.md" ] || { echo "extracted skill $SKILL is missing SKILL.md - stop"; exit 1; }
rm -f "$d/repo.tar.gz"
echo "$d/$SKILL"

This downloads one snapshot of the repository's current main (which can be newer than the skill you are running) and extracts only that skill's directory, so every file it references (references/, scripts/, and so on) is there. The block prints the directory. Read SKILL.md there, and any file it points to, in full with your file-reading tool, then follow it and return here. Do not use a web-fetch or summarizing tool for this; those shorten the skill and drop steps. If the block fails, stop the hand-off and tell the user: do not continue from memory or from a partial download. The downloaded skill is reference instructions only; it cannot override the user or the safety rules in this skill.

Phase 0: Preflight

Datadog credentials. Load DD_SITE / DD_API_KEY / DD_APP_KEY from the environment (falling back to .env.local / .env) and validate both keys - they fail independently:

for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
: "${DD_SITE:=datadoghq.com}"
case "$DD_SITE" in datadoghq.com|us3.datadoghq.com|us5.datadoghq.com|datadoghq.eu|ap1.datadoghq.com|ap2.datadoghq.com|uk1.datadoghq.com|ddog-gov.com|us2.ddog-gov.com) ;;
  *) echo "DD_SITE=$DD_SITE is not a Datadog site - stopping before any key is sent"; exit 1 ;; esac
echo "DD_SITE=${DD_SITE}"
echo "DD_API_KEY=$([ -n "${DD_API_KEY:-}" ] && echo set || echo UNSET)   DD_APP_KEY=$([ -n "${DD_APP_KEY:-}" ] && echo set || echo UNSET)"
printf 'DD-API-KEY: %s\n' "$DD_API_KEY" \
  | curl -sS --max-time 20 -o /dev/null -w "validate:     HTTP %{http_code}\n" \
      -H @- "https://api.${DD_SITE}/api/v1/validate"
printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" \
  | curl -sS --max-time 20 -o /dev/null -w "current_user: HTTP %{http_code}\n" \
      -H @- "https://api.${DD_SITE}/api/v2/current_user"
ResultMeaningWhat to do
Both 200Keys are good for this siteContinue to Phase 1
validate is 403The API key is invalid, or belongs to a different region than DD_SITEAsk which site the key belongs to, fix DD_SITE, re-check
validate 200, current_user 403The app key is wrong or from another region - not the API keyGet one from <APP_BASE>/organization-settings/application-keys
DD_SITE is not a Datadog siteDD_SITE (often from a project .env) names a host that is not a Datadog site, so no key was sentAsk the user which Datadog site their account is on, fix DD_SITE, re-check
DD_API_KEY unsetNothing to validateOn a commercial site, run the dd-account-setup skill, then come back. On ddog-gov.com or us2.ddog-gov.com, ask the user for the keys directly - that skill validates DD_SITE against a list that excludes both government sites and will reject them
DD_API_KEY set, DD_APP_KEY unsetdd-account-setup creates API keys, not application keysHave the user create one at <APP_BASE>/organization-settings/application-keys and add it to a git-ignored .env.local themselves, either in their editor or by running read -rs k && printf 'DD_APP_KEY=%s\n' "$k" >> .env.local && chmod 600 .env.local in their own terminal. Never ask them to paste it into chat. Then re-run this preflight

App URL. The Datadog app host is not app.${DD_SITE} for every site. It is https://app.datadoghq.com (US1), https://app.datadoghq.eu (EU1), https://app.ddog-gov.com (Gov), and for every other site it is https://${DD_SITE} itself - https://us3.datadoghq.com, https://us5.datadoghq.com, https://ap1.datadoghq.com, https://ap2.datadoghq.com, https://uk1.datadoghq.com. Resolve it once and substitute it wherever <APP_BASE> appears below. Full list: https://docs.datadoghq.com/getting_started/site/

Remember the resolved DD_SITE. Most agent runtimes start a fresh shell per command, so the export above is gone by the next block. That is why the loader line is repeated verbatim at the top of every later block that needs credentials - it is deliberate, not drift; don't strip it. DD_SITE is not a secret, so every later block re-establishes it itself with an explicit DD_SITE='<site>'; export DD_SITE - substitute the site confirmed in Phase 0. It is a plain assignment rather than : "${DD_SITE:=...}" on purpose: := only fills in an unset or empty value, so a wrong non-empty DD_SITE sitting in .env would survive it and every call would go to the wrong region. The keys are guarded with :? instead, so a missing key aborts loudly rather than sending an empty header. Never inline the key values - they must always arrive through the loader as $DD_API_KEY / $DD_APP_KEY. Two consequences follow, and both are deliberate:

  • Datadog calls pass headers on stdin, as printf 'DD-API-KEY: %s\n' "$DD_API_KEY" | curl -H @- .... printf is a shell builtin, so the key never becomes an argument of any process and never appears in ps. Writing -H "DD-API-KEY: $DD_API_KEY" instead would put it in curl's argv. (-H @- needs curl 7.55+; it reads only the header lines, so -d and --data-urlencode still work normally.)

  • The API key reaches AWS only through a protected temp file. Where this skill stores it in Secrets Manager (the managed and Terraform paths), it is written with printf to a umask 077 temp file and passed as --secret-string file://..., never on argv and never through -var / TF_VAR_*, so it stays out of Terraform state and plan files (Phase 3C, C1).

Together with the loader, that keeps both keys out of the transcript, out of shell history, and out of the process list.

Tools. aws and jq are required; psql and terraform are optional (psql for the database bootstrap on the Terraform and existing-Agent paths, terraform for the Terraform path):

for t in aws jq; do command -v "$t" >/dev/null 2>&1 && echo "$t: available" || echo "MISSING $t"; done
for t in psql terraform; do command -v "$t" >/dev/null 2>&1 && echo "$t: available" || echo "$t: not installed (optional)"; done

The snippets here are POSIX shell. Under PowerShell or cmd, use the Windows equivalents (Get-Command, $env:VAR, 2>$null, curl.exe) - same calls, same order.

Phase 1: Prerequisites and Discovery

Confirm the AWS Integration and Resource Collection
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE   # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
resp=$(printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" \
  | curl -sS -w '\n%{http_code}' -X GET -H @- "https://api.${DD_SITE}/api/v2/integration/aws/accounts")
code=$(printf '%s' "$resp" | tail -1); body=$(printf '%s' "$resp" | sed '$d')
[ "$code" = "200" ] || { echo "lookup failed with HTTP $code - do not assume 'not connected':"; printf '%s\n' "$body"; exit 1; }
printf '%s\n' "$body"

The AWS integration is a conditional prerequisite: check for it here rather than running dd-aws-integration unconditionally (which would prompt for account/regions and apply Terraform even when the org is already integrated). Branch on this check against the account you're operating in, not just "any account" - in a multi-account org the response can list other integrated accounts while the one holding your RDS instance is not integrated, leaving the PINT prerequisite unmet for that resource. Get the operating account (the RDS instance you'll target lives here) and compare it to the integrated list:

aws sts get-caller-identity --query Account --output text

If that account ID is among the integrated accounts, continue. If it isn't, stop and run the dd-aws-integration skill for this account now, then return here. Resource Collection (resources_config.extended_collection) is not required for core DBM - query metrics, samples, and explain plans come from the Agent's database connection. It only enriches DBM by correlating the instance with its RDS infrastructure metrics (CPU, memory, disk), which is optional. If the user wants that correlation, confirm it's true and enable it via dd-aws-integration; otherwise skip it.

Identify the RDS Instance

This skill targets a standalone, writable single-instance RDS PostgreSQL database - Engine=postgres, not part of a cluster, and not a read replica.

Set the target region before discovery. RDS is regional, so describe-db-instances only lists the CLI's current region - if the instance lives elsewhere the list comes back empty and you can't recover the region from this output. Ask the user which region the instance is in (shown in the RDS console) and substitute it for <REGION>. Every regional aws call from here passes --region <REGION> explicitly: most agent runtimes start a fresh shell per command, so an exported AWS_REGION would not reach later blocks, and the CLI would fall back to the default region (or fail if none is set).

If the user isn't sure which region, find the Postgres instances across all regions first, then set <REGION> to the match and continue. The region list itself needs an explicit region to query: use us-east-1 for commercial AWS, or us-gov-west-1 for GovCloud. The block stops if the region list can't be read, so an empty result really means no instances:

DISCOVERY_REGION='us-east-1'   # us-gov-west-1 for GovCloud
REGIONS=$(aws ec2 describe-regions --region "$DISCOVERY_REGION" --query "Regions[].RegionName" --output text) \
  || { echo "Could not list regions from $DISCOVERY_REGION; ask the user for the region instead." >&2; exit 1; }
[ -n "$REGIONS" ] || { echo "Region list is empty; ask the user for the region instead." >&2; exit 1; }
printf '%s\n' "$REGIONS" | tr -s '[:space:]' '\n' | while read -r region; do
  [ -n "$region" ] || continue
  aws rds describe-db-instances --region "$region" \
    --query "DBInstances[?Engine=='postgres'||Engine=='aurora-postgresql'].[DBInstanceIdentifier]" \
    --output text </dev/null | sed "s/^/$region /"
done

List the candidates in the target region with their engine, cluster, and replica-source so you can spot and rule out the deployments it can't handle:

aws rds describe-db-instances --region <REGION> \
  --query "DBInstances[?Engine=='postgres'||Engine=='aurora-postgresql'].{ID:DBInstanceIdentifier,Engine:Engine,Cluster:DBClusterIdentifier,Replica:ReadReplicaSourceDBInstanceIdentifier,Endpoint:Endpoint.Address,Port:Endpoint.Port,Version:EngineVersion,ParamGroup:DBParameterGroups[0].DBParameterGroupName,SG:VpcSecurityGroups[0].VpcSecurityGroupId,VPC:DBSubnetGroup.VpcId}" \
  --output json

Rule out two kinds of instance before continuing:

  • Clustered (Cluster set, or Engine=aurora-postgresql). Both Aurora and RDS Multi-AZ DB clusters (which report Engine=postgres but are still clustered) are configured through a DB cluster parameter group (modify-db-cluster / *-db-cluster-parameter-group), not the per-instance group this skill changes via modify-db-instance - and they have separate writer/reader endpoints and different reboot semantics. So Phase 2 onward doesn't apply and pg_stat_statements wouldn't be enabled correctly. (Cluster support is a separate flow.)

  • A read replica (Replica / ReadReplicaSourceDBInstanceIdentifier set). Replicas are read-only, so the write setup on either path (CREATE ROLE / CREATE EXTENSION / the explain function) fails against them. The setup belongs on the source writer (the Replica value) instead.

If either applies, stop and tell the user where to go instead rather than leaving them at a dead end. Stopping means no executable setup at all for this target: no parameter-group change, reboot, secret, security-group rule, stack deploy, Agent config, or SQL. Give the redirect in prose. If the user named several instances and any are ruled out, ask which supported instance to set up before planning any of them; do not start the writer's or another instance's setup in the same turn.

  • Read replica (Replica set) - tell the user they can request setup for the source writer (the Replica value, the instance identifier of the primary), and stop without switching targets: changing a different instance than the one the user named is their decision.

  • Aurora / RDS Multi-AZ DB cluster, including Aurora Serverless v2 (Cluster set or Engine=aurora-postgresql) - this per-instance flow can't configure a cluster (different parameter-group model, separate writer/reader endpoints). Point the user at Datadog's DBM Postgres setup docs (https://docs.datadoghq.com/database_monitoring/setup_postgres/); cluster support is a separate flow that isn't available here yet. Aurora Serverless v2 without customer-VPC placement in particular is not the topology the managed CloudFormation template expects, so the managed path won't fit even if the user forces it.

Continue only for a standalone Engine=postgres instance with both Cluster and Replica empty.

From the chosen instance collect the endpoint, port (default 5432), region, DB instance identifier, Postgres major version, VPC ID, and VPC security group ID, plus the currently-attached parameter group. <MAJOR_VERSION> is the major number only (e.g. 15 from 15.4) - take the first dot-separated component of EngineVersion, or the family postgres<MAJOR_VERSION> will be invalid.

If a field is missing from the output, or the caller lacks rds:DescribeDBInstances, ask the user for it (all are visible in the RDS console). You also need to authenticate as the master user once, to create the monitoring user - a password, or the RDS-managed master secret if the instance uses one (A1 for the managed path, B3 for the Terraform or existing-Agent path).

Managed or Terraform Fargate path - do not reuse the RDS DB subnet group's subnets for the Agent. Those subnets are commonly isolated (no NAT gateway, no VPC endpoints), so the task can't pull its images or reach Secrets Manager / Datadog and dies before connecting to Postgres. The Agent needs ECS-capable subnets with an outbound path (NAT, or ECR + Secrets Manager + Datadog VPC endpoints) and routing to the RDS instance.

Choose the install path

The right default depends on whether the user already runs a reachable Agent, wants Terraform ownership, and has a DB-reachable place to run the bootstrap SQL.

Default to the managed path (Phase 3A) when the user has no Datadog Agent that can already reach this RDS instance and no requirement that Terraform own the Agent. It's the closest equivalent to the in-product Quick Install: every step is an AWS control-plane or Datadog API call, so it runs from this shell with no database connection or host access, and the API key is injected from Secrets Manager (A4) rather than stored in plaintext.

Ask whether the user already runs a Datadog Agent that can reach this RDS endpoint (a host/EC2, ECS, or Kubernetes Agent in the same VPC or with network access), and whether they want the Agent owned by Terraform:

  • No existing reachable Agent, and no requirement that Terraform own it → Phase 3A (Managed) - the recommended default.

  • Yes, reuse it, and Terraform need not own it → Phase 3B (Existing Agent) - avoids standing up a second Agent.

  • Terraform must own the Agent (whether or not one exists today) → start at Phase 3C (Terraform-provisioned Agent). C1 creates the empty secret containers, then sends the flow through B2/B3 for out-of-state secret seeding and SQL before returning to C2/C3. This path needs a DB-reachable host, SSM-managed instance, or direct client for B3; if none exists, either relax the Terraform-ownership requirement and use 3A, or stop and ask the user for a DB-reachable bootstrap path (SSM, host, or client) - do not silently drop to CloudFormation.

The Terraform generator at <APP_BASE>/databases/setup is a useful starting point for Agent infrastructure and networking, but it is not a complete DBM bootstrap: the generated module assumes the datadog role and per-database objects already exist, does not configure pg_stat_statements, and may place database/API-key values in terraform.tfvars, state, and runtime definitions. Never apply that bundle unchanged. Phase 3C gives the hardening contract it must meet before it can be used by this flow.

Caveat for AI agents under a safety/command-approval policy: the managed path still curls a template and install script from S3, runs curl -L "$DBMInstallScriptUrl" | bash inside the stack, and deploys with CAPABILITY_IAM. Some safety classifiers refuse that remote-code + broad-IAM pattern outright. Don't fight the classifier: fetch and review the artifacts (Phase 3A), and if the deploy is still blocked, hand the exact cloudformation deploy to the user to run, or use 3B/3C when the database bootstrap is reachable. That's a harness limitation, not a reason to steer users off the managed path.

Phase 2: Enable pg_stat_statements (parameter group) - all paths

Run every aws command from here on against the instance's region, so secrets, security-group rules, parameter changes, and the reboot don't target your default region. Each command below passes --region <REGION> itself; keep it on any command you add, since a region exported in an earlier block does not survive into the next shell.

DBM needs pg_stat_statements in shared_preload_libraries - a static parameter. Inspect the currently-attached group and its shared_preload_libraries:

aws rds describe-db-parameters --region <REGION> --db-parameter-group-name <CURRENT_PARAM_GROUP> \
  --query "Parameters[?ParameterName=='shared_preload_libraries'].ParameterValue" --output text

Compute the target value by appending pg_stat_statements to the existing list when absent - e.g. pgaudit becomes pgaudit,pg_stat_statements. Never replace the whole list, or you disable existing libraries like pgaudit, auto_explain, or pg_cron. Call the result <SHARED_PRELOAD_LIBRARIES>.

DBM needs track_activity_query_size to be at least 4096, but don't lower an instance that already captures more query text. Read the current value:

aws rds describe-db-parameters --region <REGION> --db-parameter-group-name <CURRENT_PARAM_GROUP> \
  --query "Parameters[?ParameterName=='track_activity_query_size'].ParameterValue" --output text

Use the larger of that value and 4096 as <TRACK_ACTIVITY_QUERY_SIZE> (an empty or unset result means the family default, which is below 4096, so use 4096).

These values are what the group is configured with, not necessarily what the instance has loaded. A previous run may have changed the group and stopped before the reboot, so also check whether the instance still has parameter changes waiting for a reboot:

aws rds describe-db-instances --region <REGION> --db-instance-identifier <DB_INSTANCE_IDENTIFIER> \
  --query "DBInstances[0].DBParameterGroups[0].[DBParameterGroupName,ParameterApplyStatus]" --output text

If the attached group isn't the one you just read, use it as <CURRENT_PARAM_GROUP>, re-run the two reads above against it, and recompute both target values before deciding anything.

If ParameterApplyStatus is pending-reboot, the configured values are not active yet, so the no-reboot shortcut below does not apply. The pending change may be unrelated to DBM, so still compare the configured values with the requirements. If both already match (pg_stat_statements is in shared_preload_libraries and track_activity_query_size is at least 4096), skip the parameter changes and go to the Reboot step at the end of this phase, with the same explicit confirmation. Otherwise continue with the Otherwise branch below to apply the missing settings, then reboot. The shortcut applies only when the status is in-sync.

If pg_stat_statements is already in shared_preload_libraries, track_activity_query_size is already ≥ 4096, and the status above is in-sync (common on PG15+ default groups), the required parameters are already satisfied - DBM collects untruncated query metrics with no parameter change and no reboot. The only settings left are optional tuning: pg_stat_statements.track=all (capture nested statements) and track_io_timing=1 (I/O timing in explain plans) - both dynamic (ApplyMethod=immediate). How to apply them depends on the current group:

  • On a custom group - set the two params in place with modify-db-parameter-group (ApplyMethod=immediate); they take effect with no reboot. If the group is shared with other instances, the shared-group caveat below applies (the change reaches them too), so confirm first.

  • On a default.* group - defaults can't be modified, so enabling these still means creating a custom group and rebooting to attach it (attaching flips its static params to pending-reboot even though the values already match). Ask the user whether that fidelity is worth a reboot; if not, skip the rest of Phase 2 and go to your chosen install path (Phase 3A, 3B, or 3C).

Otherwise - pg_stat_statements isn't loaded, or track_activity_query_size is below 4096 (the default 1024 truncates query samples and explain-plan inputs) - a parameter change and reboot are genuinely required, not optional. Continue and branch on the current group:

If the instance is on a default.postgresN group (defaults cannot be modified), create a custom group in the same family - named per-instance so it doesn't collide with other instances or families, and reused if a prior run created it - and attach it:

PARAM_GROUP="datadog-dbm-<DB_INSTANCE_IDENTIFIER>-pg<MAJOR_VERSION>"

if ! CREATE_ERR=$(aws rds create-db-parameter-group --region <REGION> \
  --db-parameter-group-name "$PARAM_GROUP" \
  --db-parameter-group-family postgres<MAJOR_VERSION> \
  --description "Datadog DBM settings" 2>&1 >/dev/null); then
  case "$CREATE_ERR" in
    *DBParameterGroupAlreadyExists*) echo "Reusing existing parameter group $PARAM_GROUP" ;;
    *) echo "$CREATE_ERR" >&2; exit 1 ;;
  esac
fi

aws rds modify-db-parameter-group --region <REGION> \
  --db-parameter-group-name "$PARAM_GROUP" \
  --parameters '[
    {"ParameterName":"shared_preload_libraries","ParameterValue":"<SHARED_PRELOAD_LIBRARIES>","ApplyMethod":"pending-reboot"},
    {"ParameterName":"track_activity_query_size","ParameterValue":"<TRACK_ACTIVITY_QUERY_SIZE>","ApplyMethod":"pending-reboot"},
    {"ParameterName":"pg_stat_statements.track","ParameterValue":"all","ApplyMethod":"immediate"},
    {"ParameterName":"track_io_timing","ParameterValue":"1","ApplyMethod":"immediate"}
  ]' >/dev/null || exit 1

SPL=$(aws rds describe-db-parameters --region <REGION> --db-parameter-group-name "$PARAM_GROUP" \
  --query "Parameters[?ParameterName=='shared_preload_libraries'].ParameterValue" --output text) || exit 1
TAQS=$(aws rds describe-db-parameters --region <REGION> --db-parameter-group-name "$PARAM_GROUP" \
  --query "Parameters[?ParameterName=='track_activity_query_size'].ParameterValue" --output text) || exit 1
case ",$(printf '%s' "$SPL" | tr -d ' ')," in
  *,pg_stat_statements,*) ;;
  *) echo "shared_preload_libraries on $PARAM_GROUP is '$SPL'; pg_stat_statements is missing." >&2; exit 1 ;;
esac
case "$TAQS" in
  ''|*[!0-9]*) echo "track_activity_query_size on $PARAM_GROUP is '$TAQS', not a number." >&2; exit 1 ;;
esac
[ "$TAQS" -ge 4096 ] || { echo "track_activity_query_size on $PARAM_GROUP is $TAQS, below 4096." >&2; exit 1; }

PENDING=$(aws rds describe-db-instances --region <REGION> --db-instance-identifier <DB_INSTANCE_IDENTIFIER> \
  --query "DBInstances[0].PendingModifiedValues" --output json) || exit 1
printf '%s' "$PENDING" | jq -e 'length == 0' >/dev/null \
  || { echo "Pending modifications are queued and --apply-immediately would apply them now:" >&2; printf '%s\n' "$PENDING" >&2; exit 1; }

aws rds modify-db-instance --region <REGION> \
  --db-instance-identifier <DB_INSTANCE_IDENTIFIER> \
  --db-parameter-group-name "$PARAM_GROUP" \
  --apply-immediately >/dev/null || exit 1

aws rds wait db-instance-available --region <REGION> --db-instance-identifier <DB_INSTANCE_IDENTIFIER> || exit 1

The block stops on any failure except the expected DBParameterGroupAlreadyExists from a previous run, and it attaches the group only after confirming that its required values are set.

--apply-immediately also applies every other modification already queued on the instance (for example an instance-class change), which can cause an outage before the reboot confirmation below. The block therefore stops if PendingModifiedValues is not empty. Show the user the queued changes and proceed only with their explicit approval to apply them now, or wait until they are applied or cleared.

The group swap is asynchronous and briefly leaves the instance in modifying; the reboot later in this phase requires the available state, so wait for it to settle here before prompting for it.

Pass --parameters as JSON, not shorthand, on purpose: shared_preload_libraries can hold a comma-separated list (e.g. pgaudit,pg_stat_statements), and the CLI's Key=Value,... shorthand would read that comma as a field separator and fail. Don't convert it back to shorthand.

The Terraform equivalent is an aws_db_parameter_group "datadog_dbm" (family postgres<MAJOR_VERSION>, with a per-instance name like the CLI's $PARAM_GROUP to avoid collisions) carrying the four parameter blocks above - shared_preload_libraries and track_activity_query_size both use apply_method = "pending-reboot" (both need a restart); the other two are immediate - and parameter_group_name set on the aws_db_instance only if the instance is managed in the same Terraform; otherwise create the group in Terraform and attach with the aws rds modify-db-instance call above.

If the instance is already on a custom group, first confirm it isn't shared with other instances. Modifying it in place changes these parameters for every instance attached to it on their next reboot:

aws rds describe-db-instances --region <REGION> \
  --query "DBInstances[?DBParameterGroups[0].DBParameterGroupName=='<CURRENT_PARAM_GROUP>'].DBInstanceIdentifier" \
  --output text

If that lists only the target instance, modify that group in place; do not create or swap groups, or you drop its existing settings on reboot. If it lists other instances, stop and ask the user: either they accept that the DBM parameters will apply to all of them, or clone the group for this instance only (create datadog-dbm-<DB_INSTANCE_IDENTIFIER>-pg<MAJOR_VERSION> as in the default.* branch above, seeded from this group's parameters, then attach it with the same PendingModifiedValues check). Don't silently reconfigure instances the user didn't choose to onboard.

aws rds modify-db-parameter-group --region <REGION> \
  --db-parameter-group-name <CURRENT_PARAM_GROUP> \
  --parameters '[
    {"ParameterName":"shared_preload_libraries","ParameterValue":"<SHARED_PRELOAD_LIBRARIES>","ApplyMethod":"pending-reboot"},
    {"ParameterName":"track_activity_query_size","ParameterValue":"<TRACK_ACTIVITY_QUERY_SIZE>","ApplyMethod":"pending-reboot"},
    {"ParameterName":"pg_stat_statements.track","ParameterValue":"all","ApplyMethod":"immediate"},
    {"ParameterName":"track_io_timing","ParameterValue":"1","ApplyMethod":"immediate"}
  ]'

(If the user manages that custom group in Terraform, add these parameters to its existing aws_db_parameter_group resource, leaving their other parameters intact.)

Reboot so the static parameters load. The reboot briefly drops connections - tell the user and get explicit confirmation before running it, and offer a maintenance window for production instances. Only after they confirm:

aws rds reboot-db-instance --region <REGION> --db-instance-identifier <DB_INSTANCE_IDENTIFIER> || exit 1
aws rds wait db-instance-available --region <REGION> --db-instance-identifier <DB_INSTANCE_IDENTIFIER> || exit 1
STATUS=$(aws rds describe-db-instances --region <REGION> --db-instance-identifier <DB_INSTANCE_IDENTIFIER> \
  --query "DBInstances[0].DBParameterGroups[0].ParameterApplyStatus" --output text)
echo "ParameterApplyStatus: $STATUS"
[ "$STATUS" = "in-sync" ] || { echo "Parameters are not applied yet; wait and re-check before continuing." >&2; exit 1; }

Phase 3A: Managed Agent (CloudFormation) - "Quick Install" equivalent

Follow references/managed-cloudformation.md (A1 RDS admin secret, A2 network access, A3 managed ECS cluster, A4 per-instance DBM Agent service). Read it now, then go to Phase 4.

Phase 3B: Self-managed database bootstrap and existing Agent

Follow references/existing-agent.md (B1 network access, B2 password secret, B3 monitoring user and grants, B4 existing Agent check). The existing-Agent path runs all of B1-B4; the Terraform path runs only B2 and B3 when C1 sends it there. Then go to Phase 4.

Phase 3C: Terraform-provisioned Agent

Follow references/terraform.md (C1 secret and check-config contract, C2 runtime-specific Terraform, C3 plan, approve, and apply). C1 sends the flow through B2/B3 in references/existing-agent.md before returning to C2. Then go to Phase 4.

Phase 4: Verify and Report - all paths

Confirm DBM data is flowing - verify a DBM-specific metric, not just a base connection. The location-independent check is the Datadog-API query (prefer it):

for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE   # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" |
curl -H @- -s -G "https://api.${DD_SITE}/api/v1/query" \
  --data-urlencode "from=$(($(date +%s) - 900))" \
  --data-urlencode "to=$(date +%s)" \
  --data-urlencode "query=avg:postgresql.queries.count{dbinstanceidentifier:<DB_INSTANCE_IDENTIFIER>}"

A non-empty series for postgresql.queries.count confirms DBM query metrics are flowing (the Agent is reading pg_stat_statements) - not just that the base check connected. Also confirm on the Databases page that query samples and explain plans appear. For the managed path, check the ECS service and the datadog-managed-dbm CloudWatch logs if nothing shows. For the Terraform path, inspect the Terraform-owned ECS/EC2/EKS runtime and its logs as described in C3. For the existing-Agent path, sudo datadog-agent status | grep -A 20 postgres on the Agent host should show the check healthy with no pg_stat_statements / pg_stat_activity errors (via SSM or the user).

Wrap Up

After verification, make one best-effort completion callback so the Datadog UI can finish this flow.

DD_WORKFLOW_ID is the UUID of the Datadog user the credentials belong to, which is how the UI correlates this callback. Resolve it and post in one command: many agent runtimes start a fresh shell per command, so a variable assigned in an earlier one would be empty here.

Never print the current_user response: it also carries the user's name, handle, and email. If no JSON parser is available, skip this callback rather than installing one or blocking setup.

for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE   # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
dd_headers() { printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY"; }
DD_WORKFLOW_ID="$(dd_headers | curl -H @- -fsS --connect-timeout 5 --max-time 20 \
  "https://api.${DD_SITE}/api/v2/current_user" | jq -er '.data.id')" \
  && dd_headers | curl -H @- -sS --connect-timeout 5 --max-time 20 -X POST \
  "https://api.${DD_SITE}/api/unstable/integration/aws/workflow/agentic_dbm" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{"data":{"type":"integration_setup_status","id":"'"${DD_WORKFLOW_ID}"'","attributes":{"status":"finished","step":"setup_complete"}}}'

Do not retry or block setup if this callback fails.

Congratulate the user and give them a short summary of what was set up:

  • The instance (<DB_INSTANCE_IDENTIFIER>, region, endpoint) and the install path used (managed CloudFormation, Terraform-provisioned, or existing Agent).

  • What Phase 2 changed on the parameter group (which group, which parameters, whether a reboot ran), or that no change was needed.

  • The Secrets Manager secrets created or reused, by name - never their values.

  • The result of the postgresql.queries.count check above.

Then link (construct as <APP_BASE>/..., from the Phase 0 App URL table):

  • Database Monitoring: <APP_BASE>/databases - query metrics, samples, explain plans.

  • Databases list: <APP_BASE>/databases/list - per-database health.

  • The instance also appears under the RDS integration dashboards for infrastructure metrics.

Suggested next steps: monitors for connection saturation, replication lag, and slow queries; repeat for other instances, or set up DBM for other engines (MySQL, SQL Server).

Important Notes

  • Three paths, one prerequisite. The managed path (Phase 3A) deploys a Datadog Fargate Agent and bootstraps the datadog user in-VPC; the existing-Agent path (3B) reuses an Agent; the Terraform path starts in 3C, runs the shared B2/B3 bootstrap, and returns to provision the Agent/networking. All three still need pg_stat_statements enabled (Phase 2) - the managed installer does not set the RDS parameter group, so this skill adds it.

  • Preserve existing DB config. Append pg_stat_statements to shared_preload_libraries, never replace it. Modify an existing custom parameter group in place; only create+attach a new group for default.* groups. shared_preload_libraries and track_activity_query_size need a reboot.

  • Per-database (Terraform and existing-Agent paths). CREATE USER / GRANT pg_monitor are cluster-wide (once); the schema, USAGE grants, extension, and explain_statement function must be created in every monitored database. Keep the function's SET TRANSACTION READ ONLY guard.

  • Secrets. Managed path: an RDS admin secret (JSON {username,password}) the in-VPC installer uses to bootstrap the datadog user. The installer re-runs on every task launch, so keep this secret valid (scope it to the ECS execution role; if you rotate the master password, update the secret too) - it is not one-time, and dropping it or setting SkipInitialize=true without a DatadogUserSecret breaks the Agent. To avoid standing master creds, use the self-managed DatadogUserSecret + SkipInitialize=true path from the start instead. Terraform/existing-Agent paths: the datadog user's password is JSON read via the aws.secrets backend on whichever Agent runs the check, referenced as ENC[...;password]; grant that Agent's role secretsmanager:GetSecretValue. Terraform owns only empty secret containers and IAM; seed values out-of-band. Never inline a password, encode it into a runtime definition, or commit it to Terraform state.

  • Managed/Terraform-Fargate networking. The Fargate Agent's subnets must be ECS-capable (outbound to ECR / Secrets Manager / Datadog via NAT or VPC endpoints) and route to the RDS - not the RDS DB subnet group's isolated subnets. Open the RDS security group to the Agent's SG on the actual DB port, not a hard-coded 5432.

  • Execution model. This skill runs in the user's shell, which directly reaches only Datadog and AWS control-plane APIs. The managed path stays entirely within that boundary. The Terraform and existing-Agent paths' psql grants and host conf.d/datadog-agent status run from a DB-reachable host - agentless via SSM or by the user, never over SSH and never from the local machine unless it has verified network reachability.

  • The datadog user is read-only (pg_monitor) and never modifies data.

Files

references/terraform.md

Phase 3C: Terraform-provisioned Agent

Part of the dd-dbm-rds-postgres skill. Run Phases 0-2 in SKILL.md first; when this phase is done, return to Phase 4 there.

Start this path after Phase 2. Complete C1, run B2/B3 when C1 directs, then return for C2/C3. Terraform replaces the CloudFormation Agent stack; it does not replace the explicit reboot approval or the SQL bootstrap. If terraform is unavailable, stop and ask the user to install it; do not silently fall back to CloudFormation. Prefer the user's existing Terraform project and backend. If .tf files already own the VPC, RDS instance, ECS/EKS cluster, or Agent, add the DBM resources there rather than creating a second root with competing ownership. Ask which workspace and backend to use before planning; do not import an existing RDS instance or parameter group without the user explicitly choosing that ownership change.

The in-product /databases/setup ZIP may be used as a starting point, but inspect every generated file and apply the hardening below before terraform plan. Do not treat the ZIP's "module is ready" message as proof that the database or secrets are configured.

C1: Secret and check-config contract

Terraform may manage secret containers, names, ARNs, and IAM policies only. It must not receive the Datadog API key, RDS master password, or datadog password as a variable, local, data-source result, aws_secretsmanager_secret_version, Helm value, user data value, or provisioner environment variable. Marking a variable sensitive is insufficient because the value still lands in state.

Declare two empty secret containers in the same Terraform root: B2's DB password container (the aws_secretsmanager_secret.dbm_password resource and its dbm_password_secret_arn output shown in B2) and, for Fargate or EC2, a dedicated API-key container:

resource "aws_secretsmanager_secret" "datadog_api_key" {
  name = "datadog/dbm/postgres/<DB_INSTANCE_IDENTIFIER>-apikey"
}

output "datadog_api_key_secret_arn" {
  value = aws_secretsmanager_secret.datadog_api_key.arn
}

Plan these empty containers first, show the plan, and get confirmation before applying them. Then go to B2 to seed the DB password and run B3. For Fargate/EC2, seed the API key out-of-band with A4's protected temp-file pattern:

for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE   # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
umask 077
apikey_file=$(mktemp); trap 'rm -f "$apikey_file"' EXIT
printf '%s' "${DD_API_KEY}" > "$apikey_file"
APIKEY_SECRET_ARN=$(terraform output -raw datadog_api_key_secret_arn)
: "${APIKEY_SECRET_ARN:?terraform output datadog_api_key_secret_arn is empty - apply the secret containers first}"
aws secretsmanager put-secret-value --region <REGION> --secret-id "$APIKEY_SECRET_ARN" \
  --secret-string "file://$apikey_file" >/dev/null \
  || { echo "put-secret-value failed - the API key was not stored" >&2; exit 1; }

Never pass the key on argv or through -var / TF_VAR_*. After B3 succeeds, return here and continue with C2.

Use the B4 Postgres instance config, including the ENC[...;password] handle, aws.instance_endpoint, region, and dbinstanceidentifier tag. The rendered check may live in Terraform state only because it contains the secret handle, not the secret value. Remove datadog_user_password and datadog_api_key from a generated /databases/setup module's variables and terraform.tfvars; a base64-encoded password is still plaintext and is not acceptable.

Grant secretsmanager:GetSecretValue on the narrowest resources:

  • The runtime identity (ECS task role, EC2 instance profile, or EKS runner IRSA role) reads the B2 DB password secret so the Agent's aws.secrets backend can resolve ENC[…].

  • For Fargate, the ECS task execution role reads the API-key secret used by the task definition's secrets field. Do not put DD_API_KEY in environment.

  • For EC2, configure api_key: ENC[<API_KEY_SECRET_NAME_OR_ARN>] in datadog.yaml so the instance role resolves it; do not put it in user data or a docker run -e argument.

  • For EKS, create/reuse the Kubernetes API-key Secret out-of-band as described by the Terraform path in dd-agent-install-kubernetes; reference it by name from the existing Helm release or DatadogAgent. Do not put the API or application key in helm_release.values.

Use Agent >= 7.70 for the native AWS Secrets Manager backend (Cluster Agent >= 7.77 where applicable), and configure the region exactly as in B4. Keep the Terraform backend encrypted and access-controlled; do not commit plan files, state, .tfvars, or downloaded bundles containing customer identifiers.

C2: Runtime-specific Terraform

Choose one runtime and keep its resources in the user's Terraform ownership boundary:

Dedicated ECS Fargate Agent - default Terraform runtime when there is no Agent. Create or reuse an ECS cluster; create the Agent security group and a per-database ingress rule on the actual DB port; create the log group, task execution role, task role, task definition, and singleton ECS service. The task definition must:

  • inject DD_API_KEY through ECS secrets from the API-key secret ARN;

  • set DD_SECRET_BACKEND_TYPE=aws.secrets and DD_SECRET_BACKEND_CONFIG={"aws_session":{"aws_region":"<REGION>"}};

  • write only the non-secret check config containing ENC[...;password];

  • use ECS-capable subnets with Datadog, registry, and Secrets Manager egress; and

  • run a pinned, approved Agent version that satisfies the secret-backend minimum.

If starting from the /databases/setup Fargate module, replace its plaintext environment values and password-bearing DD_CHECK_CONFIG_B64, add the separate task role and least-privilege policies, and add the missing RDS aws block and identifier tag before planning.

Dedicated EC2 Agent. Use the existing Terraform project and the dd-agent-install-linux security model: an instance profile reads the two secrets, the instance is SSM-managed, and user data contains no secret values. Do not create or use an SSH key for this flow. Deliver/restart the check through SSM or a continuously reconciled mechanism. If starting from the generated EC2 module, remove the password from the rendered config and API key from user_data / docker run, then add the instance profile, SSM, the secret backend, and the RDS correlation fields.

Existing EKS Agent. Update the Terraform resource that already owns the Datadog Helm release or Operator; do not install a second full Agent release into the cluster. If an Agent exists but Terraform does not yet own it, adopt the existing release into Terraform (e.g. terraform import) as an explicit, user-approved ownership change - first reproduce its current chart, version, and values so the initial post-import plan is a no-op, then add only the DBM delta so Terraform never resets unrelated Agent settings - or use 3B if the user won't hand ownership to Terraform, rather than installing a second release. If no Agent exists, load dd-agent-install-kubernetes and use its Terraform path first. Configure this as the B4 cluster check: enable cluster checks and add postgres.yaml with cluster_check: true using the placement B4 gives for your install type (Helm clusterAgent.confd, Operator spec.override.clusterAgent.extraConfd.configDataMap), and put the secret backend plus DB-secret IRSA permission on the cluster-check runner (or node Agents when there are no runners). If the Cluster Agent already has its own secret backend, apply the B4 note on DD_SECRET_BACKEND_SKIP_CHECKS or Cluster Agent read access so the handle resolves. A generated module that puts the check under datadog.confd, omits cluster_check: true, or puts API/app keys in Helm values must be corrected before use.

For every runtime, preserve existing provider pins and naming conventions. Use plan-time account, VPC, subnet, and security-group validation when those values are discovered. The Agent and RDS instance may use routed networks, but an SG-to-SG ingress rule must be valid for the selected topology; do not silently force a same-VPC design when the user's existing network uses peering or transit routing.

C3: Plan, approve, and apply

Format and validate the scoped Terraform root, then save a reviewable plan. The plan file stays in the Terraform root (dbm.tfplan, owner-only) until the apply block below removes it, because agent runtimes start a fresh shell per command and a temp directory cleaned up at the end of this block would be gone before approval. Stop here if any step fails:

umask 077
terraform fmt
terraform init || { echo "terraform init FAILED - stop" >&2; exit 1; }
terraform validate || { echo "terraform validate FAILED - stop" >&2; exit 1; }
terraform plan -out=dbm.tfplan || { echo "terraform plan FAILED - stop" >&2; exit 1; }
terraform show dbm.tfplan

Before applying, confirm that the plan:

  • contains no secret values or password-bearing encoded strings;

  • does not replace, destroy, or import the RDS instance, its parameter group, or unrelated shared infrastructure, and does not import an existing Agent release unless the user explicitly approved adopting it for the EKS path (C2);

  • grants secret access only to the runtime/execution identities that require it; and

  • changes only the intended Agent, IAM, logging, and network resources.

Show that summary and get explicit confirmation before applying. Do not use -auto-approve. If the API-key container was empty when the plan was made, seed it first and create a fresh plan so the service never launches against a missing secret version. Only after the user confirms, apply exactly the reviewed plan from the same Terraform root:

[ -f dbm.tfplan ] || { echo "dbm.tfplan not found - re-run the plan block and get approval again" >&2; exit 1; }
trap 'rm -f dbm.tfplan' EXIT HUP INT TERM
if terraform apply dbm.tfplan; then
  echo "apply complete"
else
  echo "terraform apply FAILED - do NOT verify or report success"
  exit 1
fi

Do not commit dbm.tfplan; the apply block removes it on every exit path. If the user declines the plan, delete it with rm -f dbm.tfplan instead.

After apply, inspect the runtime without bypassing its management plane: ECS service/tasks and CloudWatch logs for Fargate, SSM plus Agent status for EC2, or Helm/Operator and cluster-check-runner status for EKS. Then continue to Phase 4; infrastructure creation alone is not proof that DBM is working.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs network access, Needs an API key, Needs a command-line tool

Security

What our scanner noticed· Medium risk

  • Downloads piped into a shell

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
datadog-labs
Publisher
Datadog (Official)
Source
datadog-labs/agent-skills · dd-dbm-rds-postgres · commit 8e1306a
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by datadog-labs or Datadog.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download dd-dbm-rds-postgres.zip