Skip to content
Open
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
c2aeee7
add migration mode
Mr-Rahul-Paul Jul 1, 2026
50d55ed
preserve existing ssm resource
Mr-Rahul-Paul Jul 1, 2026
9ec0817
create redis secret
Mr-Rahul-Paul Jul 1, 2026
8225bf7
create redis secret#2
Mr-Rahul-Paul Jul 1, 2026
31257bb
update outputs
Mr-Rahul-Paul Jul 1, 2026
0965e1e
update cache test
Mr-Rahul-Paul Jul 1, 2026
d720ab5
remove ssm duplicate
Mr-Rahul-Paul Jul 1, 2026
962e6e0
add move cause count makes it a list somehow
Mr-Rahul-Paul Jul 1, 2026
136642a
update tests
Mr-Rahul-Paul Jul 1, 2026
36b0fa3
update tests
Mr-Rahul-Paul Jul 1, 2026
792de3b
update tests
Mr-Rahul-Paul Jul 1, 2026
af458e2
update tests ... again
Mr-Rahul-Paul Jul 1, 2026
6279123
pass new secret arn into parameters
Mr-Rahul-Paul Jul 1, 2026
8dc674f
add values to Live module
Mr-Rahul-Paul Jul 1, 2026
9eef74b
Create application secrets:
Mr-Rahul-Paul Jul 1, 2026
546f3dd
Make SSM secrets conditional
Mr-Rahul-Paul Jul 1, 2026
fe55862
Add state moves
Mr-Rahul-Paul Jul 1, 2026
b45cc30
update tests for counted resources
Mr-Rahul-Paul Jul 1, 2026
e700681
update tests for counted resources
Mr-Rahul-Paul Jul 1, 2026
a7903a5
update to provider-neutral ECS maps
Mr-Rahul-Paul Jul 2, 2026
b8ce020
corrections
Mr-Rahul-Paul Jul 2, 2026
2e5fb58
rename the servicee input
Mr-Rahul-Paul Jul 2, 2026
717713c
update container secrets
Mr-Rahul-Paul Jul 2, 2026
c0aa60f
add IAM permissions
Mr-Rahul-Paul Jul 2, 2026
00fc7c4
Update live service calls
Mr-Rahul-Paul Jul 2, 2026
46e5b8d
scheduled ECS task
Mr-Rahul-Paul Jul 2, 2026
bdcf905
test complete mode
Mr-Rahul-Paul Jul 2, 2026
e391caf
steps after AWS credentials
Mr-Rahul-Paul Jul 2, 2026
b86b44d
fixes
Mr-Rahul-Paul Jul 2, 2026
037e38c
restore the deleted Redis endpoint output:
Mr-Rahul-Paul Jul 2, 2026
987557a
extend iam permissions
Mr-Rahul-Paul Jul 3, 2026
1ed6252
corrections + documenting cleanup
Mr-Rahul-Paul Jul 3, 2026
96e8c2d
short sid cause there were size limits
Mr-Rahul-Paul Jul 3, 2026
20978d1
ran terraform docs precommit
Mr-Rahul-Paul Jul 3, 2026
ff0289c
tflock update
Mr-Rahul-Paul Jul 3, 2026
50b800e
Remove test-generated lockfile hashes
Mr-Rahul-Paul Jul 4, 2026
4666110
Remove personal LocalStack fixture from PR
Mr-Rahul-Paul Jul 5, 2026
5482a06
Omit empty Secrets Manager IAM statements
Mr-Rahul-Paul Jul 6, 2026
95c1e6b
Scope runtime secret access by workload
Mr-Rahul-Paul Jul 6, 2026
05b5eb0
Report runtime secret lookup failures accurately
Mr-Rahul-Paul Jul 6, 2026
d263f7b
Limit KMS decrypt to Secrets Manager
Mr-Rahul-Paul Jul 6, 2026
52c55c8
Test runtime secret IAM policy scope
Mr-Rahul-Paul Jul 6, 2026
ed5e75f
Test complete-mode secret mappings
Mr-Rahul-Paul Jul 6, 2026
0f725e0
Test removal of legacy database secret output
Mr-Rahul-Paul Jul 6, 2026
8edc3e1
Clarify runtime secret migration documentation
Mr-Rahul-Paul Jul 6, 2026
3a9a724
Avoid template injection in deploy workflow
Mr-Rahul-Paul Jul 6, 2026
fae495d
Add missing newline to cache output
Mr-Rahul-Paul Jul 6, 2026
6fc617f
Address remaining secret migration review comments
Mr-Rahul-Paul Jul 6, 2026
c8aac6e
Test secret recovery window validation
Mr-Rahul-Paul Jul 6, 2026
9a69d23
Test valid secret recovery window boundary
Mr-Rahul-Paul Jul 6, 2026
6b76c8a
test(parameters): assert SSM source in prepare-mode container-secret …
Mr-Rahul-Paul Jul 8, 2026
ef88c0e
fix(deploy): default RUNTIME_SECRETS_MODE to prepare when env var is …
Mr-Rahul-Paul Jul 8, 2026
42ab0e3
Merge remote-tracking branch 'upstream/main' into fix#5071
Mr-Rahul-Paul Jul 11, 2026
4f436e2
add integration tests
Mr-Rahul-Paul Aug 3, 2026
5792917
fmt
Mr-Rahul-Paul Aug 4, 2026
475d833
new test assertion for github token format
Mr-Rahul-Paul Aug 4, 2026
2d51487
fmt
Mr-Rahul-Paul Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/scripts/verify-runtime-secrets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash

set -euo pipefail

usage() {
echo "Usage: $0 <environment> <enable-additional-parameters>" >&2
exit 1
}

[[ $# -eq 2 ]] || usage

environment=$1
enable_additional_parameters=$2

if [[ "$enable_additional_parameters" != "true" && "$enable_additional_parameters" != "false" ]]; then
usage
fi

check_secret() {
local secret_id=$1
local secret_metadata

# Check version metadata without retrieving or printing the secret value.
if ! secret_metadata=$(aws secretsmanager describe-secret \
--secret-id "$secret_id" \
--query 'VersionIdsToStages' \
--output json); then
echo "::error::Unable to describe required secret: ${secret_id}" >&2
exit 1
fi

if ! jq -e 'any(.[]; index("AWSCURRENT") != null)' <<<"$secret_metadata" >/dev/null; then
echo "::error::Secret has no AWSCURRENT version: ${secret_id}" >&2
exit 1
fi
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

secret_names=(
DJANGO_ALGOLIA_WRITE_API_KEY
DJANGO_OPEN_AI_SECRET_KEY
DJANGO_REDIS_PASSWORD
DJANGO_SECRET_KEY
DJANGO_SENTRY_DSN
DJANGO_SLACK_BOT_TOKEN
DJANGO_SLACK_SIGNING_SECRET
GITHUB_TOKEN
NEXTAUTH_SECRET
NEXT_SERVER_GITHUB_CLIENT_SECRET
)

if [[ "$enable_additional_parameters" == "true" ]]; then
secret_names+=(NEST_GITHUB_APP_PRIVATE_KEY SLACK_BOT_TOKEN_T04T40NHX)
fi

for secret_name in "${secret_names[@]}"; do
check_secret "/nest/${environment}/${secret_name}"
done

check_secret "nest-${environment}-db-credentials"

Comment thread
Mr-Rahul-Paul marked this conversation as resolved.
echo "All required runtime secrets have an AWSCURRENT version."
19 changes: 19 additions & 0 deletions .github/workflows/run-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ jobs:
}}
FRONTEND_IMAGE: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ vars.AWS_REGION }}.amazonaws.com/nest-${{ inputs.environment }}-frontend:${{ inputs.release_version
}}
RUNTIME_SECRETS_MODE: ${{ vars.RUNTIME_SECRETS_MODE || 'prepare' }}
TF_IN_AUTOMATION: true
TF_INPUT: false
permissions:
Expand All @@ -163,6 +164,23 @@ jobs:
role-skip-session-tagging: true
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ inputs.aws_role_name }}

- name: Validate runtime secrets migration mode
run: |
if [[ "$RUNTIME_SECRETS_MODE" != "prepare" && "$RUNTIME_SECRETS_MODE" != "complete" ]]; then
echo "::error::RUNTIME_SECRETS_MODE must be prepare or complete."
exit 1
fi

- name: Verify runtime secrets are populated
if: env.RUNTIME_SECRETS_MODE == 'complete'
env:
DEPLOY_ENVIRONMENT: ${{ inputs.environment }}
ENABLE_ADDITIONAL_PARAMETERS: ${{ inputs.enable_additional_parameters }}
run: |
bash .github/scripts/verify-runtime-secrets.sh \
"$DEPLOY_ENVIRONMENT" \
"$ENABLE_ADDITIONAL_PARAMETERS"

Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2.1.6

Expand Down Expand Up @@ -220,6 +238,7 @@ jobs:
frontend_use_fargate_spot=${{ inputs.frontend_use_fargate_spot }}
frontend_image_tag="${{ inputs.release_version }}"
project_name="nest"
runtime_secrets_mode="${{ vars.RUNTIME_SECRETS_MODE || 'prepare' }}"
tasks_use_fargate_spot=${{ inputs.tasks_use_fargate_spot }}
Comment thread
Mr-Rahul-Paul marked this conversation as resolved.
working-directory: infrastructure/live

Expand Down
21 changes: 12 additions & 9 deletions infrastructure/bootstrap/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ data "aws_iam_policy_document" "part_two" {
for_each = local.environments

statement {
sid = "AppAutoscalingManagement"
sid = "AppAutoscalingMgmt"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I shortened the Management suffix to Mgmt because AWS managed policies have a 6,144-character limit.
shortening them was the smallest low-risk fix.

effect = "Allow"
actions = [
"application-autoscaling:DeleteScalingPolicy",
Expand Down Expand Up @@ -365,7 +365,7 @@ data "aws_iam_policy_document" "part_two" {
}

statement {
sid = "ELBManagement"
sid = "ELBMgmt"
effect = "Allow"
actions = [
"elasticloadbalancing:AddTags",
Expand Down Expand Up @@ -393,7 +393,7 @@ data "aws_iam_policy_document" "part_two" {
}

statement {
sid = "EventBridgeManagement"
sid = "EventBridgeMgmt"
effect = "Allow"
actions = [
"events:DeleteRule",
Expand All @@ -412,7 +412,7 @@ data "aws_iam_policy_document" "part_two" {
}

statement {
sid = "IAMManagement"
sid = "IAMMgmt"
effect = "Allow"
actions = [
"iam:AttachRolePolicy",
Expand Down Expand Up @@ -471,7 +471,7 @@ data "aws_iam_policy_document" "part_two" {
}

statement {
sid = "KMSManagement"
sid = "KMSMgmt"
effect = "Allow"
actions = [
"kms:CreateKey",
Expand Down Expand Up @@ -525,7 +525,7 @@ data "aws_iam_policy_document" "part_two" {
}

statement {
sid = "S3Management"
sid = "S3Mgmt"
effect = "Allow"
actions = [
"s3:CreateBucket",
Expand Down Expand Up @@ -571,7 +571,7 @@ data "aws_iam_policy_document" "part_two" {
}

statement {
sid = "SecretsManagerManagement"
sid = "SecretsManagerMgmt"
effect = "Allow"
actions = [
"secretsmanager:CreateSecret",
Expand All @@ -585,11 +585,14 @@ data "aws_iam_policy_document" "part_two" {
"secretsmanager:UntagResource",
"secretsmanager:UpdateSecret",
]
resources = ["arn:aws:secretsmanager:${var.aws_region}:${data.aws_caller_identity.current.account_id}:secret:${var.project_name}-${each.key}-*"]
resources = [
"arn:aws:secretsmanager:${var.aws_region}:${data.aws_caller_identity.current.account_id}:secret:${var.project_name}-${each.key}-*",
"arn:aws:secretsmanager:${var.aws_region}:${data.aws_caller_identity.current.account_id}:secret:/${var.project_name}/${each.key}/*",
]
}

statement {
sid = "SSMManagement"
sid = "SSMMgmt"
effect = "Allow"
actions = [
"ssm:AddTagsToResource",
Expand Down
36 changes: 36 additions & 0 deletions infrastructure/bootstrap/tests/bootstrap.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,42 @@ run "test_part_two_policy_size_production" {
}
}

run "test_secrets_manager_namespace_staging" {
command = plan

assert {
condition = alltrue([
strcontains(
data.aws_iam_policy_document.part_two["staging"].json,
"arn:aws:secretsmanager:${var.aws_region}:160885282306:secret:${var.project_name}-staging-*",
),
strcontains(
data.aws_iam_policy_document.part_two["staging"].json,
"arn:aws:secretsmanager:${var.aws_region}:160885282306:secret:/${var.project_name}/staging/*",
),
])
error_message = "The staging Terraform policy must allow management of the staging Secrets Manager namespace."
}
}

run "test_secrets_manager_namespace_production" {
command = plan

assert {
condition = alltrue([
strcontains(
data.aws_iam_policy_document.part_two["production"].json,
"arn:aws:secretsmanager:${var.aws_region}:160885282306:secret:${var.project_name}-production-*",
),
strcontains(
data.aws_iam_policy_document.part_two["production"].json,
"arn:aws:secretsmanager:${var.aws_region}:160885282306:secret:/${var.project_name}/production/*",
),
])
error_message = "The production Terraform policy must allow management of the production Secrets Manager namespace."
}
}

run "test_minified_json_is_smaller_than_pretty_json" {
command = plan

Expand Down
1 change: 1 addition & 0 deletions infrastructure/live/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ No resources.
| <a name="input_redis_node_type"></a> [redis\_node\_type](#input\_redis\_node\_type) | The node type for the Redis cache. | `string` | `"cache.t3.micro"` | no |
| <a name="input_redis_num_cache_nodes"></a> [redis\_num\_cache\_nodes](#input\_redis\_num\_cache\_nodes) | The number of cache nodes in the Redis cluster. | `number` | `1` | no |
| <a name="input_redis_port"></a> [redis\_port](#input\_redis\_port) | The port for the Redis cache. | `number` | `6379` | no |
| <a name="input_runtime_secrets_mode"></a> [runtime\_secrets\_mode](#input\_runtime\_secrets\_mode) | Runtime secret migration phase: 'prepare' retains SSM injection, while 'complete' uses Secrets Manager. | `string` | n/a | yes |
| <a name="input_secret_recovery_window_in_days"></a> [secret\_recovery\_window\_in\_days](#input\_secret\_recovery\_window\_in\_days) | The number of days that Secrets Manager waits before it can delete the secret. Set to 0 to delete immediately. | `number` | `7` | no |
| <a name="input_slack_bot_token_suffix"></a> [slack\_bot\_token\_suffix](#input\_slack\_bot\_token\_suffix) | The Suffix for the Slack bot token. | `string` | `"T04T40NHX"` | no |
| <a name="input_tasks_use_fargate_spot"></a> [tasks\_use\_fargate\_spot](#input\_tasks\_use\_fargate\_spot) | Whether to use Fargate Spot for ECS tasks. | `bool` | `true` | no |
Expand Down
79 changes: 45 additions & 34 deletions infrastructure/live/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ module "backend" {
container_cpu = 1024
container_memory = 2048
container_port = 8000
container_secrets = module.parameters.django_container_secrets
desired_count = var.backend_desired_count
enable_auto_scaling = var.backend_enable_auto_scaling
environment = var.environment
Expand All @@ -59,8 +60,8 @@ module "backend" {
kms_key_arn = module.kms.key_arn
max_count = var.backend_max_count
min_count = var.backend_min_count
parameters_arns = module.parameters.django_ssm_parameter_arns
project_name = var.project_name
secretsmanager_secret_arns = module.parameters.django_secretsmanager_secret_arns
security_group_id = module.security.backend_sg_id
service_name = "backend"
subnet_ids = var.enable_nat_gateway ? module.networking.private_subnet_ids : module.networking.public_subnet_ids
Expand All @@ -72,16 +73,18 @@ module "backend" {
module "cache" {
source = "../modules/cache"

common_tags = local.common_tags
environment = var.environment
kms_key_arn = module.kms.key_arn
project_name = var.project_name
redis_engine_version = var.redis_engine_version
redis_node_type = var.redis_node_type
redis_num_cache_nodes = var.redis_num_cache_nodes
redis_port = var.redis_port
security_group_ids = [module.security.redis_sg_id]
subnet_ids = module.networking.private_subnet_ids
common_tags = local.common_tags
environment = var.environment
kms_key_arn = module.kms.key_arn
project_name = var.project_name
redis_engine_version = var.redis_engine_version
redis_node_type = var.redis_node_type
redis_num_cache_nodes = var.redis_num_cache_nodes
redis_port = var.redis_port
runtime_secrets_mode = var.runtime_secrets_mode
secret_recovery_window_in_days = var.secret_recovery_window_in_days
security_group_ids = [module.security.redis_sg_id]
subnet_ids = module.networking.private_subnet_ids
}

module "database" {
Expand All @@ -104,6 +107,7 @@ module "database" {
kms_key_arn = module.kms.key_arn
project_name = var.project_name
proxy_security_group_ids = [module.security.rds_proxy_sg_id]
runtime_secrets_mode = var.runtime_secrets_mode
secret_recovery_window_in_days = var.secret_recovery_window_in_days
security_group_ids = [module.security.rds_sg_id]
}
Expand All @@ -118,6 +122,7 @@ module "frontend" {
aws_region = var.aws_region
common_tags = local.common_tags
container_port = 3000
container_secrets = module.parameters.frontend_container_secrets
desired_count = var.frontend_desired_count
enable_auto_scaling = var.frontend_enable_auto_scaling
environment = var.environment
Expand All @@ -126,10 +131,10 @@ module "frontend" {
kms_key_arn = module.kms.key_arn
max_count = var.frontend_max_count
min_count = var.frontend_min_count
parameters_arns = module.parameters.frontend_ssm_parameter_arns
project_name = var.project_name
security_group_id = module.security.frontend_sg_id
service_name = "frontend"
secretsmanager_secret_arns = module.parameters.frontend_secretsmanager_secret_arns
subnet_ids = var.enable_nat_gateway ? module.networking.private_subnet_ids : module.networking.public_subnet_ids
target_group_arn = module.alb.frontend_target_group_arn
use_fargate_spot = var.frontend_use_fargate_spot
Expand Down Expand Up @@ -182,27 +187,32 @@ module "networking" {
module "parameters" {
source = "../modules/parameters"

common_tags = local.common_tags
db_password_arn = module.database.db_password_arn
django_configuration = var.django_configuration
django_allowed_hosts = var.domain_name
django_allowed_origins = "https://${var.domain_name}"
django_aws_static_bucket_name = module.storage.static_s3_bucket_name
django_db_host = module.database.db_proxy_endpoint
django_db_name = var.db_name
django_db_port = var.db_port
django_db_user = var.db_user
django_redis_host = module.cache.redis_primary_endpoint
django_release_version = var.django_release_version
django_settings_module = var.django_settings_module
enable_additional_parameters = var.enable_additional_parameters
environment = var.environment
next_server_csrf_url = "https://${var.domain_name}/csrf/"
next_server_graphql_url = "https://${var.domain_name}/graphql/"
nextauth_url = "https://${var.domain_name}"
project_name = var.project_name
redis_password_arn = module.cache.redis_password_arn
slack_bot_token_suffix = var.slack_bot_token_suffix
common_tags = local.common_tags
db_password_arn = module.database.db_password_arn
db_credentials_secret_arn = module.database.db_credentials_secret_arn
django_configuration = var.django_configuration
django_allowed_hosts = var.domain_name
django_allowed_origins = "https://${var.domain_name}"
django_aws_static_bucket_name = module.storage.static_s3_bucket_name
django_db_host = module.database.db_proxy_endpoint
django_db_name = var.db_name
django_db_port = var.db_port
django_db_user = var.db_user
django_redis_host = module.cache.redis_primary_endpoint
django_release_version = var.django_release_version
django_settings_module = var.django_settings_module
enable_additional_parameters = var.enable_additional_parameters
environment = var.environment
kms_key_arn = module.kms.key_arn
next_server_csrf_url = "https://${var.domain_name}/csrf/"
next_server_graphql_url = "https://${var.domain_name}/graphql/"
nextauth_url = "https://${var.domain_name}"
project_name = var.project_name
redis_password_arn = module.cache.redis_password_arn
redis_password_secret_arn = module.cache.redis_password_secret_arn
runtime_secrets_mode = var.runtime_secrets_mode
secret_recovery_window_in_days = var.secret_recovery_window_in_days
slack_bot_token_suffix = var.slack_bot_token_suffix
}

module "security" {
Expand Down Expand Up @@ -235,7 +245,7 @@ module "tasks" {
assign_public_ip = local.assign_public_ip
aws_region = var.aws_region
common_tags = local.common_tags
container_parameters_arns = module.parameters.django_ssm_parameter_arns
container_secrets = module.parameters.django_container_secrets
ecr_repository_arn = module.backend.ecr_repository_arn
ecr_repository_url = module.backend.ecr_repository_url
ecs_sg_id = module.security.tasks_sg_id
Expand All @@ -246,6 +256,7 @@ module "tasks" {
image_tag = var.backend_image_tag
kms_key_arn = module.kms.key_arn
project_name = var.project_name
secretsmanager_secret_arns = module.parameters.django_secretsmanager_secret_arns
subnet_ids = var.enable_nat_gateway ? module.networking.private_subnet_ids : module.networking.public_subnet_ids
use_fargate_spot = var.tasks_use_fargate_spot
}
1 change: 1 addition & 0 deletions infrastructure/live/terraform.production.tfvars.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ frontend_max_count = 6
frontend_min_count = 2
frontend_use_fargate_spot = false
project_name = "nest"
runtime_secrets_mode = "prepare"
tasks_use_fargate_spot = false
1 change: 1 addition & 0 deletions infrastructure/live/terraform.staging.tfvars.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ frontend_max_count = 3
frontend_min_count = 1
frontend_use_fargate_spot = true
project_name = "nest"
runtime_secrets_mode = "prepare"
tasks_use_fargate_spot = true
Loading
Loading