@hasna/uptime 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/infra/aws/main.tf CHANGED
@@ -1,5 +1,5 @@
1
1
  terraform {
2
- required_version = ">= 1.6.0"
2
+ required_version = ">= 1.9.0"
3
3
 
4
4
  required_providers {
5
5
  aws = {
@@ -23,6 +23,8 @@ locals {
23
23
  efs_gid = 10001
24
24
  hosted_sqlite_db_path = "/data/uptime/uptime.db"
25
25
  efs_enabled_services = toset(["web"])
26
+ use_alb_https = var.protected_access_mode == "alb_https_cert"
27
+ use_cloudfront = var.protected_access_mode == "cloudfront_default_domain"
26
28
  services = {
27
29
  web = {
28
30
  desired_count = lookup(var.desired_counts, "web", 0)
@@ -51,10 +53,15 @@ locals {
51
53
  }
52
54
  }
53
55
  tags = {
54
- ManagedBy = "terraform"
55
- Service = var.service_name
56
- Stage = var.stage
57
- Account = var.account_name
56
+ ManagedBy = "terraform"
57
+ Service = var.service_name
58
+ Project = var.project_name
59
+ Stage = var.stage
60
+ Environment = var.environment
61
+ Account = var.account_name
62
+ Owner = var.owner
63
+ AppType = var.app_type
64
+ CostCenter = var.cost_center
58
65
  }
59
66
  }
60
67
 
@@ -62,6 +69,11 @@ data "aws_vpc" "target" {
62
69
  id = var.vpc_id
63
70
  }
64
71
 
72
+ data "aws_ec2_managed_prefix_list" "cloudfront_origin_facing" {
73
+ count = local.use_cloudfront ? 1 : 0
74
+ name = "com.amazonaws.global.cloudfront.origin-facing"
75
+ }
76
+
65
77
  resource "aws_ecr_repository" "open_uptime" {
66
78
  name = var.ecr_repository_name
67
79
  image_tag_mutability = "IMMUTABLE"
@@ -290,7 +302,7 @@ resource "aws_security_group" "alb" {
290
302
  }
291
303
 
292
304
  resource "aws_security_group_rule" "alb_https_ingress" {
293
- count = length(var.alb_ingress_cidr_blocks) > 0 ? 1 : 0
305
+ count = local.use_alb_https && length(var.alb_ingress_cidr_blocks) > 0 ? 1 : 0
294
306
  type = "ingress"
295
307
  description = "HTTPS"
296
308
  security_group_id = aws_security_group.alb.id
@@ -300,6 +312,17 @@ resource "aws_security_group_rule" "alb_https_ingress" {
300
312
  cidr_blocks = var.alb_ingress_cidr_blocks
301
313
  }
302
314
 
315
+ resource "aws_security_group_rule" "alb_http_from_cloudfront" {
316
+ count = local.use_cloudfront ? 1 : 0
317
+ type = "ingress"
318
+ description = "HTTP from CloudFront origin-facing ranges"
319
+ security_group_id = aws_security_group.alb.id
320
+ from_port = 80
321
+ to_port = 80
322
+ protocol = "tcp"
323
+ prefix_list_ids = [data.aws_ec2_managed_prefix_list.cloudfront_origin_facing[0].id]
324
+ }
325
+
303
326
  resource "aws_security_group_rule" "alb_to_web" {
304
327
  type = "egress"
305
328
  description = "To Open Uptime web"
@@ -418,7 +441,7 @@ resource "aws_efs_access_point" "uptime" {
418
441
  }
419
442
 
420
443
  resource "aws_efs_mount_target" "data" {
421
- for_each = toset(var.private_subnet_ids)
444
+ for_each = { for index, subnet_id in var.private_subnet_ids : tostring(index) => subnet_id }
422
445
  file_system_id = aws_efs_file_system.data.id
423
446
  subnet_id = each.value
424
447
  security_groups = [aws_security_group.efs.id]
@@ -506,10 +529,25 @@ resource "aws_lb_target_group" "web" {
506
529
  }
507
530
 
508
531
  resource "aws_lb_listener" "https" {
532
+ count = local.use_alb_https ? 1 : 0
509
533
  load_balancer_arn = aws_lb.open_uptime.arn
510
534
  port = 443
511
535
  protocol = "HTTPS"
512
536
  certificate_arn = var.certificate_arn
537
+ tags = local.tags
538
+
539
+ default_action {
540
+ type = "forward"
541
+ target_group_arn = aws_lb_target_group.web.arn
542
+ }
543
+ }
544
+
545
+ resource "aws_lb_listener" "http_cloudfront" {
546
+ count = local.use_cloudfront ? 1 : 0
547
+ load_balancer_arn = aws_lb.open_uptime.arn
548
+ port = 80
549
+ protocol = "HTTP"
550
+ tags = local.tags
513
551
 
514
552
  default_action {
515
553
  type = "forward"
@@ -517,8 +555,61 @@ resource "aws_lb_listener" "https" {
517
555
  }
518
556
  }
519
557
 
558
+ resource "aws_cloudfront_distribution" "open_uptime" {
559
+ count = local.use_cloudfront ? 1 : 0
560
+ enabled = true
561
+ is_ipv6_enabled = true
562
+ comment = "Open Uptime ${local.prefix} protected HTTPS edge"
563
+ price_class = "PriceClass_100"
564
+ tags = local.tags
565
+
566
+ origin {
567
+ domain_name = aws_lb.open_uptime.dns_name
568
+ origin_id = "${local.prefix}-alb"
569
+
570
+ custom_origin_config {
571
+ http_port = 80
572
+ https_port = 443
573
+ origin_protocol_policy = "http-only"
574
+ origin_ssl_protocols = ["TLSv1.2"]
575
+ }
576
+ }
577
+
578
+ default_cache_behavior {
579
+ target_origin_id = "${local.prefix}-alb"
580
+ viewer_protocol_policy = "redirect-to-https"
581
+ compress = true
582
+ allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
583
+ cached_methods = ["GET", "HEAD"]
584
+ default_ttl = 0
585
+ max_ttl = 0
586
+ min_ttl = 0
587
+
588
+ forwarded_values {
589
+ query_string = true
590
+ headers = ["Authorization", "Content-Type", "Origin", "X-Uptime-Hosted-Token"]
591
+
592
+ cookies {
593
+ forward = "all"
594
+ }
595
+ }
596
+ }
597
+
598
+ restrictions {
599
+ geo_restriction {
600
+ restriction_type = "none"
601
+ }
602
+ }
603
+
604
+ viewer_certificate {
605
+ cloudfront_default_certificate = true
606
+ }
607
+
608
+ depends_on = [aws_lb_listener.http_cloudfront]
609
+ }
610
+
520
611
  resource "aws_route53_record" "open_uptime" {
521
- count = var.hosted_zone_id == null ? 0 : 1
612
+ count = var.hosted_zone_id == null || !local.use_alb_https ? 0 : 1
522
613
  zone_id = var.hosted_zone_id
523
614
  name = var.hostname
524
615
  type = "A"
@@ -670,7 +761,12 @@ resource "aws_ecs_task_definition" "service" {
670
761
  { name = "HASNA_UPTIME_WORKSPACE_ID", value = var.workspace_id },
671
762
  { name = "HASNA_UPTIME_COMPONENT", value = each.key },
672
763
  { name = "HASNA_UPTIME_HOSTNAME", value = var.hostname },
673
- ], contains(local.efs_enabled_services, each.key) ? [
764
+ ], each.key == "web" ? [
765
+ {
766
+ name = "HASNA_UPTIME_ALLOWED_ORIGINS"
767
+ value = local.use_cloudfront ? "https://${aws_cloudfront_distribution.open_uptime[0].domain_name}" : "https://${var.hostname}"
768
+ },
769
+ ] : [], contains(local.efs_enabled_services, each.key) ? [
674
770
  { name = "HASNA_UPTIME_HOSTED_SQLITE_DB", value = local.hosted_sqlite_db_path },
675
771
  ] : [])
676
772
  mountPoints = contains(local.efs_enabled_services, each.key) ? [
@@ -725,7 +821,7 @@ resource "aws_ecs_service" "web" {
725
821
  container_port = local.container_port
726
822
  }
727
823
 
728
- depends_on = [aws_lb_listener.https, aws_efs_mount_target.data]
824
+ depends_on = [aws_lb_listener.https, aws_lb_listener.http_cloudfront, aws_efs_mount_target.data]
729
825
  }
730
826
 
731
827
  resource "aws_ecs_service" "worker" {
@@ -793,3 +889,35 @@ resource "aws_cloudwatch_metric_alarm" "web_unhealthy" {
793
889
  TargetGroup = aws_lb_target_group.web.arn_suffix
794
890
  }
795
891
  }
892
+
893
+ resource "aws_budgets_budget" "monthly" {
894
+ count = var.monthly_budget_limit_usd > 0 && length(var.budget_alert_email_addresses) > 0 ? 1 : 0
895
+ name = "${local.prefix}-monthly-budget"
896
+ budget_type = "COST"
897
+ limit_amount = format("%.2f", var.monthly_budget_limit_usd)
898
+ limit_unit = "USD"
899
+ time_unit = "MONTHLY"
900
+
901
+ cost_filter {
902
+ name = "TagKeyValue"
903
+ values = [format("user:Service$%s", var.service_name)]
904
+ }
905
+
906
+ notification {
907
+ comparison_operator = "GREATER_THAN"
908
+ notification_type = "FORECASTED"
909
+ threshold = 80
910
+ threshold_type = "PERCENTAGE"
911
+ subscriber_email_addresses = var.budget_alert_email_addresses
912
+ }
913
+
914
+ notification {
915
+ comparison_operator = "GREATER_THAN"
916
+ notification_type = "ACTUAL"
917
+ threshold = 100
918
+ threshold_type = "PERCENTAGE"
919
+ subscriber_email_addresses = var.budget_alert_email_addresses
920
+ }
921
+
922
+ tags = local.tags
923
+ }
@@ -14,6 +14,14 @@ output "alb_dns_name" {
14
14
  value = aws_lb.open_uptime.dns_name
15
15
  }
16
16
 
17
+ output "cloudfront_domain_name" {
18
+ value = try(aws_cloudfront_distribution.open_uptime[0].domain_name, null)
19
+ }
20
+
21
+ output "protected_access_url" {
22
+ value = var.protected_access_mode == "cloudfront_default_domain" ? "https://${aws_cloudfront_distribution.open_uptime[0].domain_name}" : "https://${var.hostname}"
23
+ }
24
+
17
25
  output "evidence_bucket" {
18
26
  value = aws_s3_bucket.evidence.bucket
19
27
  }
@@ -1,23 +1,30 @@
1
1
  region = "us-east-1"
2
2
  stage = "prod"
3
3
  service_name = "open-uptime"
4
+ project_name = "open-uptime"
5
+ owner = "hasna"
6
+ app_type = "opensource"
7
+ environment = "prod"
8
+ cost_center = "opensource"
4
9
  hostname = "uptime.example.com"
5
10
  workspace_id = "workspace-id"
6
11
  vpc_id = "vpc-xxxxxxxx"
7
12
  ecr_repository_name = "open-uptime"
13
+ protected_access_mode = "cloudfront_default_domain"
8
14
  public_subnet_ids = ["subnet-replace-public-a", "subnet-replace-public-b"]
9
15
  alb_ingress_cidr_blocks = []
10
16
  private_subnet_ids = ["subnet-replace-private-a", "subnet-replace-private-b"]
11
17
  container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/open-uptime@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
12
- runtime_package_version = "0.1.7"
13
- certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/replace"
14
- hosted_zone_id = "ZREPLACE"
18
+ runtime_package_version = "0.1.9"
19
+ certificate_arn = null
20
+ hosted_zone_id = null
15
21
  app_env_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/app/env"
16
22
  hosted_token_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/hosted-token"
17
23
  public_probe_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/probe/public"
18
24
  reporting_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/reporting"
19
25
  kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/00000000-0000-0000-0000-000000000000"
20
26
  alarm_actions = []
27
+ monthly_budget_limit_usd = 0
21
28
 
22
29
  desired_counts = {
23
30
  web = 0
@@ -26,3 +33,5 @@ desired_counts = {
26
33
  reporter = 0
27
34
  migration = 0
28
35
  }
36
+
37
+ budget_alert_email_addresses = []
@@ -22,6 +22,36 @@ variable "service_name" {
22
22
  default = "open-uptime"
23
23
  }
24
24
 
25
+ variable "project_name" {
26
+ description = "Project tag value for cost allocation."
27
+ type = string
28
+ default = "open-uptime"
29
+ }
30
+
31
+ variable "owner" {
32
+ description = "Owner tag value for cost allocation and operations."
33
+ type = string
34
+ default = "hasna"
35
+ }
36
+
37
+ variable "app_type" {
38
+ description = "AppType tag value."
39
+ type = string
40
+ default = "opensource"
41
+ }
42
+
43
+ variable "environment" {
44
+ description = "Environment tag value."
45
+ type = string
46
+ default = "prod"
47
+ }
48
+
49
+ variable "cost_center" {
50
+ description = "CostCenter tag value."
51
+ type = string
52
+ default = "opensource"
53
+ }
54
+
25
55
  variable "hostname" {
26
56
  description = "Public/internal hostname for Open Uptime."
27
57
  type = string
@@ -46,13 +76,24 @@ variable "ecr_repository_name" {
46
76
  default = "open-uptime"
47
77
  }
48
78
 
79
+ variable "protected_access_mode" {
80
+ description = "Protected web access mode. cloudfront_default_domain uses the CloudFront HTTPS default domain and restricts ALB HTTP to CloudFront origin-facing ranges. alb_https_cert uses an ALB HTTPS listener with certificate_arn."
81
+ type = string
82
+ default = "cloudfront_default_domain"
83
+
84
+ validation {
85
+ condition = contains(["cloudfront_default_domain", "alb_https_cert"], var.protected_access_mode)
86
+ error_message = "protected_access_mode must be cloudfront_default_domain or alb_https_cert."
87
+ }
88
+ }
89
+
49
90
  variable "public_subnet_ids" {
50
91
  description = "Public subnets for the ALB."
51
92
  type = list(string)
52
93
  }
53
94
 
54
95
  variable "alb_ingress_cidr_blocks" {
55
- description = "Approved HTTPS source CIDR blocks for the ALB. Keep empty until edge/source policy is approved."
96
+ description = "Approved HTTPS source CIDR blocks for ALB HTTPS mode. Keep empty until edge/source policy is approved."
56
97
  type = list(string)
57
98
  default = []
58
99
  }
@@ -75,7 +116,7 @@ variable "container_image" {
75
116
  variable "runtime_package_version" {
76
117
  description = "Published @hasna/uptime package version that CodeBuild should build into the ECR image."
77
118
  type = string
78
- default = "0.1.7"
119
+ default = "0.1.9"
79
120
 
80
121
  validation {
81
122
  condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?$", var.runtime_package_version))
@@ -84,8 +125,19 @@ variable "runtime_package_version" {
84
125
  }
85
126
 
86
127
  variable "certificate_arn" {
87
- description = "ACM certificate ARN for HTTPS listener."
128
+ description = "ACM certificate ARN for ALB HTTPS mode. Leave null when protected_access_mode is cloudfront_default_domain."
88
129
  type = string
130
+ default = null
131
+
132
+ validation {
133
+ condition = var.certificate_arn == null || can(regex("^arn:aws:acm:", var.certificate_arn))
134
+ error_message = "certificate_arn must be null or an ACM certificate ARN."
135
+ }
136
+
137
+ validation {
138
+ condition = var.protected_access_mode != "alb_https_cert" || var.certificate_arn != null
139
+ error_message = "certificate_arn is required when protected_access_mode is alb_https_cert."
140
+ }
89
141
  }
90
142
 
91
143
  variable "hosted_zone_id" {
@@ -168,3 +220,20 @@ variable "alarm_actions" {
168
220
  type = list(string)
169
221
  default = []
170
222
  }
223
+
224
+ variable "monthly_budget_limit_usd" {
225
+ description = "Optional monthly AWS Budgets limit in USD. Set with budget_alert_email_addresses to create a budget alert."
226
+ type = number
227
+ default = 0
228
+
229
+ validation {
230
+ condition = var.monthly_budget_limit_usd >= 0
231
+ error_message = "monthly_budget_limit_usd must be non-negative."
232
+ }
233
+ }
234
+
235
+ variable "budget_alert_email_addresses" {
236
+ description = "Email recipients for AWS Budgets forecasted and actual alerts. Leave empty to skip budget creation."
237
+ type = list(string)
238
+ default = []
239
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/uptime",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Local-first uptime and downtime monitoring service with CLI, MCP, SDK, SQLite persistence, and a dashboard.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",