mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
* feat(terraform): add safe backend environment adoption Introduce import-guarded environment roots and retire temporary bootstrap and POC provisioning after ownership transfer. * ci(deploy): pause dev and staging deployments Prevent application releases from racing Terraform adoption while retaining production deployment and validation. * ci(deploy): require manual environment dispatch * fix: update `required_version` from `>=1.7.0` to `>=1.9.0` The deploy-boundary check interpolates `var.aws_account_id` and `var.environment`. Terraform only allows other variables inside `validation` from 1.9.0+. CI already runs against `1.9.8` so `versions.tf` setting version as `>=1.7.0` is a breaking finding * chore(deps): add `terraform` to renovate dependency coverage * ci(deploy): drop unprovisioned prod dispatch path
524 lines
14 KiB
HCL
524 lines
14 KiB
HCL
data "aws_iam_openid_connect_provider" "github" {
|
|
url = "https://token.actions.githubusercontent.com"
|
|
}
|
|
|
|
data "aws_elastic_beanstalk_hosted_zone" "current" {}
|
|
|
|
locals {
|
|
application_arn = "arn:aws:elasticbeanstalk:${var.aws_region}:${var.aws_account_id}:application/${var.eb_application_name}"
|
|
environment_arn = "arn:aws:elasticbeanstalk:${var.aws_region}:${var.aws_account_id}:environment/${var.eb_application_name}/${var.eb_environment_name}"
|
|
environment_stack_name = "awseb-${var.eb_environment_id}-stack"
|
|
eb_bucket_name = "elasticbeanstalk-${var.aws_region}-${var.aws_account_id}"
|
|
use_legacy_s3_policy = !var.adoption_complete && var.legacy_dev_s3_policy
|
|
app_config_secret_pattern = "arn:aws:secretsmanager:${var.aws_region}:${var.aws_account_id}:secret:${var.app_config_secret_name}-*"
|
|
}
|
|
|
|
data "aws_iam_policy_document" "runtime_assume" {
|
|
statement {
|
|
effect = "Allow"
|
|
actions = ["sts:AssumeRole"]
|
|
|
|
principals {
|
|
type = "Service"
|
|
identifiers = ["ec2.amazonaws.com"]
|
|
}
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_role" "runtime" {
|
|
name = var.runtime_role_name
|
|
path = "/"
|
|
description = var.metadata_before_adoption.runtime_role_description
|
|
assume_role_policy = data.aws_iam_policy_document.runtime_assume.json
|
|
max_session_duration = 3600
|
|
permissions_boundary = var.permissions_boundary_arn
|
|
tags = var.adoption_complete ? merge(var.metadata_before_adoption.runtime_role_tags, { ManagedBy = "terraform" }) : var.metadata_before_adoption.runtime_role_tags
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_role_policy_attachment" "web_tier" {
|
|
role = aws_iam_role.runtime.name
|
|
policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier"
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
}
|
|
}
|
|
|
|
data "aws_iam_policy_document" "runtime_app_config" {
|
|
statement {
|
|
sid = var.app_config_policy_sid
|
|
effect = "Allow"
|
|
actions = ["secretsmanager:GetSecretValue"]
|
|
resources = [local.app_config_secret_pattern]
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_role_policy" "runtime_app_config" {
|
|
name = var.runtime_app_config_policy_name
|
|
role = aws_iam_role.runtime.id
|
|
policy = data.aws_iam_policy_document.runtime_app_config.json
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
}
|
|
}
|
|
|
|
data "aws_iam_policy_document" "runtime_webhook" {
|
|
count = var.work_order_webhook_enabled ? 1 : 0
|
|
|
|
statement {
|
|
sid = var.webhook_read_policy_sid
|
|
effect = "Allow"
|
|
actions = [
|
|
"secretsmanager:DescribeSecret",
|
|
"secretsmanager:GetSecretValue",
|
|
]
|
|
resources = [var.webhook_secret_arn]
|
|
}
|
|
|
|
statement {
|
|
sid = var.webhook_decrypt_policy_sid
|
|
effect = "Allow"
|
|
actions = ["kms:Decrypt"]
|
|
resources = ["arn:aws:kms:us-east-1:011934824531:key/d10fd1f0-a61a-4405-8568-85e9fd11ba18"]
|
|
|
|
condition {
|
|
test = "StringEquals"
|
|
variable = "kms:ViaService"
|
|
values = ["secretsmanager.us-east-1.amazonaws.com"]
|
|
}
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_role_policy" "runtime_webhook" {
|
|
count = var.work_order_webhook_enabled ? 1 : 0
|
|
|
|
name = var.runtime_webhook_policy_name
|
|
role = aws_iam_role.runtime.id
|
|
policy = data.aws_iam_policy_document.runtime_webhook[0].json
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
}
|
|
}
|
|
|
|
data "aws_iam_policy_document" "runtime_dynamo" {
|
|
count = var.dynamo_reader_role_arn == null ? 0 : 1
|
|
|
|
statement {
|
|
sid = var.dynamo_policy_sid
|
|
effect = "Allow"
|
|
actions = ["sts:AssumeRole"]
|
|
resources = [var.dynamo_reader_role_arn]
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_role_policy" "runtime_dynamo" {
|
|
count = var.dynamo_reader_role_arn == null ? 0 : 1
|
|
|
|
name = var.runtime_dynamo_policy_name
|
|
role = aws_iam_role.runtime.id
|
|
policy = data.aws_iam_policy_document.runtime_dynamo[0].json
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_instance_profile" "runtime" {
|
|
name = var.runtime_role_name
|
|
path = "/"
|
|
role = aws_iam_role.runtime.name
|
|
tags = var.adoption_complete ? merge(var.metadata_before_adoption.instance_profile_tags, { ManagedBy = "terraform" }) : var.metadata_before_adoption.instance_profile_tags
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
}
|
|
}
|
|
|
|
resource "aws_secretsmanager_secret" "app_config" {
|
|
name = var.app_config_secret_name
|
|
description = var.metadata_before_adoption.app_config_description
|
|
tags = var.adoption_complete ? merge(var.metadata_before_adoption.app_config_tags, { ManagedBy = "terraform" }) : var.metadata_before_adoption.app_config_tags
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
ignore_changes = [
|
|
force_overwrite_replica_secret,
|
|
recovery_window_in_days,
|
|
]
|
|
}
|
|
}
|
|
|
|
data "aws_iam_policy_document" "deploy_assume" {
|
|
statement {
|
|
effect = "Allow"
|
|
actions = ["sts:AssumeRoleWithWebIdentity"]
|
|
|
|
principals {
|
|
type = "Federated"
|
|
identifiers = [data.aws_iam_openid_connect_provider.github.arn]
|
|
}
|
|
|
|
condition {
|
|
test = "StringEquals"
|
|
variable = "token.actions.githubusercontent.com:aud"
|
|
values = ["sts.amazonaws.com"]
|
|
}
|
|
|
|
condition {
|
|
test = "StringEquals"
|
|
variable = "token.actions.githubusercontent.com:sub"
|
|
values = ["repo:${var.github_repo}:environment:${var.github_environment}"]
|
|
}
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_role" "github_deploy" {
|
|
name = var.github_deploy_role_name
|
|
path = "/"
|
|
description = var.metadata_before_adoption.deploy_role_description
|
|
assume_role_policy = data.aws_iam_policy_document.deploy_assume.json
|
|
max_session_duration = 3600
|
|
permissions_boundary = var.github_deploy_permissions_boundary_arn
|
|
tags = var.adoption_complete ? merge(var.metadata_before_adoption.deploy_role_tags, { ManagedBy = "terraform" }) : var.metadata_before_adoption.deploy_role_tags
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
}
|
|
}
|
|
|
|
data "aws_iam_policy_document" "deploy" {
|
|
statement {
|
|
sid = var.environment == "tf-poc" ? "DescribeDeploymentResources" : null
|
|
effect = "Allow"
|
|
actions = [
|
|
"autoscaling:Describe*",
|
|
"ec2:Describe*",
|
|
"elasticbeanstalk:DescribeApplicationVersions",
|
|
"elasticbeanstalk:DescribeEnvironments",
|
|
"elasticbeanstalk:DescribeEvents",
|
|
"elasticloadbalancing:Describe*",
|
|
]
|
|
resources = ["*"]
|
|
}
|
|
|
|
statement {
|
|
sid = var.environment == "tf-poc" ? "CreateApplicationVersion" : null
|
|
effect = "Allow"
|
|
actions = ["elasticbeanstalk:CreateApplicationVersion"]
|
|
resources = [
|
|
local.application_arn,
|
|
"arn:aws:elasticbeanstalk:${var.aws_region}:${var.aws_account_id}:applicationversion/${var.eb_application_name}/*",
|
|
]
|
|
}
|
|
|
|
statement {
|
|
sid = var.environment == "tf-poc" ? "UpdatePocEnvironment" : null
|
|
effect = "Allow"
|
|
actions = ["elasticbeanstalk:UpdateEnvironment"]
|
|
resources = [local.environment_arn]
|
|
}
|
|
|
|
dynamic "statement" {
|
|
for_each = var.environment != "tf-poc" ? [1] : []
|
|
content {
|
|
effect = "Allow"
|
|
actions = [
|
|
"cloudformation:CancelUpdateStack",
|
|
"cloudformation:DescribeStackEvents",
|
|
"cloudformation:DescribeStackResource",
|
|
"cloudformation:DescribeStackResources",
|
|
"cloudformation:DescribeStacks",
|
|
"cloudformation:GetTemplate",
|
|
"cloudformation:ListStackResources",
|
|
"cloudformation:UpdateStack",
|
|
]
|
|
resources = [
|
|
"arn:aws:cloudformation:${var.aws_region}:${var.aws_account_id}:stack/${local.environment_stack_name}/*",
|
|
]
|
|
}
|
|
}
|
|
|
|
dynamic "statement" {
|
|
for_each = var.environment != "tf-poc" ? [1] : []
|
|
content {
|
|
effect = "Allow"
|
|
actions = [
|
|
"autoscaling:PutNotificationConfiguration",
|
|
"autoscaling:ResumeProcesses",
|
|
"autoscaling:SuspendProcesses",
|
|
]
|
|
resources = [
|
|
"arn:aws:autoscaling:${var.aws_region}:${var.aws_account_id}:autoScalingGroup:*:autoScalingGroupName/${local.environment_stack_name}-*",
|
|
]
|
|
}
|
|
}
|
|
|
|
dynamic "statement" {
|
|
for_each = local.use_legacy_s3_policy ? [1] : []
|
|
content {
|
|
effect = "Allow"
|
|
actions = ["s3:Delete*", "s3:Get*", "s3:Put*"]
|
|
resources = ["arn:aws:s3:::elasticbeanstalk-*/*"]
|
|
}
|
|
}
|
|
|
|
dynamic "statement" {
|
|
for_each = local.use_legacy_s3_policy ? [1] : []
|
|
content {
|
|
effect = "Allow"
|
|
actions = [
|
|
"s3:GetBucket*",
|
|
"s3:ListBucket",
|
|
"s3:PutBucketOwnershipControls",
|
|
"s3:PutBucketPolicy",
|
|
"s3:PutBucketPublicAccessBlock",
|
|
]
|
|
resources = ["arn:aws:s3:::elasticbeanstalk-*"]
|
|
}
|
|
}
|
|
|
|
dynamic "statement" {
|
|
for_each = local.use_legacy_s3_policy ? [] : [1]
|
|
content {
|
|
sid = var.environment == "tf-poc" ? "UploadApplicationVersion" : null
|
|
effect = "Allow"
|
|
actions = ["s3:PutObject"]
|
|
resources = ["arn:aws:s3:::${local.eb_bucket_name}/${var.eb_application_name}/*"]
|
|
}
|
|
}
|
|
|
|
dynamic "statement" {
|
|
for_each = local.use_legacy_s3_policy ? [] : [1]
|
|
content {
|
|
sid = var.environment == "tf-poc" ? "UseBeanstalkBucket" : null
|
|
effect = "Allow"
|
|
actions = ["s3:GetBucketLocation", "s3:ListBucket"]
|
|
resources = ["arn:aws:s3:::${local.eb_bucket_name}"]
|
|
}
|
|
}
|
|
|
|
dynamic "statement" {
|
|
for_each = var.environment == "tf-poc" ? [1] : []
|
|
content {
|
|
sid = "DenyLiveEnvironments"
|
|
effect = "Deny"
|
|
actions = ["elasticbeanstalk:*"]
|
|
resources = [
|
|
"arn:aws:elasticbeanstalk:${var.aws_region}:${var.aws_account_id}:environment/${var.eb_application_name}/shoc-backend-dev",
|
|
"arn:aws:elasticbeanstalk:${var.aws_region}:${var.aws_account_id}:environment/${var.eb_application_name}/shoc-backend-staging",
|
|
]
|
|
}
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_role_policy" "github_deploy" {
|
|
name = var.github_deploy_policy_name
|
|
role = aws_iam_role.github_deploy.id
|
|
policy = data.aws_iam_policy_document.deploy.json
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
}
|
|
}
|
|
|
|
resource "aws_elastic_beanstalk_environment" "this" {
|
|
name = var.eb_environment_name
|
|
application = var.eb_application_name
|
|
platform_arn = var.platform_arn
|
|
tier = "WebServer"
|
|
cname_prefix = var.eb_environment_name
|
|
|
|
setting {
|
|
namespace = "aws:elasticbeanstalk:environment"
|
|
name = "EnvironmentType"
|
|
value = "LoadBalanced"
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:elasticbeanstalk:environment"
|
|
name = "LoadBalancerType"
|
|
value = "application"
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:elasticbeanstalk:environment"
|
|
name = "ServiceRole"
|
|
value = var.eb_service_role_name
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:ec2:vpc"
|
|
name = "VPCId"
|
|
value = var.vpc_id
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:ec2:vpc"
|
|
name = "Subnets"
|
|
value = join(",", sort(var.instance_subnet_ids))
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:ec2:vpc"
|
|
name = "ELBSubnets"
|
|
value = join(",", sort(var.load_balancer_subnet_ids))
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:ec2:vpc"
|
|
name = "ELBScheme"
|
|
value = "public"
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:ec2:vpc"
|
|
name = "AssociatePublicIpAddress"
|
|
value = "true"
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:autoscaling:launchconfiguration"
|
|
name = "IamInstanceProfile"
|
|
value = aws_iam_instance_profile.runtime.name
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:autoscaling:launchconfiguration"
|
|
name = "InstanceType"
|
|
value = "t3.small"
|
|
}
|
|
|
|
dynamic "setting" {
|
|
for_each = var.instance_security_group_id == null ? [] : [var.instance_security_group_id]
|
|
content {
|
|
namespace = "aws:autoscaling:launchconfiguration"
|
|
name = "SecurityGroups"
|
|
value = setting.value
|
|
}
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:autoscaling:asg"
|
|
name = "MinSize"
|
|
value = "1"
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:autoscaling:asg"
|
|
name = "MaxSize"
|
|
value = "1"
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:elbv2:listener:443"
|
|
name = "Protocol"
|
|
value = "HTTPS"
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:elbv2:listener:443"
|
|
name = "SSLCertificateArns"
|
|
value = var.shared_certificate_arn
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:elasticbeanstalk:environment:process:default"
|
|
name = "HealthCheckPath"
|
|
value = "/"
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:elasticbeanstalk:environment:process:default"
|
|
name = "MatcherHTTPCode"
|
|
value = "200-499"
|
|
}
|
|
|
|
dynamic "setting" {
|
|
for_each = var.app_config_json_keys
|
|
content {
|
|
namespace = "aws:elasticbeanstalk:application:environmentsecrets"
|
|
name = setting.value
|
|
value = "${aws_secretsmanager_secret.app_config.arn}:${setting.value}"
|
|
}
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:elasticbeanstalk:application:environment"
|
|
name = "ASPNETCORE_ENVIRONMENT"
|
|
value = "Production"
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:elasticbeanstalk:application:environment"
|
|
name = "ASPNETCORE_URLS"
|
|
value = "http://0.0.0.0:5000"
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:elasticbeanstalk:application:environment"
|
|
name = "WorkOrderWebhook__Enabled"
|
|
value = var.work_order_webhook_enabled ? "true" : "false"
|
|
}
|
|
|
|
setting {
|
|
namespace = "aws:elasticbeanstalk:application:environment"
|
|
name = "WorkOrderWebhook__Region"
|
|
value = var.aws_region
|
|
}
|
|
|
|
dynamic "setting" {
|
|
for_each = var.webhook_secret_arn == null ? [] : [var.webhook_secret_arn]
|
|
content {
|
|
namespace = "aws:elasticbeanstalk:application:environment"
|
|
name = "WorkOrderWebhook__SecretId"
|
|
value = setting.value
|
|
}
|
|
}
|
|
|
|
tags = var.metadata_before_adoption.environment_tags
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
ignore_changes = [
|
|
wait_for_ready_timeout,
|
|
]
|
|
}
|
|
}
|
|
|
|
resource "aws_route53_record" "api_alias" {
|
|
count = var.api_record_type == "A" ? 1 : 0
|
|
|
|
zone_id = var.hosted_zone_id
|
|
name = var.api_domain
|
|
type = "A"
|
|
|
|
alias {
|
|
name = aws_elastic_beanstalk_environment.this.cname
|
|
zone_id = data.aws_elastic_beanstalk_hosted_zone.current.id
|
|
evaluate_target_health = true
|
|
}
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
}
|
|
}
|
|
|
|
resource "aws_route53_record" "api_cname" {
|
|
count = var.api_record_type == "CNAME" ? 1 : 0
|
|
|
|
zone_id = var.hosted_zone_id
|
|
name = var.api_domain
|
|
type = "CNAME"
|
|
ttl = 60
|
|
records = [aws_elastic_beanstalk_environment.this.endpoint_url]
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
}
|
|
}
|