Import home-unite-us production Cognito - #186
Merged
Merged
Conversation
Contributor
|
Terraform plan in terraform Plan: 11 to import, 0 to add, 4 to change, 0 to destroy.Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
!~ update in-place
Terraform will perform the following actions:
# module.home-unite-us.aws_cognito_identity_provider.google_client_prod will be updated in-place
# (imported from "us-west-2_VH24AGQ3p:Google")
!~ resource "aws_cognito_identity_provider" "google_client_prod" {
attribute_mapping = {
"birthdate" = "birthdays"
"email" = "email"
"family_name" = "family_name"
"gender" = "genders"
"given_name" = "given_name"
"name" = "names"
"phone_number" = "phoneNumbers"
"picture" = "picture"
"username" = "sub"
}
id = "us-west-2_VH24AGQ3p:Google"
idp_identifiers = []
!~ provider_details = {
"attributes_url" = "https://people.googleapis.com/v1/people/me?personFields="
"attributes_url_add_attributes" = "true"
"authorize_scopes" = "email profile openid"
"authorize_url" = "https://accounts.google.com/o/oauth2/v2/auth"
# Warning: this attribute value will be marked as sensitive and will not
# display in UI output after applying this change. The value is unchanged.
!~ "client_id" = (sensitive value)
# Warning: this attribute value will be marked as sensitive and will not
# display in UI output after applying this change. The value is unchanged.
!~ "client_secret" = (sensitive value)
"oidc_issuer" = "https://accounts.google.com"
"token_request_method" = "****"
"token_url" = "******************************************"
}
provider_name = "Google"
provider_type = "Google"
region = "us-west-2"
user_pool_id = "*******************"
}
# module.home-unite-us.aws_cognito_user_group.homeuniteus_prod["Admins"] will be imported
resource "aws_cognito_user_group" "homeuniteus_prod" {
description = "Managed by Terraform"
id = "us-west-2_VH24AGQ3p/Admins"
name = "Admins"
precedence = 0
region = "us-west-2"
role_arn = null
user_pool_id = "*******************"
}
# module.home-unite-us.aws_cognito_user_group.homeuniteus_prod["Coordinators"] will be imported
resource "aws_cognito_user_group" "homeuniteus_prod" {
description = "Managed by Terraform"
id = "us-west-2_VH24AGQ3p/Coordinators"
name = "Coordinators"
precedence = 0
region = "us-west-2"
role_arn = null
user_pool_id = "*******************"
}
# module.home-unite-us.aws_cognito_user_group.homeuniteus_prod["Guests"] will be imported
resource "aws_cognito_user_group" "homeuniteus_prod" {
description = "Managed by Terraform"
id = "us-west-2_VH24AGQ3p/Guests"
name = "Guests"
precedence = 0
region = "us-west-2"
role_arn = null
user_pool_id = "*******************"
}
# module.home-unite-us.aws_cognito_user_group.homeuniteus_prod["Hosts"] will be imported
resource "aws_cognito_user_group" "homeuniteus_prod" {
description = "Managed by Terraform"
id = "us-west-2_VH24AGQ3p/Hosts"
name = "Hosts"
precedence = 0
region = "us-west-2"
role_arn = null
user_pool_id = "*******************"
}
# module.home-unite-us.aws_cognito_user_pool.homeuniteus_prod will be updated in-place
# (imported from "us-west-2_VH24AGQ3p")
!~ resource "aws_cognito_user_pool" "homeuniteus_prod" {
arn = "arn:aws:cognito-idp:us-west-2:035866691871:userpool/us-west-2_VH24AGQ3p"
auto_verified_attributes = [
"email",
]
creation_date = "2024-10-08T09:26:17Z"
custom_domain = null
deletion_protection = "ACTIVE"
domain = "homeuniteus"
email_verification_message = null
email_verification_subject = null
endpoint = "cognito-idp.us-west-2.amazonaws.com/us-west-2_VH24AGQ3p"
estimated_number_of_users = 96
id = "us-west-2_VH24AGQ3p"
last_modified_date = "2024-10-08T09:26:18Z"
mfa_configuration = "OPTIONAL"
name = "Home Unite Us"
region = "us-west-2"
sms_authentication_message = null
sms_verification_message = null
tags = {}
!~ tags_all = {
+ "managed-by" = "terraform-incubator"
}
user_pool_tier = "LITE"
username_attributes = [
"email",
"phone_number",
]
account_recovery_setting {
recovery_mechanism {
name = "verified_email"
priority = 1
}
recovery_mechanism {
name = "verified_phone_number"
priority = 2
}
}
admin_create_user_config {
allow_admin_create_user_only = false
}
email_configuration {
configuration_set = null
email_sending_account = "COGNITO_DEFAULT"
from_email_address = null
reply_to_email_address = null
source_arn = null
}
lambda_config {
create_auth_challenge = null
custom_message = "arn:aws:lambda:us-west-2:035866691871:function:customMessage"
define_auth_challenge = null
kms_key_id = null
post_authentication = null
post_confirmation = null
pre_authentication = null
pre_sign_up = "arn:aws:lambda:us-west-2:035866691871:function:mergeUsers"
pre_token_generation = null
user_migration = null
verify_auth_challenge_response = null
}
password_policy {
minimum_length = 8
password_history_size = 0
require_lowercase = true
require_numbers = true
require_symbols = true
require_uppercase = true
temporary_password_validity_days = 7
}
schema {
attribute_data_type = "String"
developer_only_attribute = false
mutable = true
name = "email"
required = true
string_attribute_constraints {
max_length = "2048"
min_length = "0"
}
}
sign_in_policy {
allowed_first_auth_factors = [
"PASSWORD",
]
}
sms_configuration {
external_id = "************************************"
sns_caller_arn = "arn:aws:iam::035866691871:role/service-role/homeuniteus-cognito-idp"
sns_region = "us-west-2"
}
user_attribute_update_settings {
attributes_require_verification_before_update = [
"email",
]
}
username_configuration {
case_sensitive = false
}
verification_message_template {
default_email_option = "CONFIRM_WITH_CODE"
email_message = null
email_message_by_link = null
email_subject = null
email_subject_by_link = null
sms_message = null
}
}
# module.home-unite-us.aws_cognito_user_pool_client.homeuniteus_prod will be imported
resource "aws_cognito_user_pool_client" "homeuniteus_prod" {
access_token_validity = 30
allowed_oauth_flows = [
"code",
]
allowed_oauth_flows_user_pool_client = true
allowed_oauth_scopes = [
"aws.cognito.signin.user.admin",
"email",
"openid",
"phone",
"profile",
]
auth_session_validity = 3
callback_urls = [
"http://localhost:4040/signin",
"http://localhost:4040/signup",
"http://localhost:4040/signup/coordinator",
"http://localhost:4040/signup/host",
"https://dev.homeunite.us/signin",
"https://dev.homeunite.us/signup",
"https://dev.homeunite.us/signup/coordinator",
"https://dev.homeunite.us/signup/host",
"https://qa.homeunite.us/signin",
"https://qa.homeunite.us/signup",
"https://qa.homeunite.us/signup/coordinator",
"https://qa.homeunite.us/signup/host",
]
client_secret = (sensitive value)
default_redirect_uri = null
enable_propagate_additional_user_context_data = false
enable_token_revocation = true
explicit_auth_flows = [
"ALLOW_ADMIN_USER_PASSWORD_AUTH",
"ALLOW_CUSTOM_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH",
"ALLOW_USER_PASSWORD_AUTH",
"ALLOW_USER_SRP_AUTH",
]
id = "b76g3q852lb4us1rbo19he3uj"
id_token_validity = 60
logout_urls = []
name = "homeuniteus"
prevent_user_existence_errors = "ENABLED"
read_attributes = [
"address",
"birthdate",
"email",
"email_verified",
"family_name",
"gender",
"given_name",
"locale",
"middle_name",
"name",
"nickname",
"phone_number",
"phone_number_verified",
"picture",
"preferred_username",
"profile",
"updated_at",
"website",
"zoneinfo",
]
refresh_token_validity = 30
region = "us-west-2"
supported_identity_providers = [
"COGNITO",
"Google",
]
user_pool_id = "*******************"
write_attributes = [
"address",
"birthdate",
"email",
"family_name",
"gender",
"given_name",
"locale",
"middle_name",
"name",
"nickname",
"phone_number",
"picture",
"preferred_username",
"profile",
"updated_at",
"website",
"zoneinfo",
]
token_validity_units {
access_token = "*******"
id_token = "*******"
refresh_token = "****"
}
}
# module.home-unite-us.aws_cognito_user_pool_domain.homeuniteus_prod will be imported
resource "aws_cognito_user_pool_domain" "homeuniteus_prod" {
aws_account_id = "************"
certificate_arn = null
cloudfront_distribution = "dpp0gtxikpq3y.cloudfront.net"
cloudfront_distribution_arn = "dpp0gtxikpq3y.cloudfront.net"
cloudfront_distribution_zone_id = "**************"
domain = "homeuniteus"
id = "homeuniteus"
managed_login_version = 1
region = "us-west-2"
s3_bucket = "aws-cognito-prod-pdx-assets"
user_pool_id = "*******************"
version = "20241010065051"
}
# module.home-unite-us.aws_iam_role.cognito_idp_prod will be updated in-place
# (imported from "homeuniteus-cognito-idp")
!~ resource "aws_iam_role" "cognito_idp_prod" {
arn = "arn:aws:iam::035866691871:role/service-role/homeuniteus-cognito-idp"
assume_role_policy = jsonencode(
{
Statement = [
{
Action = "sts:AssumeRole"
Condition = {
StringEquals = {
"sts:ExternalId" = "f027bce1-d945-40d4-8b59-54e12015cdb7"
}
}
Effect = "Allow"
Principal = {
Service = "cognito-idp.amazonaws.com"
}
Sid = ""
},
]
Version = "2012-10-17"
}
)
create_date = "2024-10-08T09:25:55Z"
description = null
force_detach_policies = false
id = "homeuniteus-cognito-idp"
managed_policy_arns = []
max_session_duration = 3600
name = "homeuniteus-cognito-idp"
name_prefix = null
path = "/service-role/"
permissions_boundary = null
tags = {}
!~ tags_all = {
+ "managed-by" = "terraform-incubator"
}
unique_id = "*********************"
inline_policy {
name = "homeuniteus-cognito-idp"
policy = jsonencode(
{
Statement = [
{
Action = [
"sns:publish",
]
Effect = "Allow"
Resource = [
"*",
]
},
]
Version = "2012-10-17"
}
)
}
}
# module.home-unite-us.aws_iam_role_policy.cognito_sns_prod will be imported
resource "aws_iam_role_policy" "cognito_sns_prod" {
id = "homeuniteus-cognito-idp:homeuniteus-cognito-idp"
name = "homeuniteus-cognito-idp"
name_prefix = null
policy = jsonencode(
{
Statement = [
{
Action = [
"sns:publish",
]
Effect = "Allow"
Resource = [
"*",
]
},
]
Version = "2012-10-17"
}
)
role = "homeuniteus-cognito-idp"
}
# module.home-unite-us.aws_secretsmanager_secret.cognito_client_prod will be updated in-place
# (imported from "arn:aws:secretsmanager:us-west-2:035866691871:secret:homeuniteus-cognito-client-EEaiW4")
!~ resource "aws_secretsmanager_secret" "cognito_client_prod" {
arn = "arn:aws:secretsmanager:us-west-2:035866691871:secret:homeuniteus-cognito-client-EEaiW4"
description = null
+ force_overwrite_replica_secret = false
id = "arn:aws:secretsmanager:us-west-2:035866691871:secret:homeuniteus-cognito-client-EEaiW4"
kms_key_id = null
name = "homeuniteus-cognito-client"
name_prefix = null
policy = jsonencode(
{
Statement = [
{
Action = "secretsmanager:GetSecretValue"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::035866691871:user/tyler.thome"
}
Resource = "*"
Sid = "EnableAnotherAWSAccountToReadTheSecret"
},
]
Version = "2012-10-17"
}
)
+ recovery_window_in_days = 30
region = "us-west-2"
tags = {}
!~ tags_all = {
+ "managed-by" = "terraform-incubator"
}
type = null
}
Plan: 11 to import, 0 to add, 4 to change, 0 to destroy.✅ Plan applied in Terraform apply (OIDC) #68 |
ale210
added a commit
that referenced
this pull request
Aug 30, 2026
Second of three PRs for #166, following #186 which imported the production Cognito pool. ## What this does - Adds `environment-prod.tf`, standing up the production stack through `modules/container` with the correct naming (`home-unite-us-fullstack-prod`). - Adopts the existing `homeuniteus` ECR repository as a plain `aws_ecr_repository` resource in `ecr.tf`, with an import block. It is not routed through `modules/ecr` because that module derives the repository name from the project name, and this repository's name is referenced by the running task definition and by the project's own CI. ## Nothing here takes production traffic The new listener rule is at **priority 17**; the live unmanaged rule is at **16**, and the load balancer evaluates the lower number first. So this stack comes up alongside production, serving nothing, and can be verified before anything moves. Removing rule 16 is the cutover and belongs to the next PR, where the rollback is simply putting it back. ## Choices worth reviewing **`launch_type = "ec2"`**, not Fargate. The unmanaged service runs Fargate; the module's own guidance is to use EC2 unless there is a reason not to. The cluster has room — two container instances with roughly 6 GB free each, against a 512 MB task. **The image is the tag production already serves**, `2dc864ae50.20250220-172003`, pulled from the adopted repository. It is date-and-SHA stamped rather than a moving tag, so it cannot drift the way the mutable `:dev`/`:qa` tags elsewhere in this account have. Building a fresh production image is being handled separately with the HomeUniteUs team. **Every environment variable is declared explicitly.** The image carries a baked-in `.env` with the same keys, and environment variables take precedence over it — declaring the full set is what ends production's dependence on a file that exists only inside a container. These point at the production Cognito pool from #186, not the QA pool. Two of those values look wrong and are not: - `HUU_ENVIRONMENT = "qa"`. The image is built with `HUU_TARGET_ENV=qa`, which bakes `/var/www/qa.homeunite.us` and `/var/log/qa.homeunite.us` into it. It has to stay `qa` while this tag is in use. - `hostname = "qa.homeunite.us"` with `www.homeunite.us` as an additional host. The frontend bundle is compiled with `VITE_HUU_API_BASE_URL=https://qa.homeunite.us/api`, and the backend's `ROOT_URL` has to agree or the OAuth redirect breaks. This mirrors the live rule at priority 16, which already serves both names. Moving the application onto `www` properly needs a rebuilt image. **`COGNITO_ACCESS_ID` and `COGNITO_ACCESS_KEY` are set to empty strings** so boto3 falls back to the task role, rather than the static access key of the `homeuniteus-app` IAM user that the baked `.env` supplies. Retiring that user and its key is part of the cleanup PR. **`DATABASE_URL`** is assembled into an SSM parameter, matching how QA passes it. The production database already exists inside the shared instance and is not created here; its password is read from the `homeuniteus-rds-password` secret left behind by the Terragrunt stack. ## What to check in the plan One import (the ECR repository, which should be tag-only changes), and creates for the new service, task definition, target group, listener rule, log group, security group, task role and policy, and the SSM parameter. **No existing resource should be modified or destroyed** beyond the ECR tag. ## Verification - `terraform validate` passes on 1.16.0. `terraform fmt` is clean on the added blocks; the pre-existing `ecr_fullstack` block is left unformatted rather than mixing unrelated churn into this PR. - **After this merges**, confirm the new service reaches a steady 1/1 and its target group shows a healthy target, and that `www.homeunite.us` and `qa.homeunite.us` are still served by the *old* stack — nothing should change for users in this PR. ## Not in this PR The cutover, and deleting the legacy service, task definition family, target group, security group, log group, `homeuniteus-app` IAM user and key, the two IAM policies, and the four redundant secrets. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
ale210
added a commit
that referenced
this pull request
Aug 30, 2026
…hecks (#188) Third of four PRs for #166, after #186 (Cognito import) and #187 (production service). It corrects how the new service is configured. It still takes no traffic — the cutover is the next PR. ## Production does not use RDS, and never has #187 set `DATABASE_URL` from the `homeuniteus-rds-password` secret, following the abandoned Terragrunt state, which describes a Postgres database on the shared instance. The service came up and its API died immediately: ``` FATAL: password authentication failed for user "homeuniteus" ERROR: Application startup failed. Exiting. ``` The credential is not stale — the premise was wrong. The live production container's `.env` points `DATABASE_URL` at **`/opt/huu/homeuniteus.db`, a SQLite file on the container's own filesystem**. Nothing has connected to that Postgres database since January 2025, which is why the password no longer matches anything. This PR sets `DATABASE_URL` to the same SQLite path so the new service matches production, and removes the Postgres wiring: the `homeuniteus-rds-password` data source, the `aws_db_instance` lookup, and the `db_url_prod` SSM parameter, which was created holding a value that never worked. ## What that means, recorded here deliberately The production task definition declares no volume and no mount point. So that SQLite file is created empty when a task starts and is destroyed when the task is replaced — by a deploy, a host drain, or an AZ rebalance. **The log group holds 743 task generations.** Application data has never survived any of them. It goes unnoticed because Cognito holds the user accounts and Cognito is durable — 96 of them. The site loads, sign-in works, and only the application's own tables reset. It fails in the one way that looks fine from outside. The current file was inspected before writing this: 53,248 bytes, containing nothing beyond what the application recreates for itself on startup via `create_all` and the `after_create` role seed. So there is nothing to migrate, and nothing was lost in arriving at this PR. **This PR reproduces that behaviour rather than fixing it**, so that the cutover changes nothing about how the application runs. Making production durable is a real change to a live service and belongs in its own piece of work, not inside a migration. The comment in `environment-prod.tf` says so at the point where the next reader will meet it. ## Health checks now test the API The target group checked `/`, which nginx serves whether or not the API is alive — the container runs nginx in the foreground and the API as a background process, so the API dying does not even stop the task. When #187's service came up with a dead API, **the target group reported healthy the entire time**. It now checks `/api/health/`, which is served by the API process and returns `"UP"`. Had this been in place, #187 would have failed visibly instead of looking green. ## `LOG_LEVEL` restored to `DEBUG` #187 set `INFO`; production runs `DEBUG`. Reverted to match, keeping this migration behaviour-preserving. Whether `DEBUG` is appropriate for production is a separate question and not settled here. ## What to check in the plan A new task definition revision and a service update to use it, an in-place update to the target group's health check, and the destruction of the unused `db_url_prod` SSM parameter and its `random_password`. Nothing else should change, and no other resource should be replaced. ## Verification - `terraform validate` and `terraform fmt` pass on 1.16.0. - **After this merges**, confirm from the task's CloudWatch logs that the API reaches startup without the SQLAlchemy error — not from the target group, which is what misled us last time. With the new health check path a failed start should also surface as an unhealthy target. - Production is unaffected either way: rule 16 still routes all traffic to the old service. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First of three PRs for #166. This one only adopts the production Cognito stack into state — no infrastructure changes.
What this does
cognito.tftocognito-qa.tf. Terraform keys state on resource addresses rather than filenames, so the rename alone changes nothing.cognito-prod.tf, declaring the production poolHome Unite Us(us-west-2_VH24AGQ3p) and its client, domain, four user groups, Google identity provider, SMS role and inline policy, and thehomeuniteus-cognito-clientsecret.importblocks toterraform/import.tf, following the pattern from Bring civictechindex's AWS resources under Terraform #165.Why import rather than rebuild
The production pool holds the live user accounts, and its pool id and client id are compiled into the running production container image. Rebuilding it would mean migrating users, which Cognito does not support without resetting passwords. The
home-unite-uspool already in Terraform is a separate pool that only servesqa1.homeunite.us— both are real, and neither replaces the other.The ECS service is the opposite case and is handled separately: it holds no state, so it gets recreated with the correct naming rather than imported.
The check that matters
No resource may be added, destroyed or replaced. That is the point of this PR — it proves the adoption is inert before anything else touches production. The provider sets
default_tags, so a truly empty plan is not achievable: every imported resource gainsmanaged-by = terraform-incubator, which is exactly the tag the coverage report reads. The bar is therefore that every change is tags or Terraform-side metadata, and nothing is created or replaced.Current plan:
11 to import, 0 to add, 4 to change, 0 to destroy. The four changes aretags_allgainingmanaged-byon the pool, the IAM role and the secret; provider defaults (recovery_window_in_days,force_overwrite_replica_secret) being recorded for the secret; and the Google provider'sclient_id/client_secretbeing re-marked as sensitive, which the plan explicitly annotates as "The value is unchanged".Two defects the first plan caught, both fixed in the second commit:
generate_secretcannot be read back from the API, so Terraform sawnull → trueand that attribute forces replacement. Replacing the client would mint a new client id, which is compiled into the production frontend bundle — it would have broken sign-in. Fixed withignore_changes = [generate_secret], plusprevent_destroyso any future replacement fails loudly instead.importblock per resource address, so four static blocks left three instances unimported. Fixed by collapsing them into a singlefor_eachimport block.Three further details were needed to get the plan this clean, each commented in the file:
user_pool_tier = "LITE"is set explicitly. The pool predates the tier feature; the provider defaults the attribute toESSENTIALS, so omitting it would plan a billing upgrade on the live pool.lambda_confignames the legacycustomMessageandmergeUsersby literal ARN. Those stay unmanaged deliberately — re-pointing the pool at the already-managedhome-unite-us-*pair is a live change to production sign-up and belongs in its own change.The Google identity provider reads the same secrets as the QA one. The legacy
homeuniteus-google-*pair holds byte-identical values, verified by digest, so they are redundant copies rather than separate credentials and are left for the cleanup PR.Verification
terraform fmtclean,terraform validatepasses on 1.16.0.~> 1.16.0, so CI is the first real plan.qa.homeunite.us, which is the production hostname the current image is built against.Not in this PR
The production ECS service, the cutover, and deleting the legacy resources. The Lambda re-point is tracked as follow-on work.