77credential staleness window entirely.
88
99Environment variables (set by OpenTofu):
10- ADMIN_SECRET_ARN — ARN of the Secrets Manager secret holding RDS admin
11- credentials (the RDS-managed master-user secret).
12- DB_HOST — RDS SQL Server hostname.
13- DB_PORT — SQL Server port (default: "1433").
14- DB_NAME — Target database name for user creation and connection tests.
15- ECS_CLUSTER — ECS cluster name to redeploy after finishSecret.
16- ECS_SERVICE — ECS service name to redeploy after finishSecret.
10+ ADMIN_SECRET_ARN — ARN of the Secrets Manager secret holding RDS admin
11+ credentials (the RDS-managed master-user secret).
12+ DB_HOST — RDS SQL Server hostname.
13+ DB_PORT — SQL Server port (default: "1433").
14+ DB_NAME — Target database name for user creation and connection tests.
15+ ADDITIONAL_DB_NAMES — Comma-separated list of extra databases on the same RDS
16+ instance where the app user also needs a database-level
17+ user provisioned (e.g. DC's DcSource database). Empty/unset
18+ means no extras — the typical case for CO.
19+ ECS_CLUSTER — ECS cluster name to redeploy after finishSecret.
20+ ECS_SERVICE — ECS service name to redeploy after finishSecret.
1721"""
1822
1923import json
@@ -105,7 +109,6 @@ def set_secret(client, secret_arn, token):
105109
106110 host = os .environ ["DB_HOST" ]
107111 port = int (os .environ .get ("DB_PORT" , "1433" ))
108- dbname = pending ["dbname" ]
109112
110113 # Server-level operations (CREATE/ALTER LOGIN) require master db context.
111114 master_conn = pymssql .connect (
@@ -126,40 +129,44 @@ def set_secret(client, secret_arn, token):
126129 finally :
127130 master_conn .close ()
128131
129- # Database-level operations (CREATE USER, role grant) require the target db.
130- db_conn = pymssql .connect (
131- server = host ,
132- port = port ,
133- user = admin ["username" ],
134- password = admin ["password" ],
135- database = dbname ,
136- tds_version = "7.4" ,
137- )
138- try :
139- cursor = db_conn .cursor ()
140- _ensure_db_user_exists (cursor , pending ["username" ])
141- db_conn .commit ()
142- finally :
143- db_conn .close ()
132+ # Database-level operations (CREATE USER, role grant) run against every
133+ # database the app user needs access to, not just the primary one.
134+ for dbname in _target_db_names (pending ["dbname" ]):
135+ db_conn = pymssql .connect (
136+ server = host ,
137+ port = port ,
138+ user = admin ["username" ],
139+ password = admin ["password" ],
140+ database = dbname ,
141+ tds_version = "7.4" ,
142+ )
143+ try :
144+ cursor = db_conn .cursor ()
145+ _ensure_db_user_exists (cursor , pending ["username" ])
146+ db_conn .commit ()
147+ logger .info ("Ensured db user exists in %s" , dbname )
148+ finally :
149+ db_conn .close ()
144150
145151
146152def test_secret (client , secret_arn , token ):
147- """Verify the AWSPENDING credentials by opening a test connection."""
153+ """Verify the AWSPENDING credentials by opening a test connection to every target database ."""
148154 pending = _get_secret_dict (client , secret_arn , stage = "AWSPENDING" , version_id = token )
149155
150- conn = pymssql .connect (
151- server = os .environ ["DB_HOST" ],
152- port = int (os .environ .get ("DB_PORT" , "1433" )),
153- user = pending ["username" ],
154- password = pending ["password" ],
155- database = pending ["dbname" ],
156- tds_version = "7.4" ,
157- )
158- try :
159- conn .cursor ().execute ("SELECT 1" )
160- logger .info ("Test connection succeeded for pending login" )
161- finally :
162- conn .close ()
156+ for dbname in _target_db_names (pending ["dbname" ]):
157+ conn = pymssql .connect (
158+ server = os .environ ["DB_HOST" ],
159+ port = int (os .environ .get ("DB_PORT" , "1433" )),
160+ user = pending ["username" ],
161+ password = pending ["password" ],
162+ database = dbname ,
163+ tds_version = "7.4" ,
164+ )
165+ try :
166+ conn .cursor ().execute ("SELECT 1" )
167+ logger .info ("Test connection succeeded for pending login against %s" , dbname )
168+ finally :
169+ conn .close ()
163170
164171
165172def finish_secret (client , secret_arn , token ):
@@ -189,6 +196,19 @@ def finish_secret(client, secret_arn, token):
189196# Private helpers
190197# ---------------------------------------------------------------------------
191198
199+ def _target_db_names (primary_dbname ):
200+ """Return every database the app user login should be provisioned in.
201+
202+ Combines the primary database (from the secret) with any additional
203+ state-specific databases configured via ADDITIONAL_DB_NAMES (e.g. DC's
204+ DcSource database, which lives on the same RDS instance but doesn't
205+ exist for other states like CO).
206+ """
207+ additional = os .environ .get ("ADDITIONAL_DB_NAMES" , "" )
208+ extras = [name .strip () for name in additional .split ("," ) if name .strip ()]
209+ return [primary_dbname ] + extras
210+
211+
192212def _other_user (username ):
193213 """Return the inactive app user (the one not currently active)."""
194214 if username not in _USERS :
@@ -278,10 +298,14 @@ def _restart_ecs_service():
278298 logger .info ("Triggered rolling ECS restart for %s/%s" , cluster , service )
279299 except Exception :
280300 # The secret is already rotated; running tasks remain on the active
281- # user's valid credentials. A manual redeploy will pick up the new
282- # secret at next launch.
301+ # user's valid credentials until the next natural redeploy. Not
302+ # fatal to rotation, but silent — log a distinct, alertable marker
303+ # so an external monitor (e.g. a Datadog log monitor) can page on
304+ # it instead of this failing invisibly.
283305 logger .exception (
284- "Failed to trigger ECS restart for %s/%s — manual redeploy required" ,
306+ "DB_ROTATION_ECS_RESTART_FAILED cluster=%s service=%s — "
307+ "credentials rotated successfully but the ECS restart failed; "
308+ "manual redeploy required to pick up the new secret" ,
285309 cluster ,
286310 service ,
287311 )
0 commit comments