diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 28fcdf863..b8bd0438e 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -62,15 +62,6 @@ # Build security groups for load balancers, containers, and our Redis cache backend_cache_sg, container_sgs, lb_sgs = security_groups(project=project, resources=resources, vpc=vpc) -# Create the Redis memory cache -backend_cache, cache_dns = redis_cache( - cloudflare_zone_id=cloudflare_zone_id, - project=project, - security_group=backend_cache_sg, - vpc=vpc, - resources=resources, -) - # Fargate Service fargate_clusters, autoscalers = fargate( container_security_groups=container_sgs, @@ -91,6 +82,22 @@ for afc_name, afc_config in resources.get('tb:fargate:AutoscalingFargateCluster', {}).items() } +# Build a list of SGs to grant access to the new cache replica set +_redis_source_sgs = [backend_cache_sg] +for afc in afcs.values(): + for cont_sgs in afc.resources['container_security_groups'].values(): + _redis_source_sgs.extend([sg_with_rules for sg_with_rules in cont_sgs.values()]) + +# Create the Redis memory cache +backend_cache, cache_dns = redis_cache( + cloudflare_zone_id=cloudflare_zone_id, + project=project, + security_group=backend_cache_sg, + security_groups=_redis_source_sgs, + vpc=vpc, + resources=resources, +) + # CloudFront function to handle request rewrites headed to the backend rewrite_function = cloudfront.rewrite_function(project=project) project.resources['cf_rewrite_function'] = rewrite_function diff --git a/pulumi/config.dev.yaml b/pulumi/config.dev.yaml index fb93f6b31..c57a50d47 100644 --- a/pulumi/config.dev.yaml +++ b/pulumi/config.dev.yaml @@ -10,14 +10,17 @@ .auth_scheme: &VAR_AUTH_SCHEME {name: "AUTH_SCHEME", value: "oidc"} .database_engine: &VAR_DATABASE_ENGINE {name: "DATABASE_ENGINE", value: "postgresql"} .database_port: &VAR_DATABASE_PORT {name: "DATABASE_PORT", value: "5432"} +.frontend_url: &VAR_FRONTEND_URL {name: "FRONTEND_URL", value: "https://appointment-dev.tb.pro"} +.fxa_callback: &VAR_FXA_CALLBACK {name: "FXA_CALLBACK", value: "https://appointment-dev.tb.pro/fxa"} +.google_auth_callback: &VAR_GOOGLE_AUTH_CALLBACK {name: "GOOGLE_AUTH_CALLBACK", value: "https://appointment-dev.tb.pro/api/v1/google/callback"} .jwt_algo: &VAR_JWT_ALGO {name: "JWT_ALGO", value: "HS256"} .jwt_expire_in_mins: &VAR_JWT_EXPIRE_IN_MINS {name: "JWT_EXPIRE_IN_MINS", value: "10000"} .log_level: &VAR_LOG_LEVEL {name: "LOG_LEVEL", value: "ERROR"} .log_use_stream: &VAR_LOG_USE_STREAM {name: "LOG_USE_STREAM", value: "True"} -.oids_exp_grace_period: &VAR_OIDC_EXP_GRACE_PERIOD {name: "OIDC_EXP_GRACE_PERIOD", value: "60"} +.oidc_exp_grace_period: &VAR_OIDC_EXP_GRACE_PERIOD {name: "OIDC_EXP_GRACE_PERIOD", value: "60"} .oidc_fallback_match_by_email: &VAR_OIDC_FALLBACK_MATCH_BY_EMAIL {name: "OIDC_FALLBACK_MATCH_BY_EMAIL", value: "True"} .oidc_token_introspection_url: &VAR_OIDC_TOKEN_INTROSPECTION_URL {name: "OIDC_TOKEN_INTROSPECTION_URL", value: "https://auth-dev.tb.pro/realms/tbpro/protocol/openid-connect/token/introspect"} -.frontend_url: &VAR_FRONTEND_URL {name: "FRONTEND_URL", value: "https://appointment-dev.tb.pro"} +.posthog_host: &VAR_POSTHOG_HOST {name: "POSTHOG_HOST", value: "https://us.i.posthog.com"} .redis_db: &VAR_REDIS_DB {name: "REDIS_DB", value: "0"} .redis_port: &VAR_REDIS_PORT {name: "REDIS_PORT", value: "6379"} .redis_use_ssl: &VAR_REDIS_USE_SSL {name: "REDIS_USE_SSL", value: "True"} @@ -26,20 +29,43 @@ .sentry_dsn: &VAR_SENTRY_DSN {name: "SENTRY_DSN", value: "https://5dddca3ecc964284bb8008bc2beef808@o4505428107853824.ingest.sentry.io/4505428124827648"} .service_email: &VAR_SERVICE_EMAIL {name: "SERVICE_EMAIL", value: "no-reply@appointment-dev.tb.pro"} .short_base_url: &VAR_SHORT_BASE_URL {name: "SHORT_BASE_URL", value: "https://dev.apt.mt"} +.smtp_port: &VAR_SMTP_PORT {name: "SMTP_PORT", value: "587"} +.smtp_security: &VAR_SMTP_SECURITY {name: "SMTP_SECURITY", value: "STARTTLS"} .tb_accounts_caldav_url: &VAR_TB_ACCOUNTS_CALDAV_URL {name: "TB_ACCOUNTS_CALDAV_URL", value: "https://dev-thundermail.com"} +.tb_accounts_host: &VAR_TB_ACCOUNTS_HOST {name: "TB_ACCOUNTS_HOST", value: "https://accounts-dev.tb.pro"} .tier_basic_calendar_limit: &VAR_TIER_BASIC_CALENDAR_LIMIT {name: "TIER_BASIC_CALENDAR_LIMIT", value: "3"} .tier_plus_calendar_limit: &VAR_TIER_PLUS_CALENDAR_LIMIT {name: "TIER_PLUS_CALENDAR_LIMIT", value: "5"} .tier_pro_calendar_limit: &VAR_TIER_PRO_CALENDAR_LIMIT {name: "TIER_PRO_CALENDAR_LIMIT", value: "10"} .zoom_api_enabled: &VAR_ZOOM_API_ENABLED {name: "ZOOM_API_ENABLED", value: "True"} +.zoom_api_new_app: &VAR_ZOOM_API_NEW_APP {name: "ZOOM_API_NEW_APP", value: "False"} .zoom_auth_callback: &VAR_ZOOM_AUTH_CALLBACK {name: "ZOOM_AUTH_CALLBACK", value: "https://appointment-dev.tb.pro/api/v1/zoom/callback"} # These variables are also common to our environments, but are pulled from secret stores instead +.app_admin_allow_list: &SECRET_APP_ADMIN_ALLOW_LIST {name: "APP_ADMIN_ALLOW_LIST", valueFrom: ""} +.appointment_caldav_secret: &SECRET_APPOINTMENT_CALDAV_SECRET {name: "APPOINTMENT_CALDAV_SECRET", valueFrom: ""} .database_host: &SECRET_DATABASE_HOST {name: "DATABASE_HOST", valueFrom: "arn:aws:secretsmanager:eu-central-1:768512802988:secret:appointment/dev/database-host-pnM2d9"} .database_name: &SECRET_DATABASE_NAME {name: "DATABASE_NAME", valueFrom: "arn:aws:secretsmanager:eu-central-1:768512802988:secret:appointment/dev/database-name-slQJtj"} -.database_username: &SECRET_DATABASE_USERNAME {name: "DATABASE_USERNAME", valueFrom: "arn:aws:secretsmanager:eu-central-1:768512802988:secret:appointment/dev/database-user-9EgJWM"} .database_password: &SECRET_DATABASE_PASSWORD {name: "DATABASE_PASSWORD", valueFrom: "arn:aws:secretsmanager:eu-central-1:768512802988:secret:appointment/dev/database-password-yaGZdw"} +.database_username: &SECRET_DATABASE_USERNAME {name: "DATABASE_USERNAME", valueFrom: "arn:aws:secretsmanager:eu-central-1:768512802988:secret:appointment/dev/database-user-9EgJWM"} +.db_secret: &SECRET_DB_SECRET {name: "DB_SECRET", valueFrom: ""} +.fxa_allow_list: &SECRET_FXA_ALLOW_LIST {name: "FXA_ALLOW_LIST", valueFrom: ""} +.fxa_client_id: &SECRET_FXA_CLIENT_ID {name: "FXA_CLIENT_ID", valueFrom: ""} +.fxa_open_id_config: &SECRET_FXA_OPEN_ID_CONFIG {name: "FXA_OPEN_ID_CONFIG", valueFrom: ""} +.fxa_secret: &SECRET_FXA_SECRET {name: "FXA_SECRET", valueFrom: ""} +.google_auth_client_id: &SECRET_GOOGLE_AUTH_CLIENT_ID {name: "GOOGLE_AUTH_CLIENT_ID", valueFrom: ""} +.google_auth_project_id: &SECRET_GOOGLE_AUTH_PROJECT_ID {name: "GOOGLE_AUTH_PROJECT_ID", valueFrom: ""} +.google_auth_secret: &SECRET_GOOGLE_AUTH_SECRET {name: "GOOGLE_AUTH_SECRET", valueFrom: ""} +.jwt_secret: &SECRET_JWT_SECRET {name: "JWT_SECRET", valueFrom: ""} .oidc_client_id: &SECRET_OIDC_CLIENT_ID {name: "OIDC_CLIENT_ID", valueFrom: "arn:aws:secretsmanager:eu-central-1:768512802988:secret:appointment/dev/oidc-client-id-sbebkz"} .oidc_client_secret: &SECRET_OIDC_CLIENT_SECRET {name: "OIDC_CLIENT_SECRET", valueFrom: "arn:aws:secretsmanager:eu-central-1:768512802988:secret:appointment/dev/oidc-client-secret-I9BTf2"} +.posthog_project_key: &SECRET_POSTHOG_PROJECT_KEY {name: "POSTHOG_PROJECT_KEY", valueFrom: ""} +.session_secret: &SECRET_SESSION_SECRET {name: "SESSION_SECRET", valueFrom: ""} +.signed_secret: &SECRET_SIGNED_SECRET {name: "SIGNED_SECRET", valueFrom: ""} +.smtp_pass: &SECRET_SMTP_PASS {name: "SMTP_PASS", valueFrom: ""} +.smtp_url: &SECRET_SMTP_URL {name: "SMTP_URL", valueFrom: ""} +.smtp_user: &SECRET_SMTP_USER {name: "SMTP_USER", valueFrom: ""} +.zoom_api_secret: &SECRET_ZOOM_API_SECRET {name: "ZOOM_API_SECRET", valueFrom: ""} +.zoom_auth_client_id: &SECRET_ZOOM_AUTH_CLIENT_ID {name: "ZOOM_AUTH_CLIENT_ID", valueFrom: ""} # This forms the base of Appointment-based task definitions. The blank fields commented here must be set on any # inheriting task definition: @@ -508,6 +534,14 @@ resources: min_capacity: 1 max_capacity: 1 + # Additional properties for this environment's cache + tb:elasticache:ElastiCacheReplicationGroup: + backend: + num_cache_nodes: 1 + # Settings below should be "yes" when (num_cache_nodes > 1) + automatic_failover_enabled: no + multi_az_enabled: no + tb:cloudfront:CloudFrontS3Service: frontend: service_bucket_name: tb-appointment-dev-frontend diff --git a/pulumi/config.prod.yaml b/pulumi/config.prod.yaml index 42421f38d..72f9ebb7f 100644 --- a/pulumi/config.prod.yaml +++ b/pulumi/config.prod.yaml @@ -18,7 +18,7 @@ .jwt_expire_in_mins: &VAR_JWT_EXPIRE_IN_MINS {name: "JWT_EXPIRE_IN_MINS", value: "10000"} .log_level: &VAR_LOG_LEVEL {name: "LOG_LEVEL", value: "ERROR"} .log_use_stream: &VAR_LOG_USE_STREAM {name: "LOG_USE_STREAM", value: "True"} -.oids_exp_grace_period: &VAR_OIDC_EXP_GRACE_PERIOD {name: "OIDC_EXP_GRACE_PERIOD", value: "60"} +.oidc_exp_grace_period: &VAR_OIDC_EXP_GRACE_PERIOD {name: "OIDC_EXP_GRACE_PERIOD", value: "60"} .oidc_fallback_match_by_email: &VAR_OIDC_FALLBACK_MATCH_BY_EMAIL {name: "OIDC_FALLBACK_MATCH_BY_EMAIL", value: "True"} .oidc_token_introspection_url: &VAR_OIDC_TOKEN_INTROSPECTION_URL {name: "OIDC_TOKEN_INTROSPECTION_URL", value: "https://auth.tb.pro/realms/tbpro/protocol/openid-connect/token/introspect"} .posthog_host: &VAR_POSTHOG_HOST {name: "POSTHOG_HOST", value: "https://us.i.posthog.com"} @@ -588,6 +588,14 @@ resources: min_capacity: 0 max_capacity: 0 + # Additional properties for this environment's cache + tb:elasticache:ElastiCacheReplicationGroup: + backend: + num_cache_nodes: 2 + # Settings below should be "yes" when (num_cache_nodes > 1) + automatic_failover_enabled: yes + multi_az_enabled: yes + tb:cloudfront:CloudFrontS3Service: frontend: service_bucket_name: tb-appointment-prod-frontend diff --git a/pulumi/config.stage.yaml b/pulumi/config.stage.yaml index 3ce95a449..d3f5f089e 100644 --- a/pulumi/config.stage.yaml +++ b/pulumi/config.stage.yaml @@ -3,7 +3,7 @@ ### Special variables used throughout this file # Update this value to update all containers based on the thunderbird/appointment image -.apmt_image: &APMT_IMAGE 768512802988.dkr.ecr.eu-central-1.amazonaws.com/thunderbird/appointment:2a8e57c78807346aebf7d89cfbd1f00e065f727f +.apmt_image: &APMT_IMAGE 768512802988.dkr.ecr.eu-central-1.amazonaws.com/thunderbird/appointment:c8caacf81a3f37073588bfed17f13ef6f3131dc1 # These variables are common to Accounts application environments. Some tasks will require additional configuration. .app_env: &VAR_APP_ENV {name: "APP_ENV", value: "stage"} @@ -17,7 +17,7 @@ .jwt_expire_in_mins: &VAR_JWT_EXPIRE_IN_MINS {name: "JWT_EXPIRE_IN_MINS", value: "10000"} .log_level: &VAR_LOG_LEVEL {name: "LOG_LEVEL", value: "ERROR"} .log_use_stream: &VAR_LOG_USE_STREAM {name: "LOG_USE_STREAM", value: "True"} -.oids_exp_grace_period: &VAR_OIDC_EXP_GRACE_PERIOD {name: "OIDC_EXP_GRACE_PERIOD", value: "60"} +.oidc_exp_grace_period: &VAR_OIDC_EXP_GRACE_PERIOD {name: "OIDC_EXP_GRACE_PERIOD", value: "60"} .oidc_fallback_match_by_email: &VAR_OIDC_FALLBACK_MATCH_BY_EMAIL {name: "OIDC_FALLBACK_MATCH_BY_EMAIL", value: "True"} .oidc_token_introspection_url: &VAR_OIDC_TOKEN_INTROSPECTION_URL {name: "OIDC_TOKEN_INTROSPECTION_URL", value: "https://auth-stage.tb.pro/realms/tbpro/protocol/openid-connect/token/introspect"} .posthog_host: &VAR_POSTHOG_HOST {name: "POSTHOG_HOST", value: "https://us.i.posthog.com"} @@ -587,8 +587,16 @@ resources: min_capacity: 1 max_capacity: 1 flower: - min_capacity: 0 - max_capacity: 0 + min_capacity: 1 + max_capacity: 1 + + # Additional properties for this environment's cache + tb:elasticache:ElastiCacheReplicationGroup: + backend: + num_cache_nodes: 1 + # Settings below should be "yes" when (num_cache_nodes > 1) + automatic_failover_enabled: no + multi_az_enabled: no tb:cloudfront:CloudFrontS3Service: frontend: diff --git a/pulumi/redis.py b/pulumi/redis.py index 6667bb647..20f6c0bb8 100644 --- a/pulumi/redis.py +++ b/pulumi/redis.py @@ -3,13 +3,20 @@ import pulumi_cloudflare as cloudflare from tb_pulumi import ThunderbirdPulumiProject +from tb_pulumi.elasticache import ElastiCacheReplicationGroup from tb_pulumi.network import MultiCidrVpc, SecurityGroupWithRules def redis_cache( cloudflare_zone_id: str, project: ThunderbirdPulumiProject, + # **IN PROGRESS** + # + # Keep "security_group" as a single group that gets access to the old serverless cache + # -> Add "security_groups" as a list of groups that get access to the new replicaset + # Remove "security_group" when we destroy the old serverless cache security_group: SecurityGroupWithRules, + security_groups: list[SecurityGroupWithRules], resources: dict, vpc: MultiCidrVpc, ): @@ -19,9 +26,13 @@ def redis_cache( :param project: The project to store all the resources in. :type project: ThunderbirdPulumiProject - :param security_group: The security group to apply to the cache. + :param security_group: The security group to apply to the old serverless cache. :type security_group: SecurityGroupWithRules + :param security_groups: List of SecurityGroupWithRules resources which should have access to the new cache replica + set. + :type security_groups: list[SecurityGroupWithRules] + :param resources: The full set of configured resources. :type resources: dict @@ -33,6 +44,11 @@ def redis_cache( - The pulumi_cloudflare.DnsRecord resource pointing to the cache :rtype: _type_ """ + + # **IN PROGRESS** + # + # -> Migrate traffic to new replicaset + # Delete this code block along with relevant config sections backend_cache = aws.elasticache.ServerlessCache( f'{project.name_prefix}-cache-backend', security_group_ids=[security_group.resources.get('sg').id], @@ -43,14 +59,41 @@ def redis_cache( ) project.resources['backend_cache'] = backend_cache + # **IN PROGRESS** + # + # Build new replication group alongside the old cluster + # -> Move DNS to new replication group + # Destroy the old cluster + + redis_replica_group = ElastiCacheReplicationGroup( + name=f'{project.name_prefix}-redis-replicaset', + project=project, + at_rest_encryption_enabled=True, + cluster_mode='disabled', + source_sgids=[sg_with_rules.resources['sg'].id for sg_with_rules in security_groups], + subnets=[subnet for subnet in vpc.resources.get('subnets', {})], + transit_encryption_enabled=True, + transit_encryption_mode='preferred', + **resources.get('tb:elasticache:ElastiCacheReplicationGroup', {}).get('backend', {}), + opts=pulumi.ResourceOptions(depends_on=[vpc]), + ) + project.resources['backend_cache_replicaset'] = redis_replica_group + + # **IN PROGRESS** + # + # Build new replica set + # -> Change DNS to the new replica set in stage + # Change DNS in prod, removing the condition below + backend_cache_primary_endpoint = backend_cache.endpoints.apply(lambda endpoints: endpoints[0]['address']) + redis_replica_group_primary_endpoint = redis_replica_group.resources['replication_group'].primary_endpoint_address backend_cache_dns = cloudflare.DnsRecord( f'{project.name_prefix}-dns-redis', name=resources.get('domains', {}).get('redis', None), ttl=60, type='CNAME', zone_id=cloudflare_zone_id, - content=backend_cache_primary_endpoint, + content=backend_cache_primary_endpoint if project.stack == 'prod' else redis_replica_group_primary_endpoint, proxied=False, ) project.resources['backend_cache_dns'] = backend_cache_dns