@hasna/uptime 0.1.12 → 0.1.14
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/CHANGELOG.md +18 -0
- package/dist/cli/index.js +1 -1
- package/dist/cloud-plan.js +1 -1
- package/dist/index.js +1 -1
- package/docs/aws-deployment-runbook.md +10 -4
- package/infra/aws/README.md +26 -0
- package/infra/aws/main.tf +314 -0
- package/infra/aws/outputs.tf +7 -0
- package/infra/aws/terraform.tfvars.example +5 -1
- package/infra/aws/variables.tf +60 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.14] - 2026-06-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added explicit Terraform NAT task egress support. Infra owners can set
|
|
14
|
+
`enable_nat_task_egress = true` to allow web and non-public worker task
|
|
15
|
+
security groups to reach AWS public APIs through a private subnet NAT route on
|
|
16
|
+
TCP/443 when private VPC endpoints are not the approved egress model.
|
|
17
|
+
|
|
18
|
+
## [0.1.13] - 2026-06-28
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Added opt-in Terraform support for private AWS VPC endpoints. Infra owners can
|
|
23
|
+
enable interface endpoints for ECR API, ECR Docker, CloudWatch Logs, and
|
|
24
|
+
Secrets Manager, plus an S3 gateway endpoint when private route tables are
|
|
25
|
+
provided.
|
|
26
|
+
|
|
9
27
|
## [0.1.12] - 2026-06-28
|
|
10
28
|
|
|
11
29
|
### Added
|
package/dist/cli/index.js
CHANGED
|
@@ -6932,7 +6932,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6932
6932
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
6933
6933
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
6934
6934
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
6935
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
6935
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.14");
|
|
6936
6936
|
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
6937
6937
|
const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
|
|
6938
6938
|
const cluster = `${prefix}-${stage}`;
|
package/dist/cloud-plan.js
CHANGED
|
@@ -21,7 +21,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
21
21
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
22
22
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
23
23
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
24
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
24
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.14");
|
|
25
25
|
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
26
26
|
const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
|
|
27
27
|
const cluster = `${prefix}-${stage}`;
|
package/dist/index.js
CHANGED
|
@@ -4338,7 +4338,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
4338
4338
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
4339
4339
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
4340
4340
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
4341
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
4341
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.14");
|
|
4342
4342
|
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
4343
4343
|
const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
|
|
4344
4344
|
const cluster = `${prefix}-${stage}`;
|
|
@@ -80,8 +80,10 @@ The plan expects:
|
|
|
80
80
|
- Encrypted EFS file system, access point, mount targets, and AWS Backup plan
|
|
81
81
|
for `HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db`.
|
|
82
82
|
- S3 bucket for redacted browser evidence and generated report artifacts.
|
|
83
|
-
- Secrets Manager
|
|
84
|
-
|
|
83
|
+
- Secrets Manager refs for app env, hosted token, probe config, and reporting
|
|
84
|
+
channel refs. If any ECS secret uses an SSM Parameter Store ARN, add `ssm` to
|
|
85
|
+
`interface_vpc_endpoint_services` or document the approved alternate egress
|
|
86
|
+
path before running private-only tasks.
|
|
85
87
|
- CloudWatch log groups for every component plus initial web 5xx/unhealthy
|
|
86
88
|
alarms. Scheduler-stall, stale-probe, and report-delivery alarms remain
|
|
87
89
|
blocked until those workers emit cloud metrics.
|
|
@@ -176,8 +178,12 @@ Before setting `desired_counts.web = 1`, verify:
|
|
|
176
178
|
- `HASNA_UPTIME_ALLOWED_ORIGINS` matches the public HTTPS edge origin;
|
|
177
179
|
- CloudFront origin access is distribution-bound, not just narrowed to
|
|
178
180
|
CloudFront origin-facing ranges;
|
|
179
|
-
- web egress to ECR, Secrets Manager, CloudWatch Logs, S3, EFS, and any
|
|
180
|
-
endpoints has been proven
|
|
181
|
+
- web egress to ECR, Secrets Manager or SSM, CloudWatch Logs, S3, EFS, and any
|
|
182
|
+
required endpoints has been proven from a real ECS task. Terraform endpoint
|
|
183
|
+
ids, route tables, and security-group rules are creation evidence only; the
|
|
184
|
+
scale-up evidence must include image pull, secret injection, log delivery, S3
|
|
185
|
+
access, and EFS mount checks through the selected NAT or private-endpoint
|
|
186
|
+
path;
|
|
181
187
|
- scheduler, public-probe, reporter, and migration remain at `0`.
|
|
182
188
|
|
|
183
189
|
Scale only the web task, then capture the ECS deployment id and task definition
|
package/infra/aws/README.md
CHANGED
|
@@ -52,6 +52,32 @@ type, and cost-center tags. Set `monthly_budget_limit_usd` plus
|
|
|
52
52
|
forecasted and actual spend alerts. Leaving the email list empty skips budget
|
|
53
53
|
creation and is not sufficient for live scale-out approval.
|
|
54
54
|
|
|
55
|
+
Private AWS API egress can be pinned through opt-in VPC endpoints by setting
|
|
56
|
+
`enable_private_vpc_endpoints = true` and passing `private_route_table_ids`.
|
|
57
|
+
This creates interface endpoints for ECR API, ECR Docker, CloudWatch Logs, and
|
|
58
|
+
Secrets Manager, plus an S3 gateway endpoint when route tables are supplied. The
|
|
59
|
+
default is `false` so package consumers do not create endpoint hourly cost
|
|
60
|
+
without explicit infra-owner approval. The S3 gateway endpoint is required for
|
|
61
|
+
private ECR image layer pulls; the module adds S3 managed-prefix-list egress for
|
|
62
|
+
web and non-public worker security groups when the gateway endpoint is enabled.
|
|
63
|
+
Endpoint policies are scoped to the Open Uptime repository, log groups,
|
|
64
|
+
configured secret refs, KMS key, evidence bucket, and the regional ECR layer
|
|
65
|
+
bucket.
|
|
66
|
+
|
|
67
|
+
If private endpoints are not approved yet, infra owners can instead set
|
|
68
|
+
`enable_nat_task_egress = true` to allow web and non-public worker task security
|
|
69
|
+
groups to reach AWS public APIs through the private subnet NAT route on TCP/443.
|
|
70
|
+
Keep this disabled when private endpoints are the approved egress path. Runtime
|
|
71
|
+
scale-up still requires ECS task evidence for image pull, secret injection, log
|
|
72
|
+
delivery, S3 access, and EFS mount behavior.
|
|
73
|
+
|
|
74
|
+
Interface endpoint private DNS is VPC-wide. In shared VPCs, either keep endpoint
|
|
75
|
+
creation in the approved networking root, or pass
|
|
76
|
+
`additional_vpc_endpoint_source_security_group_ids` for every workload that must
|
|
77
|
+
keep using those private DNS names. If any ECS secret ref uses SSM Parameter
|
|
78
|
+
Store instead of Secrets Manager, add `ssm` to
|
|
79
|
+
`interface_vpc_endpoint_services` or keep an approved non-endpoint egress path.
|
|
80
|
+
|
|
55
81
|
## Current Blockers
|
|
56
82
|
|
|
57
83
|
- Hosted production auth/RBAC still needs scoped, revocable credentials.
|
package/infra/aws/main.tf
CHANGED
|
@@ -14,6 +14,7 @@ provider "aws" {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
data "aws_caller_identity" "current" {}
|
|
17
|
+
data "aws_partition" "current" {}
|
|
17
18
|
|
|
18
19
|
locals {
|
|
19
20
|
prefix = "${var.service_name}-${var.stage}"
|
|
@@ -63,6 +64,21 @@ locals {
|
|
|
63
64
|
AppType = var.app_type
|
|
64
65
|
CostCenter = var.cost_center
|
|
65
66
|
}
|
|
67
|
+
s3_gateway_endpoint_enabled = var.enable_private_vpc_endpoints && contains(var.gateway_vpc_endpoint_services, "s3") && length(var.private_route_table_ids) > 0
|
|
68
|
+
endpoint_secret_refs = distinct(flatten([for service in values(local.services) : values(service.secrets)]))
|
|
69
|
+
secretsmanager_secret_refs = [for ref in local.endpoint_secret_refs : ref if can(regex(":secretsmanager:", ref))]
|
|
70
|
+
ssm_parameter_refs = [for ref in local.endpoint_secret_refs : ref if can(regex(":ssm:", ref))]
|
|
71
|
+
secretsmanager_policy_refs = (
|
|
72
|
+
length(local.secretsmanager_secret_refs) > 0
|
|
73
|
+
? local.secretsmanager_secret_refs
|
|
74
|
+
: ["arn:${data.aws_partition.current.partition}:secretsmanager:${var.region}:${data.aws_caller_identity.current.account_id}:secret:${local.prefix}/no-secretsmanager-refs-configured-*"]
|
|
75
|
+
)
|
|
76
|
+
ssm_policy_refs = (
|
|
77
|
+
length(local.ssm_parameter_refs) > 0
|
|
78
|
+
? local.ssm_parameter_refs
|
|
79
|
+
: ["arn:${data.aws_partition.current.partition}:ssm:${var.region}:${data.aws_caller_identity.current.account_id}:parameter/${local.prefix}/no-ssm-refs-configured"]
|
|
80
|
+
)
|
|
81
|
+
service_log_group_arns = [for group in aws_cloudwatch_log_group.service : "${group.arn}:*"]
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
data "aws_vpc" "target" {
|
|
@@ -383,6 +399,304 @@ resource "aws_security_group_rule" "worker_egress" {
|
|
|
383
399
|
cidr_blocks = each.key == "public-probe" ? ["0.0.0.0/0"] : [data.aws_vpc.target.cidr_block]
|
|
384
400
|
}
|
|
385
401
|
|
|
402
|
+
resource "aws_security_group_rule" "web_nat_https_egress" {
|
|
403
|
+
count = var.enable_nat_task_egress ? 1 : 0
|
|
404
|
+
|
|
405
|
+
type = "egress"
|
|
406
|
+
description = "HTTPS egress through approved NAT path"
|
|
407
|
+
security_group_id = aws_security_group.web.id
|
|
408
|
+
from_port = 443
|
|
409
|
+
to_port = 443
|
|
410
|
+
protocol = "tcp"
|
|
411
|
+
cidr_blocks = var.nat_task_egress_cidr_blocks
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
resource "aws_security_group_rule" "worker_nat_https_egress" {
|
|
415
|
+
for_each = var.enable_nat_task_egress ? {
|
|
416
|
+
for key, value in aws_security_group.worker : key => value if key != "public-probe"
|
|
417
|
+
} : {}
|
|
418
|
+
|
|
419
|
+
type = "egress"
|
|
420
|
+
description = "HTTPS egress through approved NAT path"
|
|
421
|
+
security_group_id = each.value.id
|
|
422
|
+
from_port = 443
|
|
423
|
+
to_port = 443
|
|
424
|
+
protocol = "tcp"
|
|
425
|
+
cidr_blocks = var.nat_task_egress_cidr_blocks
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
resource "aws_security_group_rule" "web_s3_gateway_egress" {
|
|
429
|
+
count = local.s3_gateway_endpoint_enabled ? 1 : 0
|
|
430
|
+
|
|
431
|
+
type = "egress"
|
|
432
|
+
description = "HTTPS to S3 gateway endpoint prefix list"
|
|
433
|
+
security_group_id = aws_security_group.web.id
|
|
434
|
+
from_port = 443
|
|
435
|
+
to_port = 443
|
|
436
|
+
protocol = "tcp"
|
|
437
|
+
prefix_list_ids = [aws_vpc_endpoint.gateway["s3"].prefix_list_id]
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
resource "aws_security_group_rule" "worker_s3_gateway_egress" {
|
|
441
|
+
for_each = local.s3_gateway_endpoint_enabled ? {
|
|
442
|
+
for key, value in aws_security_group.worker : key => value if key != "public-probe"
|
|
443
|
+
} : {}
|
|
444
|
+
|
|
445
|
+
type = "egress"
|
|
446
|
+
description = "HTTPS to S3 gateway endpoint prefix list"
|
|
447
|
+
security_group_id = each.value.id
|
|
448
|
+
from_port = 443
|
|
449
|
+
to_port = 443
|
|
450
|
+
protocol = "tcp"
|
|
451
|
+
prefix_list_ids = [aws_vpc_endpoint.gateway["s3"].prefix_list_id]
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
resource "aws_security_group" "vpc_endpoints" {
|
|
455
|
+
count = var.enable_private_vpc_endpoints ? 1 : 0
|
|
456
|
+
name = "${local.prefix}-vpc-endpoints-sg"
|
|
457
|
+
description = "Open Uptime interface VPC endpoints"
|
|
458
|
+
vpc_id = data.aws_vpc.target.id
|
|
459
|
+
tags = merge(local.tags, { Component = "vpc-endpoints" })
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
resource "aws_security_group_rule" "vpc_endpoints_from_web" {
|
|
463
|
+
count = var.enable_private_vpc_endpoints ? 1 : 0
|
|
464
|
+
type = "ingress"
|
|
465
|
+
description = "HTTPS from Open Uptime web tasks"
|
|
466
|
+
security_group_id = aws_security_group.vpc_endpoints[0].id
|
|
467
|
+
from_port = 443
|
|
468
|
+
to_port = 443
|
|
469
|
+
protocol = "tcp"
|
|
470
|
+
source_security_group_id = aws_security_group.web.id
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
resource "aws_security_group_rule" "vpc_endpoints_from_worker" {
|
|
474
|
+
for_each = var.enable_private_vpc_endpoints ? aws_security_group.worker : {}
|
|
475
|
+
|
|
476
|
+
type = "ingress"
|
|
477
|
+
description = "HTTPS from Open Uptime ${each.key} tasks"
|
|
478
|
+
security_group_id = aws_security_group.vpc_endpoints[0].id
|
|
479
|
+
from_port = 443
|
|
480
|
+
to_port = 443
|
|
481
|
+
protocol = "tcp"
|
|
482
|
+
source_security_group_id = each.value.id
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
resource "aws_security_group_rule" "vpc_endpoints_from_additional_sources" {
|
|
486
|
+
for_each = var.enable_private_vpc_endpoints ? toset(var.additional_vpc_endpoint_source_security_group_ids) : toset([])
|
|
487
|
+
|
|
488
|
+
type = "ingress"
|
|
489
|
+
description = "HTTPS from additional approved source security group"
|
|
490
|
+
security_group_id = aws_security_group.vpc_endpoints[0].id
|
|
491
|
+
from_port = 443
|
|
492
|
+
to_port = 443
|
|
493
|
+
protocol = "tcp"
|
|
494
|
+
source_security_group_id = each.value
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
data "aws_iam_policy_document" "vpc_endpoint_ecr_api" {
|
|
498
|
+
statement {
|
|
499
|
+
sid = "AllowEcrAuthorization"
|
|
500
|
+
actions = ["ecr:GetAuthorizationToken"]
|
|
501
|
+
resources = ["*"]
|
|
502
|
+
|
|
503
|
+
principals {
|
|
504
|
+
type = "*"
|
|
505
|
+
identifiers = ["*"]
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
statement {
|
|
510
|
+
sid = "AllowOpenUptimeRepositoryRead"
|
|
511
|
+
actions = [
|
|
512
|
+
"ecr:BatchCheckLayerAvailability",
|
|
513
|
+
"ecr:BatchGetImage",
|
|
514
|
+
"ecr:DescribeImages",
|
|
515
|
+
"ecr:DescribeRepositories",
|
|
516
|
+
"ecr:GetDownloadUrlForLayer",
|
|
517
|
+
]
|
|
518
|
+
resources = [aws_ecr_repository.open_uptime.arn]
|
|
519
|
+
|
|
520
|
+
principals {
|
|
521
|
+
type = "*"
|
|
522
|
+
identifiers = ["*"]
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
data "aws_iam_policy_document" "vpc_endpoint_ecr_dkr" {
|
|
528
|
+
statement {
|
|
529
|
+
sid = "AllowOpenUptimeRegistryRead"
|
|
530
|
+
actions = [
|
|
531
|
+
"ecr:BatchCheckLayerAvailability",
|
|
532
|
+
"ecr:BatchGetImage",
|
|
533
|
+
"ecr:GetDownloadUrlForLayer",
|
|
534
|
+
]
|
|
535
|
+
resources = [aws_ecr_repository.open_uptime.arn]
|
|
536
|
+
|
|
537
|
+
principals {
|
|
538
|
+
type = "*"
|
|
539
|
+
identifiers = ["*"]
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
data "aws_iam_policy_document" "vpc_endpoint_logs" {
|
|
545
|
+
statement {
|
|
546
|
+
sid = "AllowOpenUptimeLogDelivery"
|
|
547
|
+
actions = [
|
|
548
|
+
"logs:CreateLogStream",
|
|
549
|
+
"logs:DescribeLogStreams",
|
|
550
|
+
"logs:PutLogEvents",
|
|
551
|
+
]
|
|
552
|
+
resources = local.service_log_group_arns
|
|
553
|
+
|
|
554
|
+
principals {
|
|
555
|
+
type = "*"
|
|
556
|
+
identifiers = ["*"]
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
data "aws_iam_policy_document" "vpc_endpoint_secretsmanager" {
|
|
562
|
+
statement {
|
|
563
|
+
sid = "AllowOpenUptimeSecretReads"
|
|
564
|
+
actions = [
|
|
565
|
+
"secretsmanager:DescribeSecret",
|
|
566
|
+
"secretsmanager:GetSecretValue",
|
|
567
|
+
]
|
|
568
|
+
resources = local.secretsmanager_policy_refs
|
|
569
|
+
|
|
570
|
+
principals {
|
|
571
|
+
type = "*"
|
|
572
|
+
identifiers = ["*"]
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
data "aws_iam_policy_document" "vpc_endpoint_ssm" {
|
|
578
|
+
statement {
|
|
579
|
+
sid = "AllowOpenUptimeParameterReads"
|
|
580
|
+
actions = [
|
|
581
|
+
"ssm:GetParameter",
|
|
582
|
+
"ssm:GetParameters",
|
|
583
|
+
]
|
|
584
|
+
resources = local.ssm_policy_refs
|
|
585
|
+
|
|
586
|
+
principals {
|
|
587
|
+
type = "*"
|
|
588
|
+
identifiers = ["*"]
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
data "aws_iam_policy_document" "vpc_endpoint_sts" {
|
|
594
|
+
statement {
|
|
595
|
+
sid = "AllowCallerIdentity"
|
|
596
|
+
actions = ["sts:GetCallerIdentity"]
|
|
597
|
+
resources = ["*"]
|
|
598
|
+
|
|
599
|
+
principals {
|
|
600
|
+
type = "*"
|
|
601
|
+
identifiers = ["*"]
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
data "aws_iam_policy_document" "vpc_endpoint_kms" {
|
|
607
|
+
statement {
|
|
608
|
+
sid = "AllowOpenUptimeKeyUse"
|
|
609
|
+
actions = [
|
|
610
|
+
"kms:Decrypt",
|
|
611
|
+
"kms:DescribeKey",
|
|
612
|
+
"kms:GenerateDataKey*",
|
|
613
|
+
]
|
|
614
|
+
resources = [var.kms_key_arn]
|
|
615
|
+
|
|
616
|
+
principals {
|
|
617
|
+
type = "*"
|
|
618
|
+
identifiers = ["*"]
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
data "aws_iam_policy_document" "vpc_endpoint_s3" {
|
|
624
|
+
statement {
|
|
625
|
+
sid = "AllowOpenUptimeEvidenceBucket"
|
|
626
|
+
actions = [
|
|
627
|
+
"s3:AbortMultipartUpload",
|
|
628
|
+
"s3:GetBucketLocation",
|
|
629
|
+
"s3:GetObject",
|
|
630
|
+
"s3:ListBucket",
|
|
631
|
+
"s3:PutObject",
|
|
632
|
+
]
|
|
633
|
+
resources = [
|
|
634
|
+
aws_s3_bucket.evidence.arn,
|
|
635
|
+
"${aws_s3_bucket.evidence.arn}/*",
|
|
636
|
+
]
|
|
637
|
+
|
|
638
|
+
principals {
|
|
639
|
+
type = "*"
|
|
640
|
+
identifiers = ["*"]
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
statement {
|
|
645
|
+
sid = "AllowEcrLayerBucket"
|
|
646
|
+
actions = ["s3:GetObject"]
|
|
647
|
+
resources = ["arn:${data.aws_partition.current.partition}:s3:::prod-${var.region}-starport-layer-bucket/*"]
|
|
648
|
+
|
|
649
|
+
principals {
|
|
650
|
+
type = "*"
|
|
651
|
+
identifiers = ["*"]
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
resource "aws_vpc_endpoint" "interface" {
|
|
657
|
+
for_each = var.enable_private_vpc_endpoints ? toset(var.interface_vpc_endpoint_services) : toset([])
|
|
658
|
+
|
|
659
|
+
vpc_id = data.aws_vpc.target.id
|
|
660
|
+
service_name = "com.amazonaws.${var.region}.${each.key}"
|
|
661
|
+
vpc_endpoint_type = "Interface"
|
|
662
|
+
subnet_ids = var.private_subnet_ids
|
|
663
|
+
security_group_ids = [aws_security_group.vpc_endpoints[0].id]
|
|
664
|
+
private_dns_enabled = true
|
|
665
|
+
policy = {
|
|
666
|
+
"ecr.api" = data.aws_iam_policy_document.vpc_endpoint_ecr_api.json
|
|
667
|
+
"ecr.dkr" = data.aws_iam_policy_document.vpc_endpoint_ecr_dkr.json
|
|
668
|
+
logs = data.aws_iam_policy_document.vpc_endpoint_logs.json
|
|
669
|
+
secretsmanager = data.aws_iam_policy_document.vpc_endpoint_secretsmanager.json
|
|
670
|
+
ssm = data.aws_iam_policy_document.vpc_endpoint_ssm.json
|
|
671
|
+
sts = data.aws_iam_policy_document.vpc_endpoint_sts.json
|
|
672
|
+
kms = data.aws_iam_policy_document.vpc_endpoint_kms.json
|
|
673
|
+
}[each.key]
|
|
674
|
+
|
|
675
|
+
tags = merge(local.tags, {
|
|
676
|
+
Name = "${local.prefix}-${replace(each.key, ".", "-")}-endpoint"
|
|
677
|
+
Component = "vpc-endpoint"
|
|
678
|
+
Endpoint = each.key
|
|
679
|
+
})
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
resource "aws_vpc_endpoint" "gateway" {
|
|
683
|
+
for_each = var.enable_private_vpc_endpoints && length(var.private_route_table_ids) > 0 ? toset(var.gateway_vpc_endpoint_services) : toset([])
|
|
684
|
+
|
|
685
|
+
vpc_id = data.aws_vpc.target.id
|
|
686
|
+
service_name = "com.amazonaws.${var.region}.${each.key}"
|
|
687
|
+
vpc_endpoint_type = "Gateway"
|
|
688
|
+
route_table_ids = var.private_route_table_ids
|
|
689
|
+
policy = {
|
|
690
|
+
s3 = data.aws_iam_policy_document.vpc_endpoint_s3.json
|
|
691
|
+
}[each.key]
|
|
692
|
+
|
|
693
|
+
tags = merge(local.tags, {
|
|
694
|
+
Name = "${local.prefix}-${each.key}-endpoint"
|
|
695
|
+
Component = "vpc-endpoint"
|
|
696
|
+
Endpoint = each.key
|
|
697
|
+
})
|
|
698
|
+
}
|
|
699
|
+
|
|
386
700
|
resource "aws_security_group" "efs" {
|
|
387
701
|
name = "${local.prefix}-efs-sg"
|
|
388
702
|
description = "Open Uptime EFS data store"
|
package/infra/aws/outputs.tf
CHANGED
|
@@ -75,3 +75,10 @@ output "service_names" {
|
|
|
75
75
|
[for service in aws_ecs_service.worker : service.name],
|
|
76
76
|
)
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
output "vpc_endpoint_ids" {
|
|
80
|
+
value = {
|
|
81
|
+
interface = { for service, endpoint in aws_vpc_endpoint.interface : service => endpoint.id }
|
|
82
|
+
gateway = { for service, endpoint in aws_vpc_endpoint.gateway : service => endpoint.id }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -14,8 +14,9 @@ protected_access_mode = "cloudfront_default_domain"
|
|
|
14
14
|
public_subnet_ids = ["subnet-replace-public-a", "subnet-replace-public-b"]
|
|
15
15
|
alb_ingress_cidr_blocks = []
|
|
16
16
|
private_subnet_ids = ["subnet-replace-private-a", "subnet-replace-private-b"]
|
|
17
|
+
private_route_table_ids = ["rtb-replace-private"]
|
|
17
18
|
container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/open-uptime@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
18
|
-
runtime_package_version = "0.1.
|
|
19
|
+
runtime_package_version = "0.1.14"
|
|
19
20
|
certificate_arn = null
|
|
20
21
|
hosted_zone_id = null
|
|
21
22
|
app_env_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/app/env"
|
|
@@ -25,6 +26,9 @@ reporting_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret
|
|
|
25
26
|
kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/00000000-0000-0000-0000-000000000000"
|
|
26
27
|
alarm_actions = []
|
|
27
28
|
monthly_budget_limit_usd = 0
|
|
29
|
+
enable_nat_task_egress = false
|
|
30
|
+
enable_private_vpc_endpoints = false
|
|
31
|
+
additional_vpc_endpoint_source_security_group_ids = []
|
|
28
32
|
|
|
29
33
|
desired_counts = {
|
|
30
34
|
web = 0
|
package/infra/aws/variables.tf
CHANGED
|
@@ -116,7 +116,7 @@ variable "container_image" {
|
|
|
116
116
|
variable "runtime_package_version" {
|
|
117
117
|
description = "Published @hasna/uptime package version that CodeBuild should build into the ECR image."
|
|
118
118
|
type = string
|
|
119
|
-
default = "0.1.
|
|
119
|
+
default = "0.1.14"
|
|
120
120
|
|
|
121
121
|
validation {
|
|
122
122
|
condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?$", var.runtime_package_version))
|
|
@@ -237,3 +237,62 @@ variable "budget_alert_email_addresses" {
|
|
|
237
237
|
type = list(string)
|
|
238
238
|
default = []
|
|
239
239
|
}
|
|
240
|
+
|
|
241
|
+
variable "enable_nat_task_egress" {
|
|
242
|
+
description = "Allow web and non-public worker tasks to reach AWS public APIs through NAT on TCP/443. Keep false when private VPC endpoints are the approved egress path."
|
|
243
|
+
type = bool
|
|
244
|
+
default = false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
variable "nat_task_egress_cidr_blocks" {
|
|
248
|
+
description = "CIDR blocks allowed for NAT-backed HTTPS egress when enable_nat_task_egress is true."
|
|
249
|
+
type = list(string)
|
|
250
|
+
default = ["0.0.0.0/0"]
|
|
251
|
+
|
|
252
|
+
validation {
|
|
253
|
+
condition = length(var.nat_task_egress_cidr_blocks) > 0
|
|
254
|
+
error_message = "nat_task_egress_cidr_blocks must not be empty when NAT task egress is enabled."
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
variable "enable_private_vpc_endpoints" {
|
|
259
|
+
description = "Create private VPC endpoints for ECS access to AWS APIs. Requires private subnet ids; S3 gateway endpoint also requires private_route_table_ids."
|
|
260
|
+
type = bool
|
|
261
|
+
default = false
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
variable "interface_vpc_endpoint_services" {
|
|
265
|
+
description = "Regional interface endpoint service short names to create when enable_private_vpc_endpoints is true."
|
|
266
|
+
type = list(string)
|
|
267
|
+
default = ["ecr.api", "ecr.dkr", "logs", "secretsmanager"]
|
|
268
|
+
|
|
269
|
+
validation {
|
|
270
|
+
condition = alltrue([
|
|
271
|
+
for service in var.interface_vpc_endpoint_services : contains(["ecr.api", "ecr.dkr", "logs", "secretsmanager", "sts", "ssm", "kms"], service)
|
|
272
|
+
])
|
|
273
|
+
error_message = "interface_vpc_endpoint_services must contain only approved AWS service short names."
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
variable "additional_vpc_endpoint_source_security_group_ids" {
|
|
278
|
+
description = "Additional source security groups allowed to use Open Uptime interface VPC endpoints in a shared VPC. Keep empty for dedicated Open Uptime subnets."
|
|
279
|
+
type = list(string)
|
|
280
|
+
default = []
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
variable "gateway_vpc_endpoint_services" {
|
|
284
|
+
description = "Regional gateway endpoint service short names to create when enable_private_vpc_endpoints is true."
|
|
285
|
+
type = list(string)
|
|
286
|
+
default = ["s3"]
|
|
287
|
+
|
|
288
|
+
validation {
|
|
289
|
+
condition = alltrue([for service in var.gateway_vpc_endpoint_services : contains(["s3"], service)])
|
|
290
|
+
error_message = "gateway_vpc_endpoint_services currently supports only s3."
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
variable "private_route_table_ids" {
|
|
295
|
+
description = "Private route table ids for gateway VPC endpoints such as S3. Leave empty to skip gateway endpoint creation."
|
|
296
|
+
type = list(string)
|
|
297
|
+
default = []
|
|
298
|
+
}
|
package/package.json
CHANGED