@hasna/uptime 0.1.6 → 0.1.7

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
@@ -13,40 +13,41 @@ provider "aws" {
13
13
  region = var.region
14
14
  }
15
15
 
16
+ data "aws_caller_identity" "current" {}
17
+
16
18
  locals {
17
- prefix = "${var.service_name}-${var.stage}"
18
- container_port = 3899
19
- evidence_bucket = "hasna-${var.stage}-${var.service_name}-evidence"
19
+ prefix = "${var.service_name}-${var.stage}"
20
+ container_port = 3899
21
+ evidence_bucket = "hasna-${var.stage}-${var.service_name}-evidence"
22
+ efs_uid = 10001
23
+ efs_gid = 10001
24
+ hosted_sqlite_db_path = "/data/uptime/uptime.db"
25
+ efs_enabled_services = toset(["web"])
20
26
  services = {
21
27
  web = {
22
28
  desired_count = lookup(var.desired_counts, "web", 0)
23
- db_access = true
24
29
  command = ["bun", "dist/cli/index.js", "serve", "--mode", "hosted", "--host", "0.0.0.0", "--port", tostring(local.container_port)]
25
- secrets = { HASNA_UPTIME_DATABASE_URL = var.database_secret_arn, APP_ENV = var.app_env_secret_arn, HASNA_UPTIME_HOSTED_TOKEN = var.hosted_token_secret_arn }
30
+ secrets = { APP_ENV = var.app_env_secret_arn, HASNA_UPTIME_HOSTED_TOKEN = var.hosted_token_secret_arn }
26
31
  }
27
32
  scheduler = {
28
33
  desired_count = lookup(var.desired_counts, "scheduler", 0)
29
- db_access = true
30
34
  command = ["bun", "dist/cli/index.js", "cloud", "plan"]
31
- secrets = { HASNA_UPTIME_DATABASE_URL = var.database_secret_arn, APP_ENV = var.app_env_secret_arn }
35
+ secrets = { APP_ENV = var.app_env_secret_arn }
32
36
  }
33
37
  "public-probe" = {
34
38
  desired_count = lookup(var.desired_counts, "public-probe", 0)
35
- db_access = false
36
39
  command = ["bun", "dist/cli/index.js", "cloud", "plan"]
37
40
  secrets = { PROBE_CONFIG = var.public_probe_secret_arn }
38
41
  }
39
42
  reporter = {
40
43
  desired_count = lookup(var.desired_counts, "reporter", 0)
41
- db_access = true
42
44
  command = ["bun", "dist/cli/index.js", "cloud", "plan"]
43
- secrets = { HASNA_UPTIME_DATABASE_URL = var.database_secret_arn, REPORTING_CONFIG = var.reporting_secret_arn }
45
+ secrets = { REPORTING_CONFIG = var.reporting_secret_arn }
44
46
  }
45
47
  migration = {
46
48
  desired_count = lookup(var.desired_counts, "migration", 0)
47
- db_access = true
48
49
  command = ["bun", "dist/cli/index.js", "cloud", "plan"]
49
- secrets = { HASNA_UPTIME_DATABASE_URL = var.database_secret_arn, APP_ENV = var.app_env_secret_arn }
50
+ secrets = { APP_ENV = var.app_env_secret_arn }
50
51
  }
51
52
  }
52
53
  tags = {
@@ -62,7 +63,7 @@ data "aws_vpc" "target" {
62
63
  }
63
64
 
64
65
  resource "aws_ecr_repository" "open_uptime" {
65
- name = "hasna/opensource/${var.service_name}"
66
+ name = var.ecr_repository_name
66
67
  image_tag_mutability = "IMMUTABLE"
67
68
 
68
69
  image_scanning_configuration {
@@ -76,6 +77,113 @@ resource "aws_ecr_repository" "open_uptime" {
76
77
  tags = local.tags
77
78
  }
78
79
 
80
+ resource "aws_cloudwatch_log_group" "image_builder" {
81
+ name = "/aws/codebuild/${local.prefix}-image-builder"
82
+ retention_in_days = 14
83
+ kms_key_id = var.kms_key_arn
84
+ tags = local.tags
85
+ }
86
+
87
+ data "aws_iam_policy_document" "codebuild_assume_role" {
88
+ statement {
89
+ actions = ["sts:AssumeRole"]
90
+
91
+ principals {
92
+ type = "Service"
93
+ identifiers = ["codebuild.amazonaws.com"]
94
+ }
95
+ }
96
+ }
97
+
98
+ resource "aws_iam_role" "image_builder" {
99
+ name = "${local.prefix}-image-builder-role"
100
+ assume_role_policy = data.aws_iam_policy_document.codebuild_assume_role.json
101
+ tags = local.tags
102
+ }
103
+
104
+ data "aws_iam_policy_document" "image_builder" {
105
+ statement {
106
+ actions = ["ecr:GetAuthorizationToken"]
107
+ resources = ["*"]
108
+ }
109
+
110
+ statement {
111
+ actions = [
112
+ "ecr:BatchCheckLayerAvailability",
113
+ "ecr:CompleteLayerUpload",
114
+ "ecr:DescribeImages",
115
+ "ecr:DescribeRepositories",
116
+ "ecr:InitiateLayerUpload",
117
+ "ecr:PutImage",
118
+ "ecr:UploadLayerPart",
119
+ ]
120
+ resources = [aws_ecr_repository.open_uptime.arn]
121
+ }
122
+
123
+ statement {
124
+ actions = [
125
+ "logs:CreateLogStream",
126
+ "logs:PutLogEvents",
127
+ ]
128
+ resources = ["${aws_cloudwatch_log_group.image_builder.arn}:*"]
129
+ }
130
+ }
131
+
132
+ resource "aws_iam_role_policy" "image_builder" {
133
+ name = "${local.prefix}-image-builder-policy"
134
+ role = aws_iam_role.image_builder.id
135
+ policy = data.aws_iam_policy_document.image_builder.json
136
+ }
137
+
138
+ resource "aws_codebuild_project" "image_builder" {
139
+ name = "${local.prefix}-image-builder"
140
+ description = "Build published @hasna/uptime package into the Open Uptime ECR image"
141
+ service_role = aws_iam_role.image_builder.arn
142
+ tags = local.tags
143
+
144
+ artifacts {
145
+ type = "NO_ARTIFACTS"
146
+ }
147
+
148
+ environment {
149
+ compute_type = "BUILD_GENERAL1_SMALL"
150
+ image = "aws/codebuild/standard:7.0"
151
+ type = "LINUX_CONTAINER"
152
+ privileged_mode = true
153
+ }
154
+
155
+ logs_config {
156
+ cloudwatch_logs {
157
+ group_name = aws_cloudwatch_log_group.image_builder.name
158
+ status = "ENABLED"
159
+ }
160
+ }
161
+
162
+ source {
163
+ type = "NO_SOURCE"
164
+ buildspec = <<-YAML
165
+ version: 0.2
166
+ phases:
167
+ pre_build:
168
+ commands:
169
+ - aws --version
170
+ - aws ecr get-login-password --region ${var.region} | docker login --username AWS --password-stdin ${data.aws_caller_identity.current.account_id}.dkr.ecr.${var.region}.amazonaws.com
171
+ build:
172
+ commands:
173
+ - npm pack @hasna/uptime@${var.runtime_package_version}
174
+ - mkdir package
175
+ - tar -xzf hasna-uptime-*.tgz -C package --strip-components=1
176
+ - cd package
177
+ - docker build -f Dockerfile.package -t ${aws_ecr_repository.open_uptime.repository_url}:${var.runtime_package_version} .
178
+ - docker push ${aws_ecr_repository.open_uptime.repository_url}:${var.runtime_package_version}
179
+ - IMAGE_DIGEST=$(aws ecr describe-images --region ${var.region} --repository-name ${aws_ecr_repository.open_uptime.name} --image-ids imageTag=${var.runtime_package_version} --query 'imageDetails[0].imageDigest' --output text)
180
+ - printf '%s@%s\n' '${aws_ecr_repository.open_uptime.repository_url}' "$IMAGE_DIGEST"
181
+ YAML
182
+ }
183
+
184
+ depends_on = [aws_iam_role_policy.image_builder]
185
+ }
186
+
79
187
  resource "aws_s3_bucket" "evidence" {
80
188
  bucket = local.evidence_bucket
81
189
  tags = local.tags
@@ -221,7 +329,7 @@ resource "aws_security_group_rule" "web_from_alb" {
221
329
 
222
330
  resource "aws_security_group_rule" "web_egress" {
223
331
  type = "egress"
224
- description = "Controlled egress to AWS endpoints and database"
332
+ description = "Controlled egress to AWS endpoints and EFS"
225
333
  security_group_id = aws_security_group.web.id
226
334
  from_port = 0
227
335
  to_port = 0
@@ -244,7 +352,7 @@ resource "aws_security_group_rule" "worker_egress" {
244
352
  for_each = aws_security_group.worker
245
353
 
246
354
  type = "egress"
247
- description = each.key == "public-probe" ? "Public probe egress for approved public targets" : "Controlled egress to AWS endpoints and database"
355
+ description = each.key == "public-probe" ? "Public probe egress for approved public targets" : "Controlled egress to AWS endpoints"
248
356
  security_group_id = each.value.id
249
357
  from_port = 0
250
358
  to_port = 0
@@ -252,28 +360,124 @@ resource "aws_security_group_rule" "worker_egress" {
252
360
  cidr_blocks = each.key == "public-probe" ? ["0.0.0.0/0"] : [data.aws_vpc.target.cidr_block]
253
361
  }
254
362
 
255
- resource "aws_security_group_rule" "rds_from_web" {
363
+ resource "aws_security_group" "efs" {
364
+ name = "${local.prefix}-efs-sg"
365
+ description = "Open Uptime EFS data store"
366
+ vpc_id = data.aws_vpc.target.id
367
+ tags = local.tags
368
+ }
369
+
370
+ resource "aws_security_group_rule" "efs_from_web" {
256
371
  type = "ingress"
257
- from_port = 5432
258
- to_port = 5432
372
+ description = "Open Uptime web to EFS"
373
+ security_group_id = aws_security_group.efs.id
374
+ from_port = 2049
375
+ to_port = 2049
259
376
  protocol = "tcp"
260
- security_group_id = var.rds_security_group_id
261
377
  source_security_group_id = aws_security_group.web.id
262
- description = "Open Uptime web to RDS"
263
378
  }
264
379
 
265
- resource "aws_security_group_rule" "rds_from_workers" {
266
- for_each = {
267
- for key, value in local.services : key => value if key != "web" && value.db_access
380
+ resource "aws_efs_file_system" "data" {
381
+ creation_token = "${local.prefix}-data"
382
+ encrypted = true
383
+ kms_key_id = var.kms_key_arn
384
+ tags = merge(local.tags, { Name = "${local.prefix}-data" })
385
+
386
+ lifecycle_policy {
387
+ transition_to_ia = "AFTER_30_DAYS"
268
388
  }
389
+ }
269
390
 
270
- type = "ingress"
271
- from_port = 5432
272
- to_port = 5432
273
- protocol = "tcp"
274
- security_group_id = var.rds_security_group_id
275
- source_security_group_id = aws_security_group.worker[each.key].id
276
- description = "Open Uptime ${each.key} to RDS"
391
+ resource "aws_efs_backup_policy" "data" {
392
+ file_system_id = aws_efs_file_system.data.id
393
+
394
+ backup_policy {
395
+ status = "ENABLED"
396
+ }
397
+ }
398
+
399
+ resource "aws_efs_access_point" "uptime" {
400
+ file_system_id = aws_efs_file_system.data.id
401
+
402
+ posix_user {
403
+ uid = local.efs_uid
404
+ gid = local.efs_gid
405
+ }
406
+
407
+ root_directory {
408
+ path = "/uptime"
409
+
410
+ creation_info {
411
+ owner_uid = local.efs_uid
412
+ owner_gid = local.efs_gid
413
+ permissions = "0750"
414
+ }
415
+ }
416
+
417
+ tags = merge(local.tags, { Name = "${local.prefix}-uptime" })
418
+ }
419
+
420
+ resource "aws_efs_mount_target" "data" {
421
+ for_each = toset(var.private_subnet_ids)
422
+ file_system_id = aws_efs_file_system.data.id
423
+ subnet_id = each.value
424
+ security_groups = [aws_security_group.efs.id]
425
+ }
426
+
427
+ resource "aws_backup_vault" "data" {
428
+ name = "${local.prefix}-data"
429
+ kms_key_arn = var.kms_key_arn
430
+ tags = local.tags
431
+ }
432
+
433
+ resource "aws_backup_plan" "data" {
434
+ name = "${local.prefix}-data"
435
+
436
+ rule {
437
+ rule_name = "daily"
438
+ target_vault_name = aws_backup_vault.data.name
439
+ schedule = "cron(0 5 * * ? *)"
440
+
441
+ lifecycle {
442
+ delete_after = 35
443
+ }
444
+ }
445
+
446
+ tags = local.tags
447
+ }
448
+
449
+ data "aws_iam_policy_document" "backup_assume_role" {
450
+ statement {
451
+ actions = ["sts:AssumeRole"]
452
+
453
+ principals {
454
+ type = "Service"
455
+ identifiers = ["backup.amazonaws.com"]
456
+ }
457
+ }
458
+ }
459
+
460
+ resource "aws_iam_role" "backup" {
461
+ name = "${local.prefix}-backup-role"
462
+ assume_role_policy = data.aws_iam_policy_document.backup_assume_role.json
463
+ tags = local.tags
464
+ }
465
+
466
+ resource "aws_iam_role_policy_attachment" "backup" {
467
+ role = aws_iam_role.backup.name
468
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup"
469
+ }
470
+
471
+ resource "aws_iam_role_policy_attachment" "backup_restore" {
472
+ role = aws_iam_role.backup.name
473
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores"
474
+ }
475
+
476
+ resource "aws_backup_selection" "data" {
477
+ iam_role_arn = aws_iam_role.backup.arn
478
+ name = "${local.prefix}-data"
479
+ plan_id = aws_backup_plan.data.id
480
+ resources = [aws_efs_file_system.data.arn]
277
481
  }
278
482
 
279
483
  resource "aws_lb" "open_uptime" {
@@ -395,6 +599,24 @@ data "aws_iam_policy_document" "task" {
395
599
  actions = ["kms:Decrypt", "kms:GenerateDataKey"]
396
600
  resources = [var.kms_key_arn]
397
601
  }
602
+
603
+ dynamic "statement" {
604
+ for_each = contains(local.efs_enabled_services, each.key) ? [1] : []
605
+
606
+ content {
607
+ actions = [
608
+ "elasticfilesystem:ClientMount",
609
+ "elasticfilesystem:ClientWrite",
610
+ ]
611
+ resources = [aws_efs_file_system.data.arn]
612
+
613
+ condition {
614
+ test = "StringEquals"
615
+ variable = "elasticfilesystem:AccessPointArn"
616
+ values = [aws_efs_access_point.uptime.arn]
617
+ }
618
+ }
619
+ }
398
620
  }
399
621
 
400
622
  resource "aws_iam_role_policy" "task" {
@@ -414,6 +636,24 @@ resource "aws_ecs_task_definition" "service" {
414
636
  execution_role_arn = aws_iam_role.execution.arn
415
637
  task_role_arn = aws_iam_role.task[each.key].arn
416
638
 
639
+ dynamic "volume" {
640
+ for_each = contains(local.efs_enabled_services, each.key) ? [1] : []
641
+
642
+ content {
643
+ name = "uptime-data"
644
+
645
+ efs_volume_configuration {
646
+ file_system_id = aws_efs_file_system.data.id
647
+ transit_encryption = "ENABLED"
648
+
649
+ authorization_config {
650
+ access_point_id = aws_efs_access_point.uptime.id
651
+ iam = "ENABLED"
652
+ }
653
+ }
654
+ }
655
+ }
656
+
417
657
  container_definitions = jsonencode([
418
658
  {
419
659
  name = each.key
@@ -425,12 +665,21 @@ resource "aws_ecs_task_definition" "service" {
425
665
  hostPort = local.container_port
426
666
  protocol = "tcp"
427
667
  }] : []
428
- environment = [
668
+ environment = concat([
429
669
  { name = "HASNA_UPTIME_MODE", value = "hosted" },
430
670
  { name = "HASNA_UPTIME_WORKSPACE_ID", value = var.workspace_id },
431
671
  { name = "HASNA_UPTIME_COMPONENT", value = each.key },
432
672
  { name = "HASNA_UPTIME_HOSTNAME", value = var.hostname },
433
- ]
673
+ ], contains(local.efs_enabled_services, each.key) ? [
674
+ { name = "HASNA_UPTIME_HOSTED_SQLITE_DB", value = local.hosted_sqlite_db_path },
675
+ ] : [])
676
+ mountPoints = contains(local.efs_enabled_services, each.key) ? [
677
+ {
678
+ sourceVolume = "uptime-data"
679
+ containerPath = "/data/uptime"
680
+ readOnly = false
681
+ }
682
+ ] : []
434
683
  secrets = [
435
684
  for name, value_from in each.value.secrets : {
436
685
  name = name
@@ -476,7 +725,7 @@ resource "aws_ecs_service" "web" {
476
725
  container_port = local.container_port
477
726
  }
478
727
 
479
- depends_on = [aws_lb_listener.https]
728
+ depends_on = [aws_lb_listener.https, aws_efs_mount_target.data]
480
729
  }
481
730
 
482
731
  resource "aws_ecs_service" "worker" {
@@ -2,6 +2,10 @@ output "ecr_repository_url" {
2
2
  value = aws_ecr_repository.open_uptime.repository_url
3
3
  }
4
4
 
5
+ output "image_builder_project_name" {
6
+ value = aws_codebuild_project.image_builder.name
7
+ }
8
+
5
9
  output "ecs_cluster_name" {
6
10
  value = aws_ecs_cluster.open_uptime.name
7
11
  }
@@ -14,6 +18,14 @@ output "evidence_bucket" {
14
18
  value = aws_s3_bucket.evidence.bucket
15
19
  }
16
20
 
21
+ output "efs_file_system_id" {
22
+ value = aws_efs_file_system.data.id
23
+ }
24
+
25
+ output "efs_access_point_id" {
26
+ value = aws_efs_access_point.uptime.id
27
+ }
28
+
17
29
  output "service_names" {
18
30
  value = concat(
19
31
  [aws_ecs_service.web.name],
@@ -1,21 +1,21 @@
1
1
  region = "us-east-1"
2
2
  stage = "prod"
3
3
  service_name = "open-uptime"
4
- hostname = "uptime.hasna.xyz"
5
- workspace_id = "wks_2tyysw05cwap"
6
- vpc_id = "vpc-04c7f7abc1d3c3f56"
4
+ hostname = "uptime.example.com"
5
+ workspace_id = "workspace-id"
6
+ vpc_id = "vpc-xxxxxxxx"
7
+ ecr_repository_name = "open-uptime"
7
8
  public_subnet_ids = ["subnet-replace-public-a", "subnet-replace-public-b"]
8
9
  alb_ingress_cidr_blocks = []
9
10
  private_subnet_ids = ["subnet-replace-private-a", "subnet-replace-private-b"]
10
- rds_security_group_id = "sg-replace-rds"
11
- container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/hasna/opensource/open-uptime@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
11
+ container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/open-uptime@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
12
+ runtime_package_version = "0.1.7"
12
13
  certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/replace"
13
14
  hosted_zone_id = "ZREPLACE"
14
- database_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:hasna/xyz/opensource/uptime/prod/rds"
15
- app_env_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:hasna/xyz/opensource/uptime/prod/app/env"
16
- hosted_token_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:hasna/xyz/opensource/uptime/prod/hosted-token"
17
- public_probe_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:hasna/xyz/opensource/uptime/prod/probe/public"
18
- reporting_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:hasna/xyz/opensource/uptime/prod/reporting"
15
+ app_env_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/app/env"
16
+ hosted_token_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/hosted-token"
17
+ public_probe_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/probe/public"
18
+ reporting_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/reporting"
19
19
  kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/00000000-0000-0000-0000-000000000000"
20
20
  alarm_actions = []
21
21
 
@@ -1,7 +1,7 @@
1
1
  variable "account_name" {
2
2
  description = "Human-readable AWS account/profile label."
3
3
  type = string
4
- default = "hasna-xyz-infra"
4
+ default = "aws-profile"
5
5
  }
6
6
 
7
7
  variable "region" {
@@ -25,19 +25,25 @@ variable "service_name" {
25
25
  variable "hostname" {
26
26
  description = "Public/internal hostname for Open Uptime."
27
27
  type = string
28
- default = "uptime.hasna.xyz"
28
+ default = "uptime.example.com"
29
29
  }
30
30
 
31
31
  variable "workspace_id" {
32
32
  description = "Hosted Open Uptime workspace id."
33
33
  type = string
34
- default = "wks_2tyysw05cwap"
34
+ default = "workspace-id"
35
35
  }
36
36
 
37
37
  variable "vpc_id" {
38
38
  description = "Existing VPC id."
39
39
  type = string
40
- default = "vpc-04c7f7abc1d3c3f56"
40
+ default = "vpc-xxxxxxxx"
41
+ }
42
+
43
+ variable "ecr_repository_name" {
44
+ description = "ECR repository name for the Open Uptime image."
45
+ type = string
46
+ default = "open-uptime"
41
47
  }
42
48
 
43
49
  variable "public_subnet_ids" {
@@ -56,11 +62,6 @@ variable "private_subnet_ids" {
56
62
  type = list(string)
57
63
  }
58
64
 
59
- variable "rds_security_group_id" {
60
- description = "Existing RDS security group id that should allow Open Uptime client access."
61
- type = string
62
- }
63
-
64
65
  variable "container_image" {
65
66
  description = "Immutable Open Uptime image URI, preferably with digest."
66
67
  type = string
@@ -71,6 +72,17 @@ variable "container_image" {
71
72
  }
72
73
  }
73
74
 
75
+ variable "runtime_package_version" {
76
+ description = "Published @hasna/uptime package version that CodeBuild should build into the ECR image."
77
+ type = string
78
+ default = "0.1.7"
79
+
80
+ validation {
81
+ condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?$", var.runtime_package_version))
82
+ error_message = "runtime_package_version must be a semver version without the package name."
83
+ }
84
+ }
85
+
74
86
  variable "certificate_arn" {
75
87
  description = "ACM certificate ARN for HTTPS listener."
76
88
  type = string
@@ -82,16 +94,6 @@ variable "hosted_zone_id" {
82
94
  default = null
83
95
  }
84
96
 
85
- variable "database_secret_arn" {
86
- description = "Secrets Manager/SSM ARN containing DATABASE_URL."
87
- type = string
88
-
89
- validation {
90
- condition = can(regex("^arn:aws:(secretsmanager|ssm):", var.database_secret_arn))
91
- error_message = "database_secret_arn must be a Secrets Manager or SSM ARN, not a plaintext database URL."
92
- }
93
- }
94
-
95
97
  variable "app_env_secret_arn" {
96
98
  description = "Secrets Manager/SSM ARN containing hosted app environment refs."
97
99
  type = string
@@ -154,8 +156,10 @@ variable "desired_counts" {
154
156
  }
155
157
 
156
158
  validation {
157
- condition = alltrue([for count in values(var.desired_counts) : count >= 0])
158
- error_message = "desired_counts values must be non-negative."
159
+ condition = alltrue([for count in values(var.desired_counts) : count >= 0]) && lookup(var.desired_counts, "web", 0) <= 1 && alltrue([
160
+ for key in ["scheduler", "public-probe", "reporter", "migration"] : lookup(var.desired_counts, key, 0) == 0
161
+ ])
162
+ error_message = "EFS SQLite bridge requires web desired count 0 or 1 and scheduler/public-probe/reporter/migration desired counts 0."
159
163
  }
160
164
  }
161
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/uptime",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",
@@ -24,6 +24,7 @@
24
24
  "dist",
25
25
  "README.md",
26
26
  "Dockerfile",
27
+ "Dockerfile.package",
27
28
  ".dockerignore",
28
29
  "infra/aws/README.md",
29
30
  "infra/aws/.terraform.lock.hcl",