This is an automated email from the ASF dual-hosted git repository.
lahirujayathilake pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
The following commit(s) were added to refs/heads/master by this push:
new 19b6f5e06 AWS Terraform configuration for network, keycloak and vault
deployment
19b6f5e06 is described below
commit 19b6f5e06fb44007cdeaef2b1b2b2a6cd25ab1be
Author: lahiruj <[email protected]>
AuthorDate: Sun Nov 3 23:47:25 2024 -0500
AWS Terraform configuration for network, keycloak and vault deployment
---
deployment/terraform/aws/README.md | 27 ++
deployment/terraform/aws/main.tf | 115 ++++++++
deployment/terraform/aws/modules/keycloak/main.tf | 282 +++++++++++++++++++
.../terraform/aws/modules/keycloak/outputs.tf | 35 +++
.../aws/modules/keycloak/resources/Dockerfile | 33 +++
.../keycloak/resources/cache-ispn-jdbc-ping.xml | 86 ++++++
.../keycloak/resources/docker-entrypoint.sh | 26 ++
.../keycloak/templates/container_definition.json | 113 ++++++++
.../terraform/aws/modules/keycloak/variables.tf | 231 ++++++++++++++++
.../terraform/aws/modules/keycloak/versions.tf | 10 +
deployment/terraform/aws/modules/network/main.tf | 88 ++++++
.../terraform/aws/modules/network/outputs.tf | 27 ++
.../terraform/aws/modules/network/variables.tf | 19 ++
.../terraform/aws/modules/network/versions.tf | 10 +
deployment/terraform/aws/modules/vault/README.md | 8 +
deployment/terraform/aws/modules/vault/main.tf | 250 +++++++++++++++++
.../aws/modules/vault/resources/openssl-vault.cnf | 21 ++
.../vault/templates/install_vault_script.sh.tpl | 75 +++++
.../terraform/aws/modules/vault/variables.tf | 124 +++++++++
deployment/terraform/aws/modules/vault/versions.tf | 10 +
deployment/terraform/aws/outputs.tf | 52 ++++
deployment/terraform/aws/terraform.tfvars | 39 +++
deployment/terraform/aws/variables.tf | 302 +++++++++++++++++++++
23 files changed, 1983 insertions(+)
diff --git a/deployment/terraform/aws/README.md
b/deployment/terraform/aws/README.md
new file mode 100644
index 000000000..cea07ed81
--- /dev/null
+++ b/deployment/terraform/aws/README.md
@@ -0,0 +1,27 @@
+# Custos AWS Deployment
+
+### Create the Deployment
+
+Follow the instructions below to create the AWS deployment:
+
+1. Navigate to `airavata-custos/terraform/aws` and run the `terraform init`
command to initialize the Terraform
+ workspace, which includes downloading the required Terraform modules and
providers.
+2. Run `terraform plan` to preview the infrastructure changes that Terraform
plans to make based on the configuration
+ files. This command allows you to review what will be created, updated, or
destroyed.
+3. Run `terraform apply -auto-approve` to execute the proposed changes and
create the resources, including the state
+ bucket and DynamoDB locking table. When this command is executed for the
first time, the Terraform state will be
+ stored locally. To migrate the Terraform state to AWS, run `terraform init
-force-copy` only once after the resources
+ are created.
+
+### Destroy the Deployment
+
+To safely destroy the deployment, follow these steps:
+
+1. Set the `force_destroy` parameter to `true` in the
`terraform_state_backend` module within the `main.tf` file. This
+ step is necessary to ensure the S3 state bucket can be deleted even if it
contains objects.
+2. Run `terraform apply -target module.terraform_state_backend -auto-approve`
to apply this change, enabling the
+ deletion of the S3 state bucket.
+3. Run `terraform init -force-copy` to migrate the Terraform state from the S3
bucket back to local storage. This step
+ prepares for a complete destruction of the infrastructure.
+4. Finally, run `terraform destroy -auto-approve` to remove all resources
defined in the Terraform configuration. This
+ command deletes the deployment from AWS.
diff --git a/deployment/terraform/aws/main.tf b/deployment/terraform/aws/main.tf
new file mode 100644
index 000000000..352a1d74c
--- /dev/null
+++ b/deployment/terraform/aws/main.tf
@@ -0,0 +1,115 @@
+provider "aws" {
+ region = var.region
+}
+
+module "terraform_state_backend" {
+ source = "cloudposse/tfstate-backend/aws"
+ version = "1.4.1"
+ environment = var.environment
+ name = "tf-state"
+ namespace = var.namespace
+ tags = var.tags
+ terraform_backend_config_file_path = "./config"
+ terraform_backend_config_file_name = "backend.tf"
+ force_destroy = false
+}
+
+module "network" {
+ count = var.enable_network ? 1 : 0
+ source = "./modules/network"
+ private_cidr = var.private_cidr
+ public_cidr = var.public_cidr
+ tags = merge(
+ var.tags,
+ {
+ "Environment" = var.environment
+ }
+ )
+ vpc_cidr = var.vpc_cidr
+}
+
+data "aws_subnet" "selected" {
+ for_each = var.enable_network ? [] : toset(var.private_subnet_ids)
+ id = each.value
+}
+
+locals {
+ private_subnet_ids = var.enable_network ?
module.network[0].private_subnet_ids : var.private_subnet_ids
+ private_subnet_cidrs = var.enable_network ?
module.network[0].private_subnet_cidrs : [
+ for s in data.aws_subnet.selected : s.cidr_block
+ ]
+ public_subnet_ids = var.enable_network ? module.network[0].public_subnet_ids
: var.public_subnet_ids
+ vpc_id = var.enable_network ? module.network[0].vpc_id :
var.vpc_id
+ rds_source_region = var.enable_network ?
slice(module.network[0].availability_zones, 0, 1)[0] : var.rds_source_region
+}
+
+module "keycloak" {
+ source = "./modules/keycloak"
+ alb_certificate_arn = var.keycloak_alb_certificate_arn
+ alb_destroy_log_bucket = var.alb_destroy_log_bucket
+ container_cpu_units = var.container_cpu_units
+ container_memory_limit = var.container_memory_limit
+ container_memory_reserved = var.container_memory_reserved
+ container_port = var.keycloak_container_port
+ db_backup_retention_days = var.db_backup_retention_days
+ db_backup_window = var.db_backup_window
+ db_cluster_family = var.db_cluster_family
+ db_cluster_size = var.db_cluster_size
+ db_engine_version = var.db_engine_version
+ db_instance_type = var.db_instance_type
+ db_maintenance_window = var.db_maintenance_window
+ deletion_protection = var.deletion_protection
+ deployment_maximum_percent = var.deployment_maximum_percent
+ deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent
+ desired_count = var.desired_count
+ dns_name = var.keycloak_dns_name
+ dns_zone_id = var.dns_zone_id
+ encryption_configuration = var.encryption_configuration
+ environment = var.environment
+ http_redirect = var.http_redirect
+ http_ingress_cidr_blocks = var.http_ingress_cidr_blocks
+ https_ingress_cidr_blocks = var.https_ingress_cidr_blocks
+ jvm_heap_min = var.jvm_heap_min
+ jvm_heap_max = var.jvm_heap_max
+ jvm_meta_min = var.jvm_meta_min
+ jvm_meta_max = var.jvm_meta_max
+ internal = var.internal
+ log_retention_days = var.log_retention_days
+ name = "Keycloak"
+ namespace = var.namespace
+ private_subnet_ids = local.private_subnet_ids
+ private_subnet_cidrs = local.private_subnet_cidrs
+ public_subnet_ids = local.public_subnet_ids
+ rds_source_region = local.rds_source_region
+ region = var.region
+ route_table_ids = var.route_table_ids
+ stickiness = var.stickiness
+ tags = var.tags
+ vpc_id = local.vpc_id
+}
+
+module "vault" {
+ source = "./modules/vault"
+ environment = var.environment
+ region = var.region
+ instance_type = var.vault_instance_type
+ vault_version = var.vault_version
+ tags = var.tags
+ namespace = var.namespace
+ vpc_id = local.vpc_id
+ alb_destroy_log_bucket = var.alb_destroy_log_bucket
+ alb_certificate_arn = var.vault_alb_certificate_arn
+ deletion_protection = var.deletion_protection
+ http_ingress_cidr_blocks = var.http_ingress_cidr_blocks
+ http_redirect = var.http_redirect
+ https_ingress_cidr_blocks = var.https_ingress_cidr_blocks
+ private_subnet_ids = local.private_subnet_ids
+ container_port = var.vault_container_port
+ stickiness = var.stickiness
+ ubuntu_ami = var.vault_ami
+ ssh_key_name = var.ec2_ssh_key_name
+ leader_tls_servername = var.vault_leader_tls_servername
+ secrets_manager_arn = var.vault_secrets_manager_arn
+ min_nodes = var.vault_min_nodes
+ max_nodes = var.vault_max_nodes
+}
diff --git a/deployment/terraform/aws/modules/keycloak/main.tf
b/deployment/terraform/aws/modules/keycloak/main.tf
new file mode 100644
index 000000000..4bd8b9f5c
--- /dev/null
+++ b/deployment/terraform/aws/modules/keycloak/main.tf
@@ -0,0 +1,282 @@
+module "label" {
+ source = "cloudposse/label/null"
+ version = "0.25.0"
+ environment = var.environment
+ label_order = ["namespace", "name", "environment"]
+ name = var.name
+ namespace = var.namespace
+ tags = var.tags
+}
+
+resource "random_password" "db_password" {
+ length = 30
+ special = false
+}
+
+resource "random_password" "keycloak_password" {
+ length = 30
+ special = false
+}
+
+resource "aws_ssm_parameter" "db_password" {
+ name = "/${var.name}/${var.environment}/DB_PASSWORD"
+ description = "RDS password for ${module.label.id}"
+ tags = module.label.tags
+ type = "SecureString"
+ value = random_password.db_password.result
+}
+
+resource "aws_ssm_parameter" "keycloak_password" {
+ name = "/${var.name}/${var.environment}/KEYCLOAK_PASSWORD"
+ description = "keycloak_admin password for ${module.label.id}"
+ # overwrite = true
+ tags = module.label.tags
+ type = "SecureString"
+ value = random_password.keycloak_password.result
+}
+
+module "alb" {
+ source = "cloudposse/alb/aws"
+ version = "1.11.1"
+ alb_access_logs_s3_bucket_force_destroy = var.alb_destroy_log_bucket
+ attributes = ["alb"]
+ certificate_arn = var.alb_certificate_arn
+ deletion_protection_enabled = var.deletion_protection
+ health_check_interval = 60
+ health_check_path = "/auth/health"
+ health_check_timeout = 10
+ http_ingress_cidr_blocks = var.http_ingress_cidr_blocks
+ http_redirect = var.http_redirect
+ https_enabled = true
+ https_ingress_cidr_blocks = var.https_ingress_cidr_blocks
+ internal = var.internal
+ lifecycle_rule_enabled = true
+ name = module.label.id
+ subnet_ids = var.internal ?
var.private_subnet_ids : var.public_subnet_ids
+ tags = module.label.tags
+ target_group_name = module.label.id
+ target_group_port = var.container_port
+ target_group_target_type = "ip"
+ vpc_id = var.vpc_id
+ stickiness = var.stickiness
+}
+
+resource "aws_route53_record" "alb" {
+ zone_id = var.dns_zone_id
+ name = var.dns_name
+ type = "A"
+
+ alias {
+ name = module.alb.alb_dns_name
+ zone_id = module.alb.alb_zone_id
+ evaluate_target_health = false
+ }
+}
+
+resource "aws_cloudwatch_log_group" "keycloak_log_group" {
+ name = "/aws/ecs/cluster/${module.label.id}"
+ retention_in_days = var.log_retention_days
+ tags = module.label.tags
+}
+
+resource "aws_ecs_cluster" "keycloak" {
+ name = module.label.id
+ tags = module.label.tags
+
+ setting {
+ name = "containerInsights"
+ value = "enabled"
+ }
+}
+
+module "ecr" {
+ source = "cloudposse/ecr/aws"
+ version = "0.41.0"
+ encryption_configuration = var.encryption_configuration
+ image_tag_mutability = "MUTABLE"
+ max_image_count = 3
+ name = "${var.name}-${var.environment}"
+ principals_readonly_access = [module.ecs.task_role_arn]
+ scan_images_on_push = true
+ tags = module.label.tags
+}
+
+resource "aws_vpc_endpoint" "cloudwatch_logs" {
+ count = var.internal ? 1 : 0
+ auto_accept = true
+ private_dns_enabled = true
+ security_group_ids = [aws_security_group.vpc_endpoints.id]
+ service_name = "com.amazonaws.${var.region}.logs"
+ subnet_ids = var.private_subnet_ids
+ tags = module.label.tags
+ vpc_endpoint_type = "Interface"
+ vpc_id = var.vpc_id
+}
+
+resource "aws_vpc_endpoint" "ecr_api" {
+ count = var.internal ? 1 : 0
+ auto_accept = true
+ private_dns_enabled = true
+ security_group_ids = [aws_security_group.vpc_endpoints.id]
+ service_name = "com.amazonaws.${var.region}.ecr.api"
+ subnet_ids = var.private_subnet_ids
+ tags = module.label.tags
+ vpc_endpoint_type = "Interface"
+ vpc_id = var.vpc_id
+}
+
+resource "aws_vpc_endpoint" "ecr_dkr" {
+ count = var.internal ? 1 : 0
+ auto_accept = true
+ private_dns_enabled = true
+ security_group_ids = [aws_security_group.vpc_endpoints.id]
+ service_name = "com.amazonaws.${var.region}.ecr.dkr"
+ subnet_ids = var.private_subnet_ids
+ tags = module.label.tags
+ vpc_endpoint_type = "Interface"
+ vpc_id = var.vpc_id
+}
+
+resource "aws_vpc_endpoint" "s3" {
+ count = var.internal ? 1 : 0
+ auto_accept = true
+ route_table_ids = var.route_table_ids
+ service_name = "com.amazonaws.${var.region}.s3"
+ tags = module.label.tags
+ vpc_id = var.vpc_id
+}
+
+resource "aws_vpc_endpoint" "ssm" {
+ count = var.internal ? 1 : 0
+ auto_accept = true
+ private_dns_enabled = true
+ security_group_ids = [aws_security_group.vpc_endpoints.id]
+ service_name = "com.amazonaws.${var.region}.ssm"
+ subnet_ids = var.private_subnet_ids
+ tags = module.label.tags
+ vpc_endpoint_type = "Interface"
+ vpc_id = var.vpc_id
+}
+
+resource "aws_vpc_endpoint" "ssm_messages" {
+ count = var.internal ? 1 : 0
+ auto_accept = true
+ private_dns_enabled = true
+ security_group_ids = [aws_security_group.vpc_endpoints.id]
+ service_name = "com.amazonaws.${var.region}.ssmmessages"
+ subnet_ids = var.private_subnet_ids
+ tags = module.label.tags
+ vpc_endpoint_type = "Interface"
+ vpc_id = var.vpc_id
+}
+
+resource "aws_security_group" "vpc_endpoints" {
+ name = "vpc-endpoints"
+ description = "Allow traffic for PrivateLink endpoints"
+ vpc_id = var.vpc_id
+
+ ingress {
+ description = "TLS from VPC"
+ from_port = 443
+ to_port = 443
+ protocol = "tcp"
+ security_groups = [module.ecs.service_security_group_id]
+ }
+
+ egress {
+ from_port = 0
+ to_port = 0
+ protocol = "-1"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ tags = module.label.tags
+}
+
+data "aws_caller_identity" "current" {}
+
+module "ecs" {
+ source = "cloudposse/ecs-alb-service-task/aws"
+ version = "0.74.0"
+ container_definition_json =
templatefile("${path.module}/templates/container_definition.json", {
+ aws_account_id = data.aws_caller_identity.current.account_id
+ container_cpu_units = var.container_cpu_units
+ container_memory_limit = var.container_memory_limit
+ container_memory_reserved = var.container_memory_reserved
+ db_addr = module.rds_cluster.endpoint
+ dns_name = module.alb.alb_dns_name
+ environment = var.environment
+ image =
"${module.ecr.repository_url}:${var.docker_image_tag}"
+ jvm_heap_min = var.jvm_heap_min
+ jvm_heap_max = var.jvm_heap_max
+ jvm_meta_min = var.jvm_meta_min
+ jvm_meta_max = var.jvm_meta_max
+ log_group =
aws_cloudwatch_log_group.keycloak_log_group.name
+ name = var.name
+ region = var.region
+ })
+ alb_security_group = module.alb.security_group_id
+ container_port = var.container_port
+ deployment_maximum_percent = var.deployment_maximum_percent
+ deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent
+ desired_count = var.desired_count
+ ecs_cluster_arn = aws_ecs_cluster.keycloak.arn
+ health_check_grace_period_seconds = 600
+ ignore_changes_task_definition = false
+ name = module.label.id
+ subnet_ids = var.private_subnet_ids
+ tags = module.label.tags
+ task_cpu = var.container_cpu_units
+ task_memory = var.container_memory_limit
+ use_alb_security_group = true
+ vpc_id = var.vpc_id
+
+ ecs_load_balancers = [
+ {
+ container_name = var.name
+ container_port = var.container_port
+ elb_name = null
+ target_group_arn = module.alb.default_target_group_arn
+ }
+ ]
+
+ depends_on = [module.alb]
+}
+
+resource "aws_security_group_rule" "jdbc_ping" {
+ type = "ingress"
+ from_port = 7800
+ to_port = 7800
+ protocol = "tcp"
+ cidr_blocks = var.private_subnet_cidrs
+ security_group_id = module.ecs.service_security_group_id
+}
+
+
+module "rds_cluster" {
+ source = "cloudposse/rds-cluster/aws"
+ version = "1.9.0"
+ admin_password = random_password.db_password.result
+ admin_user = "keycloak"
+ allowed_cidr_blocks = var.db_allowed_cidr_blocks
+ attributes = ["rds"]
+ backup_window = var.db_backup_window
+ cluster_family = var.db_cluster_family
+ cluster_size = var.db_cluster_size
+ copy_tags_to_snapshot = true
+ db_name = "keycloak"
+ db_port = 5432
+ deletion_protection = var.deletion_protection
+ engine = "aurora-postgresql"
+ engine_version = var.db_engine_version
+ instance_type = var.db_instance_type
+ maintenance_window = var.db_maintenance_window
+ name = module.label.id
+ retention_period = var.db_backup_retention_days
+ security_groups = [module.ecs.service_security_group_id]
+ source_region = var.rds_source_region
+ storage_encrypted = true
+ subnets = var.private_subnet_ids
+ tags = module.label.tags
+ vpc_id = var.vpc_id
+}
diff --git a/deployment/terraform/aws/modules/keycloak/outputs.tf
b/deployment/terraform/aws/modules/keycloak/outputs.tf
new file mode 100644
index 000000000..e1638fa1c
--- /dev/null
+++ b/deployment/terraform/aws/modules/keycloak/outputs.tf
@@ -0,0 +1,35 @@
+output "alb_dns_name" {
+ value = module.alb.alb_dns_name
+}
+
+output "alb_log_bucket" {
+ value = module.alb.access_logs_bucket_id
+}
+
+output "ecr_repo" {
+ value = module.ecr.repository_url
+}
+
+output "ecs_cluster" {
+ value = aws_ecs_cluster.keycloak.name
+}
+
+output "ecs_service" {
+ value = module.ecs.service_name
+}
+
+output "rds_cluster_endpoint" {
+ value = module.rds_cluster.endpoint
+}
+
+output "rds_cluster_reader_endpoint" {
+ value = module.rds_cluster.reader_endpoint
+}
+
+output "rds_cluster_database_name" {
+ value = module.rds_cluster.database_name
+}
+
+output "rds_cluster_master_username" {
+ value = module.rds_cluster.master_username
+}
diff --git a/deployment/terraform/aws/modules/keycloak/resources/Dockerfile
b/deployment/terraform/aws/modules/keycloak/resources/Dockerfile
new file mode 100644
index 000000000..10b1c8308
--- /dev/null
+++ b/deployment/terraform/aws/modules/keycloak/resources/Dockerfile
@@ -0,0 +1,33 @@
+FROM quay.io/keycloak/keycloak:20.0.3 as builder
+
+ENV KC_METRICS_ENABLED=true
+ENV KC_HEALTH_ENABLED=true
+ENV KC_FEATURES=preview
+ENV KC_DB=postgres
+ENV KC_HTTP_RELATIVE_PATH=/auth
+
+COPY ./cache-ispn-jdbc-ping.xml /opt/keycloak/conf/cache-ispn-jdbc-ping.xml
+ENV KC_CACHE_CONFIG_FILE=cache-ispn-jdbc-ping.xml
+
+RUN /opt/keycloak/bin/kc.sh build
+
+FROM quay.io/keycloak/keycloak:20.0.3
+
+USER root
+RUN microdnf update -y && \
+ microdnf install -y jq && \
+ microdnf clean all
+
+USER keycloak
+COPY --from=builder /opt/keycloak /opt/keycloak
+
+WORKDIR /opt/keycloak
+
+RUN keytool -genkeypair -storepass password -storetype PKCS12 -keyalg RSA
-keysize 2048 -dname "CN=server" -alias server -ext
"SAN:c=DNS:localhost,IP:127.0.0.1" -keystore conf/server.keystore
+
+COPY docker-entrypoint.sh /docker-entrypoint.sh
+
+ENV FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS true
+
+EXPOSE 7800
+ENTRYPOINT ["/docker-entrypoint.sh"]
diff --git
a/deployment/terraform/aws/modules/keycloak/resources/cache-ispn-jdbc-ping.xml
b/deployment/terraform/aws/modules/keycloak/resources/cache-ispn-jdbc-ping.xml
new file mode 100644
index 000000000..fc2e28890
--- /dev/null
+++
b/deployment/terraform/aws/modules/keycloak/resources/cache-ispn-jdbc-ping.xml
@@ -0,0 +1,86 @@
+<infinispan
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="urn:infinispan:config:11.0
http://www.infinispan.org/schemas/infinispan-config-11.0.xsd"
+ xmlns="urn:infinispan:config:11.0">
+
+ <jgroups>
+ <stack name="jdbc-ping-tcp" extends="tcp">
+
+ <JDBC_PING connection_driver="org.postgresql.Driver"
+ connection_username="${env.KC_DB_USERNAME}"
connection_password="${env.KC_DB_PASSWORD}"
+ connection_url="${env.KC_DB_URL}"
+ initialize_sql="CREATE TABLE IF NOT EXISTS JGROUPSPING
(own_addr varchar(200) NOT NULL, bind_addr VARCHAR(200) NOT NULL, created
timestamp NOT NULL, cluster_name varchar(200) NOT NULL, ping_data BYTEA,
constraint PK_JGROUPSPING PRIMARY KEY (own_addr, cluster_name));"
+ insert_single_sql="INSERT INTO JGROUPSPING (own_addr,
bind_addr, created, cluster_name, ping_data) values
(?,'${env.EXTERNAL_ADDR:127.0.0.1}',NOW(), ?, ?);"
+ delete_single_sql="DELETE FROM JGROUPSPING WHERE
own_addr=? AND cluster_name=?;"
+ select_all_pingdata_sql="SELECT ping_data FROM
JGROUPSPING WHERE cluster_name=?;"
+ info_writer_sleep_time="5000"
+ info_writer_max_writes_after_view="2"
+ remove_all_data_on_view_change="true"
+ stack.combine="REPLACE"
+ stack.position="MPING"/>
+ </stack>
+ </jgroups>
+
+ <cache-container name="keycloak">
+ <transport lock-timeout="60000" stack="jdbc-ping-tcp"/>
+
+ <local-cache name="realms">
+ <encoding>
+ <key media-type="application/x-java-object"/>
+ <value media-type="application/x-java-object"/>
+ </encoding>
+ <memory max-count="10000"/>
+ </local-cache>
+ <local-cache name="users">
+ <encoding>
+ <key media-type="application/x-java-object"/>
+ <value media-type="application/x-java-object"/>
+ </encoding>
+ <memory max-count="10000"/>
+ </local-cache>
+ <distributed-cache name="sessions" owners="2">
+ <expiration lifespan="-1"/>
+ </distributed-cache>
+ <distributed-cache name="authenticationSessions" owners="2">
+ <expiration lifespan="-1"/>
+ </distributed-cache>
+ <distributed-cache name="offlineSessions" owners="2">
+ <expiration lifespan="-1"/>
+ </distributed-cache>
+ <distributed-cache name="clientSessions" owners="2">
+ <expiration lifespan="-1"/>
+ </distributed-cache>
+ <distributed-cache name="offlineClientSessions" owners="2">
+ <expiration lifespan="-1"/>
+ </distributed-cache>
+ <distributed-cache name="loginFailures" owners="2">
+ <expiration lifespan="-1"/>
+ </distributed-cache>
+ <local-cache name="authorization">
+ <encoding>
+ <key media-type="application/x-java-object"/>
+ <value media-type="application/x-java-object"/>
+ </encoding>
+ <memory max-count="10000"/>
+ </local-cache>
+ <replicated-cache name="work">
+ <expiration lifespan="-1"/>
+ </replicated-cache>
+ <local-cache name="keys">
+ <encoding>
+ <key media-type="application/x-java-object"/>
+ <value media-type="application/x-java-object"/>
+ </encoding>
+ <expiration max-idle="3600000"/>
+ <memory max-count="1000"/>
+ </local-cache>
+ <distributed-cache name="actionTokens" owners="2">
+ <encoding>
+ <key media-type="application/x-java-object"/>
+ <value media-type="application/x-java-object"/>
+ </encoding>
+ <expiration max-idle="-1" lifespan="-1" interval="300000"/>
+ <memory max-count="-1"/>
+ </distributed-cache>
+ </cache-container>
+</infinispan>
diff --git
a/deployment/terraform/aws/modules/keycloak/resources/docker-entrypoint.sh
b/deployment/terraform/aws/modules/keycloak/resources/docker-entrypoint.sh
new file mode 100755
index 000000000..c290a0a27
--- /dev/null
+++ b/deployment/terraform/aws/modules/keycloak/resources/docker-entrypoint.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# ECS 1.3
+if [ -n "${ECS_CONTAINER_METADATA_URI}" ]; then
+ EXTERNAL_ADDR=$(curl -fs "${ECS_CONTAINER_METADATA_URI}" \
+ | jq -r '.Networks[0].IPv4Addresses[0]')
+fi
+
+# ECS 1.4
+if [ -n "${ECS_CONTAINER_METADATA_URI_V4}" ]; then
+ EXTERNAL_ADDR=$(curl -fs "${ECS_CONTAINER_METADATA_URI_V4}" \
+ | jq -r '.Networks[0].IPv4Addresses[0]')
+fi
+
+if [ -z "${EXTERNAL_ADDR}" ]; then
+ EXTERNAL_ADDR=127.0.0.1
+fi
+export EXTERNAL_ADDR
+
+
+if [ -z "${HOSTNAME}" ]; then
+ HOSTNAME="localhost"
+fi
+
+exec /opt/keycloak/bin/kc.sh start --optimized "$@"
+exit $?
diff --git
a/deployment/terraform/aws/modules/keycloak/templates/container_definition.json
b/deployment/terraform/aws/modules/keycloak/templates/container_definition.json
new file mode 100644
index 000000000..aed66f1f1
--- /dev/null
+++
b/deployment/terraform/aws/modules/keycloak/templates/container_definition.json
@@ -0,0 +1,113 @@
+[
+ {
+ "logConfiguration": {
+ "logDriver": "awslogs",
+ "options": {
+ "awslogs-group": "${log_group}",
+ "awslogs-region": "${region}",
+ "awslogs-stream-prefix": "${environment}"
+ }
+ },
+ "portMappings": [
+ {
+ "protocol": "tcp",
+ "containerPort": 8080
+ },
+ {
+ "protocol": "tcp",
+ "containerPort": 7800
+ },
+ {
+ "protocol": "tcp",
+ "containerPort": 9990
+ }
+ ],
+ "cpu": ${container_cpu_units},
+ "environment": [
+ {
+ "name": "KC_DB",
+ "value": "postgres"
+ },
+ {
+ "name": "KC_DB_URL",
+ "value": "jdbc:postgresql://${db_addr}:5432/keycloak"
+ },
+ {
+ "name": "KC_DB_ADDR",
+ "value": "${db_addr}"
+ },
+ {
+ "name": "KC_DB_USERNAME",
+ "value": "keycloak"
+ },
+ {
+ "name": "DNS_NAME",
+ "value": "${dns_name}"
+ },
+ {
+ "name": "ENVIRONMENT_NAME",
+ "value": "${environment}"
+ },
+ {
+ "name": "JAVA_OPTS",
+ "value": "-XX:+DisableExplicitGC -XX:+UseG1GC -Xms${jvm_heap_min}m
-Xmx${jvm_heap_max}m -XX:MetaspaceSize=${jvm_meta_min}m
-XX:MaxMetaspaceSize=${jvm_meta_max}m -Djava.net.preferIPv4Stack=true
-Djava.net.preferIPv4Addresses -Djava.security.egd=file:/dev/urandom
-Dnashorn.args=--no-deprecation-warning"
+ },
+ {
+ "name": "KEYCLOAK_ADMIN",
+ "value": "keycloak_admin"
+ },
+ {
+ "name": "KC_LOG_LEVEL",
+ "value": "INFO,org.infinispan:ERROR,org.jgroups:ERROR"
+ },
+ {
+ "name": "KC_HTTP_ENABLED",
+ "value": "true"
+ },
+ {
+ "name": "KC_PROXY",
+ "value": "edge"
+ },
+ {
+ "name": "KC_HOSTNAME_STRICT",
+ "value": "false"
+ },
+ {
+ "name": "KC_HOSTNAME_PATH",
+ "value": "/auth"
+ },
+ {
+ "name": "KC_HTTP_RELATIVE_PATH",
+ "value": "/auth"
+ },
+ {
+ "name": "ROOT_LOGLEVEL",
+ "value": "INFO"
+ }
+ ],
+ "secrets": [
+ {
+ "name": "KC_DB_PASSWORD",
+ "valueFrom":
"arn:aws:ssm:${region}:${aws_account_id}:parameter/${name}/${environment}/DB_PASSWORD"
+ },
+ {
+ "name": "KEYCLOAK_ADMIN_PASSWORD",
+ "valueFrom":
"arn:aws:ssm:${region}:${aws_account_id}:parameter/${name}/${environment}/KEYCLOAK_PASSWORD"
+ }
+ ],
+ "memory": ${container_memory_limit},
+ "memoryReservation": ${container_memory_reserved},
+ "stopTimeout": 20,
+ "image": "${image}",
+ "startTimeout": 30,
+ "healthCheck": null,
+ "essential": true,
+ "readonlyRootFilesystem": false,
+ "dockerLabels": {
+ "environment": "${environment}",
+ "service": "${name}"
+ },
+ "privileged": false,
+ "name": "${name}"
+ }
+]
diff --git a/deployment/terraform/aws/modules/keycloak/variables.tf
b/deployment/terraform/aws/modules/keycloak/variables.tf
new file mode 100644
index 000000000..549ead70a
--- /dev/null
+++ b/deployment/terraform/aws/modules/keycloak/variables.tf
@@ -0,0 +1,231 @@
+variable "alb_certificate_arn" {
+ description = "ACM certificate ARN used by ALB"
+ type = string
+}
+
+variable "alb_destroy_log_bucket" {
+ description = "Destroy ALB log bucket on teardown"
+ type = bool
+}
+
+variable "rds_source_region" {
+ description = "Region of primary RDS cluster (needed to support encryption)"
+ type = string
+}
+
+variable "container_cpu_units" {
+ description = "CPU units to reserve for container (1024 units == 1 CPU)"
+ type = number
+}
+
+variable "container_memory_limit" {
+ description = "Container memory hard limit"
+ type = number
+}
+
+variable "container_memory_reserved" {
+ description = "Container memory starting reservation"
+ type = number
+}
+
+variable "container_port" {
+ description = "Keycloak port exposed in container"
+ type = number
+}
+
+variable "db_allowed_cidr_blocks" {
+ description = "List of CIDR blocks allowed to access DB cluster"
+ type = list(string)
+ default = []
+}
+
+variable "db_backup_retention_days" {
+ description = "How long Database backups are retained"
+ type = number
+}
+
+variable "db_backup_window" {
+ description = "Daily time range during which backups happen"
+ type = string
+}
+
+variable "db_cluster_family" {
+ description = "Family of DB cluster parameter group"
+ type = string
+}
+
+variable "db_cluster_size" {
+ description = "Number of RDS cluster instances"
+ type = number
+}
+
+variable "db_engine_version" {
+ description = "Version of DB engine to use"
+ type = string
+}
+
+variable "db_instance_type" {
+ description = "Instance type used for RDS instances"
+ type = string
+}
+
+variable "db_maintenance_window" {
+ description = "Weekly time range during which system maintenance can occur
(UTC)"
+ type = string
+}
+
+variable "deletion_protection" {
+ description = "Protect resources from being deleted"
+ type = bool
+}
+
+variable "deployment_maximum_percent" {
+ description = "Maximum task instances allowed to run"
+ type = number
+}
+
+variable "deployment_minimum_healthy_percent" {
+ description = "Minimum percentage of healthy task instances"
+ type = number
+}
+
+variable "desired_count" {
+ description = "Number of ECS task instances to run"
+ type = number
+}
+
+variable "dns_name" {
+ description = "Keycloak DNS"
+ type = string
+}
+
+variable "dns_zone_id" {
+ description = "Route53 Zone ID hosting Keycloak"
+ type = string
+}
+
+variable "encryption_configuration" {
+ type = object({
+ encryption_type = string
+ kms_key = any
+ })
+ description = "ECR encryption configuration"
+}
+
+variable "environment" {
+ description = "Environment name (development, production, etc)"
+ type = string
+}
+
+variable "http_redirect" {
+ description = "Controls whether port 80 should redirect to 443 (or not
listen)"
+ type = bool
+}
+
+variable "http_ingress_cidr_blocks" {
+ description = "CIDR ranges allowed to connect to service port 80"
+ type = list(string)
+}
+
+variable "https_ingress_cidr_blocks" {
+ description = "CIDR ranges allowed to connect to service port 443"
+ type = list(string)
+}
+
+variable "internal" {
+ description = "Whether environment should be exposed to Internet (if not
using network module)"
+ type = bool
+}
+
+variable "jvm_heap_min" {
+ description = "Minimum JVM heap size for application in MB"
+ type = number
+}
+
+variable "jvm_heap_max" {
+ description = "Maximum JVM heap size for application in MB"
+ type = number
+}
+
+variable "jvm_meta_min" {
+ description = "Minimum JVM meta space size for application in MB"
+ type = number
+}
+
+variable "jvm_meta_max" {
+ description = "Maximum JVM meta space size for application in MB"
+ type = number
+}
+
+variable "log_retention_days" {
+ description = "Log retention for CloudWatch logs"
+ type = number
+}
+
+variable "name" {
+ description = "Used by modules to construct labels"
+ type = string
+}
+
+variable "namespace" {
+ description = "Used by modules to construct labels"
+ type = string
+}
+
+variable "private_subnet_ids" {
+ description = "List of private subnet IDs"
+ type = list(string)
+}
+
+variable "private_subnet_cidrs" {
+ description = "List of private subnet CIDR ranges"
+ type = list(string)
+}
+
+variable "public_subnet_ids" {
+ description = "List of public subnet IDs"
+ type = list(string)
+}
+
+variable "region" {
+ description = "AWS region to target"
+ type = string
+}
+
+variable "route_table_ids" {
+ description = "List of route tables used by s3 VPC endpoint (if not using
network module)"
+ type = list(string)
+}
+
+variable "stickiness" {
+ type = object({
+ cookie_duration = number
+ enabled = bool
+ })
+ description = "Target group sticky configuration"
+}
+
+variable "tags" {
+ description = "Default tags applied to resources"
+ type = map(string)
+}
+
+variable "vpc_id" {
+ description = "AWS VPC ID"
+ type = string
+}
+
+variable "ecr_repository_name" {
+ description = "Name of the ECR Repository"
+ default = "keycloak"
+}
+
+variable "docker_image_name" {
+ description = "Name of the Docker Image"
+ default = "keycloak-custos"
+}
+
+variable "docker_image_tag" {
+ description = "The Version Tag for the Docker Image"
+ default = "latest"
+}
diff --git a/deployment/terraform/aws/modules/keycloak/versions.tf
b/deployment/terraform/aws/modules/keycloak/versions.tf
new file mode 100644
index 000000000..21c24a6e3
--- /dev/null
+++ b/deployment/terraform/aws/modules/keycloak/versions.tf
@@ -0,0 +1,10 @@
+terraform {
+ required_version = ">= 0.15.3"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 4.0"
+ }
+ }
+}
diff --git a/deployment/terraform/aws/modules/network/main.tf
b/deployment/terraform/aws/modules/network/main.tf
new file mode 100644
index 000000000..d112eaf85
--- /dev/null
+++ b/deployment/terraform/aws/modules/network/main.tf
@@ -0,0 +1,88 @@
+data "aws_availability_zones" "available" {
+ state = "available"
+}
+
+resource "aws_vpc" "main" {
+ cidr_block = var.vpc_cidr
+ tags = var.tags
+}
+
+resource "aws_internet_gateway" "main" {
+ vpc_id = aws_vpc.main.id
+ tags = var.tags
+}
+
+resource "aws_nat_gateway" "main" {
+ count = 2
+ subnet_id = element(aws_subnet.public[*].id, count.index)
+ allocation_id = element(aws_eip.nat[*].id, count.index)
+ depends_on = [aws_internet_gateway.main]
+ tags = var.tags
+}
+
+resource "aws_eip" "nat" {
+ count = 2
+ domain = "vpc"
+ depends_on = [aws_internet_gateway.main]
+ tags = var.tags
+}
+
+resource "aws_route_table" "public" {
+ vpc_id = aws_vpc.main.id
+ tags = var.tags
+
+ route {
+ cidr_block = "0.0.0.0/0"
+ gateway_id = aws_internet_gateway.main.id
+ }
+}
+
+resource "aws_route_table" "private" {
+ count = 2
+ vpc_id = aws_vpc.main.id
+ tags = var.tags
+
+ route {
+ cidr_block = "0.0.0.0/0"
+ nat_gateway_id = element(aws_nat_gateway.main[*].id, count.index)
+ }
+}
+
+resource "aws_route_table_association" "public" {
+ count = 2
+ subnet_id = element(aws_subnet.public[*].id, count.index)
+ route_table_id = aws_route_table.public.id
+}
+
+resource "aws_route_table_association" "private" {
+ count = 2
+ subnet_id = element(aws_subnet.private[*].id, count.index)
+ route_table_id = element(aws_route_table.private[*].id, count.index)
+}
+
+resource "aws_subnet" "public" {
+ count = 2
+ vpc_id = aws_vpc.main.id
+ cidr_block = cidrsubnet(var.public_cidr, 1, count.index)
+ availability_zone =
element(data.aws_availability_zones.available.names, count.index)
+ map_public_ip_on_launch = true
+ tags = merge(
+ var.tags,
+ {
+ "Name" = "public-subnet-custos-${format("%02d", count.index + 1)}"
+ }
+ )
+}
+
+resource "aws_subnet" "private" {
+ count = 2
+ vpc_id = aws_vpc.main.id
+ cidr_block = cidrsubnet(var.private_cidr, 1, count.index)
+ availability_zone = element(data.aws_availability_zones.available.names,
count.index)
+ tags = merge(
+ var.tags,
+ {
+ "Name" = "private-subnet-custos-${format("%02d", count.index + 1)}"
+ }
+ )
+}
diff --git a/deployment/terraform/aws/modules/network/outputs.tf
b/deployment/terraform/aws/modules/network/outputs.tf
new file mode 100644
index 000000000..e7159a238
--- /dev/null
+++ b/deployment/terraform/aws/modules/network/outputs.tf
@@ -0,0 +1,27 @@
+output "vpc_id" {
+ value = aws_vpc.main.id
+}
+
+output "vpc_cidr" {
+ value = aws_vpc.main.cidr_block
+}
+
+output "public_subnet_cidrs" {
+ value = aws_subnet.public[*].cidr_block
+}
+
+output "public_subnet_ids" {
+ value = aws_subnet.public[*].id
+}
+
+output "private_subnet_cidrs" {
+ value = aws_subnet.private[*].cidr_block
+}
+
+output "private_subnet_ids" {
+ value = aws_subnet.private[*].id
+}
+
+output "availability_zones" {
+ value = data.aws_availability_zones.available.names
+}
\ No newline at end of file
diff --git a/deployment/terraform/aws/modules/network/variables.tf
b/deployment/terraform/aws/modules/network/variables.tf
new file mode 100644
index 000000000..29c9777ca
--- /dev/null
+++ b/deployment/terraform/aws/modules/network/variables.tf
@@ -0,0 +1,19 @@
+variable "vpc_cidr" {
+ description = "RFC1918 CIDR range for VPC"
+ type = string
+}
+
+variable "public_cidr" {
+ description = "RFC1918 CIDR range for public subnets (subset of vpc_cidr)"
+ type = string
+}
+
+variable "private_cidr" {
+ description = "RFC1918 CIDR range for private subnets (subset of vpc_cidr)"
+ type = string
+}
+
+variable "tags" {
+ description = "Tags applied to AWS resources"
+ type = map(string)
+}
diff --git a/deployment/terraform/aws/modules/network/versions.tf
b/deployment/terraform/aws/modules/network/versions.tf
new file mode 100644
index 000000000..21c24a6e3
--- /dev/null
+++ b/deployment/terraform/aws/modules/network/versions.tf
@@ -0,0 +1,10 @@
+terraform {
+ required_version = ">= 0.15.3"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 4.0"
+ }
+ }
+}
diff --git a/deployment/terraform/aws/modules/vault/README.md
b/deployment/terraform/aws/modules/vault/README.md
new file mode 100644
index 000000000..4d88758af
--- /dev/null
+++ b/deployment/terraform/aws/modules/vault/README.md
@@ -0,0 +1,8 @@
+# Vault Configurations
+
+### Generating the Self Signed Certificate (Development Purpose Only)
+- Navigate to the `airavata-custos/terraform/modules/vault/resources` and run
the command
+`openssl req -x509 -nodes -newkey rsa:2048 -keyout vault.key -out vault.crt
-days 365 -config openssl-vault.cnf -extensions req_ext`
+to generate the `vault.cert` and `vault.key`
+- Create a Secret using AWS Secrets Manager and copy the `vault.cert` content
into the `vault_cert` and `vault_ca`. The `vault.key` content should go to the
key `vault_pk`
+Then use the corresponding ARN value for variable `vault_secrets_manager_arn`
in `terraform.vars`
\ No newline at end of file
diff --git a/deployment/terraform/aws/modules/vault/main.tf
b/deployment/terraform/aws/modules/vault/main.tf
new file mode 100644
index 000000000..9945edb13
--- /dev/null
+++ b/deployment/terraform/aws/modules/vault/main.tf
@@ -0,0 +1,250 @@
+module "label" {
+ source = "cloudposse/label/null"
+ version = "0.25.0"
+ environment = var.environment
+ label_order = ["namespace", "name", "environment"]
+ name = var.name
+ namespace = var.namespace
+ tags = var.tags
+}
+
+resource "aws_iam_role" "instance_role" {
+ name = "${var.namespace}-${var.name}"
+ description = "Vault role for ${module.label.id}"
+ assume_role_policy = data.aws_iam_policy_document.instance_role.json
+}
+
+data "aws_iam_policy_document" "instance_role" {
+ statement {
+ effect = "Allow"
+ actions = [
+ "sts:AssumeRole",
+ ]
+ principals {
+ type = "Service"
+ identifiers = ["ec2.amazonaws.com"]
+ }
+ }
+}
+
+resource "aws_iam_instance_profile" "vault" {
+ name_prefix = "${var.namespace}-${var.name}"
+ role = aws_iam_role.instance_role.name
+ tags = module.label.tags
+}
+
+resource "aws_iam_role_policy" "auto_join" {
+ name = "${var.namespace}-${var.name}-auto-join"
+ policy = data.aws_iam_policy_document.auto_join.json
+ role = aws_iam_role.instance_role.id
+}
+
+data "aws_iam_policy_document" "auto_join" {
+ statement {
+ effect = "Allow"
+ actions = [
+ "ec2:DescribeInstances",
+ "ec2:DescribeTags"
+ ]
+ resources = ["*"]
+ }
+}
+
+resource "aws_iam_role_policy" "auto_unseal" {
+ name = "${var.namespace}-${var.name}-auto-unseal"
+ policy = data.aws_iam_policy_document.auto_unseal.json
+ role = aws_iam_role.instance_role.id
+}
+
+resource "aws_kms_key" "vault" {
+ description = "AWS KMS key used for Vault auto-unseal and
encryption"
+ key_usage = "ENCRYPT_DECRYPT"
+ deletion_window_in_days = 7
+ is_enabled = true
+ tags = merge(
+ var.tags,
+ {
+ "Name" = "${var.namespace}-${var.name}-key"
+ }
+ )
+}
+
+data "aws_iam_policy_document" "auto_unseal" {
+ statement {
+ effect = "Allow"
+ actions = [
+ "kms:DescribeKey",
+ "kms:Encrypt",
+ "kms:Decrypt",
+ ]
+ resources = [
+ aws_kms_key.vault.arn,
+ ]
+ }
+}
+
+resource "aws_iam_role_policy" "secrets_manager" {
+ name = "${var.namespace}-${var.name}-secrets-manager"
+ policy = data.aws_iam_policy_document.secrets_manager.json
+ role = aws_iam_role.instance_role.id
+}
+
+data "aws_iam_policy_document" "secrets_manager" {
+ statement {
+ effect = "Allow"
+ actions = [
+ "secretsmanager:GetSecretValue",
+ ]
+ resources = [
+ var.secrets_manager_arn,
+ ]
+ }
+}
+
+module "alb" {
+ source = "cloudposse/alb/aws"
+ version = "1.11.1"
+ alb_access_logs_s3_bucket_force_destroy = var.alb_destroy_log_bucket
+ attributes = ["alb"]
+ certificate_arn = var.alb_certificate_arn
+ deletion_protection_enabled = var.deletion_protection
+ health_check_interval = 60
+ health_check_path = var.alb_health_check_path
+ health_check_timeout = 10
+ http_ingress_cidr_blocks = var.http_ingress_cidr_blocks
+ http_redirect = var.http_redirect
+ https_enabled = true
+ https_ingress_cidr_blocks = var.https_ingress_cidr_blocks
+ internal = true
+ lifecycle_rule_enabled = true
+ name = module.label.id
+ subnet_ids = var.private_subnet_ids
+ tags = module.label.tags
+ target_group_name = module.label.id
+ target_group_port = var.container_port
+ target_group_target_type = "instance"
+ vpc_id = var.vpc_id
+ stickiness = var.stickiness
+}
+
+resource "aws_security_group" "vault_private" {
+ name = "${var.namespace}-${var.name}"
+ description = "Vault Security Group"
+ vpc_id = var.vpc_id
+ tags = merge(
+ var.tags,
+ {
+ "Name" = "${var.namespace}-${var.name}-sg"
+ }
+ )
+}
+
+resource "aws_security_group_rule" "vault_internal_api" {
+ security_group_id = aws_security_group.vault_private.id
+ description = "Vault Internal API"
+ type = "ingress"
+ from_port = 8200
+ to_port = 8200
+ protocol = "tcp"
+ self = true
+}
+
+resource "aws_security_group_rule" "vault_internal_raft" {
+ security_group_id = aws_security_group.vault_private.id
+ description = "Vault Raft"
+ type = "ingress"
+ from_port = 8201
+ to_port = 8201
+ protocol = "tcp"
+ self = true
+}
+
+resource "aws_security_group_rule" "vault_ssh_inbound" {
+ description = "Allow SSH"
+ security_group_id = aws_security_group.vault_private.id
+ type = "ingress"
+ from_port = 22
+ to_port = 22
+ protocol = "tcp"
+ self = true
+}
+
+resource "aws_security_group_rule" "vault_alb_inbound" {
+ security_group_id = aws_security_group.vault_private.id
+ description = "Allow AWS ALB to reach Vault nodes"
+ type = "ingress"
+ from_port = 8200
+ to_port = 8200
+ protocol = "tcp"
+ source_security_group_id = module.alb.security_group_id
+}
+
+resource "aws_security_group_rule" "vault_outbound" {
+ security_group_id = aws_security_group.vault_private.id
+ description = "Allow Vault nodes to send outbound traffic"
+ type = "egress"
+ from_port = 0
+ to_port = 0
+ protocol = "-1"
+ cidr_blocks = ["0.0.0.0/0"]
+}
+
+resource "aws_launch_template" "vault" {
+ name = "${var.namespace}-${var.name}"
+ image_id = var.ubuntu_ami
+ instance_type = var.instance_type
+ key_name = var.ssh_key_name
+ user_data =
base64encode(templatefile("${path.module}/templates/install_vault_script.sh.tpl",
+ {
+ region = var.region
+ name = var.namespace
+ vault_version = var.vault_version
+ kms_key_arn = aws_kms_key.vault.arn
+ secrets_manager_arn = var.secrets_manager_arn
+ leader_tls_servername = var.leader_tls_servername
+ }))
+ vpc_security_group_ids = [
+ aws_security_group.vault_private.id,
+ ]
+
+ block_device_mappings {
+ device_name = "/dev/sda1"
+
+ ebs {
+ volume_type = "gp3"
+ volume_size = 100
+ throughput = 150
+ iops = 3000
+ delete_on_termination = true
+ }
+ }
+
+ iam_instance_profile {
+ name = aws_iam_instance_profile.vault.name
+ }
+
+ metadata_options {
+ http_endpoint = "enabled"
+ http_tokens = "required"
+ }
+}
+
+resource "aws_autoscaling_group" "vault" {
+ name = "${var.namespace}-${var.name}-asg"
+ min_size = var.min_nodes
+ max_size = var.max_nodes
+ desired_capacity = var.min_nodes
+ vpc_zone_identifier = var.private_subnet_ids
+ target_group_arns = [module.alb.default_target_group_arn]
+
+ launch_template {
+ id = aws_launch_template.vault.id
+ version = "$Latest"
+ }
+
+ tag {
+ key = "Name"
+ value = "${var.namespace}-${var.name}-server"
+ propagate_at_launch = true
+ }
+}
\ No newline at end of file
diff --git a/deployment/terraform/aws/modules/vault/resources/openssl-vault.cnf
b/deployment/terraform/aws/modules/vault/resources/openssl-vault.cnf
new file mode 100644
index 000000000..618080e77
--- /dev/null
+++ b/deployment/terraform/aws/modules/vault/resources/openssl-vault.cnf
@@ -0,0 +1,21 @@
+[req]
+default_bits = 2048
+prompt = no
+default_md = sha256
+distinguished_name = dn
+req_extensions = req_ext
+
+[dn]
+C = US
+ST = Georgia
+L = Atlanta
+O = "Apache Airavata Custos"
+CN = secrets.usecustos.org
+
+[req_ext]
+subjectAltName = @alt_names
+
+[alt_names]
+DNS.1 = secrets.usecustos.org
+DNS.2 = localhost
+IP.1 = 127.0.0.1
\ No newline at end of file
diff --git
a/deployment/terraform/aws/modules/vault/templates/install_vault_script.sh.tpl
b/deployment/terraform/aws/modules/vault/templates/install_vault_script.sh.tpl
new file mode 100644
index 000000000..f97465adf
--- /dev/null
+++
b/deployment/terraform/aws/modules/vault/templates/install_vault_script.sh.tpl
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+
+imds_token=$( curl -Ss -H "X-aws-ec2-metadata-token-ttl-seconds: 30" -XPUT
169.254.169.254/latest/api/token )
+instance_id=$( curl -Ss -H "X-aws-ec2-metadata-token: $imds_token"
169.254.169.254/latest/meta-data/instance-id )
+local_ipv4=$( curl -Ss -H "X-aws-ec2-metadata-token: $imds_token"
169.254.169.254/latest/meta-data/local-ipv4 )
+
+curl -fsSL https://apt.releases.hashicorp.com/gpg | apt-key add -
+apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com
$(lsb_release -cs) main"
+apt-get update
+apt-get install -y vault=${vault_version}-* awscli jq
+
+timedatectl set-timezone UTC
+rm -rf /opt/vault/tls/*
+
+# /opt/vault/tls should be readable by all the users
+chmod 0755 /opt/vault/tls
+
+# Only the vault group be able to read the vault-key.pem
+touch /opt/vault/tls/vault-key.pem
+chown root:vault /opt/vault/tls/vault-key.pem
+chmod 0640 /opt/vault/tls/vault-key.pem
+
+secret_result=$(aws secretsmanager get-secret-value --secret-id
${secrets_manager_arn} --region ${region} --output text --query SecretString)
+jq -r .vault_cert <<< "$secret_result" > /opt/vault/tls/vault-cert.pem
+jq -r .vault_ca <<< "$secret_result" > /opt/vault/tls/vault-ca.pem
+jq -r .vault_pk <<< "$secret_result" > /opt/vault/tls/vault-key.pem
+
+cat << EOF > /etc/vault.d/vault.hcl
+ui = true
+disable_mlock = true
+
+storage "raft" {
+ path = "/opt/vault/data"
+ node_id = "$instance_id"
+ retry_join {
+ auto_join = "provider=aws region=${region} tag_key=${name}-vault
tag_value=server"
+ auto_join_scheme = "https"
+ leader_tls_servername = "${leader_tls_servername}"
+ leader_ca_cert_file = "/opt/vault/tls/vault-ca.pem"
+ leader_client_cert_file = "/opt/vault/tls/vault-cert.pem"
+ leader_client_key_file = "/opt/vault/tls/vault-key.pem"
+ }
+}
+
+cluster_addr = "https://$local_ipv4:8201"
+api_addr = "https://$local_ipv4:8200"
+
+listener "tcp" {
+ address = "0.0.0.0:8200"
+ tls_disable = false
+ tls_cert_file = "/opt/vault/tls/vault-cert.pem"
+ tls_key_file = "/opt/vault/tls/vault-key.pem"
+ tls_client_ca_file = "/opt/vault/tls/vault-ca.pem"
+}
+
+seal "awskms" {
+ region = "${region}"
+ kms_key_id = "${kms_key_arn}"
+}
+
+EOF
+
+# Only the vault group should be able to read vault.hcl
+chown root:root /etc/vault.d
+chown root:vault /etc/vault.d/vault.hcl
+chmod 640 /etc/vault.d/vault.hcl
+
+systemctl enable vault
+systemctl start vault
+
+echo "Vault profile"
+cat <<PROFILE | sudo tee /etc/profile.d/vault.sh
+export VAULT_ADDR="https://127.0.0.1:8200"
+export VAULT_CACERT="/opt/vault/tls/vault-ca.pem"
+PROFILE
diff --git a/deployment/terraform/aws/modules/vault/variables.tf
b/deployment/terraform/aws/modules/vault/variables.tf
new file mode 100644
index 000000000..bf1784067
--- /dev/null
+++ b/deployment/terraform/aws/modules/vault/variables.tf
@@ -0,0 +1,124 @@
+variable "environment" {
+ description = "Environment name (development, production, etc)"
+ type = string
+}
+
+variable "name" {
+ description = "Used by modules to construct labels"
+ type = string
+ default = "vault"
+}
+
+variable "tags" {
+ description = "Default tags applied to resources"
+ type = map(string)
+}
+
+variable "region" {
+ description = "AWS region to target"
+ type = string
+}
+
+variable "namespace" {
+ description = "Application namespace"
+ type = string
+}
+
+variable "vpc_id" {
+ description = "AWS VPC ID"
+ type = string
+}
+
+variable "alb_destroy_log_bucket" {
+ description = "Destroy ALB log bucket on teardown"
+ type = bool
+}
+
+variable "alb_certificate_arn" {
+ description = "ACM certificate ARN used by ALB"
+ type = string
+}
+
+variable "deletion_protection" {
+ description = "Protect resources from being deleted"
+ type = bool
+}
+
+variable "http_ingress_cidr_blocks" {
+ description = "CIDR ranges allowed to connect to service port 80"
+ type = list(string)
+}
+
+variable "http_redirect" {
+ description = "Controls whether port 80 should redirect to 443 (or not
listen)"
+ type = bool
+}
+
+variable "https_ingress_cidr_blocks" {
+ description = "CIDR ranges allowed to connect to service port 443"
+ type = list(string)
+}
+
+variable "private_subnet_ids" {
+ description = "List of private subnet IDs"
+ type = list(string)
+}
+
+variable "container_port" {
+ description = "Vault port exposed in container"
+ type = number
+}
+
+variable "stickiness" {
+ type = object({
+ cookie_duration = number
+ enabled = bool
+ })
+ description = "Target group sticky configuration"
+}
+
+variable "alb_health_check_path" {
+ type = string
+ description = "Vault health check path"
+ default = "/v1/sys/health"
+}
+
+variable "ubuntu_ami" {
+ type = string
+ description = "AMI for Ubuntu"
+}
+
+variable "instance_type" {
+ type = string
+ description = "EC2 instance type"
+}
+
+variable "ssh_key_name" {
+ type = string
+ description = "key pair to use for SSH access to instance"
+}
+
+variable "vault_version" {
+ type = string
+ description = "Vault version"
+}
+
+variable "leader_tls_servername" {
+ type = string
+ description = "One of the shared DNS SAN used to create the certs use for
mTLS"
+}
+
+variable "secrets_manager_arn" {
+ type = string
+ description = "Secrets manager ARN"
+}
+
+variable "min_nodes" {
+ type = number
+ description = "Minimum number of Vault nodes to deploy in ASG"
+}
+
+variable "max_nodes" {
+ type = number
+ description = "Minimum number of Vault nodes to deploy in ASG"
+}
\ No newline at end of file
diff --git a/deployment/terraform/aws/modules/vault/versions.tf
b/deployment/terraform/aws/modules/vault/versions.tf
new file mode 100644
index 000000000..21c24a6e3
--- /dev/null
+++ b/deployment/terraform/aws/modules/vault/versions.tf
@@ -0,0 +1,10 @@
+terraform {
+ required_version = ">= 0.15.3"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 4.0"
+ }
+ }
+}
diff --git a/deployment/terraform/aws/outputs.tf
b/deployment/terraform/aws/outputs.tf
new file mode 100644
index 000000000..d7778a970
--- /dev/null
+++ b/deployment/terraform/aws/outputs.tf
@@ -0,0 +1,52 @@
+output "public_subnet_cidrs" {
+ value = var.enable_network ? module.network[0].public_subnet_cidrs : []
+}
+
+output "private_subnet_cidrs" {
+ value = var.enable_network ? module.network[0].private_subnet_cidrs :
local.private_subnet_cidrs
+}
+
+output "state_table" {
+ value = module.terraform_state_backend.dynamodb_table_name
+}
+
+output "state_bucket" {
+ value = module.terraform_state_backend.s3_bucket_domain_name
+}
+
+output "alb_dns_name" {
+ value = module.keycloak.alb_dns_name
+}
+
+output "alb_log_bucket" {
+ value = module.keycloak.alb_log_bucket
+}
+
+output "ecr_repo" {
+ value = module.keycloak.ecr_repo
+}
+
+output "ecs_cluster" {
+ value = module.keycloak.ecs_cluster
+}
+
+output "ecs_service" {
+ value = module.keycloak.ecs_service
+}
+
+output "rds_cluster_endpoint" {
+ value = module.keycloak.rds_cluster_endpoint
+}
+
+output "rds_cluster_reader_endpoint" {
+ value = module.keycloak.rds_cluster_reader_endpoint
+}
+
+output "rds_cluster_database_name" {
+ value = module.keycloak.rds_cluster_database_name
+}
+
+output "rds_cluster_master_username" {
+ value = module.keycloak.rds_cluster_master_username
+ sensitive = true
+}
diff --git a/deployment/terraform/aws/terraform.tfvars
b/deployment/terraform/aws/terraform.tfvars
new file mode 100644
index 000000000..ea827dfc1
--- /dev/null
+++ b/deployment/terraform/aws/terraform.tfvars
@@ -0,0 +1,39 @@
+environment = "dev"
+namespace = "custos"
+region = "us-east-2"
+
+vpc_cidr = "10.20.30.0/24"
+public_cidr = "10.20.30.0/25"
+private_cidr = "10.20.30.128/25"
+
+
+keycloak_alb_certificate_arn = "<KEYCLOAK_ALB_CERT_ARN>"
+keycloak_dns_name = "auth.usecustos.org"
+dns_zone_id = "<DNS_ZONE_ID>"
+
+container_cpu_units = 1024
+container_memory_limit = 2048
+container_memory_reserved = 1024
+jvm_heap_min = 512
+jvm_heap_max = 1024
+jvm_meta_min = 128
+jvm_meta_max = 512
+deployment_maximum_percent = 100
+deployment_minimum_healthy_percent = 50
+desired_count = 1
+log_retention_days = 5
+
+db_instance_type = "db.r6g.large"
+db_backup_retention_days = 5
+db_cluster_size = 2
+
+
+vault_alb_certificate_arn = "<VAULT_ALB_CERT_ARN>"
+vault_ami = "<AMI_ID>"
+ec2_ssh_key_name = "custos-auth"
+vault_instance_type = "m5.xlarge"
+vault_version = "1.11.0"
+vault_leader_tls_servername = "vault.usecustos.org"
+vault_secrets_manager_arn = "<SECRETS_MANAGER_ARN>"
+vault_min_nodes = 1
+vault_max_nodes = 5
diff --git a/deployment/terraform/aws/variables.tf
b/deployment/terraform/aws/variables.tf
new file mode 100644
index 000000000..f04456c85
--- /dev/null
+++ b/deployment/terraform/aws/variables.tf
@@ -0,0 +1,302 @@
+variable "environment" {
+ description = "Environment name (development, production, etc)"
+ type = string
+}
+
+variable "namespace" {
+ description = "Application namespace"
+ type = string
+}
+
+variable "region" {
+ description = "AWS region to target"
+ type = string
+}
+
+variable "enable_network" {
+ description = "Use network module. Set to false to use your own network
resources"
+ type = bool
+ default = true
+}
+
+variable "vpc_id" {
+ description = "AWS VPC ID (if not using network module)"
+ type = string
+ default = ""
+}
+
+variable "vpc_cidr" {
+ description = "RFC1918 CIDR range for VPC"
+ type = string
+ default = ""
+}
+
+variable "public_cidr" {
+ description = "RFC1918 CIDR range for public subnets (subset of vpc_cidr)"
+ type = string
+ default = ""
+}
+
+variable "private_cidr" {
+ description = "RFC1918 CIDR range for private subnets (subset of vpc_cidr)"
+ type = string
+ default = ""
+}
+
+variable "public_subnet_ids" {
+ description = "List of public subnet IDs for deployment if not using network
module"
+ type = list(string)
+ default = []
+}
+
+variable "private_subnet_ids" {
+ description = "List of private subnet IDs for deployment if not using
network module"
+ type = list(string)
+ default = []
+}
+
+variable "tags" {
+ description = "Standard tags for all resources"
+ type = map(any)
+ default = {
+ ManagedBy = "Terraform"
+ }
+}
+
+variable "keycloak_alb_certificate_arn" {
+ description = "ACM certificate used by Keycloak ALB"
+ type = string
+}
+
+variable "alb_destroy_log_bucket" {
+ description = "Destroy ALB log bucket on teardown"
+ type = bool
+ default = true
+}
+
+variable "container_cpu_units" {
+ description = "CPU units to reserve for container (1024 units == 1 CPU)"
+ type = number
+}
+
+variable "container_memory_limit" {
+ description = "Container memory hard limit"
+ type = number
+}
+
+variable "container_memory_reserved" {
+ description = "Container memory starting reservation"
+ type = number
+}
+
+variable "keycloak_container_port" {
+ description = "Keycloak port exposed in container"
+ type = number
+ default = 8080
+}
+
+variable "db_backup_retention_days" {
+ description = "How long Database backups are retained"
+ type = number
+}
+
+variable "db_backup_window" {
+ description = "Daily time range during which backups happen"
+ type = string
+ default = "00:00-02:00"
+}
+
+variable "db_cluster_family" {
+ description = "Family of DB cluster parameter group"
+ type = string
+ default = "aurora-postgresql15"
+}
+
+variable "db_cluster_size" {
+ description = "Number of RDS cluster instances"
+ type = number
+}
+
+variable "db_engine_version" {
+ description = "Version of DB engine to use"
+ type = string
+ default = "15.4"
+}
+
+variable "db_instance_type" {
+ description = "Instance type used for RDS instances"
+ type = string
+}
+
+variable "db_maintenance_window" {
+ description = "Weekly time range during which system maintenance can occur
(UTC)"
+ type = string
+ default = "sat:03:00-sat:04:00"
+}
+
+variable "deletion_protection" {
+ description = "Protect supporting resources from being deleted (ALB and RDS)"
+ type = bool
+ default = false
+}
+
+variable "deployment_maximum_percent" {
+ description = "Maximum task instances allowed to run"
+ type = number
+}
+
+variable "deployment_minimum_healthy_percent" {
+ description = "Minimum percentage of healthy task instances"
+ type = number
+}
+
+variable "desired_count" {
+ description = "Number of ECS task instances to run"
+ type = number
+}
+
+variable "keycloak_dns_name" {
+ description = "Keycloak DNS"
+ type = string
+}
+
+variable "dns_zone_id" {
+ description = "Route53 Zone ID hosting Services"
+ type = string
+}
+
+variable "encryption_configuration" {
+ type = object({
+ encryption_type = string
+ kms_key = any
+ })
+ description = "ECR encryption configuration"
+ default = {
+ encryption_type = "AES256"
+ kms_key = null
+ }
+}
+
+variable "http_redirect" {
+ description = "Controls whether port 80 should redirect to 443 (or not
listen)"
+ type = bool
+ default = true
+}
+
+variable "http_ingress_cidr_blocks" {
+ description = "CIDR ranges allowed to connect to service port 80"
+ type = list(string)
+ default = ["0.0.0.0/0"]
+}
+
+variable "https_ingress_cidr_blocks" {
+ description = "CIDR ranges allowed to connect to service port 443"
+ type = list(string)
+ default = ["0.0.0.0/0"]
+}
+
+variable "jvm_heap_min" {
+ description = "Minimum JVM heap size for application in MB"
+ type = number
+}
+
+variable "jvm_heap_max" {
+ description = "Maximum JVM heap size for application in MB"
+ type = number
+}
+
+variable "jvm_meta_min" {
+ description = "Minimum JVM meta space size for application in MB"
+ type = number
+}
+
+variable "jvm_meta_max" {
+ description = "Maximum JVM meta space size for application in MB"
+ type = number
+}
+
+variable "internal" {
+ description = "Whether environment should be exposed to Internet (if not
using network module)"
+ type = string
+ default = false
+}
+
+variable "log_retention_days" {
+ description = "Log retention for CloudWatch logs"
+ type = number
+}
+
+variable "rds_source_region" {
+ description = "Region of primary RDS cluster (needed to support encryption)"
+ type = string
+ default = ""
+}
+
+variable "route_table_ids" {
+ description = "List of route tables used by s3 VPC endpoint (if not using
network module)"
+ type = list(string)
+ default = []
+}
+
+variable "stickiness" {
+ type = object({
+ cookie_duration = number
+ enabled = bool
+ })
+ description = "Target group sticky configuration"
+ default = {
+ cookie_duration = null
+ enabled = false
+ }
+}
+
+variable "vault_alb_certificate_arn" {
+ description = "ACM certificate used by Vault ALB"
+ type = string
+}
+
+variable "vault_container_port" {
+ description = "Vault port"
+ type = number
+ default = 8200
+}
+
+variable "vault_ami" {
+ description = "AMI used for Vault"
+ type = string
+}
+
+variable "ec2_ssh_key_name" {
+ description = "key pair to use for SSH access to instance"
+ type = string
+}
+
+variable "vault_instance_type" {
+ type = string
+ description = "EC2 instance type"
+}
+
+variable "vault_version" {
+ type = string
+ description = "Vault version"
+}
+
+variable "vault_leader_tls_servername" {
+ type = string
+ description = "One of the shared DNS SAN used to create the certs use for
mTLS"
+}
+
+variable "vault_secrets_manager_arn" {
+ type = string
+ description = "Secrets manager ARN"
+}
+
+variable "vault_min_nodes" {
+ type = number
+ description = "Minimum number of Vault nodes to deploy in ASG"
+}
+
+variable "vault_max_nodes" {
+ type = number
+ description = "Minimum number of Vault nodes to deploy in ASG"
+}