@hasna/uptime 0.1.5 → 0.1.6

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.
@@ -0,0 +1,546 @@
1
+ terraform {
2
+ required_version = ">= 1.6.0"
3
+
4
+ required_providers {
5
+ aws = {
6
+ source = "hashicorp/aws"
7
+ version = "~> 5.0"
8
+ }
9
+ }
10
+ }
11
+
12
+ provider "aws" {
13
+ region = var.region
14
+ }
15
+
16
+ locals {
17
+ prefix = "${var.service_name}-${var.stage}"
18
+ container_port = 3899
19
+ evidence_bucket = "hasna-${var.stage}-${var.service_name}-evidence"
20
+ services = {
21
+ web = {
22
+ desired_count = lookup(var.desired_counts, "web", 0)
23
+ db_access = true
24
+ 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 }
26
+ }
27
+ scheduler = {
28
+ desired_count = lookup(var.desired_counts, "scheduler", 0)
29
+ db_access = true
30
+ 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 }
32
+ }
33
+ "public-probe" = {
34
+ desired_count = lookup(var.desired_counts, "public-probe", 0)
35
+ db_access = false
36
+ command = ["bun", "dist/cli/index.js", "cloud", "plan"]
37
+ secrets = { PROBE_CONFIG = var.public_probe_secret_arn }
38
+ }
39
+ reporter = {
40
+ desired_count = lookup(var.desired_counts, "reporter", 0)
41
+ db_access = true
42
+ command = ["bun", "dist/cli/index.js", "cloud", "plan"]
43
+ secrets = { HASNA_UPTIME_DATABASE_URL = var.database_secret_arn, REPORTING_CONFIG = var.reporting_secret_arn }
44
+ }
45
+ migration = {
46
+ desired_count = lookup(var.desired_counts, "migration", 0)
47
+ db_access = true
48
+ 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
+ }
51
+ }
52
+ tags = {
53
+ ManagedBy = "terraform"
54
+ Service = var.service_name
55
+ Stage = var.stage
56
+ Account = var.account_name
57
+ }
58
+ }
59
+
60
+ data "aws_vpc" "target" {
61
+ id = var.vpc_id
62
+ }
63
+
64
+ resource "aws_ecr_repository" "open_uptime" {
65
+ name = "hasna/opensource/${var.service_name}"
66
+ image_tag_mutability = "IMMUTABLE"
67
+
68
+ image_scanning_configuration {
69
+ scan_on_push = true
70
+ }
71
+
72
+ encryption_configuration {
73
+ encryption_type = "AES256"
74
+ }
75
+
76
+ tags = local.tags
77
+ }
78
+
79
+ resource "aws_s3_bucket" "evidence" {
80
+ bucket = local.evidence_bucket
81
+ tags = local.tags
82
+ }
83
+
84
+ resource "aws_s3_bucket_public_access_block" "evidence" {
85
+ bucket = aws_s3_bucket.evidence.id
86
+ block_public_acls = true
87
+ block_public_policy = true
88
+ ignore_public_acls = true
89
+ restrict_public_buckets = true
90
+ }
91
+
92
+ resource "aws_s3_bucket_versioning" "evidence" {
93
+ bucket = aws_s3_bucket.evidence.id
94
+
95
+ versioning_configuration {
96
+ status = "Enabled"
97
+ }
98
+ }
99
+
100
+ resource "aws_s3_bucket_server_side_encryption_configuration" "evidence" {
101
+ bucket = aws_s3_bucket.evidence.id
102
+
103
+ rule {
104
+ apply_server_side_encryption_by_default {
105
+ kms_master_key_id = var.kms_key_arn
106
+ sse_algorithm = "aws:kms"
107
+ }
108
+ }
109
+ }
110
+
111
+ resource "aws_s3_bucket_lifecycle_configuration" "evidence" {
112
+ bucket = aws_s3_bucket.evidence.id
113
+
114
+ rule {
115
+ id = "evidence-retention"
116
+ status = "Enabled"
117
+
118
+ filter {
119
+ prefix = ""
120
+ }
121
+
122
+ abort_incomplete_multipart_upload {
123
+ days_after_initiation = 7
124
+ }
125
+
126
+ noncurrent_version_expiration {
127
+ noncurrent_days = 30
128
+ }
129
+
130
+ expiration {
131
+ days = 365
132
+ }
133
+ }
134
+ }
135
+
136
+ data "aws_iam_policy_document" "evidence_bucket" {
137
+ statement {
138
+ sid = "DenyInsecureTransport"
139
+ effect = "Deny"
140
+ actions = ["s3:*"]
141
+ resources = [
142
+ aws_s3_bucket.evidence.arn,
143
+ "${aws_s3_bucket.evidence.arn}/*",
144
+ ]
145
+
146
+ principals {
147
+ type = "*"
148
+ identifiers = ["*"]
149
+ }
150
+
151
+ condition {
152
+ test = "Bool"
153
+ variable = "aws:SecureTransport"
154
+ values = ["false"]
155
+ }
156
+ }
157
+ }
158
+
159
+ resource "aws_s3_bucket_policy" "evidence" {
160
+ bucket = aws_s3_bucket.evidence.id
161
+ policy = data.aws_iam_policy_document.evidence_bucket.json
162
+ }
163
+
164
+ resource "aws_cloudwatch_log_group" "service" {
165
+ for_each = local.services
166
+ name = "/ecs/${local.prefix}-${each.key}"
167
+ retention_in_days = 30
168
+ kms_key_id = var.kms_key_arn
169
+ tags = local.tags
170
+ }
171
+
172
+ resource "aws_ecs_cluster" "open_uptime" {
173
+ name = local.prefix
174
+ tags = local.tags
175
+ }
176
+
177
+ resource "aws_security_group" "alb" {
178
+ name = "${local.prefix}-alb-sg"
179
+ description = "Open Uptime ALB ingress"
180
+ vpc_id = data.aws_vpc.target.id
181
+ tags = local.tags
182
+ }
183
+
184
+ resource "aws_security_group_rule" "alb_https_ingress" {
185
+ count = length(var.alb_ingress_cidr_blocks) > 0 ? 1 : 0
186
+ type = "ingress"
187
+ description = "HTTPS"
188
+ security_group_id = aws_security_group.alb.id
189
+ from_port = 443
190
+ to_port = 443
191
+ protocol = "tcp"
192
+ cidr_blocks = var.alb_ingress_cidr_blocks
193
+ }
194
+
195
+ resource "aws_security_group_rule" "alb_to_web" {
196
+ type = "egress"
197
+ description = "To Open Uptime web"
198
+ security_group_id = aws_security_group.alb.id
199
+ from_port = local.container_port
200
+ to_port = local.container_port
201
+ protocol = "tcp"
202
+ source_security_group_id = aws_security_group.web.id
203
+ }
204
+
205
+ resource "aws_security_group" "web" {
206
+ name = "${local.prefix}-web-sg"
207
+ description = "Open Uptime web tasks"
208
+ vpc_id = data.aws_vpc.target.id
209
+ tags = local.tags
210
+ }
211
+
212
+ resource "aws_security_group_rule" "web_from_alb" {
213
+ type = "ingress"
214
+ description = "From ALB"
215
+ security_group_id = aws_security_group.web.id
216
+ from_port = local.container_port
217
+ to_port = local.container_port
218
+ protocol = "tcp"
219
+ source_security_group_id = aws_security_group.alb.id
220
+ }
221
+
222
+ resource "aws_security_group_rule" "web_egress" {
223
+ type = "egress"
224
+ description = "Controlled egress to AWS endpoints and database"
225
+ security_group_id = aws_security_group.web.id
226
+ from_port = 0
227
+ to_port = 0
228
+ protocol = "-1"
229
+ cidr_blocks = [data.aws_vpc.target.cidr_block]
230
+ }
231
+
232
+ resource "aws_security_group" "worker" {
233
+ for_each = {
234
+ for key, value in local.services : key => value if key != "web"
235
+ }
236
+
237
+ name = "${local.prefix}-${each.key}-sg"
238
+ description = "Open Uptime ${each.key} tasks"
239
+ vpc_id = data.aws_vpc.target.id
240
+ tags = local.tags
241
+ }
242
+
243
+ resource "aws_security_group_rule" "worker_egress" {
244
+ for_each = aws_security_group.worker
245
+
246
+ type = "egress"
247
+ description = each.key == "public-probe" ? "Public probe egress for approved public targets" : "Controlled egress to AWS endpoints and database"
248
+ security_group_id = each.value.id
249
+ from_port = 0
250
+ to_port = 0
251
+ protocol = "-1"
252
+ cidr_blocks = each.key == "public-probe" ? ["0.0.0.0/0"] : [data.aws_vpc.target.cidr_block]
253
+ }
254
+
255
+ resource "aws_security_group_rule" "rds_from_web" {
256
+ type = "ingress"
257
+ from_port = 5432
258
+ to_port = 5432
259
+ protocol = "tcp"
260
+ security_group_id = var.rds_security_group_id
261
+ source_security_group_id = aws_security_group.web.id
262
+ description = "Open Uptime web to RDS"
263
+ }
264
+
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
268
+ }
269
+
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"
277
+ }
278
+
279
+ resource "aws_lb" "open_uptime" {
280
+ name = "${local.prefix}-alb"
281
+ internal = false
282
+ load_balancer_type = "application"
283
+ security_groups = [aws_security_group.alb.id]
284
+ subnets = var.public_subnet_ids
285
+ tags = local.tags
286
+ }
287
+
288
+ resource "aws_lb_target_group" "web" {
289
+ name = "${local.prefix}-web-tg"
290
+ port = local.container_port
291
+ protocol = "HTTP"
292
+ target_type = "ip"
293
+ vpc_id = data.aws_vpc.target.id
294
+ tags = local.tags
295
+
296
+ health_check {
297
+ path = "/health"
298
+ healthy_threshold = 2
299
+ unhealthy_threshold = 3
300
+ matcher = "200"
301
+ }
302
+ }
303
+
304
+ resource "aws_lb_listener" "https" {
305
+ load_balancer_arn = aws_lb.open_uptime.arn
306
+ port = 443
307
+ protocol = "HTTPS"
308
+ certificate_arn = var.certificate_arn
309
+
310
+ default_action {
311
+ type = "forward"
312
+ target_group_arn = aws_lb_target_group.web.arn
313
+ }
314
+ }
315
+
316
+ resource "aws_route53_record" "open_uptime" {
317
+ count = var.hosted_zone_id == null ? 0 : 1
318
+ zone_id = var.hosted_zone_id
319
+ name = var.hostname
320
+ type = "A"
321
+
322
+ alias {
323
+ name = aws_lb.open_uptime.dns_name
324
+ zone_id = aws_lb.open_uptime.zone_id
325
+ evaluate_target_health = true
326
+ }
327
+ }
328
+
329
+ data "aws_iam_policy_document" "ecs_assume_role" {
330
+ statement {
331
+ actions = ["sts:AssumeRole"]
332
+
333
+ principals {
334
+ type = "Service"
335
+ identifiers = ["ecs-tasks.amazonaws.com"]
336
+ }
337
+ }
338
+ }
339
+
340
+ resource "aws_iam_role" "execution" {
341
+ name = "${local.prefix}-execution-role"
342
+ assume_role_policy = data.aws_iam_policy_document.ecs_assume_role.json
343
+ tags = local.tags
344
+ }
345
+
346
+ resource "aws_iam_role_policy_attachment" "execution_managed" {
347
+ role = aws_iam_role.execution.name
348
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
349
+ }
350
+
351
+ data "aws_iam_policy_document" "execution_secrets" {
352
+ statement {
353
+ actions = [
354
+ "secretsmanager:GetSecretValue",
355
+ "ssm:GetParameter",
356
+ "ssm:GetParameters",
357
+ ]
358
+ resources = distinct(flatten([
359
+ for service in values(local.services) : values(service.secrets)
360
+ ]))
361
+ }
362
+
363
+ statement {
364
+ actions = ["kms:Decrypt"]
365
+ resources = [var.kms_key_arn]
366
+ }
367
+ }
368
+
369
+ resource "aws_iam_role_policy" "execution_secrets" {
370
+ name = "${local.prefix}-execution-secrets"
371
+ role = aws_iam_role.execution.id
372
+ policy = data.aws_iam_policy_document.execution_secrets.json
373
+ }
374
+
375
+ resource "aws_iam_role" "task" {
376
+ for_each = local.services
377
+ name = "${local.prefix}-${each.key}-task-role"
378
+ assume_role_policy = data.aws_iam_policy_document.ecs_assume_role.json
379
+ tags = local.tags
380
+ }
381
+
382
+ data "aws_iam_policy_document" "task" {
383
+ for_each = local.services
384
+
385
+ statement {
386
+ actions = [
387
+ "s3:GetObject",
388
+ "s3:PutObject",
389
+ "s3:AbortMultipartUpload",
390
+ ]
391
+ resources = ["${aws_s3_bucket.evidence.arn}/${each.key}/*"]
392
+ }
393
+
394
+ statement {
395
+ actions = ["kms:Decrypt", "kms:GenerateDataKey"]
396
+ resources = [var.kms_key_arn]
397
+ }
398
+ }
399
+
400
+ resource "aws_iam_role_policy" "task" {
401
+ for_each = local.services
402
+ name = "${local.prefix}-${each.key}-task-policy"
403
+ role = aws_iam_role.task[each.key].id
404
+ policy = data.aws_iam_policy_document.task[each.key].json
405
+ }
406
+
407
+ resource "aws_ecs_task_definition" "service" {
408
+ for_each = local.services
409
+ family = "${local.prefix}-${each.key}"
410
+ network_mode = "awsvpc"
411
+ requires_compatibilities = ["FARGATE"]
412
+ cpu = "512"
413
+ memory = "1024"
414
+ execution_role_arn = aws_iam_role.execution.arn
415
+ task_role_arn = aws_iam_role.task[each.key].arn
416
+
417
+ container_definitions = jsonencode([
418
+ {
419
+ name = each.key
420
+ image = var.container_image
421
+ essential = true
422
+ command = each.value.command
423
+ portMappings = each.key == "web" ? [{
424
+ containerPort = local.container_port
425
+ hostPort = local.container_port
426
+ protocol = "tcp"
427
+ }] : []
428
+ environment = [
429
+ { name = "HASNA_UPTIME_MODE", value = "hosted" },
430
+ { name = "HASNA_UPTIME_WORKSPACE_ID", value = var.workspace_id },
431
+ { name = "HASNA_UPTIME_COMPONENT", value = each.key },
432
+ { name = "HASNA_UPTIME_HOSTNAME", value = var.hostname },
433
+ ]
434
+ secrets = [
435
+ for name, value_from in each.value.secrets : {
436
+ name = name
437
+ valueFrom = value_from
438
+ }
439
+ ]
440
+ logConfiguration = {
441
+ logDriver = "awslogs"
442
+ options = {
443
+ awslogs-group = aws_cloudwatch_log_group.service[each.key].name
444
+ awslogs-region = var.region
445
+ awslogs-stream-prefix = "ecs"
446
+ }
447
+ }
448
+ }
449
+ ])
450
+
451
+ tags = local.tags
452
+ }
453
+
454
+ resource "aws_ecs_service" "web" {
455
+ name = "${local.prefix}-web"
456
+ cluster = aws_ecs_cluster.open_uptime.id
457
+ task_definition = aws_ecs_task_definition.service["web"].arn
458
+ desired_count = local.services.web.desired_count
459
+ launch_type = "FARGATE"
460
+ tags = local.tags
461
+
462
+ deployment_circuit_breaker {
463
+ enable = true
464
+ rollback = true
465
+ }
466
+
467
+ network_configuration {
468
+ subnets = var.private_subnet_ids
469
+ security_groups = [aws_security_group.web.id]
470
+ assign_public_ip = false
471
+ }
472
+
473
+ load_balancer {
474
+ target_group_arn = aws_lb_target_group.web.arn
475
+ container_name = "web"
476
+ container_port = local.container_port
477
+ }
478
+
479
+ depends_on = [aws_lb_listener.https]
480
+ }
481
+
482
+ resource "aws_ecs_service" "worker" {
483
+ for_each = {
484
+ for key, value in local.services : key => value if key != "web" && key != "migration"
485
+ }
486
+
487
+ name = "${local.prefix}-${each.key}"
488
+ cluster = aws_ecs_cluster.open_uptime.id
489
+ task_definition = aws_ecs_task_definition.service[each.key].arn
490
+ desired_count = each.value.desired_count
491
+ launch_type = "FARGATE"
492
+ tags = local.tags
493
+
494
+ deployment_circuit_breaker {
495
+ enable = true
496
+ rollback = true
497
+ }
498
+
499
+ network_configuration {
500
+ subnets = var.private_subnet_ids
501
+ security_groups = [aws_security_group.worker[each.key].id]
502
+ assign_public_ip = false
503
+ }
504
+ }
505
+
506
+ resource "aws_cloudwatch_metric_alarm" "web_5xx" {
507
+ alarm_name = "${local.prefix}-web-5xx"
508
+ alarm_description = "Open Uptime web target group 5xx responses"
509
+ namespace = "AWS/ApplicationELB"
510
+ metric_name = "HTTPCode_Target_5XX_Count"
511
+ statistic = "Sum"
512
+ period = 60
513
+ evaluation_periods = 5
514
+ threshold = 5
515
+ comparison_operator = "GreaterThanOrEqualToThreshold"
516
+ treat_missing_data = "notBreaching"
517
+ alarm_actions = var.alarm_actions
518
+ ok_actions = var.alarm_actions
519
+ tags = local.tags
520
+
521
+ dimensions = {
522
+ LoadBalancer = aws_lb.open_uptime.arn_suffix
523
+ TargetGroup = aws_lb_target_group.web.arn_suffix
524
+ }
525
+ }
526
+
527
+ resource "aws_cloudwatch_metric_alarm" "web_unhealthy" {
528
+ alarm_name = "${local.prefix}-web-unhealthy"
529
+ alarm_description = "Open Uptime unhealthy web targets"
530
+ namespace = "AWS/ApplicationELB"
531
+ metric_name = "UnHealthyHostCount"
532
+ statistic = "Maximum"
533
+ period = 60
534
+ evaluation_periods = 3
535
+ threshold = 1
536
+ comparison_operator = "GreaterThanOrEqualToThreshold"
537
+ treat_missing_data = "notBreaching"
538
+ alarm_actions = var.alarm_actions
539
+ ok_actions = var.alarm_actions
540
+ tags = local.tags
541
+
542
+ dimensions = {
543
+ LoadBalancer = aws_lb.open_uptime.arn_suffix
544
+ TargetGroup = aws_lb_target_group.web.arn_suffix
545
+ }
546
+ }
@@ -0,0 +1,22 @@
1
+ output "ecr_repository_url" {
2
+ value = aws_ecr_repository.open_uptime.repository_url
3
+ }
4
+
5
+ output "ecs_cluster_name" {
6
+ value = aws_ecs_cluster.open_uptime.name
7
+ }
8
+
9
+ output "alb_dns_name" {
10
+ value = aws_lb.open_uptime.dns_name
11
+ }
12
+
13
+ output "evidence_bucket" {
14
+ value = aws_s3_bucket.evidence.bucket
15
+ }
16
+
17
+ output "service_names" {
18
+ value = concat(
19
+ [aws_ecs_service.web.name],
20
+ [for service in aws_ecs_service.worker : service.name],
21
+ )
22
+ }
@@ -0,0 +1,28 @@
1
+ region = "us-east-1"
2
+ stage = "prod"
3
+ service_name = "open-uptime"
4
+ hostname = "uptime.hasna.xyz"
5
+ workspace_id = "wks_2tyysw05cwap"
6
+ vpc_id = "vpc-04c7f7abc1d3c3f56"
7
+ public_subnet_ids = ["subnet-replace-public-a", "subnet-replace-public-b"]
8
+ alb_ingress_cidr_blocks = []
9
+ 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"
12
+ certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/replace"
13
+ 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"
19
+ kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/00000000-0000-0000-0000-000000000000"
20
+ alarm_actions = []
21
+
22
+ desired_counts = {
23
+ web = 0
24
+ scheduler = 0
25
+ "public-probe" = 0
26
+ reporter = 0
27
+ migration = 0
28
+ }