diff --git a/argocd/config/management-cluster/kube-applier/templates/deployment.yaml b/argocd/config/management-cluster/kube-applier/templates/deployment.yaml index ebe9261a1..65117c39b 100644 --- a/argocd/config/management-cluster/kube-applier/templates/deployment.yaml +++ b/argocd/config/management-cluster/kube-applier/templates/deployment.yaml @@ -45,6 +45,12 @@ spec: - --leader-election-id={{ .Values.kubeApplier.config.leaderElectionId }} - --log-verbosity={{ .Values.kubeApplier.config.logVerbosity }} - --exit-on-panic={{ .Values.kubeApplier.config.exitOnPanic }} + {{- if .Values.kubeApplier.config.sqsQueueUrl }} + - --sqs-queue-url={{ .Values.kubeApplier.config.sqsQueueUrl }} + {{- end }} + {{- if .Values.kubeApplier.config.snsStatusTopicArn }} + - --sns-status-topic-arn={{ .Values.kubeApplier.config.snsStatusTopicArn }} + {{- end }} {{- if .Values.kubeApplier.config.awsEndpointUrl }} - --aws-endpoint-url={{ .Values.kubeApplier.config.awsEndpointUrl }} {{- end }} diff --git a/argocd/config/management-cluster/kube-applier/values.yaml b/argocd/config/management-cluster/kube-applier/values.yaml index 75c1cbe9b..43f525828 100644 --- a/argocd/config/management-cluster/kube-applier/values.yaml +++ b/argocd/config/management-cluster/kube-applier/values.yaml @@ -7,7 +7,7 @@ kubeApplier: image: registry: "quay.io" repository: "psav/kube-applier-aws" - tag: "remove-delete-desire-18d87ca" + tag: "1f8498a" pullPolicy: IfNotPresent # Deployment configuration @@ -32,6 +32,11 @@ kubeApplier: leaderElectionId: "kube-applier" logVerbosity: 4 exitOnPanic: true + # Injected at runtime from the local-cluster-identity secret annotations via the + # ArgoCD ApplicationSet valuesObject. These must not be set here — they are always + # provided by the annotation pattern (MC Terraform output → bootstrap → annotation → here). + sqsQueueUrl: "" + snsStatusTopicArn: "" serviceAccount: name: kube-applier diff --git a/argocd/config/regional-cluster/hyperfleet/templates/application.yaml b/argocd/config/regional-cluster/hyperfleet/templates/application.yaml index e0afd45ed..b6806595a 100644 --- a/argocd/config/regional-cluster/hyperfleet/templates/application.yaml +++ b/argocd/config/regional-cluster/hyperfleet/templates/application.yaml @@ -19,6 +19,7 @@ spec: {{- end }} awsRegion: {{ .Values.global.aws_region }} baseDomain: {{ .Values.baseDomain }} + sqsQueueUrlPrefix: {{ index .Values "hyperfleet" "sqsQueueUrlPrefix" | default "" | quote }} postgres: secretName: {{ .Values.global.postgres_secret_name | default "hyperfleet-db-dsn" }} secretKey: {{ .Values.global.postgres_secret_key | default "dsn" }} diff --git a/argocd/config/regional-cluster/hyperfleet/values.yaml b/argocd/config/regional-cluster/hyperfleet/values.yaml index 197c2ea23..928751eea 100644 --- a/argocd/config/regional-cluster/hyperfleet/values.yaml +++ b/argocd/config/regional-cluster/hyperfleet/values.yaml @@ -7,10 +7,12 @@ hyperfleet: project: default + sqsQueueUrlPrefix: "" + source: - repoURL: https://github.com/typeid/hyperfleet-operator.git - targetRevision: main - path: charts/hyperfleet-operator + repoURL: https://github.com/rrp-bot/rosa-hyperfleet-api.git + targetRevision: feature/sns-sqs + path: hyperfleet-operator/charts syncPolicy: automated: @@ -19,8 +21,8 @@ hyperfleet: helmValues: image: - repository: quay.io/redhat-user-workloads/rosa-tenant/hyperfleet-operator - tag: "42e0e77996c25cd4daaf4655f7d882fff0724227" + repository: quay.io/psav/hyperfleet-operator + tag: "c756faf" pullPolicy: Always replicaCount: 2 diff --git a/config/templates/argocd-bootstrap/applicationset.yaml.j2 b/config/templates/argocd-bootstrap/applicationset.yaml.j2 index 3644f07e4..bbb5904de 100644 --- a/config/templates/argocd-bootstrap/applicationset.yaml.j2 +++ b/config/templates/argocd-bootstrap/applicationset.yaml.j2 @@ -130,6 +130,18 @@ spec: sre: targetGroup: arn: '{{ '{{ .metadata.annotations.sre_prometheus_target_group_arn }}' }}' +{%- if cluster_type == 'management-cluster' %} + + kubeApplier: + config: + sqsQueueUrl: '{{ '{{ .metadata.annotations.kube_applier_specs_queue_url }}' }}' + snsStatusTopicArn: '{{ '{{ .metadata.annotations.kube_applier_status_topic_arn }}' }}' +{%- endif %} +{%- if cluster_type == 'regional-cluster' %} + + hyperfleet: + sqsQueueUrlPrefix: 'https://sqs.{{ '{{ .metadata.labels.aws_region }}' }}.amazonaws.com/{{ '{{ .metadata.annotations.aws_account_id }}' }}/{{ '{{ .metadata.labels.cluster_name }}' }}-hyperfleet-operator-' +{%- endif %} path: '{{ '{{ .path.path }}' }}' repoURL: '{{ '{{ .metadata.annotations.git_repo }}' }}' targetRevision: '{% if pinned %}{{ pinned }}{% else %}{{ "{{ .metadata.annotations.git_revision }}" }}{% endif %}' diff --git a/deploy/ephemeral/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml b/deploy/ephemeral/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml index 404b249c5..596232bc1 100644 --- a/deploy/ephemeral/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml +++ b/deploy/ephemeral/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml @@ -129,6 +129,11 @@ spec: sre: targetGroup: arn: '{{ .metadata.annotations.sre_prometheus_target_group_arn }}' + + kubeApplier: + config: + sqsQueueUrl: '{{ .metadata.annotations.kube_applier_specs_queue_url }}' + snsStatusTopicArn: '{{ .metadata.annotations.kube_applier_status_topic_arn }}' path: '{{ .path.path }}' repoURL: '{{ .metadata.annotations.git_repo }}' targetRevision: '{{ .metadata.annotations.git_revision }}' diff --git a/deploy/ephemeral/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml b/deploy/ephemeral/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml index fdff0e336..cecd48f28 100644 --- a/deploy/ephemeral/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml +++ b/deploy/ephemeral/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml @@ -127,6 +127,9 @@ spec: sre: targetGroup: arn: '{{ .metadata.annotations.sre_prometheus_target_group_arn }}' + + hyperfleet: + sqsQueueUrlPrefix: 'https://sqs.{{ .metadata.labels.aws_region }}.amazonaws.com/{{ .metadata.annotations.aws_account_id }}/{{ .metadata.labels.cluster_name }}-hyperfleet-operator-' path: '{{ .path.path }}' repoURL: '{{ .metadata.annotations.git_repo }}' targetRevision: '{{ .metadata.annotations.git_revision }}' diff --git a/deploy/integration/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml b/deploy/integration/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml index 13d2a9eb3..d05b9a5e9 100644 --- a/deploy/integration/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml +++ b/deploy/integration/us-east-1/argocd-bootstrap-management-cluster/applicationset.yaml @@ -129,6 +129,11 @@ spec: sre: targetGroup: arn: '{{ .metadata.annotations.sre_prometheus_target_group_arn }}' + + kubeApplier: + config: + sqsQueueUrl: '{{ .metadata.annotations.kube_applier_specs_queue_url }}' + snsStatusTopicArn: '{{ .metadata.annotations.kube_applier_status_topic_arn }}' path: '{{ .path.path }}' repoURL: '{{ .metadata.annotations.git_repo }}' targetRevision: '{{ .metadata.annotations.git_revision }}' diff --git a/deploy/integration/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml b/deploy/integration/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml index e39904232..96b2de2e9 100644 --- a/deploy/integration/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml +++ b/deploy/integration/us-east-1/argocd-bootstrap-regional-cluster/applicationset.yaml @@ -127,6 +127,9 @@ spec: sre: targetGroup: arn: '{{ .metadata.annotations.sre_prometheus_target_group_arn }}' + + hyperfleet: + sqsQueueUrlPrefix: 'https://sqs.{{ .metadata.labels.aws_region }}.amazonaws.com/{{ .metadata.annotations.aws_account_id }}/{{ .metadata.labels.cluster_name }}-hyperfleet-operator-' path: '{{ .path.path }}' repoURL: '{{ .metadata.annotations.git_repo }}' targetRevision: '{{ .metadata.annotations.git_revision }}' diff --git a/scripts/bootstrap-argocd.sh b/scripts/bootstrap-argocd.sh index 259260b27..49707ebfb 100755 --- a/scripts/bootstrap-argocd.sh +++ b/scripts/bootstrap-argocd.sh @@ -108,6 +108,15 @@ else REDIS_ENDPOINT="" fi +KUBE_APPLIER_SQS_QUEUE_URL="${KUBE_APPLIER_SQS_QUEUE_URL:-}" +KUBE_APPLIER_SNS_STATUS_TOPIC_ARN="${KUBE_APPLIER_SNS_STATUS_TOPIC_ARN:-}" + +# For management clusters, read the messaging outputs from terraform state. +if [[ "$CLUSTER_TYPE" == "management-cluster" ]]; then + KUBE_APPLIER_SQS_QUEUE_URL=$(echo "$OUTPUTS" | jq -r '.kube_applier_specs_queue_url.value // ""') + KUBE_APPLIER_SNS_STATUS_TOPIC_ARN=$(echo "$OUTPUTS" | jq -r '.kube_applier_status_topic_arn.value // ""') +fi + RHOBS_API_URL="${RHOBS_API_URL:-}" DNS_ZONE_OPERATOR_ROLE_ARN="${DNS_ZONE_OPERATOR_ROLE_ARN:-}" @@ -149,7 +158,9 @@ RUN_TASK_OUTPUT=$(aws ecs run-task \ {\"name\": \"SRE_THANOS_TARGET_GROUP_ARN\", \"value\": \"$SRE_THANOS_TARGET_GROUP_ARN\"}, {\"name\": \"SRE_ALB_DNS_NAME\", \"value\": \"$SRE_ALB_DNS_NAME\"}, {\"name\": \"SRE_DOMAIN\", \"value\": \"$SRE_DOMAIN\"}, - {\"name\": \"REDIS_ENDPOINT\", \"value\": \"$REDIS_ENDPOINT\"} + {\"name\": \"REDIS_ENDPOINT\", \"value\": \"$REDIS_ENDPOINT\"}, + {\"name\": \"KUBE_APPLIER_SQS_QUEUE_URL\", \"value\": \"$KUBE_APPLIER_SQS_QUEUE_URL\"}, + {\"name\": \"KUBE_APPLIER_SNS_STATUS_TOPIC_ARN\", \"value\": \"$KUBE_APPLIER_SNS_STATUS_TOPIC_ARN\"} ] }] }" 2>&1) diff --git a/scripts/buildspec/provision-kube-applier-dynamodb.sh b/scripts/buildspec/provision-kube-applier-dynamodb.sh index 68549156d..d8dad743b 100755 --- a/scripts/buildspec/provision-kube-applier-dynamodb.sh +++ b/scripts/buildspec/provision-kube-applier-dynamodb.sh @@ -43,6 +43,8 @@ export TF_VAR_mc_aws_account_id="${TARGET_ACCOUNT_ID}" export TF_VAR_rc_id="${_RC_REGIONAL_ID}" TF_VAR_enable_pitr=$(parseBool '.kube_applier_dynamodb_enable_pitr' false "$DEPLOY_CONFIG_FILE") export TF_VAR_enable_pitr +TF_VAR_operator_replica_count=$(jq -r '.operator_replica_count // 3' "$DEPLOY_CONFIG_FILE") +export TF_VAR_operator_replica_count export TF_VAR_app_code="${APP_CODE}" export TF_VAR_service_phase="${SERVICE_PHASE}" export TF_VAR_cost_center="${COST_CENTER}" diff --git a/scripts/buildspec/register.sh b/scripts/buildspec/register.sh index 117f76466..fb37e5189 100755 --- a/scripts/buildspec/register.sh +++ b/scripts/buildspec/register.sh @@ -155,3 +155,59 @@ if [ "$REG_OK" != "true" ]; then cat /tmp/register-response.json >&2 exit 1 fi + +# Wire SNS→SQS subscriptions. +# +# Both subscriptions are created here — after API registration succeeds — because +# this is the first point in the pipeline where all four resources are guaranteed +# to exist: +# Stage 1 (Deploy MC): MC SQS + MC SNS created +# Stage 2 (Provision-KubeApplier-DynamoDB): RC SNS + RC SQS created +# Stage 4 (Register, this script): safe to subscribe +# +# AWS only auto-confirms an SNS→SQS subscription when the caller is from the +# same account as the queue. The two subscriptions therefore need different +# caller identities: +# +# Specs (RC SNS → MC SQS): call subscribe from the MC account, which owns +# the specs SQS queue. The RC specs topic policy grants sns:Subscribe to +# the MC account root (AllowMCAccountSubscribe). +# +# Status (MC SNS → RC SQS): call subscribe from the RC account, which owns +# the status SQS queues. The MC status topic policy grants sns:Subscribe +# to the RC account root (AllowRCAccountSubscribe). +# +# AWS automatically removes subscriptions when their SNS topic is deleted, so +# no explicit teardown is needed — Terraform destroying a topic cleans up its +# subscriptions for free. + +SPECS_TOPIC_ARN="arn:aws:sns:${TARGET_REGION}:${RESOLVED_REGIONAL_ACCOUNT_ID}:${CLUSTER_ID}-specs-notifications" +SPECS_QUEUE_ARN="arn:aws:sqs:${TARGET_REGION}:${TARGET_ACCOUNT_ID}:${CLUSTER_ID}-specs-notifications" +STATUS_TOPIC_ARN="arn:aws:sns:${TARGET_REGION}:${TARGET_ACCOUNT_ID}:${CLUSTER_ID}-status-notifications" +OPERATOR_REPLICA_COUNT=$(jq -r '.operator_replica_count // 3' "$DEPLOY_CONFIG_FILE") + +# Specs subscription: must be called from the MC account (queue owner). +echo "Subscribing specs queue to specs topic (as MC account)" +use_mc_account +aws sns subscribe \ + --topic-arn "$SPECS_TOPIC_ARN" \ + --protocol sqs \ + --notification-endpoint "$SPECS_QUEUE_ARN" \ + --attributes '{"RawMessageDelivery":"true"}' \ + --region "$TARGET_REGION" + +# Status subscriptions: must be called from the RC account (queue owner). +echo "Subscribing ${OPERATOR_REPLICA_COUNT} operator replica queue(s) to status topic (as RC account)" +use_rc_account +for i in $(seq 0 $((OPERATOR_REPLICA_COUNT - 1))); do + STATUS_QUEUE_ARN="arn:aws:sqs:${TARGET_REGION}:${RESOLVED_REGIONAL_ACCOUNT_ID}:${RC_REGIONAL_ID}-hyperfleet-operator-${i}" + echo " Subscribing replica ${i}: ${STATUS_QUEUE_ARN}" + aws sns subscribe \ + --topic-arn "$STATUS_TOPIC_ARN" \ + --protocol sqs \ + --notification-endpoint "$STATUS_QUEUE_ARN" \ + --attributes '{"RawMessageDelivery":"true"}' \ + --region "$TARGET_REGION" +done + +echo "SNS→SQS subscriptions wired successfully" diff --git a/terraform/config/kube-applier-dynamodb-provisioning/main.tf b/terraform/config/kube-applier-dynamodb-provisioning/main.tf index cd58e1c98..ec84c8dc1 100644 --- a/terraform/config/kube-applier-dynamodb-provisioning/main.tf +++ b/terraform/config/kube-applier-dynamodb-provisioning/main.tf @@ -90,3 +90,29 @@ resource "aws_iam_role_policy" "hyperfleet_operator_dynamodb" { ] }) } + +# ============================================================================= +# kube-applier RC-side Messaging (SNS/SQS cross-account notifications) +# +# Creates the specs SNS topic in the RC account (the operator publishes here +# after writing a desire document) and the per-replica status SQS queues +# (the operator polls its own queue for status notifications from kube-applier). +# +# Both modules use predictable ARNs to reference cross-account resources, so +# there is no dependency on MC Terraform outputs and no count gate. Both sides +# can be provisioned independently in a single pipeline run. +# +# Subscriptions (RC SNS → MC SQS and MC SNS → RC SQS × N) are created by +# the Register buildspec step, after both modules have run and all four +# resources are guaranteed to exist. +# ============================================================================= + +module "kube_applier_rc_messaging" { + source = "../../modules/kube-applier-rc-messaging" + + mc_name = var.mc_name + mc_aws_account_id = var.mc_aws_account_id + rc_id = var.rc_id + aws_region = var.region + operator_replica_count = var.operator_replica_count +} diff --git a/terraform/config/kube-applier-dynamodb-provisioning/outputs.tf b/terraform/config/kube-applier-dynamodb-provisioning/outputs.tf index dcbc20536..095a2679d 100644 --- a/terraform/config/kube-applier-dynamodb-provisioning/outputs.tf +++ b/terraform/config/kube-applier-dynamodb-provisioning/outputs.tf @@ -12,3 +12,20 @@ output "status_readdesires_stream_arn" { description = "Stream ARN for the status-readdesires table" value = module.kube_applier_dynamodb.status_readdesires_stream_arn } + +# ============================================================================= +# Messaging Outputs +# Read by the MC management-cluster buildspec to pass rc_specs_sns_topic_arn +# into the MC terraform run. +# ============================================================================= + +output "specs_sns_topic_arn" { + description = "ARN of the RC-account specs SNS topic for this MC (operator publishes here after writing a desire document)." + value = module.kube_applier_rc_messaging.specs_topic_arn +} + +output "status_sqs_queue_urls" { + description = "URLs of the RC-account operator status SQS queues (one per replica)." + value = module.kube_applier_rc_messaging.status_queue_urls +} + diff --git a/terraform/config/kube-applier-dynamodb-provisioning/variables.tf b/terraform/config/kube-applier-dynamodb-provisioning/variables.tf index 0a5969c03..038daa7ea 100644 --- a/terraform/config/kube-applier-dynamodb-provisioning/variables.tf +++ b/terraform/config/kube-applier-dynamodb-provisioning/variables.tf @@ -53,3 +53,9 @@ variable "environment" { description = "Environment name (staging, production, etc.)" type = string } + +variable "operator_replica_count" { + description = "Number of hyperfleet-operator replicas. One status SQS queue is created per replica in the RC account." + type = number + default = 3 +} diff --git a/terraform/config/management-cluster/main.tf b/terraform/config/management-cluster/main.tf index 63ff13b14..42aa9cd92 100755 --- a/terraform/config/management-cluster/main.tf +++ b/terraform/config/management-cluster/main.tf @@ -68,6 +68,9 @@ module "ecs_bootstrap" { repository_url = var.repository_url repository_branch = var.repository_branch + + kube_applier_specs_queue_url = module.kube_applier_mc_messaging.specs_queue_url + kube_applier_status_topic_arn = module.kube_applier_mc_messaging.status_topic_arn } # ============================================================================= @@ -187,3 +190,26 @@ module "kube_applier" { rc_aws_account_id = var.regional_aws_account_id aws_region = var.region } + +# ============================================================================= +# kube-applier MC-side Messaging (SNS/SQS cross-account notifications) +# +# Creates the specs SQS queue (receives notifications from the RC specs SNS +# topic when the operator writes a new desire document) and the status SNS +# topic (kube-applier publishes here after writing a status document so the +# RC-side operator queues are notified immediately). +# +# rc_specs_sns_topic_arn is read from the RC kube-applier-dynamodb terraform +# state by the buildspec script and passed in as TF_VAR_rc_specs_sns_topic_arn. +# When empty (e.g. during initial bootstrap before the RC run completes) the +# module is skipped and messaging falls back to 5-minute safety polling. +# ============================================================================= + +module "kube_applier_mc_messaging" { + source = "../../modules/kube-applier-mc-messaging" + + mc_name = var.management_id + rc_aws_account_id = var.regional_aws_account_id + eks_cluster_name = module.management_cluster.cluster_name + aws_region = var.region +} diff --git a/terraform/config/management-cluster/outputs.tf b/terraform/config/management-cluster/outputs.tf index ec584de3c..180b6dddf 100755 --- a/terraform/config/management-cluster/outputs.tf +++ b/terraform/config/management-cluster/outputs.tf @@ -166,3 +166,27 @@ output "kube_applier_role_arn" { description = "IAM role ARN for the kube-applier-aws controller" value = module.kube_applier.kube_applier_role_arn } + +# ============================================================================= +# kube-applier Messaging Outputs +# Read by bootstrap-argocd.sh to wire the queue URL and topic ARN into the +# ArgoCD cluster secret annotations, which the ApplicationSet then passes +# to the kube-applier Helm chart as --sqs-queue-url and --sns-status-topic-arn. +# Also read by the RC kube-applier-dynamodb buildspec to wire cross-account +# SNS subscriptions. +# ============================================================================= + +output "kube_applier_specs_queue_arn" { + description = "ARN of the MC-side specs SQS queue (receives RC specs SNS notifications)." + value = module.kube_applier_mc_messaging.specs_queue_arn +} + +output "kube_applier_specs_queue_url" { + description = "URL of the MC-side specs SQS queue (polled by kube-applier for spec change notifications)." + value = module.kube_applier_mc_messaging.specs_queue_url +} + +output "kube_applier_status_topic_arn" { + description = "ARN of the MC-side status SNS topic (kube-applier publishes here after writing status)." + value = module.kube_applier_mc_messaging.status_topic_arn +} diff --git a/terraform/config/management-cluster/variables.tf b/terraform/config/management-cluster/variables.tf index e9b0dbc54..694f25827 100755 --- a/terraform/config/management-cluster/variables.tf +++ b/terraform/config/management-cluster/variables.tf @@ -138,4 +138,3 @@ variable "oidc_cloudfront_domain" { type = string default = "" } - diff --git a/terraform/modules/ecs-bootstrap/main.tf b/terraform/modules/ecs-bootstrap/main.tf index 622d2db08..a422db5b9 100644 --- a/terraform/modules/ecs-bootstrap/main.tf +++ b/terraform/modules/ecs-bootstrap/main.tf @@ -226,6 +226,8 @@ resource "aws_ecs_task_definition" "bootstrap" { sre_alb_dns_name: "$SRE_ALB_DNS_NAME" sre_domain: "$SRE_DOMAIN" redis_endpoint: "$REDIS_ENDPOINT" + kube_applier_specs_queue_url: "$KUBE_APPLIER_SQS_QUEUE_URL" + kube_applier_status_topic_arn: "$KUBE_APPLIER_SNS_STATUS_TOPIC_ARN" type: Opaque stringData: name: in-cluster @@ -298,6 +300,14 @@ resource "aws_ecs_task_definition" "bootstrap" { { name = "REDIS_ENDPOINT" value = var.redis_endpoint + }, + { + name = "KUBE_APPLIER_SQS_QUEUE_URL" + value = var.kube_applier_specs_queue_url + }, + { + name = "KUBE_APPLIER_SNS_STATUS_TOPIC_ARN" + value = var.kube_applier_status_topic_arn } ] diff --git a/terraform/modules/ecs-bootstrap/variables.tf b/terraform/modules/ecs-bootstrap/variables.tf index 07e002171..ea4f24f39 100644 --- a/terraform/modules/ecs-bootstrap/variables.tf +++ b/terraform/modules/ecs-bootstrap/variables.tf @@ -82,3 +82,15 @@ variable "redis_endpoint" { default = "" } +variable "kube_applier_specs_queue_url" { + description = "URL of the MC-side SQS queue that kube-applier polls for spec change notifications. Written as an annotation on the local-cluster-identity secret so the ApplicationSet can pass it to the kube-applier Helm chart." + type = string + default = "" +} + +variable "kube_applier_status_topic_arn" { + description = "ARN of the MC-side SNS topic that kube-applier publishes status updates to. Written as an annotation on the local-cluster-identity secret so the ApplicationSet can pass it to the kube-applier Helm chart." + type = string + default = "" +} + diff --git a/terraform/modules/kube-applier-mc-messaging/main.tf b/terraform/modules/kube-applier-mc-messaging/main.tf new file mode 100644 index 000000000..1beeb7b9a --- /dev/null +++ b/terraform/modules/kube-applier-mc-messaging/main.tf @@ -0,0 +1,266 @@ +# ============================================================================= +# kube-applier-mc-messaging Module +# +# Provisions the MC-side messaging resources for the two-way SNS/SQS +# notification system between the hyperfleet-operator (RC account) and +# kube-applier-aws (MC account). +# +# Specs path (RC → MC): The RC account publishes to an SNS topic when it +# writes a new desire document. This module creates the SQS queue in the MC +# account that receives those notifications. kube-applier polls this queue +# instead of DynamoDB Streams. +# +# Status path (MC → RC): kube-applier publishes to an SNS topic in the MC +# account after writing a status document. This module creates that topic. +# The RC account provisions the corresponding SQS queues and subscriptions. +# +# Resource naming: +# Specs SQS queue: ${mc_name}-specs-notifications (MC account) +# Status SNS topic: ${mc_name}-status-notifications (MC account) +# KMS key alias: alias/${mc_name}-kube-applier-messaging +# ============================================================================= + +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +locals { + common_tags = merge( + var.tags, + { + ManagedBy = "terraform" + Module = "kube-applier-mc-messaging" + ManagementCluster = var.mc_name + } + ) + + # IAM role ARN for the kube-applier pod in this MC account + kube_applier_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.mc_name}-kube-applier" + + # RC specs SNS topic ARN — predictable, constructed from known values. + # Used in the SQS queue policy so delivery is authorised from day one, + # before the RC messaging module has run. + rc_specs_sns_topic_arn = "arn:aws:sns:${var.aws_region}:${var.rc_aws_account_id}:${var.mc_name}-specs-notifications" +} + +# ============================================================================= +# KMS Key — shared encryption key for MC-side messaging resources +# ============================================================================= + +resource "aws_kms_key" "messaging" { + description = "KMS key for ${var.mc_name} kube-applier messaging (SQS + SNS)" + deletion_window_in_days = 7 + enable_key_rotation = true + rotation_period_in_days = 90 + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "EnableRootAccess" + Effect = "Allow" + Principal = { + AWS = "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:root" + } + Action = "kms:*" + Resource = "*" + }, + { + # SNS must be able to encrypt/decrypt when delivering messages to SQS. + # For cross-account delivery (RC SNS → MC SQS), SNS acts on behalf of + # the RC account, so aws:SourceAccount will be the RC account ID. + Sid = "AllowSNSDelivery" + Effect = "Allow" + Principal = { + Service = "sns.amazonaws.com" + } + Action = [ + "kms:Decrypt", + "kms:GenerateDataKey*", + ] + Resource = "*" + Condition = { + StringEquals = { + "aws:SourceAccount" = [ + data.aws_caller_identity.current.account_id, + var.rc_aws_account_id, + ] + } + } + }, + { + Sid = "AllowSQS" + Effect = "Allow" + Principal = { + Service = "sqs.amazonaws.com" + } + Action = [ + "kms:Decrypt", + "kms:GenerateDataKey*", + ] + Resource = "*" + Condition = { + StringEquals = { + "aws:SourceAccount" = data.aws_caller_identity.current.account_id + } + } + }, + ] + }) + + tags = merge(local.common_tags, { + Name = "${var.mc_name}-kube-applier-messaging" + }) +} + +resource "aws_kms_alias" "messaging" { + name = "alias/${var.mc_name}-kube-applier-messaging" + target_key_id = aws_kms_key.messaging.key_id +} + +# ============================================================================= +# Specs SQS Queue (specs path receiver — RC SNS → MC SQS) +# +# kube-applier polls this queue for notifications that the operator has written +# a new desire document. On receipt, it immediately re-queues the affected +# documentID for reconciliation instead of waiting for the 5-minute safety poll. +# ============================================================================= + +resource "aws_sqs_queue" "specs" { + name = "${var.mc_name}-specs-notifications" + kms_master_key_id = aws_kms_key.messaging.id + message_retention_seconds = 300 # 5 minutes — notifications are ephemeral wake-up signals + visibility_timeout_seconds = 30 + receive_wait_time_seconds = 20 # long-polling + + tags = merge(local.common_tags, { + Name = "${var.mc_name}-specs-notifications" + Direction = "specs-rc-to-mc" + }) +} + +# Allow the RC-account specs SNS topic to deliver messages to this queue. +# AWS requires both an identity-based policy on the SNS topic AND a +# resource-based policy on the SQS queue for cross-account delivery. +resource "aws_sqs_queue_policy" "specs" { + queue_url = aws_sqs_queue.specs.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Sid = "AllowRCSpecsSNSDelivery" + Effect = "Allow" + Principal = { + Service = "sns.amazonaws.com" + } + Action = "sqs:SendMessage" + Resource = aws_sqs_queue.specs.arn + Condition = { + ArnEquals = { + "aws:SourceArn" = local.rc_specs_sns_topic_arn + } + } + }] + }) +} + +# ============================================================================= +# Status SNS Topic (status path sender — MC SNS → RC SQS) +# +# kube-applier publishes a lightweight notification here after successfully +# writing a status document. The RC account subscribes its per-replica operator +# SQS queues to this topic for cross-account delivery. +# ============================================================================= + +resource "aws_sns_topic" "status" { + name = "${var.mc_name}-status-notifications" + kms_master_key_id = aws_kms_key.messaging.id + + tags = merge(local.common_tags, { + Name = "${var.mc_name}-status-notifications" + Direction = "status-mc-to-rc" + }) +} + +# Allow the kube-applier pod role to publish status notifications. +# The RC account (as subscriber) is also given sns:Subscribe so that the +# subscription created in the RC account's kube-applier-rc-messaging module +# can be confirmed without requiring manual approval. +resource "aws_sns_topic_policy" "status" { + arn = aws_sns_topic.status.arn + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowKubeApplierPublish" + Effect = "Allow" + Principal = { + AWS = local.kube_applier_role_arn + } + Action = "sns:Publish" + Resource = aws_sns_topic.status.arn + }, + { + # Allow the RC account to create cross-account SQS subscriptions. + # Without this, aws_sns_topic_subscription from the RC module would + # fail with an AuthorizationError even if the SQS queue policy permits + # delivery. + Sid = "AllowRCAccountSubscribe" + Effect = "Allow" + Principal = { + AWS = "arn:${data.aws_partition.current.partition}:iam::${var.rc_aws_account_id}:root" + } + Action = [ + "sns:Subscribe", + ] + Resource = aws_sns_topic.status.arn + }, + ] + }) +} + +# ============================================================================= +# IAM: extend kube-applier role with messaging permissions +# +# The kube-applier role is created by the kube-applier module. We add a +# supplementary inline policy here so that all messaging IAM is co-located +# with the messaging infrastructure rather than scattered across modules. +# ============================================================================= + +resource "aws_iam_role_policy" "kube_applier_messaging" { + name = "${var.mc_name}-kube-applier-messaging" + role = "${var.mc_name}-kube-applier" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "SpecsQueueReceive" + Effect = "Allow" + Action = [ + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + ] + Resource = aws_sqs_queue.specs.arn + }, + { + Sid = "StatusTopicPublish" + Effect = "Allow" + Action = [ + "sns:Publish", + ] + Resource = aws_sns_topic.status.arn + }, + { + Sid = "MessagingKMSAccess" + Effect = "Allow" + Action = [ + "kms:Decrypt", + "kms:GenerateDataKey*", + ] + Resource = aws_kms_key.messaging.arn + }, + ] + }) +} diff --git a/terraform/modules/kube-applier-mc-messaging/outputs.tf b/terraform/modules/kube-applier-mc-messaging/outputs.tf new file mode 100644 index 000000000..a31c72790 --- /dev/null +++ b/terraform/modules/kube-applier-mc-messaging/outputs.tf @@ -0,0 +1,28 @@ +# ============================================================================= +# kube-applier-mc-messaging Module Outputs +# ============================================================================= + +output "specs_queue_arn" { + description = "ARN of the specs SQS queue that receives notifications from the RC specs SNS topic" + value = aws_sqs_queue.specs.arn +} + +output "specs_queue_url" { + description = "URL of the specs SQS queue for use by kube-applier" + value = aws_sqs_queue.specs.url +} + +output "status_topic_arn" { + description = "ARN of the status SNS topic that kube-applier publishes to after writing status documents" + value = aws_sns_topic.status.arn +} + +output "status_topic_name" { + description = "Name of the status SNS topic" + value = aws_sns_topic.status.name +} + +output "kms_key_arn" { + description = "ARN of the KMS key used to encrypt MC-side messaging resources" + value = aws_kms_key.messaging.arn +} diff --git a/terraform/modules/kube-applier-mc-messaging/variables.tf b/terraform/modules/kube-applier-mc-messaging/variables.tf new file mode 100644 index 000000000..a32267814 --- /dev/null +++ b/terraform/modules/kube-applier-mc-messaging/variables.tf @@ -0,0 +1,39 @@ +# ============================================================================= +# kube-applier-mc-messaging Module - Input Variables +# ============================================================================= + +variable "mc_name" { + description = "Management cluster identifier (e.g., 'mc01'). Used as a prefix for resource names." + type = string + + validation { + condition = can(regex("^[a-z0-9-]+$", var.mc_name)) + error_message = "mc_name must contain only lowercase letters, numbers, and hyphens" + } +} + +variable "rc_aws_account_id" { + description = "AWS account ID of the regional cluster. Used to scope IAM and queue policies." + type = string + + validation { + condition = can(regex("^[0-9]{12}$", var.rc_aws_account_id)) + error_message = "rc_aws_account_id must be a 12-digit AWS account ID" + } +} + +variable "eks_cluster_name" { + description = "Name of the EKS management cluster. Used for the Pod Identity association." + type = string +} + +variable "aws_region" { + description = "AWS region where resources are created." + type = string +} + +variable "tags" { + description = "Additional tags to apply to resources." + type = map(string) + default = {} +} diff --git a/terraform/modules/kube-applier-mc-messaging/versions.tf b/terraform/modules/kube-applier-mc-messaging/versions.tf new file mode 100644 index 000000000..ddfcb0e05 --- /dev/null +++ b/terraform/modules/kube-applier-mc-messaging/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} diff --git a/terraform/modules/kube-applier-rc-messaging/main.tf b/terraform/modules/kube-applier-rc-messaging/main.tf new file mode 100644 index 000000000..9407ed6a2 --- /dev/null +++ b/terraform/modules/kube-applier-rc-messaging/main.tf @@ -0,0 +1,298 @@ +# ============================================================================= +# kube-applier-rc-messaging Module +# +# Provisions the RC-side messaging resources for the two-way SNS/SQS +# notification system between the hyperfleet-operator (RC account) and +# kube-applier-aws (MC account). +# +# Specs path (RC → MC): The hyperfleet-operator publishes to an SNS topic in +# the RC account after writing a desire document. This module creates that +# topic and subscribes the MC-side specs SQS queue (mc_specs_queue_arn) to +# it, forming the cross-account delivery link. +# +# Status path (MC → RC): kube-applier publishes status notifications to an SNS +# topic in the MC account. This module creates one SQS queue per operator +# replica in the RC account and subscribes each to the MC status SNS topic +# (mc_status_sns_topic_arn). Each operator pod drains its own queue. +# +# Resource naming: +# Specs SNS topic: ${mc_name}-specs-notifications (RC account) +# Status SQS queues: ${rc_id}-hyperfleet-operator-{0..N-1} (RC account) +# KMS key alias: alias/${mc_name}-kube-applier-messaging +# +# Incremental IAM pattern: +# Like the existing ${mc_name}-dynamodb-access policy, this module attaches +# a per-MC inline policy (${mc_name}-messaging-access) to the shared +# ${rc_id}-hyperfleet-operator role. Each MC pipeline run adds its own +# policy, so parallel per-MC state files never collide. +# ============================================================================= + +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +locals { + common_tags = merge( + var.tags, + { + ManagedBy = "terraform" + Module = "kube-applier-rc-messaging" + ManagementCluster = var.mc_name + } + ) + + # Ordinal indices for the operator replica queues (0-based) + replica_indices = range(var.operator_replica_count) + + # IAM role for the hyperfleet-operator (RC account, shared across all MCs) + hyperfleet_operator_role_name = "${var.rc_id}-hyperfleet-operator" + + # MC ARNs — predictable, constructed from known values. + # Used in policies and subscriptions so the RC module is self-contained + # and does not depend on outputs from the MC Terraform apply. + mc_specs_queue_arn = "arn:aws:sqs:${var.aws_region}:${var.mc_aws_account_id}:${var.mc_name}-specs-notifications" + mc_status_sns_topic_arn = "arn:aws:sns:${var.aws_region}:${var.mc_aws_account_id}:${var.mc_name}-status-notifications" +} + +# ============================================================================= +# KMS Key — shared encryption key for RC-side messaging resources (per MC) +# ============================================================================= + +resource "aws_kms_key" "messaging" { + description = "KMS key for ${var.mc_name} kube-applier messaging (SNS + SQS) in RC account" + deletion_window_in_days = 7 + enable_key_rotation = true + rotation_period_in_days = 90 + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "EnableRootAccess" + Effect = "Allow" + Principal = { + AWS = "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:root" + } + Action = "kms:*" + Resource = "*" + }, + { + # SNS must be able to encrypt/decrypt when delivering messages to SQS. + # For cross-account delivery (MC SNS → RC SQS), SNS acts on behalf of + # the MC account, so aws:SourceAccount will be the MC account ID. + # Both RC and MC account IDs are required. + Sid = "AllowSNSDelivery" + Effect = "Allow" + Principal = { + Service = "sns.amazonaws.com" + } + Action = [ + "kms:Decrypt", + "kms:GenerateDataKey*", + ] + Resource = "*" + Condition = { + StringEquals = { + "aws:SourceAccount" = [ + data.aws_caller_identity.current.account_id, + var.mc_aws_account_id, + ] + } + } + }, + { + Sid = "AllowSQS" + Effect = "Allow" + Principal = { + Service = "sqs.amazonaws.com" + } + Action = [ + "kms:Decrypt", + "kms:GenerateDataKey*", + ] + Resource = "*" + Condition = { + StringEquals = { + "aws:SourceAccount" = data.aws_caller_identity.current.account_id + } + } + }, + ] + }) + + tags = merge(local.common_tags, { + Name = "${var.mc_name}-kube-applier-messaging" + }) +} + +resource "aws_kms_alias" "messaging" { + name = "alias/${var.mc_name}-kube-applier-messaging" + target_key_id = aws_kms_key.messaging.key_id +} + +# ============================================================================= +# Specs SNS Topic (specs path sender — RC SNS → MC SQS) +# +# The hyperfleet-operator publishes a lightweight notification here after +# writing an ApplyDesire or ReadDesire document. The cross-account subscription +# below delivers that notification to the MC-side SQS queue. +# ============================================================================= + +resource "aws_sns_topic" "specs" { + name = "${var.mc_name}-specs-notifications" + kms_master_key_id = aws_kms_key.messaging.id + + tags = merge(local.common_tags, { + Name = "${var.mc_name}-specs-notifications" + Direction = "specs-rc-to-mc" + }) +} + +# Allow the hyperfleet-operator pod role to publish specs notifications. +# The MC account root is also granted sns:Subscribe so that register.sh +# (running as OrganizationAccountAccessRole in the MC account) can create the +# cross-account subscription and have it auto-confirmed. AWS only auto-confirms +# SNS→SQS subscriptions when the caller is from the same account as the queue; +# since the specs SQS queue is in the MC account, the subscribe call must come +# from the MC account. +resource "aws_sns_topic_policy" "specs" { + arn = aws_sns_topic.specs.arn + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowHyperfleetOperatorPublish" + Effect = "Allow" + Principal = { + AWS = "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:role/${local.hyperfleet_operator_role_name}" + } + Action = "sns:Publish" + Resource = aws_sns_topic.specs.arn + }, + { + # Allow the MC account to create the cross-account SQS subscription. + # register.sh assumes OrganizationAccountAccessRole in the MC account + # before calling aws sns subscribe on this topic. AWS only auto-confirms + # an SNS→SQS subscription when the subscriber caller is from the same + # account as the queue — so the subscribe call must come from the MC + # account (which owns the specs SQS queue), not the RC account. + Sid = "AllowMCAccountSubscribe" + Effect = "Allow" + Principal = { + AWS = "arn:${data.aws_partition.current.partition}:iam::${var.mc_aws_account_id}:root" + } + Action = "sns:Subscribe" + Resource = aws_sns_topic.specs.arn + }, + ] + }) +} + +# ============================================================================= +# Status SQS Queues (status path receiver — MC SNS → RC SQS) +# +# One queue per hyperfleet-operator pod replica. Each pod polls only its own +# queue (named after its hostname, e.g. hyperfleet-operator-2), eliminating +# competing-consumer problems and making queue drain deterministic on scale-down. +# ============================================================================= + +resource "aws_sqs_queue" "status" { + count = var.operator_replica_count + + name = "${var.rc_id}-hyperfleet-operator-${count.index}" + kms_master_key_id = aws_kms_key.messaging.id + message_retention_seconds = 300 # 5 minutes — notifications are ephemeral wake-up signals + visibility_timeout_seconds = 30 + receive_wait_time_seconds = 20 # long-polling + + tags = merge(local.common_tags, { + Name = "${var.rc_id}-hyperfleet-operator-${count.index}" + Direction = "status-mc-to-rc" + Replica = tostring(count.index) + }) +} + +# Allow the MC-account status SNS topic to deliver messages to each queue. +resource "aws_sqs_queue_policy" "status" { + count = var.operator_replica_count + queue_url = aws_sqs_queue.status[count.index].id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Sid = "AllowMCStatusSNSDelivery" + Effect = "Allow" + Principal = { + Service = "sns.amazonaws.com" + } + Action = "sqs:SendMessage" + Resource = aws_sqs_queue.status[count.index].arn + Condition = { + ArnEquals = { + "aws:SourceArn" = local.mc_status_sns_topic_arn + } + } + }] + }) +} + +# ============================================================================= +# IAM: extend hyperfleet-operator role with messaging permissions (per-MC) +# +# Follows the same incremental pattern as ${mc_name}-dynamodb-access: each MC +# pipeline run attaches its own named policy to the shared operator role. +# Parallel per-MC state files never collide because policy names are unique. +# ============================================================================= + +resource "aws_iam_role_policy" "hyperfleet_operator_messaging" { + name = "${var.mc_name}-messaging-access" + role = local.hyperfleet_operator_role_name + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "SpecsTopicPublish" + Effect = "Allow" + Action = [ + "sns:Publish", + ] + Resource = aws_sns_topic.specs.arn + }, + { + Sid = "StatusQueuesReceive" + Effect = "Allow" + Action = [ + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + ] + Resource = aws_sqs_queue.status[*].arn + }, + { + Sid = "MessagingKMSAccess" + Effect = "Allow" + Action = [ + "kms:Decrypt", + "kms:GenerateDataKey*", + ] + Resource = aws_kms_key.messaging.arn + }, + ] + }) +} + +# ============================================================================= +# SSM Parameter — specs topic ARN for operator configuration +# ============================================================================= + +resource "aws_ssm_parameter" "specs_topic_arn" { + name = "/${var.rc_id}/${var.mc_name}/messaging/specs-topic-arn" + description = "SNS topic ARN for ${var.mc_name} specs change notifications (RC → MC)" + type = "String" + value = aws_sns_topic.specs.arn + + tags = merge(local.common_tags, { + Name = "${var.rc_id}-${var.mc_name}-specs-topic-arn" + }) +} diff --git a/terraform/modules/kube-applier-rc-messaging/outputs.tf b/terraform/modules/kube-applier-rc-messaging/outputs.tf new file mode 100644 index 000000000..7d2b50b92 --- /dev/null +++ b/terraform/modules/kube-applier-rc-messaging/outputs.tf @@ -0,0 +1,28 @@ +# ============================================================================= +# kube-applier-rc-messaging Module Outputs +# ============================================================================= + +output "specs_topic_arn" { + description = "ARN of the specs SNS topic in the RC account that the hyperfleet-operator publishes to" + value = aws_sns_topic.specs.arn +} + +output "specs_topic_name" { + description = "Name of the specs SNS topic" + value = aws_sns_topic.specs.name +} + +output "status_queue_arns" { + description = "ARNs of the RC-side status SQS queues (one per operator replica, indexed 0..N-1)" + value = aws_sqs_queue.status[*].arn +} + +output "status_queue_urls" { + description = "URLs of the RC-side status SQS queues (one per operator replica, indexed 0..N-1)" + value = aws_sqs_queue.status[*].url +} + +output "kms_key_arn" { + description = "ARN of the KMS key used to encrypt RC-side messaging resources for this MC" + value = aws_kms_key.messaging.arn +} diff --git a/terraform/modules/kube-applier-rc-messaging/variables.tf b/terraform/modules/kube-applier-rc-messaging/variables.tf new file mode 100644 index 000000000..bced11cda --- /dev/null +++ b/terraform/modules/kube-applier-rc-messaging/variables.tf @@ -0,0 +1,50 @@ +# ============================================================================= +# kube-applier-rc-messaging Module - Input Variables +# ============================================================================= + +variable "mc_name" { + description = "Management cluster identifier (e.g., 'mc01'). Used as a prefix for per-MC resource names." + type = string + + validation { + condition = can(regex("^[a-z0-9-]+$", var.mc_name)) + error_message = "mc_name must contain only lowercase letters, numbers, and hyphens" + } +} + +variable "mc_aws_account_id" { + description = "AWS account ID of the management cluster. Used to scope IAM and queue policies." + type = string + + validation { + condition = can(regex("^[0-9]{12}$", var.mc_aws_account_id)) + error_message = "mc_aws_account_id must be a 12-digit AWS account ID" + } +} + +variable "rc_id" { + description = "Regional cluster identifier for resource naming (e.g., 'regional'). Used to name the operator SQS queues." + type = string +} + +variable "aws_region" { + description = "AWS region where resources are created." + type = string +} + +variable "operator_replica_count" { + description = "Number of hyperfleet-operator pod replicas. One SQS queue is created per replica so that each pod drains its own queue without competing consumers." + type = number + default = 3 + + validation { + condition = var.operator_replica_count >= 1 && var.operator_replica_count <= 10 + error_message = "operator_replica_count must be between 1 and 10" + } +} + +variable "tags" { + description = "Additional tags to apply to resources." + type = map(string) + default = {} +} diff --git a/terraform/modules/kube-applier-rc-messaging/versions.tf b/terraform/modules/kube-applier-rc-messaging/versions.tf new file mode 100644 index 000000000..ddfcb0e05 --- /dev/null +++ b/terraform/modules/kube-applier-rc-messaging/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +}