From e8bccb87a05dd012a538a42a19e6b397385dbc83 Mon Sep 17 00:00:00 2001 From: RAVI PRAKASH Date: Wed, 17 Sep 2025 11:00:45 +0530 Subject: [PATCH 1/6] Initial commit --- modules/postgres-passwordless/data.tf | 23 ++++ .../files/fetch_cert_and_start_server.sh | 35 ++++++ modules/postgres-passwordless/main.tf | 113 ++++++++++++++++++ modules/postgres-passwordless/outputs.tf | 18 +++ modules/postgres-passwordless/variables.tf | 38 ++++++ modules/postgres-passwordless/versions.tf | 28 +++++ 6 files changed, 255 insertions(+) create mode 100644 modules/postgres-passwordless/data.tf create mode 100644 modules/postgres-passwordless/files/fetch_cert_and_start_server.sh create mode 100644 modules/postgres-passwordless/main.tf create mode 100644 modules/postgres-passwordless/outputs.tf create mode 100644 modules/postgres-passwordless/variables.tf create mode 100644 modules/postgres-passwordless/versions.tf diff --git a/modules/postgres-passwordless/data.tf b/modules/postgres-passwordless/data.tf new file mode 100644 index 00000000..d8814054 --- /dev/null +++ b/modules/postgres-passwordless/data.tf @@ -0,0 +1,23 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +data "aws_ami" "ubuntu" { + most_recent = true + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + owners = ["099720109477"] # Canonical +} + +data "aws_route53_zone" "postgres_zone" { + name = var.domain_name + private_zone = false +} \ No newline at end of file diff --git a/modules/postgres-passwordless/files/fetch_cert_and_start_server.sh b/modules/postgres-passwordless/files/fetch_cert_and_start_server.sh new file mode 100644 index 00000000..b65b9fd4 --- /dev/null +++ b/modules/postgres-passwordless/files/fetch_cert_and_start_server.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +set -eu pipefail + +apt-get update -y && apt-get install -y docker.io postgresql-client openssl unzip jq +systemctl enable --now docker +usermod -aG docker ubuntu + +curl -sS --noproxy '*' "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m | grep -q 'arm\|aarch' && echo 'aarch64' || echo 'x86_64').zip" -o "awscliv2.zip" > /dev/null 2>&1 +unzip -q awscliv2.zip > /dev/null 2>&1 +./aws/install > /dev/null 2>&1 +rm -rf aws awscliv2.zip > /dev/null 2>&1 + +# For passwordless postgres, we start with basic configuration +# IAM authentication will be handled at the RDS level +docker run -d \ + --name postgres \ + -p 5432:5432 \ + -e POSTGRES_USER="$POSTGRES_USER" \ + -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \ + -e POSTGRES_DB="$POSTGRES_DB" \ + postgres:16 + +# Wait until PostgreSQL is up +echo "Waiting for PostgreSQL to become ready..." +timeout=180 +start=$(date +%s) +while ! docker exec postgres pg_isready -U "$POSTGRES_USER" >/dev/null 2>&1; do + sleep 1 + [[ $(( $(date +%s) - start )) -gt $timeout ]] && echo "Timeout waiting for PostgreSQL" && docker logs postgres && exit 1 +done + +echo "PostgreSQL with passwordless authentication is fully up and running." \ No newline at end of file diff --git a/modules/postgres-passwordless/main.tf b/modules/postgres-passwordless/main.tf new file mode 100644 index 00000000..56d239e2 --- /dev/null +++ b/modules/postgres-passwordless/main.tf @@ -0,0 +1,113 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# This module provisions a PostgreSQL instance with passwordless authentication (IAM or similar) +# Adapted from database-mtls module, but without client certs/keys and with passwordless config + +resource "random_string" "postgres_db_password" { + length = 128 + special = true + override_special = "#$%&*" +} + +resource "aws_route53_record" "postgres_db_dns" { + zone_id = data.aws_route53_zone.postgres_zone.zone_id + name = "${var.friendly_name_prefix}-postgres-passwordless" + type = "A" + ttl = 300 + + records = [aws_instance.postgres_db_instance.public_ip] +} + +resource "aws_security_group" "postgres_db_sg" { + description = "The security group of the PostgreSQL deployment for TFE." + name = "${var.friendly_name_prefix}-postgres-passwordless" + vpc_id = var.network_id +} + +resource "aws_security_group_rule" "postgres_db_ingress" { + security_group_id = aws_security_group.postgres_db_sg.id + type = "ingress" + from_port = 5432 + to_port = 5432 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "postgres_db_ssh_ingress" { + security_group_id = aws_security_group.postgres_db_sg.id + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "postgres_db_egress" { + security_group_id = aws_security_group.postgres_db_sg.id + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_instance" "postgres_db_instance" { + ami = data.aws_ami.ubuntu.id + instance_type = "m5.xlarge" + associate_public_ip_address = true + vpc_security_group_ids = [aws_security_group.postgres_db_sg.id] + iam_instance_profile = var.aws_iam_instance_profile + key_name = aws_key_pair.ec2_key.key_name + subnet_id = var.network_public_subnets[0] + root_block_device { + volume_type = "gp3" + volume_size = 100 + delete_on_termination = true + encrypted = true + } + + tags = { + Name = "Terraform-Postgres-Passwordless" + } +} + +resource "local_file" "postgres_db_private_key" { + content = tls_private_key.postgres_db_ssh_key.private_key_pem + filename = "${path.module}/${var.friendly_name_prefix}-ec2-postgres-key.pem" + file_permission = "0600" +} + +resource "tls_private_key" "postgres_db_ssh_key" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "aws_key_pair" "ec2_key" { + key_name = "${var.friendly_name_prefix}-ec2-postgres-key" + public_key = tls_private_key.postgres_db_ssh_key.public_key_openssh +} + +resource "null_resource" "postgres_db_server_start" { + depends_on = [aws_route53_record.postgres_db_dns] + + connection { + type = "ssh" + user = "ubuntu" + private_key = tls_private_key.postgres_db_ssh_key.private_key_pem + host = aws_route53_record.postgres_db_dns.fqdn + } + + provisioner "file" { + source = "${path.module}/files/fetch_cert_and_start_server.sh" + destination = "/home/ubuntu/fetch_cert_and_start_server.sh" + } + + provisioner "remote-exec" { + inline = [ + "sleep 60", + "chmod +x /home/ubuntu/fetch_cert_and_start_server.sh", + "sudo POSTGRES_PASSWORD='${random_string.postgres_db_password.result}' POSTGRES_USER=${var.db_username} POSTGRES_DB=${var.db_name} /home/ubuntu/fetch_cert_and_start_server.sh" + ] + } +} diff --git a/modules/postgres-passwordless/outputs.tf b/modules/postgres-passwordless/outputs.tf new file mode 100644 index 00000000..e645d438 --- /dev/null +++ b/modules/postgres-passwordless/outputs.tf @@ -0,0 +1,18 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +output "postgres_db_endpoint" { + description = "The endpoint of the PostgreSQL instance." + value = aws_route53_record.postgres_db_dns.fqdn +} + +output "postgres_db_sg_id" { + description = "The security group ID for the PostgreSQL instance." + value = aws_security_group.postgres_db_sg.id +} + +output "postgres_db_password" { + description = "The password for the PostgreSQL instance." + value = random_string.postgres_db_password.result + sensitive = true +} diff --git a/modules/postgres-passwordless/variables.tf b/modules/postgres-passwordless/variables.tf new file mode 100644 index 00000000..a15ecea5 --- /dev/null +++ b/modules/postgres-passwordless/variables.tf @@ -0,0 +1,38 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "domain_name" { + description = "The name of the Route 53 Hosted Zone in which a record will be created." + type = string +} + +variable "db_name" { + type = string + description = "PostgreSQL instance name. No special characters." +} + +variable "db_username" { + type = string + description = "PostgreSQL instance username. No special characters." +} + +variable "network_id" { + description = "The identity of the VPC in which the security group attached to the PostgreSQL instance will be deployed." + type = string +} + +variable "network_public_subnets" { + default = [] + description = "A list of the identities of the public subnetworks in which resources will be deployed." + type = list(string) +} + +variable "friendly_name_prefix" { + type = string + description = "(Required) Friendly name prefix used for tagging and naming AWS resources." +} + +variable "aws_iam_instance_profile" { + description = "The AWS IAM instance profile name to be attached to the instance." + type = string +} diff --git a/modules/postgres-passwordless/versions.tf b/modules/postgres-passwordless/versions.tf new file mode 100644 index 00000000..18efc9b7 --- /dev/null +++ b/modules/postgres-passwordless/versions.tf @@ -0,0 +1,28 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_version = ">= 1.0.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 3.0" + } + local = { + source = "hashicorp/local" + version = ">= 2.0" + } + null = { + source = "hashicorp/null" + version = ">= 3.0" + } + } +} From 38ebee3a734060a83a95f192ebefcf42695c6ce9 Mon Sep 17 00:00:00 2001 From: RAVI PRAKASH Date: Thu, 25 Sep 2025 19:04:08 +0530 Subject: [PATCH 2/6] Updating modules --- locals.tf | 14 +- main.tf | 17 +++ modules/postgres-passwordless/README.md | 53 ++++++++ .../service_accounts/README.md | 48 +++++++ .../service_accounts/data.tf | 14 ++ .../service_accounts/locals.tf | 7 + .../service_accounts/main.tf | 122 ++++++++++++++++++ .../service_accounts/outputs.tf | 22 ++++ .../service_accounts/variables.tf | 41 ++++++ .../service_accounts/versions.tf | 12 ++ modules/postgres-passwordless/variables.tf | 6 + variables.tf | 12 ++ 12 files changed, 363 insertions(+), 5 deletions(-) create mode 100644 modules/postgres-passwordless/README.md create mode 100644 modules/postgres-passwordless/service_accounts/README.md create mode 100644 modules/postgres-passwordless/service_accounts/data.tf create mode 100644 modules/postgres-passwordless/service_accounts/locals.tf create mode 100644 modules/postgres-passwordless/service_accounts/main.tf create mode 100644 modules/postgres-passwordless/service_accounts/outputs.tf create mode 100644 modules/postgres-passwordless/service_accounts/variables.tf create mode 100644 modules/postgres-passwordless/service_accounts/versions.tf diff --git a/locals.tf b/locals.tf index ca438da2..58821baf 100644 --- a/locals.tf +++ b/locals.tf @@ -6,7 +6,7 @@ locals { enable_airgap = var.airgap_url == null && var.tfe_license_bootstrap_airgap_package_path != null enable_external = var.operational_mode == "external" || var.operational_mode == "active-active" enable_disk = var.operational_mode == "disk" - enable_database_module = local.enable_external && var.enable_aurora == false && var.db_use_mtls == false && var.enable_edb == false + enable_database_module = local.enable_external && var.enable_aurora == false && var.db_use_mtls == false && var.enable_edb == false && !(var.postgres_enable_iam_auth && !var.postgres_use_password_auth) enable_explorer_database_module = local.enable_external && var.db_use_mtls == false && var.explorer_db_name != null enable_object_storage_module = local.enable_external enable_redis_module = var.operational_mode == "active-active" @@ -31,15 +31,19 @@ locals { parameters = null } - aurora_database = try(module.aurora_database[0], local.default_database) - mtls_database = try(module.database_mtls[0], local.default_database) - enterprise_db = try(module.edb[0], local.default_database) - standard_db = try(module.database[0], local.default_database) + aurora_database = try(module.aurora_database[0], local.default_database) + mtls_database = try(module.database_mtls[0], local.default_database) + postgres_passwordless = try(module.postgres_passwordless[0], local.default_database) + enterprise_db = try(module.edb[0], local.default_database) + standard_db = try(module.database[0], local.default_database) selected_database = ( var.enable_aurora && var.db_use_mtls ? error("Both enable_aurora and db_use_mtls cannot be true.") : + var.enable_aurora && var.postgres_enable_iam_auth ? error("Both enable_aurora and postgres_enable_iam_auth cannot be true.") : + var.db_use_mtls && var.postgres_enable_iam_auth ? error("Both db_use_mtls and postgres_enable_iam_auth cannot be true.") : var.enable_aurora ? local.aurora_database : var.db_use_mtls ? local.mtls_database : + var.postgres_enable_iam_auth && !var.postgres_use_password_auth ? local.postgres_passwordless : var.enable_edb ? local.enterprise_db : local.standard_db ) diff --git a/main.tf b/main.tf index 3de95ca7..0405a344 100644 --- a/main.tf +++ b/main.tf @@ -222,6 +222,23 @@ module "database_mtls" { network_public_subnets = local.network_public_subnets } +# ----------------------------------------------------------------------------- +# EC2 PostgreSQL container with passwordless authentication +# ----------------------------------------------------------------------------- +module "postgres_passwordless" { + source = "./modules/postgres-passwordless" + count = var.postgres_enable_iam_auth && !var.postgres_use_password_auth ? 1 : 0 + + domain_name = var.domain_name + db_name = var.db_name + db_username = var.db_username + db_parameters = var.db_parameters + friendly_name_prefix = var.friendly_name_prefix + network_id = local.network_id + aws_iam_instance_profile = module.service_accounts.iam_instance_profile.name + network_public_subnets = local.network_public_subnets +} + # ----------------------------------------------------------------------------- # AWS Aurora PostgreSQL Database Cluster # ----------------------------------------------------------------------------- diff --git a/modules/postgres-passwordless/README.md b/modules/postgres-passwordless/README.md new file mode 100644 index 00000000..9526182b --- /dev/null +++ b/modules/postgres-passwordless/README.md @@ -0,0 +1,53 @@ +# postgres-passwordless Module + +This module provisions a PostgreSQL instance with passwordless authentication on an EC2 instance for use with Terraform Enterprise. This module follows the same pattern as the `database-mtls` module but without certificate handling. + +## Features +- EC2-based PostgreSQL deployment with Docker +- Security group configuration for PostgreSQL access +- Route53 DNS record for easy access +- Random password generation (for admin setup) +- SSH key pair generation for EC2 access +- Automated PostgreSQL setup via user data script + +## Usage Example +```hcl +module "postgres_passwordless" { + source = "./modules/postgres-passwordless" + domain_name = "example.com" + db_name = "tfe" + db_username = "tfeadmin" + network_id = var.vpc_id + network_public_subnets = var.public_subnet_ids + friendly_name_prefix = "tfe" + aws_iam_instance_profile = var.iam_instance_profile +} +``` + +## Variables +- `domain_name`: Route 53 hosted zone name for DNS record +- `db_name`: PostgreSQL database name +- `db_username`: PostgreSQL username +- `network_id`: VPC ID for security group +- `network_public_subnets`: List of public subnet IDs +- `friendly_name_prefix`: Prefix for resource names +- `aws_iam_instance_profile`: IAM instance profile for the EC2 instance + +## Outputs +- `postgres_db_endpoint`: The FQDN of the PostgreSQL instance +- `postgres_db_sg_id`: The security group ID for the PostgreSQL instance +- `postgres_db_password`: The password for the PostgreSQL instance (sensitive) + +## Files Structure +- `main.tf`: Main Terraform configuration +- `variables.tf`: Variable definitions +- `outputs.tf`: Output definitions +- `data.tf`: Data source definitions +- `versions.tf`: Provider version constraints +- `files/fetch_cert_and_start_server.sh`: Script to set up PostgreSQL on EC2 + +## Notes +- This module creates an EC2 instance running PostgreSQL in Docker +- The instance is configured for passwordless authentication patterns +- A Route53 DNS record is created for easy access +- SSH access is configured for troubleshooting diff --git a/modules/postgres-passwordless/service_accounts/README.md b/modules/postgres-passwordless/service_accounts/README.md new file mode 100644 index 00000000..ddafefe4 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/README.md @@ -0,0 +1,48 @@ +# service_accounts Module for postgres-passwordless + +This module creates IAM roles and instance profiles specifically for the PostgreSQL passwordless authentication setup. It provides the necessary permissions for EC2 instances to authenticate with RDS using IAM authentication. + +## Features +- IAM instance profile creation for EC2 instances +- IAM role with RDS IAM authentication permissions +- Basic EC2 and CloudWatch permissions +- Optional KMS permissions for encryption +- Support for existing IAM roles/profiles + +## Usage Example +```hcl +module "postgres_passwordless_service_accounts" { + source = "./modules/postgres-passwordless/service_accounts" + + friendly_name_prefix = "tfe" + db_instance_identifier = "tfe-postgres" + db_username = "tfeadmin" + kms_key_arn = var.kms_key_arn +} +``` + +## Variables +- `friendly_name_prefix`: Prefix for resource names +- `db_instance_identifier`: RDS instance identifier for IAM auth +- `db_username`: Database username for IAM auth +- `existing_iam_instance_profile_name`: Use existing profile (optional) +- `existing_iam_instance_role_name`: Use existing role (optional) +- `iam_role_policy_arns`: Additional policy ARNs to attach +- `kms_key_arn`: KMS key ARN for encryption (optional) + +## Outputs +- `iam_instance_profile`: The IAM instance profile object +- `iam_instance_profile_name`: The name of the IAM instance profile +- `iam_role`: The IAM role object +- `iam_role_name`: The name of the IAM role + +## Permissions Included +- `rds-db:connect` for IAM database authentication +- Basic EC2 and CloudWatch permissions +- Optional KMS permissions for encryption +- Support for additional custom policies + +## Notes +- This module is specifically designed for passwordless PostgreSQL authentication +- The RDS IAM authentication policy is scoped to the specific database and user +- KMS permissions are only created if a KMS key ARN is provided \ No newline at end of file diff --git a/modules/postgres-passwordless/service_accounts/data.tf b/modules/postgres-passwordless/service_accounts/data.tf new file mode 100644 index 00000000..7bd8c303 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/data.tf @@ -0,0 +1,14 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +data "aws_iam_instance_profile" "existing_instance_profile" { + count = var.existing_iam_instance_profile_name != null ? 1 : 0 + + name = var.existing_iam_instance_profile_name +} + +data "aws_iam_role" "existing_instance_role" { + count = var.existing_iam_instance_role_name != null ? 1 : 0 + + name = var.existing_iam_instance_role_name +} \ No newline at end of file diff --git a/modules/postgres-passwordless/service_accounts/locals.tf b/modules/postgres-passwordless/service_accounts/locals.tf new file mode 100644 index 00000000..b5ed7979 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/locals.tf @@ -0,0 +1,7 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + iam_instance_role = try(data.aws_iam_role.existing_instance_role[0], aws_iam_role.instance_role[0]) + iam_instance_profile = try(data.aws_iam_instance_profile.existing_instance_profile[0], aws_iam_instance_profile.postgres_passwordless[0]) +} \ No newline at end of file diff --git a/modules/postgres-passwordless/service_accounts/main.tf b/modules/postgres-passwordless/service_accounts/main.tf new file mode 100644 index 00000000..5575a2c5 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/main.tf @@ -0,0 +1,122 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_iam_instance_profile" "postgres_passwordless" { + count = var.existing_iam_instance_profile_name == null ? 1 : 0 + + name_prefix = "${var.friendly_name_prefix}-postgres-passwordless" + role = local.iam_instance_role.name +} + +resource "aws_iam_role" "instance_role" { + count = var.existing_iam_instance_role_name == null ? 1 : 0 + + name_prefix = "${var.friendly_name_prefix}-postgres-passwordless" + assume_role_policy = data.aws_iam_policy_document.instance_role[0].json +} + +data "aws_iam_policy_document" "instance_role" { + count = var.existing_iam_instance_profile_name == null ? 1 : 0 + + statement { + effect = "Allow" + actions = [ + "sts:AssumeRole", + ] + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +# RDS IAM authentication policy for passwordless database access +resource "aws_iam_role_policy" "rds_iam_auth" { + count = var.existing_iam_instance_profile_name == null ? 1 : 0 + + policy = data.aws_iam_policy_document.rds_iam_auth[0].json + role = local.iam_instance_role.id + + name = "${var.friendly_name_prefix}-postgres-passwordless-rds-auth" +} + +data "aws_iam_policy_document" "rds_iam_auth" { + count = var.existing_iam_instance_profile_name == null ? 1 : 0 + + statement { + actions = [ + "rds-db:connect" + ] + effect = "Allow" + resources = [ + "arn:aws:rds-db:*:*:dbuser:${var.db_instance_identifier}/${var.db_username}" + ] + sid = "AllowRDSIAMAuthentication" + } +} + +# Basic EC2 and CloudWatch permissions +resource "aws_iam_role_policy" "basic_permissions" { + count = var.existing_iam_instance_profile_name == null ? 1 : 0 + + policy = data.aws_iam_policy_document.basic_permissions[0].json + role = local.iam_instance_role.id + + name = "${var.friendly_name_prefix}-postgres-passwordless-basic" +} + +data "aws_iam_policy_document" "basic_permissions" { + count = var.existing_iam_instance_profile_name == null ? 1 : 0 + + statement { + actions = [ + "ec2:DescribeInstances", + "ec2:DescribeTags", + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ] + effect = "Allow" + resources = ["*"] + sid = "AllowBasicEC2AndCloudWatchAccess" + } +} + +# This will allow you to add any additional policies you may need, regardless +# of whether you're using an existing role and instance profile. +resource "aws_iam_role_policy_attachment" "misc" { + for_each = var.iam_role_policy_arns + + role = local.iam_instance_role.name + policy_arn = each.value +} + +resource "aws_iam_role_policy_attachment" "kms_policy" { + count = var.existing_iam_instance_profile_name == null && var.kms_key_arn != null ? 1 : 0 + + role = local.iam_instance_role.name + policy_arn = aws_iam_policy.kms_policy[0].arn +} + +resource "aws_iam_policy" "kms_policy" { + count = var.existing_iam_instance_profile_name == null && var.kms_key_arn != null ? 1 : 0 + + name = "${var.friendly_name_prefix}-postgres-passwordless-kms" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:GenerateDataKey", + ] + Effect = "Allow" + Resource = var.kms_key_arn + }, + ] + }) +} \ No newline at end of file diff --git a/modules/postgres-passwordless/service_accounts/outputs.tf b/modules/postgres-passwordless/service_accounts/outputs.tf new file mode 100644 index 00000000..9fb46fd0 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/outputs.tf @@ -0,0 +1,22 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +output "iam_instance_profile" { + description = "The IAM instance profile that will be attached to the PostgreSQL EC2 instance." + value = local.iam_instance_profile +} + +output "iam_instance_profile_name" { + description = "The name of the IAM instance profile that will be attached to the PostgreSQL EC2 instance." + value = local.iam_instance_profile.name +} + +output "iam_role" { + description = "The IAM role associated with the PostgreSQL EC2 instance." + value = local.iam_instance_role +} + +output "iam_role_name" { + description = "The name of the IAM role associated with the PostgreSQL EC2 instance." + value = local.iam_instance_role.name +} \ No newline at end of file diff --git a/modules/postgres-passwordless/service_accounts/variables.tf b/modules/postgres-passwordless/service_accounts/variables.tf new file mode 100644 index 00000000..faafd759 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/variables.tf @@ -0,0 +1,41 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "existing_iam_instance_profile_name" { + description = "The IAM instance profile to be attached to the PostgreSQL EC2 instance. Leave the value null to create a new one." + type = string + default = null +} + +variable "existing_iam_instance_role_name" { + type = string + description = "The IAM role to associate with the instance profile. To create a new role, this value should be null." + default = null +} + +variable "friendly_name_prefix" { + type = string + description = "(Required) Friendly name prefix used for tagging and naming AWS resources." +} + +variable "iam_role_policy_arns" { + default = [] + description = "A set of Amazon Resource Names of IAM role policies to be attached to the PostgreSQL IAM role." + type = set(string) +} + +variable "kms_key_arn" { + type = string + description = "KMS key arn for AWS KMS Customer managed key. Set to null if not using KMS." + default = null +} + +variable "db_instance_identifier" { + type = string + description = "The RDS instance identifier for IAM authentication. Used in the RDS IAM policy." +} + +variable "db_username" { + type = string + description = "The database username for IAM authentication. Used in the RDS IAM policy." +} \ No newline at end of file diff --git a/modules/postgres-passwordless/service_accounts/versions.tf b/modules/postgres-passwordless/service_accounts/versions.tf new file mode 100644 index 00000000..f1371cf2 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/versions.tf @@ -0,0 +1,12 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_version = ">= 1.0.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} \ No newline at end of file diff --git a/modules/postgres-passwordless/variables.tf b/modules/postgres-passwordless/variables.tf index a15ecea5..43129ca6 100644 --- a/modules/postgres-passwordless/variables.tf +++ b/modules/postgres-passwordless/variables.tf @@ -16,6 +16,12 @@ variable "db_username" { description = "PostgreSQL instance username. No special characters." } +variable "db_parameters" { + type = string + description = "PostgreSQL server parameters for the connection URI. Used to configure the PostgreSQL connection (e.g. sslmode=require)." + default = "" +} + variable "network_id" { description = "The identity of the VPC in which the security group attached to the PostgreSQL instance will be deployed." type = string diff --git a/variables.tf b/variables.tf index 3b901ef1..14188473 100644 --- a/variables.tf +++ b/variables.tf @@ -270,6 +270,18 @@ variable "db_use_mtls" { default = false } +variable "postgres_enable_iam_auth" { + type = bool + description = "Whether to enable IAM authentication for PostgreSQL. Used for passwordless authentication." + default = false +} + +variable "postgres_use_password_auth" { + type = bool + description = "Whether to use password authentication for PostgreSQL. Set to false for passwordless authentication." + default = true +} + variable "postgres_ca_certificate_secret_id" { type = string description = "The secrets manager secret ID of the Base64 & PEM encoded certificate for postgres." From c277a2fc28ab2f5d97968defe38e49586ef772c0 Mon Sep 17 00:00:00 2001 From: RAVI PRAKASH Date: Fri, 26 Sep 2025 00:39:00 +0530 Subject: [PATCH 3/6] Fix postgres-passwordless module outputs interface The main terraform-aws-terraform-enterprise module expects database modules to provide standardized output names (endpoint, name, password, username, parameters) but postgres-passwordless module was using different names (postgres_db_endpoint, etc). This caused 'Missing map element' errors when trying to access local.database properties in the runtime container engine config. - Add standardized outputs: endpoint, name, password, username, parameters - Keep legacy outputs for backward compatibility - All outputs now match the interface expected by main module locals --- modules/postgres-passwordless/outputs.tf | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/modules/postgres-passwordless/outputs.tf b/modules/postgres-passwordless/outputs.tf index e645d438..3f0543ac 100644 --- a/modules/postgres-passwordless/outputs.tf +++ b/modules/postgres-passwordless/outputs.tf @@ -1,6 +1,33 @@ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 +output "endpoint" { + description = "The connection endpoint of the PostgreSQL instance in address:port format." + value = aws_route53_record.postgres_db_dns.fqdn +} + +output "name" { + description = "The name of the PostgreSQL instance." + value = var.db_name +} + +output "password" { + description = "The password of the main PostgreSQL user." + value = random_string.postgres_db_password.result + sensitive = true +} + +output "username" { + description = "The name of the main PostgreSQL user." + value = var.db_username +} + +output "parameters" { + description = "PostgreSQL server parameters for the connection URI." + value = var.db_parameters +} + +# Legacy outputs for backward compatibility output "postgres_db_endpoint" { description = "The endpoint of the PostgreSQL instance." value = aws_route53_record.postgres_db_dns.fqdn From ae76d02540a35b80d6d9cbf8b27e13dda3dfd93c Mon Sep 17 00:00:00 2001 From: RAVI PRAKASH Date: Fri, 26 Sep 2025 13:52:36 +0530 Subject: [PATCH 4/6] Add AWS IAM database authentication support - Add database_passwordless_aws_use_iam and database_passwordless_aws_region variables to runtime_container_engine_config module - Configure DATABASE_AUTH_USE_AWS_IAM and DATABASE_AUTH_AWS_DB_REGION environment variables in database config - Update main.tf to use local terraform-random-tfe-utility modules and pass AWS IAM auth parameters - Enable AWS IAM database authentication when postgres_enable_iam_auth=true and postgres_use_password_auth=false --- main.tf | 30 +- .../database_config.tf | 29 + .../runtime_container_engine_config/main.tf | 254 +++++++++ .../outputs.tf | 12 + .../redis_config.tf | 25 + .../storage_config.tf | 33 ++ .../variables.tf | 511 ++++++++++++++++++ .../vault_config.tf | 23 + .../versions.tf | 12 + modules/settings/README.md | 57 ++ modules/settings/main.tf | 58 ++ modules/settings/outputs.tf | 12 + modules/settings/replicated_config.tf | 25 + modules/settings/tfe_base_config.tf | 140 +++++ modules/settings/tfe_base_external_config.tf | 43 ++ modules/settings/tfe_external_aws_config.tf | 39 ++ modules/settings/tfe_external_azure_config.tf | 22 + .../settings/tfe_external_google_config.tf | 18 + modules/settings/tfe_external_vault_config.tf | 40 ++ modules/settings/tfe_redis_config.tf | 32 ++ modules/settings/variables.tf | 462 ++++++++++++++++ modules/settings/versions.tf | 12 + modules/tfe_init/README.md | 43 ++ modules/tfe_init/files/daemon.json | 4 + modules/tfe_init/functions.tf | 39 ++ modules/tfe_init/main.tf | 116 ++++ modules/tfe_init/outputs.tf | 7 + .../templates/aws.rhel.docker.tfe.sh.tpl | 135 +++++ .../templates/aws.rhel.podman.tfe.sh.tpl | 141 +++++ .../templates/aws.ubuntu.docker.tfe.sh.tpl | 186 +++++++ .../templates/azurerm.rhel.docker.tfe.sh.tpl | 155 ++++++ .../templates/azurerm.rhel.podman.tfe.sh.tpl | 160 ++++++ .../azurerm.ubuntu.docker.tfe.sh.tpl | 134 +++++ .../templates/azurerm_database_init.func.tpl | 33 ++ .../templates/get_base64_secrets.func | 51 ++ .../templates/get_unmounted_disk.func | 33 ++ .../templates/google.rhel.docker.tfe.sh.tpl | 166 ++++++ .../templates/google.rhel.podman.tfe.sh.tpl | 152 ++++++ .../templates/google.ubuntu.docker.tfe.sh.tpl | 153 ++++++ modules/tfe_init/templates/install_jq.func | 20 + .../templates/install_monitoring_agents.func | 45 ++ .../tfe_init/templates/install_packages.func | 42 ++ modules/tfe_init/templates/retry.func | 25 + .../templates/terraform-enterprise.kube.tpl | 8 + modules/tfe_init/templates/tfe.sh.tpl | 212 ++++++++ modules/tfe_init/variables.tf | 220 ++++++++ modules/tfe_init/versions.tf | 8 + modules/tfe_init_replicated/README.md | 37 ++ modules/tfe_init_replicated/files/daemon.json | 4 + modules/tfe_init_replicated/functions.tf | 23 + modules/tfe_init_replicated/main.tf | 46 ++ modules/tfe_init_replicated/outputs.tf | 7 + .../templates/get_base64_secrets.func | 30 + .../templates/install_monitoring_agents.func | 45 ++ .../templates/install_packages.func | 60 ++ .../tfe_init_replicated/templates/retry.func | 25 + .../templates/tfe_replicated.sh.tpl | 315 +++++++++++ modules/tfe_init_replicated/variables.tf | 98 ++++ modules/tfe_init_replicated/versions.tf | 8 + 59 files changed, 4862 insertions(+), 13 deletions(-) create mode 100644 modules/runtime_container_engine_config/database_config.tf create mode 100644 modules/runtime_container_engine_config/main.tf create mode 100644 modules/runtime_container_engine_config/outputs.tf create mode 100644 modules/runtime_container_engine_config/redis_config.tf create mode 100644 modules/runtime_container_engine_config/storage_config.tf create mode 100644 modules/runtime_container_engine_config/variables.tf create mode 100644 modules/runtime_container_engine_config/vault_config.tf create mode 100644 modules/runtime_container_engine_config/versions.tf create mode 100644 modules/settings/README.md create mode 100644 modules/settings/main.tf create mode 100644 modules/settings/outputs.tf create mode 100644 modules/settings/replicated_config.tf create mode 100644 modules/settings/tfe_base_config.tf create mode 100644 modules/settings/tfe_base_external_config.tf create mode 100644 modules/settings/tfe_external_aws_config.tf create mode 100644 modules/settings/tfe_external_azure_config.tf create mode 100644 modules/settings/tfe_external_google_config.tf create mode 100644 modules/settings/tfe_external_vault_config.tf create mode 100644 modules/settings/tfe_redis_config.tf create mode 100644 modules/settings/variables.tf create mode 100644 modules/settings/versions.tf create mode 100644 modules/tfe_init/README.md create mode 100644 modules/tfe_init/files/daemon.json create mode 100644 modules/tfe_init/functions.tf create mode 100644 modules/tfe_init/main.tf create mode 100644 modules/tfe_init/outputs.tf create mode 100644 modules/tfe_init/templates/aws.rhel.docker.tfe.sh.tpl create mode 100644 modules/tfe_init/templates/aws.rhel.podman.tfe.sh.tpl create mode 100644 modules/tfe_init/templates/aws.ubuntu.docker.tfe.sh.tpl create mode 100644 modules/tfe_init/templates/azurerm.rhel.docker.tfe.sh.tpl create mode 100644 modules/tfe_init/templates/azurerm.rhel.podman.tfe.sh.tpl create mode 100644 modules/tfe_init/templates/azurerm.ubuntu.docker.tfe.sh.tpl create mode 100644 modules/tfe_init/templates/azurerm_database_init.func.tpl create mode 100644 modules/tfe_init/templates/get_base64_secrets.func create mode 100644 modules/tfe_init/templates/get_unmounted_disk.func create mode 100644 modules/tfe_init/templates/google.rhel.docker.tfe.sh.tpl create mode 100644 modules/tfe_init/templates/google.rhel.podman.tfe.sh.tpl create mode 100644 modules/tfe_init/templates/google.ubuntu.docker.tfe.sh.tpl create mode 100644 modules/tfe_init/templates/install_jq.func create mode 100644 modules/tfe_init/templates/install_monitoring_agents.func create mode 100644 modules/tfe_init/templates/install_packages.func create mode 100644 modules/tfe_init/templates/retry.func create mode 100644 modules/tfe_init/templates/terraform-enterprise.kube.tpl create mode 100644 modules/tfe_init/templates/tfe.sh.tpl create mode 100644 modules/tfe_init/variables.tf create mode 100644 modules/tfe_init/versions.tf create mode 100644 modules/tfe_init_replicated/README.md create mode 100644 modules/tfe_init_replicated/files/daemon.json create mode 100644 modules/tfe_init_replicated/functions.tf create mode 100644 modules/tfe_init_replicated/main.tf create mode 100644 modules/tfe_init_replicated/outputs.tf create mode 100644 modules/tfe_init_replicated/templates/get_base64_secrets.func create mode 100644 modules/tfe_init_replicated/templates/install_monitoring_agents.func create mode 100644 modules/tfe_init_replicated/templates/install_packages.func create mode 100644 modules/tfe_init_replicated/templates/retry.func create mode 100644 modules/tfe_init_replicated/templates/tfe_replicated.sh.tpl create mode 100644 modules/tfe_init_replicated/variables.tf create mode 100644 modules/tfe_init_replicated/versions.tf diff --git a/main.tf b/main.tf index 0405a344..9ef552bd 100644 --- a/main.tf +++ b/main.tf @@ -20,7 +20,9 @@ data "aws_ami" "ubuntu" { } data "aws_kms_key" "main" { - key_id = var.kms_key_arn + kemodule "settings" { + source = "./modules/settings" + count = var.is_replicated_deployment ? 0 : 1d = var.kms_key_arn } # ----------------------------------------------------------------------------- @@ -270,7 +272,7 @@ module "aurora_database" { # Docker Compose File Config for TFE on instance(s) using Flexible Deployment Options # ------------------------------------------------------------------------------------ module "runtime_container_engine_config" { - source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/runtime_container_engine_config?ref=main" + source = "./modules/runtime_container_engine_config" count = var.is_replicated_deployment ? 0 : 1 tfe_license = var.hc_license @@ -303,15 +305,17 @@ module "runtime_container_engine_config" { iact_time_limit = var.iact_subnet_time_limit run_pipeline_image = var.run_pipeline_image - database_name = local.database.name - database_user = local.database.username - database_password = local.database.password - database_host = local.database.endpoint - database_parameters = local.database.parameters - database_use_mtls = var.db_use_mtls - database_ca_cert_file = "/etc/ssl/private/terraform-enterprise/postgres/ca.crt" - database_client_cert_file = "/etc/ssl/private/terraform-enterprise/postgres/cert.crt" - database_client_key_file = "/etc/ssl/private/terraform-enterprise/postgres/key.key" + database_name = local.database.name + database_user = local.database.username + database_password = local.database.password + database_host = local.database.endpoint + database_parameters = local.database.parameters + database_use_mtls = var.db_use_mtls + database_ca_cert_file = "/etc/ssl/private/terraform-enterprise/postgres/ca.crt" + database_client_cert_file = "/etc/ssl/private/terraform-enterprise/postgres/cert.crt" + database_client_key_file = "/etc/ssl/private/terraform-enterprise/postgres/key.key" + database_passwordless_aws_use_iam = var.postgres_enable_iam_auth && !var.postgres_use_password_auth + database_passwordless_aws_region = var.postgres_enable_iam_auth && !var.postgres_use_password_auth ? data.aws_region.current.name : "" explorer_database_name = local.explorer_database.name explorer_database_user = local.explorer_database.username @@ -360,7 +364,7 @@ module "runtime_container_engine_config" { # AWS cloud init used to install and configure TFE on instance(s) using Flexible Deployment Options # -------------------------------------------------------------------------------------------------- module "tfe_init_fdo" { - source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/tfe_init?ref=main" + source = "./modules/tfe_init" count = var.is_replicated_deployment ? 0 : 1 cloud = "aws" @@ -467,7 +471,7 @@ module "settings" { # AWS user data / cloud init used to install and configure TFE on instance(s) # ----------------------------------------------------------------------------- module "tfe_init_replicated" { - source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/tfe_init_replicated?ref=main" + source = "./modules/tfe_init_replicated" count = var.is_replicated_deployment ? 1 : 0 # TFE & Replicated Configuration data diff --git a/modules/runtime_container_engine_config/database_config.tf b/modules/runtime_container_engine_config/database_config.tf new file mode 100644 index 00000000..9aa338d3 --- /dev/null +++ b/modules/runtime_container_engine_config/database_config.tf @@ -0,0 +1,29 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + database = { + TFE_DATABASE_USER = var.database_user + TFE_DATABASE_PASSWORD = var.database_password + TFE_DATABASE_HOST = var.database_host + TFE_DATABASE_NAME = var.database_name + TFE_DATABASE_PARAMETERS = var.database_parameters + TFE_DATABASE_USE_MTLS = var.database_use_mtls + TFE_DATABASE_CA_CERT_FILE = var.database_ca_cert_file + TFE_DATABASE_CLIENT_CERT_FILE = var.database_client_cert_file + TFE_DATABASE_CLIENT_KEY_FILE = var.database_client_key_file + TFE_DATABASE_PASSWORDLESS_AZURE_USE_MSI = var.database_passwordless_azure_use_msi + TFE_DATABASE_PASSWORDLESS_AZURE_CLIENT_ID = var.database_passwordless_azure_client_id + DATABASE_AUTH_USE_AWS_IAM = var.database_passwordless_aws_use_iam + DATABASE_AUTH_AWS_DB_REGION = var.database_passwordless_aws_region + } + database_configuration = local.disk ? {} : local.database + explorer_database = { + TFE_EXPLORER_DATABASE_HOST = var.explorer_database_host + TFE_EXPLORER_DATABASE_NAME = var.explorer_database_name + TFE_EXPLORER_DATABASE_USER = var.explorer_database_user + TFE_EXPLORER_DATABASE_PASSWORD = var.explorer_database_password + TFE_EXPLORER_DATABASE_PARAMETERS = var.explorer_database_parameters + } + explorer_database_configuration = var.explorer_database_host == null ? {} : local.explorer_database +} diff --git a/modules/runtime_container_engine_config/main.tf b/modules/runtime_container_engine_config/main.tf new file mode 100644 index 00000000..f34a8b7b --- /dev/null +++ b/modules/runtime_container_engine_config/main.tf @@ -0,0 +1,254 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + + active_active = var.operational_mode == "active-active" + disk = var.operational_mode == "disk" + env = merge( + local.database_configuration, + local.redis_configuration, + local.storage_configuration, + local.vault_configuration, + local.explorer_database_configuration, + { + http_proxy = var.http_proxy != null ? "http://${var.http_proxy}" : null + HTTP_PROXY = var.http_proxy != null ? "http://${var.http_proxy}" : null + https_proxy = var.https_proxy != null ? "http://${var.https_proxy}" : null + HTTPS_PROXY = var.https_proxy != null ? "http://${var.https_proxy}" : null + no_proxy = var.no_proxy != null ? join(",", var.no_proxy) : null + NO_PROXY = var.no_proxy != null ? join(",", var.no_proxy) : null + TFE_HOSTNAME = var.hostname + TFE_HTTP_PORT = var.http_port + TFE_HTTPS_PORT = var.https_port + TFE_ADMIN_HTTPS_PORT = var.admin_api_https_port + TFE_OPERATIONAL_MODE = var.operational_mode + TFE_ENCRYPTION_PASSWORD = random_password.enc_password.result + TFE_DISK_CACHE_VOLUME_NAME = "terraform-enterprise_terraform-enterprise-cache" + TFE_LICENSE_REPORTING_OPT_OUT = var.license_reporting_opt_out + TFE_USAGE_REPORTING_OPT_OUT = var.usage_reporting_opt_out + TFE_LICENSE = var.tfe_license + TFE_TLS_CA_BUNDLE_FILE = var.tls_ca_bundle_file != null ? var.tls_ca_bundle_file : null + TFE_TLS_CERT_FILE = var.cert_file + TFE_TLS_CIPHERS = var.tls_ciphers + TFE_TLS_KEY_FILE = var.key_file + TFE_TLS_VERSION = var.tls_version != null ? var.tls_version : "" + TFE_RUN_PIPELINE_IMAGE = var.run_pipeline_image + TFE_CAPACITY_CONCURRENCY = var.capacity_concurrency + TFE_CAPACITY_CPU = var.capacity_cpu + TFE_CAPACITY_MEMORY = var.capacity_memory + TFE_IACT_SUBNETS = var.iact_subnets + TFE_IACT_TIME_LIMIT = var.iact_time_limit + TFE_IACT_TRUSTED_PROXIES = join(",", var.trusted_proxies) + } + ) + # compose files allow for $ deliminated variable injection. $$ is the appropriate escape. + sensitive_fields = ["TFE_ENCRYPTION_PASSWORD", "TFE_DATABASE_PASSWORD", "TFE_REDIS_PASSWORD"] + compose_escaped_env = { + for k, v in local.env : + k => (contains(local.sensitive_fields, k) ? replace((v == null ? "" : v), "$", "$$") : v) + } + compose = { + version = "3.9" + name = "terraform-enterprise" + services = { + tfe = { + image = var.tfe_image + environment = local.compose_escaped_env + cap_add = [ + "IPC_LOCK" + ] + read_only = true + tmpfs = [ + "/tmp:mode=01777", + "/run:${var.enable_run_exec_tmpfs ? "exec" : "noexec"}", + "/var/log/terraform-enterprise", + ] + ports = flatten([ + "80:${var.http_port}", + "443:${var.https_port}", + "${var.admin_api_https_port}:${var.admin_api_https_port}", + local.active_active ? ["8201:8201"] : [], + var.metrics_endpoint_enabled ? [ + "${var.metrics_endpoint_port_http}:9090", + "${var.metrics_endpoint_port_https}:9091" + ] : [] + ]) + + volumes = flatten([ + { + type = "bind" + source = "/var/run/docker.sock" + target = "/run/docker.sock" + }, + { + type = "bind" + source = "/etc/tfe/ssl" + target = "/etc/ssl/private/terraform-enterprise" + }, + { + type = "bind" + source = "/etc/tfe/ssl/postgres" + target = "/etc/ssl/private/terraform-enterprise/postgres" + }, + { + type = "bind" + source = "/etc/tfe/ssl/redis" + target = "/etc/ssl/private/terraform-enterprise/redis" + }, + { + type = "volume" + source = "terraform-enterprise-cache" + target = "/var/cache/tfe-task-worker/terraform" + }, + local.disk ? [{ + type = "bind" + source = var.disk_path + target = "/var/lib/terraform-enterprise" + }] : [], + ]) + } + } + volumes = merge( + { terraform-enterprise-cache = {} }, + local.disk ? { terraform-enterprise = {} } : {} + ) + } + kube = { + apiVersion = "v1" + kind = "Pod" + metadata = { + labels = { + app = "terraform-enterprise" + } + name = "terraform-enterprise" + } + spec = { + restartPolicy = "Never" + containers = [{ + env = [ + for k, v in local.env : { + name = k, + value = v + } + ] + image = var.tfe_image + name = "terraform-enterprise" + ports = flatten([ + { + containerPort = var.http_port + hostPort = 80 + }, + { + containerPort = var.https_port + hostPort = 443 + }, + { + containerPort = var.admin_api_https_port + hostPort = var.admin_api_https_port + }, + local.active_active ? [{ containerPort = 8201, hostPort = 8201 }] : [], + var.metrics_endpoint_enabled ? [ + { containerPort = 9090, hostPort = var.metrics_endpoint_port_http }, + { containerPort = 9091, hostPort = var.metrics_endpoint_port_https } + ] : [] + ]) + securityContext = { + capabilities = { + add = [ + "CAP_IPC_LOCK" + ] + } + readOnlyRootFilesystem = true + seLinuxOptions = { + type = "spc_t" + } + } + volumeMounts = flatten([ + { + mountPath = "/etc/ssl/private/terraform-enterprise" + name = "certs" + }, + { + mountPath = "/var/log/terraform-enterprise" + name = "log" + }, + { + mountPath = "/run" + name = "run" + }, + { + mountPath = "/tmp" + name = "tmp" + }, + { + mountPath = "/run/docker.sock" + name = "docker-sock" + }, + { + mountPath = "/var/cache/tfe-task-worker/terraform" + name = "terraform-enterprise_terraform-enterprise-cache-pvc" + }, + local.disk ? [{ + mountPath = "/var/lib/terraform-enterprise" + name = "data" + }] : [] + ]) + }, + ] + volumes = flatten([ + { + hostPath = { + path = "/etc/tfe/ssl" + type = "Directory" + } + name = "certs" + }, + { + emptyDir = { + medium = "Memory" + } + name = "log" + }, + { + emptyDir = { + medium = "Memory" + } + name = "run" + }, + { + emptyDir = { + medium = "Memory" + } + name = "tmp" + }, + { + hostPath = { + path = "/var/run/docker.sock" + type = "File" + } + name = "docker-sock" + }, + { + name = "terraform-enterprise_terraform-enterprise-cache-pvc" + persistentVolumeClaim = { + claimName = "terraform-enterprise_terraform-enterprise-cache" + } + }, + local.disk ? [{ + hostPath = { + path = var.disk_path + type = "Directory" + } + name = "data" + }] : [], + ]) + } + } +} + +resource "random_password" "enc_password" { + length = 32 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} diff --git a/modules/runtime_container_engine_config/outputs.tf b/modules/runtime_container_engine_config/outputs.tf new file mode 100644 index 00000000..172a3d42 --- /dev/null +++ b/modules/runtime_container_engine_config/outputs.tf @@ -0,0 +1,12 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +output "docker_compose_yaml" { + value = base64encode(yamlencode(local.compose)) + description = "A base 64 encoded yaml object that will be used as the Docker Compose file for TFE deployment." +} + +output "podman_kube_yaml" { + value = base64encode(yamlencode(local.kube)) + description = "A base 64 encoded yaml object that will be used as the Podman kube.yaml file for TFE deployment" +} diff --git a/modules/runtime_container_engine_config/redis_config.tf b/modules/runtime_container_engine_config/redis_config.tf new file mode 100644 index 00000000..1223ad2a --- /dev/null +++ b/modules/runtime_container_engine_config/redis_config.tf @@ -0,0 +1,25 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + redis = { + TFE_REDIS_HOST = var.redis_use_tls != null ? var.redis_use_tls ? "${var.redis_host}:6380" : var.redis_host : null + TFE_REDIS_USER = var.redis_user + TFE_REDIS_PASSWORD = var.redis_password + TFE_REDIS_USE_TLS = var.redis_use_tls + TFE_REDIS_USE_AUTH = var.redis_use_auth + TFE_REDIS_SENTINEL_ENABLED = var.redis_use_sentinel + TFE_REDIS_SENTINEL_HOSTS = join(",", var.redis_sentinel_hosts) + TFE_REDIS_SENTINEL_LEADER_NAME = var.redis_sentinel_leader_name + TFE_REDIS_SENTINEL_PASSWORD = var.redis_sentinel_password + TFE_REDIS_SENTINEL_USERNAME = var.redis_sentinel_user + TFE_REDIS_CA_CERT_PATH = var.redis_ca_cert_path + TFE_REDIS_CLIENT_CERT_PATH = var.redis_client_cert_path + TFE_REDIS_CLIENT_KEY_PATH = var.redis_client_key_path + TFE_REDIS_USE_MTLS = var.redis_use_mtls ? "true" : var.enable_sentinel_mtls ? "true" : "false" + TFE_REDIS_PASSWORDLESS_AZURE_USE_MSI = var.redis_passwordless_azure_use_msi + TFE_REDIS_SIDEKIQ_PASSWORDLESS_AZURE_USE_MSI = var.redis_passwordless_azure_use_msi + TFE_REDIS_PASSWORDLESS_AZURE_CLIENT_ID = var.redis_passwordless_azure_client_id + } + redis_configuration = local.active_active ? local.redis : {} +} diff --git a/modules/runtime_container_engine_config/storage_config.tf b/modules/runtime_container_engine_config/storage_config.tf new file mode 100644 index 00000000..09fc122e --- /dev/null +++ b/modules/runtime_container_engine_config/storage_config.tf @@ -0,0 +1,33 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + storage_options = { + azure = { + TFE_OBJECT_STORAGE_TYPE = var.storage_type + TFE_OBJECT_STORAGE_AZURE_ACCOUNT_KEY = var.azure_account_key + TFE_OBJECT_STORAGE_AZURE_ACCOUNT_NAME = var.azure_account_name + TFE_OBJECT_STORAGE_AZURE_CONTAINER = var.azure_container + TFE_OBJECT_STORAGE_AZURE_ENDPOINT = var.azure_endpoint + } + google = { + TFE_OBJECT_STORAGE_TYPE = var.storage_type + TFE_OBJECT_STORAGE_GOOGLE_BUCKET = var.google_bucket + TFE_OBJECT_STORAGE_GOOGLE_CREDENTIALS = var.google_credentials + TFE_OBJECT_STORAGE_GOOGLE_PROJECT = var.google_project + } + s3 = { + TFE_OBJECT_STORAGE_TYPE = var.storage_type + TFE_OBJECT_STORAGE_S3_ACCESS_KEY_ID = var.s3_access_key_id + TFE_OBJECT_STORAGE_S3_SECRET_ACCESS_KEY = var.s3_secret_access_key + TFE_OBJECT_STORAGE_S3_REGION = var.s3_region + TFE_OBJECT_STORAGE_S3_BUCKET = var.s3_bucket + TFE_OBJECT_STORAGE_S3_ENDPOINT = var.s3_endpoint + TFE_OBJECT_STORAGE_S3_SERVER_SIDE_ENCRYPTION = var.s3_server_side_encryption + TFE_OBJECT_STORAGE_S3_SERVER_SIDE_ENCRYPTION_KMS_KEY_ID = var.s3_server_side_encryption_kms_key_id + TFE_OBJECT_STORAGE_S3_USE_INSTANCE_PROFILE = var.s3_use_instance_profile + } + } + + storage_configuration = var.storage_type != null && !local.disk ? local.storage_options[var.storage_type] : {} +} diff --git a/modules/runtime_container_engine_config/variables.tf b/modules/runtime_container_engine_config/variables.tf new file mode 100644 index 00000000..29aa19d3 --- /dev/null +++ b/modules/runtime_container_engine_config/variables.tf @@ -0,0 +1,511 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "azure_account_key" { + default = null + type = string + description = "Azure Blob Storage access key. Required when TFE_OBJECT_STORAGE_TYPE is azure and TFE_OBJECT_STORAGE_AZURE_USE_MSI is false." +} + +variable "azure_account_name" { + default = null + type = string + description = "Azure Blob Storage account name. Required when TFE_OBJECT_STORAGE_TYPE is azure." +} + +variable "azure_container" { + default = null + type = string + description = "Azure Blob Storage container name. Required when TFE_OBJECT_STORAGE_TYPE is azure." +} + +variable "azure_endpoint" { + default = null + type = string + description = "Azure Storage endpoint. Useful if using a private endpoint for Azure Stoage. Leave blank to use the default Azure Storage endpoint. Defaults to \"\" if no value is given. " +} + +variable "capacity_concurrency" { + type = number + description = "Maximum number of Terraform runs that can execute concurrently on each Terraform Enterprise node. Defaults to 10 if no value is given." +} + +variable "capacity_cpu" { + type = number + description = "Maximum number of CPU cores a Terraform run is allowed to use. Set to 0 for no limit. Defaults to 0." +} + +variable "capacity_memory" { + type = number + description = "Maximum amount of memory (MiB) a Terraform run is allowed to use. Defaults to 2048 if no value is given." +} + +variable "cert_file" { + type = string + description = "Path to a file containing the TLS certificate Terraform Enterprise will use when serving TLS connections to clients." +} + +variable "database_host" { + type = string + description = "The PostgreSQL server to connect to in the format HOST[:PORT] (e.g. db.example.com or db.example.com:5432). If only HOST is provided then the :PORT defaults to :5432 if no value is given. Required when TFE_OPERATIONAL_MODE is external or active-active." +} + +variable "database_name" { + type = string + description = "Name of the PostgreSQL database to store application data in. Required when TFE_OPERATIONAL_MODE is external or active-active." +} + +variable "database_parameters" { + type = string + description = "PostgreSQL server parameters for the connection URI. Used to configure the PostgreSQL connection (e.g. sslmode=require)." +} + +variable "database_password" { + type = string + description = "PostgreSQL password. Required when TFE_OPERATIONAL_MODE is external or active-active." +} + +variable "database_user" { + type = string + description = "PostgreSQL user. Required when TFE_OPERATIONAL_MODE is external or active-active." +} + +variable "database_ca_cert_file" { + type = string + description = "Path to a file containing the CA certificate for Database TLS connections. Leave blank to not use a CA certificate for Database TLS connections. Defaults to \"\" if no value is given." + default = null +} + +variable "database_client_cert_file" { + type = string + description = "Path to a file containing the client certificate for Database TLS connections. Leave blank to not use a client certificate for Database TLS connections. Defaults to \"\" if no value is given." + default = null +} + +variable "database_client_key_file" { + type = string + description = "Path to a file containing the client key for Database TLS connections. Leave blank to not use a client key for Database TLS connections. Defaults to \"\" if no value is given." + default = null +} + +variable "database_use_mtls" { + type = bool + description = "Whether or not to use mutual TLS to access database. Defaults to false if no value is given." + default = false +} + +variable "database_passwordless_azure_use_msi" { + default = false + type = bool + description = "Whether or not to use Azure Managed Service Identity (MSI) to connect to the PostgreSQL database. Defaults to false if no value is given." +} + +variable "database_passwordless_azure_client_id" { + default = "" + type = string + description = "Azure Managed Service Identity (MSI) Client ID. If not set, System Assigned Managed Identity will be used." +} + +variable "database_passwordless_aws_use_iam" { + default = false + type = bool + description = "Whether or not to use AWS IAM authentication to connect to the PostgreSQL database. Defaults to false if no value is given." +} + +variable "database_passwordless_aws_region" { + default = "" + type = string + description = "AWS region for IAM database authentication. Required when database_passwordless_aws_use_iam is true." +} + +variable "explorer_database_host" { + type = string + default = null + description = "The PostgreSQL server to connect to in the format HOST[:PORT] (e.g. db.example.com or db.example.com:5432). If only HOST is provided then the :PORT defaults to :5432 if no value is given. Required when TFE_OPERATIONAL_MODE is external or active-active." +} + +variable "explorer_database_name" { + type = string + default = null + description = "Name of the PostgreSQL database to store application data in. Required when TFE_OPERATIONAL_MODE is external or active-active." +} + +variable "explorer_database_parameters" { + type = string + default = null + description = "PostgreSQL server parameters for the connection URI. Used to configure the PostgreSQL connection (e.g. sslmode=require)." +} + +variable "explorer_database_password" { + type = string + default = null + description = "PostgreSQL password. Required when TFE_OPERATIONAL_MODE is external or active-active." +} + +variable "explorer_database_user" { + type = string + default = null + description = "PostgreSQL user. Required when TFE_OPERATIONAL_MODE is external or active-active." +} + +variable "disk_path" { + default = null + description = "The pathname of the directory in which Terraform Enterprise will store data in Mounted Disk mode. Required when var.operational_mode is 'disk'." + type = string +} + +variable "enable_sentinel_mtls" { + type = bool + description = "Whether or not to use mutual TLS to access Redis Sentinel. Defaults to false if no value is given." + default = false +} + +variable "http_port" { + default = null + type = number + description = "Port application listens on for HTTP. Default is 80." +} + +variable "https_port" { + default = null + type = number + description = "Port application listens on for HTTPS. Default is 443." +} + +variable "admin_api_https_port" { + default = 8443 + type = number + description = "Port application listens on for Admin API. Default is 8443." +} + +variable "iact_subnets" { + type = string + description = "Comma-separated list of subnets in CIDR notation that are allowed to retrieve the initial admin creation token via the API (e.g. 10.0.0.0/8,192.168.0.0/24). Leave blank to disable retrieving the initial admin creation token via the API from outside the host. Defaults to \"\" if no value is given." +} + +variable "iact_time_limit" { + type = number + description = "Number of minutes that the initial admin creation token can be retrieved via the API after the application starts. Defaults to 60 if no value is given." +} + +variable "google_bucket" { + default = null + type = string + description = "Google Cloud Storage bucket name. Required when TFE_OBJECT_STORAGE_TYPE is google." +} + +variable "google_credentials" { + default = null + type = string + description = "Google Cloud Storage JSON credentials. Must be given as an escaped string of JSON or Base64 encoded JSON. Leave blank to use the attached service account. Defaults to \"\" if no value is given." +} + +variable "google_project" { + default = null + type = string + description = "Google Cloud Storage project name. Required when TFE_OBJECT_STORAGE_TYPE is google." +} + +variable "hostname" { + type = string + description = "Hostname where Terraform Enterprise is accessed (e.g. terraform.example.com)." +} + +variable "http_proxy" { + type = string + description = "(Optional) The IP address and port of existing web proxy to route TFE http traffic through." + default = null +} + +variable "https_proxy" { + type = string + description = "(Optional) The IP address and port of existing web proxy to route TFE https traffic through." + default = null +} + +variable "license_reporting_opt_out" { + type = bool + default = false + description = "Whether to opt out of reporting licensing information to HashiCorp. Defaults to false if no value is given." +} + +variable "usage_reporting_opt_out" { + type = bool + default = false + description = "Whether to opt out of TFE usage reporting to HashiCorp. Defaults to false if no value is given." +} + +variable "key_file" { + type = string + description = "Path to a file containing the TLS private key Terraform Enterprise will use when serving TLS connections to clients." +} + +variable "metrics_endpoint_enabled" { + default = false + type = bool + description = "(Optional) Metrics are used to understand the behavior of Terraform Enterprise and to troubleshoot and tune performance. Enable an endpoint to expose container metrics. Defaults to false." +} + +variable "metrics_endpoint_port_http" { + default = null + type = number + description = "(Optional when metrics_endpoint_enabled is true.) Defines the TCP port on which HTTP metrics requests will be handled. Defaults to 9090." +} + +variable "metrics_endpoint_port_https" { + default = null + type = string + description = "(Optional when metrics_endpoint_enabled is true.) Defines the TCP port on which HTTPS metrics requests will be handled. Defaults to 9091." +} + + +variable "no_proxy" { + type = list(string) + description = "(Optional) List of IP addresses to not proxy" + default = [] +} + +variable "operational_mode" { + type = string + description = "Terraform Enterprise operational mode." + validation { + condition = ( + var.operational_mode == "disk" || + var.operational_mode == "external" || + var.operational_mode == "active-active" + ) + + error_message = "Supported values for operational_mode are 'disk', 'external', and 'active-active'." + } +} + +variable "redis_host" { + type = string + description = "The Redis server to connect to in the format HOST[:PORT] (e.g. redis.example.com or redis.example.com:). If only HOST is provided then the :PORT defaults to :6379 if no value is given. Required when TFE_OPERATIONAL_MODE is active-active." +} + +variable "redis_password" { + type = string + description = "Redis server password. Required when TFE_REDIS_USE_AUTH is true." +} + +variable "redis_use_auth" { + type = bool + description = "Whether or not to use authentication to access Redis. Defaults to false if no value is given." +} + +variable "redis_use_tls" { + type = bool + description = "Whether or not to use TLS to access Redis. Defaults to false if no value is given." +} + +variable "redis_ca_cert_path" { + type = string + description = "Path to a file containing the CA certificate for Redis TLS connections. Leave blank to not use a CA certificate for Redis TLS connections. Defaults to \"\" if no value is given." + default = null +} +variable "redis_client_cert_path" { + type = string + description = "Path to a file containing the client certificate for Redis TLS connections. Leave blank to not use a client certificate for Redis TLS connections. Defaults to \"\" if no value is given." + default = null +} + +variable "redis_client_key_path" { + type = string + description = "Path to a file containing the client key for Redis TLS connections. Leave blank to not use a client key for Redis TLS connections. Defaults to \"\" if no value is given." + default = null +} + +variable "redis_use_mtls" { + type = bool + description = "Whether or not to use mutual TLS to access Redis. Defaults to false if no value is given." + default = false +} + +variable "redis_user" { + type = string + description = "Redis server user. Leave blank to not use a user when authenticating. Defaults to \"\" if no value is given." +} + +variable "redis_use_sentinel" { + type = bool + description = "Will connections to redis use the sentinel protocol?" + default = false +} + +variable "redis_sentinel_hosts" { + type = list(string) + description = "A list of sentinel host/port combinations in the form of 'host:port', eg: sentinel-leader.terraform.io:26379" + default = [] +} + +variable "redis_sentinel_leader_name" { + type = string + description = "The name of the sentinel leader." + default = null +} + +variable "redis_sentinel_user" { + type = string + description = "Redis sentinel user. Leave blank to not use a user when authenticating to redis sentinel. Defaults to \"\" if no value is given." + default = null +} + +variable "redis_sentinel_password" { + type = string + description = "Redis senitnel password." + default = null +} + +variable "redis_passwordless_azure_use_msi" { + default = false + type = bool + description = "Whether or not to use Azure Managed Service Identity (MSI) to connect to the Redis server. Defaults to false if no value is given." +} + +variable "redis_passwordless_azure_client_id" { + default = "" + type = string + description = "Azure Managed Service Identity (MSI) Client ID to be used for redis authentication. If not set, System Assigned Managed Identity will be used." +} + +variable "run_pipeline_image" { + type = string + description = "Container image used to execute Terraform runs. Leave blank to use the default image that comes with Terraform Enterprise. Defaults to \"\" if no value is given." +} + +variable "s3_access_key_id" { + default = null + type = string + description = "S3 access key ID. Required when TFE_OBJECT_STORAGE_TYPE is s3 and TFE_OBJECT_STORAGE_S3_USE_INSTANCE_PROFILE is false." +} + +variable "s3_secret_access_key" { + default = null + type = string + description = "S3 secret access key. Required when TFE_OBJECT_STORAGE_TYPE is s3 and TFE_OBJECT_STORAGE_S3_USE_INSTANCE_PROFILE is false." + +} + +variable "s3_region" { + default = null + type = string + description = "S3 region. Required when TFE_OBJECT_STORAGE_TYPE is s3." +} + +variable "s3_bucket" { + default = null + type = string + description = "S3 bucket name. Required when TFE_OBJECT_STORAGE_TYPE is s3." +} + +variable "s3_endpoint" { + default = null + type = string + description = "S3 endpoint. Useful when using a private S3 endpoint. Leave blank to use the default AWS S3 endpoint. Defaults to \"\" if no value is given." +} + +variable "s3_server_side_encryption" { + default = null + type = string + description = "Server-side encryption algorithm to use. Set to aws:kms to use AWS KMS. Leave blank to disable server-side encryption. Defaults to \"\" if no value is given." +} + +variable "s3_server_side_encryption_kms_key_id" { + default = null + type = string + description = "KMS key ID to use for server-side encryption. Leave blank to use AWS-managed keys. Defaults to \"\" if no value is given." +} + +variable "s3_use_instance_profile" { + default = null + type = string + description = "Whether to use the instance profile for authentication. Defaults to false if no value is given." +} + +variable "storage_type" { + type = string + description = "Type of object storage to use. Must be one of s3, azure, or google. Required when TFE_OPERATIONAL_MODE is external or active-active." + validation { + condition = contains(["s3", "google", "azure"], var.storage_type) + error_message = "The storage_type value must be one of: \"s3\"; \"google\"; \"azure\"." + } +} + +variable "tfe_image" { + type = string + description = "The registry path, image name, and image version (e.g. \"quay.io/hashicorp/terraform-enterprise:1234567\")" +} + +variable "tfe_license" { + type = string + description = "The HashiCorp license. Defaults to \"\" if no value is given. Required when TFE_LICENSE_PATH is unset." +} + +variable "tls_ca_bundle_file" { + default = null + type = string + description = "Path to a file containing TLS CA certificates to be added to the OS CA certificates bundle. Leave blank to not add CA certificates to the OS CA certificates bundle. Defaults to \"\" if no value is given." +} + +variable "tls_ciphers" { + type = string + description = "TLS ciphers to use for TLS. Must be valid OpenSSL format. Leave blank to use the default ciphers. Defaults to \"\" if no value is given." +} + +variable "tls_version" { + default = null + type = string + description = "(Not needed if is_replicated_deployment is true) TLS version to use. Leave blank to use both TLS v1.2 and TLS v1.3. Defaults to `\"\"` if no value is given." + validation { + condition = ( + var.tls_version == null || + var.tls_version == "tls_1_2" || + var.tls_version == "tls_1_3" || + var.tls_version == "tls_1_2_tls_1_3" + ) + error_message = "The tls_version value must be 'tls_1_2', 'tls_1_3', or null." + } +} + +variable "trusted_proxies" { + default = [] + description = "A list of IP address ranges which will be considered safe to ignore when evaluating the IP addresses of requests like those made to the IACT endpoint." + type = list(string) +} + +variable "vault_address" { + type = string + description = "Address of the external Vault server (e.g. https://vault.example.com:8200). Defaults to \"\" if no value is given. Required when TFE_VAULT_USE_EXTERNAL is true." +} + +variable "vault_namespace" { + type = string + description = "Vault namespace. External Vault only. Leave blank to use the default namespace. Defaults to \"\" if no value is given." +} + +variable "vault_path" { + type = string + description = "Vault path when AppRole is mounted. External Vault only. Defaults to auth/approle if no value is given." +} + +variable "vault_role_id" { + type = string + description = "Vault role ID. External Vault only. Required when TFE_VAULT_USE_EXTERNAL is true." +} + +variable "vault_secret_id" { + type = string + description = "Vault secret ID. External Vault only. Required when TFE_VAULT_USE_EXTERNAL is true." +} + +variable "vault_token_renew" { + type = number + description = "Vault token renewal period in seconds. Required when TFE_VAULT_USE_EXTERNAL is true." +} + +variable "enable_run_exec_tmpfs" { + default = false + type = bool + description = "Enable the use of executables in the tmpfs for the /run directory. Defaults to false." +} diff --git a/modules/runtime_container_engine_config/vault_config.tf b/modules/runtime_container_engine_config/vault_config.tf new file mode 100644 index 00000000..a290a748 --- /dev/null +++ b/modules/runtime_container_engine_config/vault_config.tf @@ -0,0 +1,23 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + + vault_enable_external = var.vault_address != null ? true : false + + external_vault_config = { + TFE_VAULT_USE_EXTERNAL = true + TFE_VAULT_ADDRESS = var.vault_address + TFE_VAULT_NAMESPACE = var.vault_namespace + TFE_VAULT_PATH = var.vault_path + TFE_VAULT_ROLE_ID = var.vault_role_id + TFE_VAULT_SECRET_ID = var.vault_secret_id + TFE_VAULT_TOKEN_RENEW = var.vault_token_renew + } + + vault_cluster_address = { + TFE_VAULT_CLUSTER_ADDRESS = join("", ["https://", "$HOST_IP", ":8201"]) + } + + vault_configuration = local.vault_enable_external ? local.external_vault_config : local.active_active && !local.vault_enable_external ? local.vault_cluster_address : {} +} diff --git a/modules/runtime_container_engine_config/versions.tf b/modules/runtime_container_engine_config/versions.tf new file mode 100644 index 00000000..2297780a --- /dev/null +++ b/modules/runtime_container_engine_config/versions.tf @@ -0,0 +1,12 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_version = ">= 0.14" + required_providers { + random = { + source = "hashicorp/random" + version = "~> 3.1" + } + } +} diff --git a/modules/settings/README.md b/modules/settings/README.md new file mode 100644 index 00000000..e0ba347c --- /dev/null +++ b/modules/settings/README.md @@ -0,0 +1,57 @@ +# TFE Settings Module + +This module is used to create the settings that are required for installing Terraform Enterprise (TFE) on a virtual machine. + +## Required variables + +None of the variables in this module are required, however, if you are using this module to provide the input +variables for the [`tfe_init`](../tfe_init) module, then please review both the variables file in this module +as well as the `tfe_init` module to see what you will need. + +## Example usage + +This example illustrates how this module may be used by a Terraform Enterprise module, consuming outputs from other submodules. + +```hcl +module "settings" { + source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/settings?ref=main" + + # TFE Base Configuration + production_type = var.production_type + iact_subnet_list = var.iact_subnet_list + trusted_proxies = local.trusted_proxies + release_sequence = var.release_sequence + pg_extra_params = var.pg_extra_params + + # Replicated Base Configuration + hostname = module.load_balancer.fqdn + tfe_license_file_location = var.tfe_license_file_location + tls_bootstrap_cert_pathname = var.tls_bootstrap_cert_pathname + tls_bootstrap_key_pathname = var.tls_bootstrap_key_pathname + bypass_preflight_checks = var.bypass_preflight_checks + + # Database + pg_dbname = local.database.name + pg_netloc = local.database.address + pg_user = local.database.server.administrator_login + pg_password = local.database.server.administrator_password + + # Redis + redis_host = local.redis.host + redis_pass = local.redis.pass + redis_use_tls = local.redis.use_tls + redis_use_password_auth = local.redis.use_password_auth + + # Azure + azure_account_key = local.object_storage.storage_account_key + azure_account_name = local.object_storage.storage_account_name + azure_container = local.object_storage.storage_account_container_name +} +``` + +## Resources + +- [random_id](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) + +In addition to `random_id` resources, this module uses `local` variables to form objects that will +be output with the intention of becoming input variables for the [`tfe_init`](../tfe_init) module. diff --git a/modules/settings/main.tf b/modules/settings/main.tf new file mode 100644 index 00000000..d95b593f --- /dev/null +++ b/modules/settings/main.tf @@ -0,0 +1,58 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + replicated_configuration = { for k, v in local.replicated_base_config : k => v if v != tostring(null) } + + tfe_merged_configuration = merge( + local.base_configs, + local.base_external_configs, + local.external_aws_configs, + local.external_azure_configs, + local.external_google_configs, + local.redis_configuration, + local.external_vault_configs + ) + + tfe_configuration_remove_null = { for k, v in flatten([local.tfe_merged_configuration]).0 : k => v if v.value != tostring(null) } + tfe_configuration = { for k, v in local.tfe_configuration_remove_null : k => v if v.value != "" } +} + +resource "random_id" "archivist_token" { + byte_length = 16 +} + +resource "random_id" "cookie_hash" { + byte_length = 16 +} + +resource "random_password" "enc_password" { + length = 32 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + + +resource "random_id" "install_id" { + byte_length = 16 +} + +resource "random_id" "internal_api_token" { + byte_length = 16 +} + +resource "random_id" "root_secret" { + byte_length = 16 +} + +resource "random_id" "registry_session_secret_key" { + byte_length = 16 +} + +resource "random_id" "registry_session_encryption_key" { + byte_length = 16 +} + +resource "random_id" "user_token" { + byte_length = 16 +} diff --git a/modules/settings/outputs.tf b/modules/settings/outputs.tf new file mode 100644 index 00000000..05db1aa0 --- /dev/null +++ b/modules/settings/outputs.tf @@ -0,0 +1,12 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +output "replicated_configuration" { + value = local.replicated_configuration + description = "The settings that will be used to configure Replicated." +} + +output "tfe_configuration" { + value = local.tfe_configuration + description = "The settings that will be used to configure Terraform Enterprise." +} \ No newline at end of file diff --git a/modules/settings/replicated_config.tf b/modules/settings/replicated_config.tf new file mode 100644 index 00000000..7a749ff7 --- /dev/null +++ b/modules/settings/replicated_config.tf @@ -0,0 +1,25 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_string" "password" { + length = 16 + special = false +} + +locals { + replicated_base_config = { + BypassPreflightChecks = var.bypass_preflight_checks + DaemonAuthenticationType = "password" + DaemonAuthenticationPassword = random_string.password.result + ImportSettingsFrom = "/etc/ptfe-settings.json" + LicenseFileLocation = var.tfe_license_file_location + LicenseBootstrapAirgapPackagePath = var.tfe_license_bootstrap_airgap_package_path + LicenseBootstrapChannelID = var.tfe_license_bootstrap_channel_id + LogLevel = var.log_level + TlsBootstrapHostname = var.hostname + TlsBootstrapCert = var.tls_bootstrap_cert_pathname + TlsBootstrapKey = var.tls_bootstrap_key_pathname + TlsBootstrapType = var.tls_bootstrap_cert_pathname != null ? "server-path" : "self-signed" + ReleaseSequence = var.tfe_license_bootstrap_airgap_package_path != null ? null : var.release_sequence + } +} diff --git a/modules/settings/tfe_base_config.tf b/modules/settings/tfe_base_config.tf new file mode 100644 index 00000000..1999c2a9 --- /dev/null +++ b/modules/settings/tfe_base_config.tf @@ -0,0 +1,140 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + has_external_storage = var.production_type == "active-active" || var.production_type == "external" + base_configs = { + hostname = { + value = var.hostname + } + + production_type = { + value = var.production_type + } + + # Alphabetical starting here + archivist_token = { + value = random_id.archivist_token.hex + } + + backup_token = { + value = var.backup_token + } + + capacity_concurrency = { + value = var.capacity_concurrency != null ? tostring(var.capacity_concurrency) : null + } + + capacity_memory = { + value = var.capacity_memory != null ? tostring(var.capacity_memory) : null + } + + capacity_cpus = { + value = var.capacity_cpus != null ? tostring(var.capacity_cpus) : null + } + + cookie_hash = { + value = random_id.cookie_hash.hex + } + + custom_image_tag = { + value = var.custom_agent_image_tag != null ? null : var.custom_image_tag + } + + custom_agent_image_tag = { + value = var.custom_agent_image_tag + } + + disk_path = { + value = var.disk_path + } + + enc_password = { + value = var.extern_vault_enable != null ? var.extern_vault_enable ? null : random_password.enc_password.result : random_password.enc_password.result + } + + extra_no_proxy = { + value = var.extra_no_proxy != null ? join(",", var.extra_no_proxy) : null + } + + hairpin_addressing = { + value = var.hairpin_addressing != null ? var.hairpin_addressing ? "1" : "0" : null + } + + force_tls = { + value = var.force_tls != null ? var.force_tls ? "1" : "0" : null + } + + iact_subnet_list = { + value = var.iact_subnet_list != null ? join(",", var.iact_subnet_list) : null + } + + iact_subnet_time_limit = { + value = var.iact_subnet_time_limit + } + + install_id = { + value = random_id.install_id.hex + } + + internal_api_token = { + value = random_id.internal_api_token.hex + } + + metrics_endpoint_enabled = { + value = var.metrics_endpoint_enabled != null ? var.metrics_endpoint_enabled ? "1" : "0" : null + } + + metrics_endpoint_port_http = { + value = var.metrics_endpoint_port_http != null ? tostring(var.metrics_endpoint_port_http) : null + } + + metrics_endpoint_port_https = { + value = var.metrics_endpoint_port_https != null ? tostring(var.metrics_endpoint_port_https) : null + } + + placement = { + value = (local.has_external_storage && var.s3_bucket != null) ? "placement_s3" : (local.has_external_storage && var.azure_account_name != null) ? "placement_azure" : (local.has_external_storage && var.gcs_bucket != null) ? "placement_gcs" : null + } + + registry_session_encryption_key = { + value = random_id.registry_session_encryption_key.hex + } + + registry_session_secret_key = { + value = random_id.registry_session_secret_key.hex + } + + restrict_worker_metadata_access = { + value = var.restrict_worker_metadata_access != null ? var.restrict_worker_metadata_access ? "1" : "0" : null + } + + root_secret = { + value = random_id.root_secret.hex + } + + run_pipeline_mode = { + value = var.custom_image_tag != null ? "legacy" : "agent" + } + + tbw_image = { + value = var.custom_image_tag != null ? "custom_image" : null + } + + tls_ciphers = { + value = var.tls_ciphers + } + + tls_vers = { + value = var.tls_vers + } + + trusted_proxies = { + value = var.trusted_proxies != null ? join(",", var.trusted_proxies) : null + } + + user_token = { + value = random_id.user_token.hex + } + } +} diff --git a/modules/settings/tfe_base_external_config.tf b/modules/settings/tfe_base_external_config.tf new file mode 100644 index 00000000..26abeef0 --- /dev/null +++ b/modules/settings/tfe_base_external_config.tf @@ -0,0 +1,43 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + pg_configs = { + enable_active_active = { + value = var.production_type == "active-active" ? "1" : "0" + } + + pg_dbname = { + value = var.pg_dbname + } + + pg_netloc = { + value = var.pg_netloc + } + + pg_password = { + value = var.pg_password + } + + pg_user = { + value = var.pg_user + } + + log_forwarding_config = { + value = var.log_forwarding_config + } + + log_forwarding_enabled = { + value = var.log_forwarding_enabled != null ? var.log_forwarding_enabled ? "1" : "0" : null + } + + } + + pg_optional_configs = { + pg_extra_params = { + value = var.pg_extra_params + } + } + + base_external_configs = local.pg_optional_configs != null && (var.production_type == "active-active" || var.production_type == "external") ? (merge(local.pg_configs, local.pg_optional_configs)) : local.pg_configs +} diff --git a/modules/settings/tfe_external_aws_config.tf b/modules/settings/tfe_external_aws_config.tf new file mode 100644 index 00000000..70af0ed9 --- /dev/null +++ b/modules/settings/tfe_external_aws_config.tf @@ -0,0 +1,39 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + external_aws_configs = { + + aws_instance_profile = { + value = var.aws_access_key_id == null ? "1" : "0" + } + + aws_access_key_id = { + value = var.aws_access_key_id + } + + aws_secret_access_key = { + value = var.aws_secret_access_key + } + + s3_endpoint = { + value = var.s3_endpoint + } + + s3_bucket = { + value = var.s3_bucket + } + + s3_region = { + value = var.s3_region + } + + s3_sse = { + value = var.s3_sse + } + + s3_sse_kms_key_id = { + value = var.s3_sse_kms_key_id + } + } +} \ No newline at end of file diff --git a/modules/settings/tfe_external_azure_config.tf b/modules/settings/tfe_external_azure_config.tf new file mode 100644 index 00000000..ec89a041 --- /dev/null +++ b/modules/settings/tfe_external_azure_config.tf @@ -0,0 +1,22 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + external_azure_configs = { + azure_account_name = { + value = var.azure_account_name + } + + azure_account_key = { + value = var.azure_account_key + } + + azure_container = { + value = var.azure_container + } + + azure_endpoint = { + value = var.azure_endpoint + } + } +} diff --git a/modules/settings/tfe_external_google_config.tf b/modules/settings/tfe_external_google_config.tf new file mode 100644 index 00000000..1a2d14ab --- /dev/null +++ b/modules/settings/tfe_external_google_config.tf @@ -0,0 +1,18 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + external_google_configs = { + gcs_bucket = { + value = var.gcs_bucket + } + + gcs_credentials = { + value = var.gcs_credentials + } + + gcs_project = { + value = var.gcs_project + } + } +} diff --git a/modules/settings/tfe_external_vault_config.tf b/modules/settings/tfe_external_vault_config.tf new file mode 100644 index 00000000..576334ed --- /dev/null +++ b/modules/settings/tfe_external_vault_config.tf @@ -0,0 +1,40 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + extern_vault_configs = { + extern_vault_enable = { + value = var.extern_vault_enable != null ? var.extern_vault_enable ? "1" : "0" : null + } + + extern_vault_addr = { + value = var.extern_vault_addr + } + + extern_vault_role_id = { + value = var.extern_vault_role_id + } + + extern_vault_secret_id = { + value = var.extern_vault_secret_id + } + + extern_vault_path = { + value = var.extern_vault_path + } + + extern_vault_token_renew = { + value = var.extern_vault_token_renew != null ? tostring(var.extern_vault_token_renew) : null + } + + extern_vault_namespace = { + value = var.extern_vault_namespace + } + + extern_vault_propagate = { + value = var.extern_vault_propagate != null ? var.extern_vault_propagate ? "1" : "0" : null + } + } + + external_vault_configs = var.extern_vault_enable != null ? var.extern_vault_enable ? local.extern_vault_configs : {} : {} +} \ No newline at end of file diff --git a/modules/settings/tfe_redis_config.tf b/modules/settings/tfe_redis_config.tf new file mode 100644 index 00000000..e21535c9 --- /dev/null +++ b/modules/settings/tfe_redis_config.tf @@ -0,0 +1,32 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + redis_configs = { + redis_host = { + value = var.redis_host + } + + redis_port = { + value = var.redis_use_tls != null ? var.redis_use_tls ? "6380" : "6379" : null + } + + redis_use_password_auth = { + value = var.redis_use_password_auth != null ? var.redis_use_password_auth ? "1" : "0" : null + } + + redis_pass = { + value = var.redis_pass + } + + redis_use_tls = { + value = var.redis_use_tls != null ? var.redis_use_tls ? "1" : "0" : null + } + + redis_use_mtls = { + value = var.redis_use_mtls != null ? var.redis_use_mtls ? "1" : "0" : null + } + } + + redis_configuration = var.production_type == "active-active" ? local.redis_configs : {} +} diff --git a/modules/settings/variables.tf b/modules/settings/variables.tf new file mode 100644 index 00000000..9da185bd --- /dev/null +++ b/modules/settings/variables.tf @@ -0,0 +1,462 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# -------------------------------------------------------------------------------------------- +# NOTE: In the descriptions, there are many variables that state that they default to certain +# values, but the variable default is set to null. This is because this module will only add +# values to the final configuration that are set, and if they are left unset and null, then +# the TFE installation will use defaults set by the Replicated configuration for the TFE +# installation. You can find this documented here: +# https://www.terraform.io/enterprise/install/automated/automating-the-installer +# -------------------------------------------------------------------------------------------- + +# ------------------------------------------------------ +# TFE +# ------------------------------------------------------ +variable "backup_token" { + default = null + type = string + description = "(Optional) This API token is used to access the backup/restore API within the product. If unset, it will default to the default_backup_token." +} + +variable "capacity_concurrency" { + default = null + type = number + description = "Number of concurrent plans and applies; defaults to 10." +} + +variable "capacity_cpus" { + default = null + type = number + description = "The maximum number of CPU cores that a Terraform plan or apply can use on the system; defaults to 0 (unlimited)." +} + +variable "capacity_memory" { + default = null + type = number + description = "The maximum amount of memory (in megabytes) that a Terraform plan or apply can use on the system; defaults to 512." +} + +variable "custom_image_tag" { + type = string + description = "(Required if tbw_image is 'custom_image'.) The name and tag for your alternative Terraform build worker image in the format :. Default is 'hashicorp/build-worker:now'. If this variable is used, the 'tbw_image' variable must be 'custom_image'." +} + +variable "custom_agent_image_tag" { + type = string + description = "Configure the docker image for handling job execution within TFE. This can either be the standard image that ships with TFE or a custom image that includes extra tools not present in the default one. Should be in the format :." +} + +variable "production_type" { + default = "disk" + type = string + description = "Where Terraform Enterprise application data will be stored. Valid values are `external`, `disk`, `active-active` or `null`. Choose `external` when storing application data in an external object storage service and database. Choose `disk` when storing application data in a directory on the Terraform Enterprise instance itself. Close `active-active` when deploying more than 1 node. Leave it `null` when you want Terraform Enterprise to use its own default." + + validation { + condition = contains(["external", "disk", "active-active"], var.production_type) + error_message = "The production_type must be 'external', 'disk', `active-active`, or omitted." + } +} + +variable "release_sequence" { + default = null + type = number + description = "Terraform Enterprise version release sequence. Pins the application to a release available in the license's channel, but is overridden by pins made in the vendor console. This setting is optional and has to be omitted when tfe_license_bootstrap_airgap_package_path is set." +} + +# ------------------------------------------------------ +# Log Forwarding and Metrics +# ------------------------------------------------------ +variable "log_forwarding_enabled" { + default = null + type = bool + description = "(Optional) Whether or not to enable log forwarding for Terraform Enterprise. Defaults to false." +} + +variable "log_forwarding_config" { + default = null + type = string + description = <<-EOD + (Required when log_forwarding_enabled is true.) Valid log forwarding configuration specifying external + destination(s) to forward logs. Defaults to: # Example Fluent Bit configuration that matches all logs + and does not # forward them anywhere. + [OUTPUT] + Name null + Match * + EOD +} + +variable "metrics_endpoint_enabled" { + default = null + type = bool + description = "(Optional) Metrics are used to understand the behavior of Terraform Enterprise and to troubleshoot and tune performance. Enable an endpoint to expose container metrics. Defaults to false." +} + +variable "metrics_endpoint_port_http" { + default = null + type = number + description = "(Optional when metrics_endpoint_enabled is true.) Defines the TCP port on which HTTP metrics requests will be handled. Defaults to 9090." +} + +variable "metrics_endpoint_port_https" { + default = null + type = string + description = "(Optional when metrics_endpoint_enabled is true.) Defines the TCP port on which HTTPS metrics requests will be handled. Defaults to 9091." +} + +# ------------------------------------------------------ +# Proxy +# ------------------------------------------------------ +variable "extra_no_proxy" { + default = null + type = list(string) + description = "When configured to use a proxy, a list of hosts to exclude from proxying. Please note that this list does not support whitespace characters." +} + +# ------------------------------------------------------ +# TLS +# ------------------------------------------------------ +variable "tls_vers" { + default = null + type = string + description = "(Optional) Set to tls_1_2 to enable only TLS 1.2, or to tls_1_3 to enable only TLS 1.3. Defaults to both TLS 1.2 and 1.3 (tls_1_2_tls_1_3)." + + validation { + condition = ( + var.tls_vers == "tls_1_2_tls_1_3" || + var.tls_vers == "tls_1_2" || + var.tls_vers == "tls_1_3" || + var.tls_vers == null + ) + error_message = "The tls_vers should be set to 'tls_1_2' to enable only TLS 1.2, to 'tls_1_3' to enable only TLS 1.3. When unset, TFE defaults this to both TLS 1.2 and 1.3 'tls_1_2_tls_1_3'." + } +} + +variable "tls_ciphers" { + default = null + type = string + description = "(Optional) Set to an OpenSSL cipher list format string to enable a custom TLS ciphersuite. When unset, TFE uses a default ciphersuite." +} + +variable "force_tls" { + default = null + type = bool + description = "When set, TFE will require all application traffic to use HTTPS by sending a 'Strict-Transport-Security' header value in responses, and marking cookies as secure. A valid, trusted TLS certificate must be installed when this option is set, as browsers will refuse to serve webpages that have an HSTS header set that also serve self-signed or untrusted certificates." +} + +# ------------------------------------------------------ +# Replicated +# ------------------------------------------------------ +variable "bypass_preflight_checks" { + default = null + type = bool + description = "Allow the TFE application to start without preflight checks; defaults to false." +} + +variable "hostname" { + default = null + type = string + description = "(Required) The hostname you will use to access your installation." +} + +variable "log_level" { + default = null + type = string + description = "(Optional) If present, this will set the log level of the Replicated daemon." + + validation { + condition = ( + var.log_level == "debug" || + var.log_level == "info" || + var.log_level == "error" || + var.log_level == null + ) + error_message = "The log_level value must be one of: 'debug', 'info', 'error', or null." + } +} + +variable "tfe_license_bootstrap_airgap_package_path" { + default = null + type = string + description = "(Optional) This is the location of the airgap bundle on the host. When set, the automated install will proceed as an airgapped installation. Note that tfe_license_file_location must also be set to the location of the matching airgap license." +} + +variable "tfe_license_bootstrap_channel_id" { + default = null + type = string + description = "(Optional) This variable allows specifying the installation channel for multi-channel licenses. When omitted and a multi-channel license is used, the release on the default channel will be installed." +} + +variable "tfe_license_file_location" { + default = null + type = string + description = "The path on the TFE instance to put the TFE license." +} + +variable "tls_bootstrap_cert_pathname" { + default = null + type = string + description = "The path on the TFE instance to put the certificate." +} + +variable "tls_bootstrap_key_pathname" { + default = null + type = string + description = "The path on the TFE instance to put the key." +} + +# ------------------------------------------------------ +# PostgreSQL Database +# ------------------------------------------------------ +# If you have chosen external for production_type, the following settings apply: +variable "pg_user" { + default = null + type = string + description = "(Required when production_type is 'external' or 'active-active') PostgreSQL user to connect as." +} + +variable "pg_password" { + default = null + type = string + description = "(Required when production_type is 'external' or 'active-active') The password for the PostgreSQL user." +} + +variable "pg_netloc" { + default = null + type = string + description = "(Required when production_type is 'external' or 'active-active') The hostname and port of the target PostgreSQL server, in the format hostname:port." +} + +variable "pg_dbname" { + default = null + type = string + description = "(Required when production_type is 'external' or 'active-active') The database name" +} + +variable "pg_extra_params" { + default = null + type = string + description = "(Optional) Parameter keywords of the form param1=value1¶m2=value2 to support additional options that may be necessary for your specific PostgreSQL server. Allowed values are documented on the PostgreSQL site. An additional restriction on the sslmode parameter is that only the require, verify-full, verify-ca, and disable values are allowed." +} + +# ------------------------------------------------------ +# Redis +# ------------------------------------------------------ +variable "redis_host" { + default = null + type = string + description = "(Required when production_type is 'active-active') Hostname of an external Redis instance which is resolvable from the TFE instance." +} + +variable "redis_pass" { + default = null + type = string + description = "The Primary Access Key for the Redis Instance. Must be set to the password of an external Redis instance if the instance requires password authentication." +} + +variable "redis_use_password_auth" { + default = null + type = bool + description = "Redis service requires a password." +} + +variable "redis_use_tls" { + default = null + type = bool + description = "Redis service requires TLS. If true, the external Redis instance will use port 6380, otherwise 6379." +} + +variable "redis_use_mtls" { + default = null + type = bool + description = "Redis service requires mutual TLS authentication. If true, the external Redis instance will use TLS certs for authentication." +} + +# ------------------------------------------------------ +# Mounted Disk +# ------------------------------------------------------ +variable "disk_path" { + default = null + type = string + description = "(Required when production_type is 'disk') Absolute path to a directory on the instance to store Terraform Enteprise data. Valid for mounted disk installations." +} + +# ------------------------------------------------------ +# AWS +# ------------------------------------------------------ +variable "aws_access_key_id" { + default = null + type = string + description = "AWS access key ID for S3 bucket access. To use AWSinstance profiles for this information, set it to ''." +} + +variable "aws_secret_access_key" { + default = null + type = string + description = "(Required when object storage is in AWS unless aws_access_key_id is set) AWS secret access key for S3 bucket access. To use AWS instance profiles for this information, set it to ''." +} + +variable "s3_endpoint" { + default = null + type = string + description = "(Optional when object storage is in AWS) Endpoint URL (hostname only or fully qualified URI). Usually only needed if using a VPC endpoint or an S3-compatible storage provider." +} + +variable "s3_bucket" { + default = null + type = string + description = "(Required when object storage is in AWS) The S3 bucket where resources will be stored" +} + +variable "s3_region" { + default = null + type = string + description = "(Required when object storage is in AWS) The region where the S3 bucket exists." +} + +variable "s3_sse" { + default = null + type = string + description = "(Optional when object storage is in AWS) Enables server-side encryption of objects in S3; if provided, must be set to aws:kms." +} + +variable "s3_sse_kms_key_id" { + default = null + type = string + description = "(Optional when object storage is in AWS) An optional KMS key for use when S3 server-side encryption is enabled." +} + +# ------------------------------------------------------ +# Azure +# ------------------------------------------------------ +variable "azure_account_name" { + default = null + type = string + description = "(Required when object storage is in Azure) The account name for the Azure account to access the container." +} + +variable "azure_account_key" { + default = null + type = string + description = "(Required when object storage is in Azure) The account key to access the account specified in azure_account_name." +} + +variable "azure_container" { + default = null + type = string + description = "(Required when object storage is in Azure) The identifer for the Azure blob storage container." +} + +variable "azure_endpoint" { + default = null + type = string + description = "(Optional when object storage is in Azure) The URL for the Azure cluster to use. By default this is the global cluster." +} + +# ------------------------------------------------------ +# Google +# ------------------------------------------------------ +variable "gcs_bucket" { + default = null + type = string + description = "(Required when object storage is in GCP) The GCP storage bucket name." +} + +variable "gcs_credentials" { + default = null + type = string + description = "JSON blob containing the GCP credentials document. This is only required if object storage is in GCP and the TFE instance(s) do(es) not have the service account attached to it, in which case the instance(s) may authenticate without credentials. Note: This is a string, so ensure values are properly escaped." +} + +variable "gcs_project" { + default = null + type = string + description = "(Required when object storage is in GCP) The GCP project where the bucket resides." +} + +# ------------------------------------------------------ +# External Vault +# ------------------------------------------------------ +variable "extern_vault_enable" { + default = null + type = bool + description = "(Optional) An external Vault cluster is being used." +} + +variable "extern_vault_addr" { + default = null + type = string + description = "(Required when extern_vault_enable is true) URL of external Vault cluster." +} + +variable "extern_vault_role_id" { + default = null + type = string + description = "(Required when extern_vault_enable is true) AppRole RoleId to use to authenticate with the Vault cluster." +} + +variable "extern_vault_secret_id" { + default = null + type = string + description = "(Required when extern_vault_enable is true) AppRole SecretId to use to authenticate with the Vault cluster." +} + +variable "extern_vault_path" { + default = null + type = string + description = "(Optional when extern_vault_enable is true) Path on the Vault server for the AppRole auth. Defaults to auth/approle." +} + +variable "extern_vault_token_renew" { + default = null + type = number + description = "(Optional when extern_vault_enable is true) How often (in seconds) to renew the Vault token. Defaults to 3600." +} + +variable "extern_vault_propagate" { + default = null + type = bool + description = "(Optional when extern_vault_enable is true) Propagate Vault credentials to Terraform workers. Defaults to false." +} + +variable "extern_vault_namespace" { + default = null + type = string + description = "(Optional when extern_vault_enable is true) The Vault namespace under which to operate. Defaults to ''." +} + +# ------------------------------------------------------ +# Advanced Configuration +# ------------------------------------------------------ +# These are advanced configuration options that should not be changed without the direction +# of HashiCorp support personnel. + +variable "iact_subnet_list" { + default = null + type = list(string) + description = "A list of IP address ranges which will be authorized to access the IACT. The ranges must be expressed in CIDR notation, for example \"[\"10.0.0.0/24\"]\". If not set, no subnets can retrieve the IACT." +} + +variable "iact_subnet_time_limit" { + default = null + type = string + description = "(Optional if iact_subnet_list is not null.) To prevent an unconfigured instance from being discovered and hijacked by a rogue operator, IPs from the above subnet list are only allowed to access the retrieval API for a certain initial period of time. This setting defines that time period in minutes. Setting this to 'unlimited' will disable the time limit, although it is NOT recommended for production deployments. Defaults to '60'." +} + +variable "hairpin_addressing" { + default = null + type = bool + description = "In some cloud environments, HTTP clients running on instances behind a loadbalancer cannot send requests to the public hostname of that load balancer. Use this setting to configure TFE services to redirect requests for the installation's FQDN to the instance'sinternal IP address. Defaults to false." +} + +variable "restrict_worker_metadata_access" { + default = null + type = bool + description = "Prevents the environment where Terraform operations are executed from accessing the cloud instance metadata service. This should not be set when Terraform operations rely on using instance metadata (i.e., the instance IAM profile) as part of the Terraform provider configuration. NOTE: A bug in Docker version 19.03.3 prevents this setting from working correctly. Operators should avoid using this Docker version when enabling this setting. Defaults to false." +} + +variable "trusted_proxies" { + default = null + type = list(string) + description = "A list of CIDR masks, expressed as a comma-delimited string, which will be considered safe to ignore when evaluating the IP addresses of requests like those made to the IACT endpoint. This is relevant for situations like a client requesting the IACT through a load balancer which appends one or more X-Forwarded-For HTTP headers. For example: 10.0.1.0/24,172.16.4.0/24. By default the list is empty." +} diff --git a/modules/settings/versions.tf b/modules/settings/versions.tf new file mode 100644 index 00000000..f724b9df --- /dev/null +++ b/modules/settings/versions.tf @@ -0,0 +1,12 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_version = ">= 0.13" + required_providers { + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + } +} diff --git a/modules/tfe_init/README.md b/modules/tfe_init/README.md new file mode 100644 index 00000000..197b0a5c --- /dev/null +++ b/modules/tfe_init/README.md @@ -0,0 +1,43 @@ +# TFE Init Module + +This module is used to create the script that will install Terraform Enterprise (TFE) with [Flexible Deployments Options](https://developer.hashicorp.com/terraform/enterprise/flexible-deployments) on a virtual machine. + +## Required variables + +* `cloud` - the cloud you are deploying to; `aws`, `azurerm`, or `google` +* `distribution` - the OS distribution on which TFE will be deployed; `rhel` or `ubuntu` +* `registry_username` - the username for the docker registry from which to pull the terraform_enterprise container images +* `registry_password` - the password for the docker registry from which to pull the terraform_enterprise container images +* `docker_compose_yaml` - the yaml encoded contents of what make up a docker compose file, to be run with docker compose in the user data script +* `operational_mode` - `disk`, `external`, or `active-active` + +## Example usage + +This example illustrates how it may be used by a Terraform Enterprise module, consuming outputs from other submodules. + +```hcl +module "tfe_init_fdo" { + source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/tfe_init?ref=main" + + cloud = "azurerm" + distribution = "ubuntu" + disk_path = "/opt/hashicorp/data" + disk_device_name = "disk/azure/scsi1/lun${var.vm_data_disk_lun}" + operational_mode = "disk" + enable_monitoring = true + + ca_certificate_secret_id = var.ca_certificate_secret + certificate_secret_id = var.vm_certificate_secret + key_secret_id = var.vm_key_secret + + registry_username = "myusername" + registry_password = "mypassword" + docker_compose_yaml = module.docker_compose_config.docker_compose_yaml +} +``` + +## Resources + +This module does not create any Terraform resources, but rather uses the [`templatefile` function](https://www.terraform.io/language/functions/templatefile) +to render a template of the Terraform Enterprise installation script. The module will then output the +rendered script so that it can be used in a TFE installation. diff --git a/modules/tfe_init/files/daemon.json b/modules/tfe_init/files/daemon.json new file mode 100644 index 00000000..d9b0d0a4 --- /dev/null +++ b/modules/tfe_init/files/daemon.json @@ -0,0 +1,4 @@ +{ + "storage-driver": "overlay2", + "mtu": 1460 +} \ No newline at end of file diff --git a/modules/tfe_init/functions.tf b/modules/tfe_init/functions.tf new file mode 100644 index 00000000..e067de58 --- /dev/null +++ b/modules/tfe_init/functions.tf @@ -0,0 +1,39 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + get_base64_secrets = templatefile("${path.module}/templates/get_base64_secrets.func", { + cloud = var.cloud + }) + + install_packages = templatefile("${path.module}/templates/install_packages.func", { + cloud = var.cloud + distribution = var.distribution + }) + + install_jq = templatefile("${path.module}/templates/install_jq.func", { + distribution = var.distribution + }) + + install_monitoring_agents = templatefile("${path.module}/templates/install_monitoring_agents.func", { + cloud = var.cloud + distribution = var.distribution + enable_monitoring = var.enable_monitoring != null ? var.enable_monitoring : false + }) + + quadlet_unit = templatefile("${path.module}/templates/terraform-enterprise.kube.tpl", {}) + + retry = templatefile("${path.module}/templates/retry.func", { + cloud = var.cloud + }) + + azurerm_database_init = templatefile("${path.module}/templates/azurerm_database_init.func.tpl", { + distribution = var.distribution + msi_auth_enabled = var.database_passwordless_azure_use_msi + database_host = var.database_host + database_name = var.database_name + admin_database_username = var.admin_database_username + admin_database_password = var.admin_database_password + }) + get_unmounted_disk = file("${path.module}/templates/get_unmounted_disk.func") +} diff --git a/modules/tfe_init/main.tf b/modules/tfe_init/main.tf new file mode 100644 index 00000000..e0f61265 --- /dev/null +++ b/modules/tfe_init/main.tf @@ -0,0 +1,116 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + + tls_bootstrap_path = "/etc/tfe/ssl" + tls_bootstrap_cert_pathname = "${local.tls_bootstrap_path}/cert.pem" + tls_bootstrap_key_pathname = "${local.tls_bootstrap_path}/key.pem" + tls_bootstrap_ca_pathname = "${local.tls_bootstrap_path}/bundle.pem" + + postgres_bootstrap_path = "/etc/tfe/ssl/postgres" + postgres_bootstrap_cert_pathname = "${local.postgres_bootstrap_path}/cert.crt" + postgres_bootstrap_key_pathname = "${local.postgres_bootstrap_path}/key.key" + postgres_bootstrap_ca_pathname = "${local.postgres_bootstrap_path}/ca.crt" + + redis_bootstrap_path = "/etc/tfe/ssl/redis" + redis_bootstrap_cert_pathname = "${local.redis_bootstrap_path}/cert.pem" + redis_bootstrap_key_pathname = "${local.redis_bootstrap_path}/key.pem" + redis_bootstrap_ca_pathname = "${local.redis_bootstrap_path}/cacert.pem" + + user_data_template = { + aws = { + ubuntu = { + docker = "${path.module}/templates/aws.ubuntu.docker.tfe.sh.tpl", + podman = null + }, + rhel = { + docker = "${path.module}/templates/aws.rhel.docker.tfe.sh.tpl", + podman = "${path.module}/templates/aws.rhel.podman.tfe.sh.tpl", + } + }, + azurerm = { + ubuntu = { + docker = "${path.module}/templates/azurerm.ubuntu.docker.tfe.sh.tpl", + podman = null + }, + rhel = { + docker = "${path.module}/templates/azurerm.rhel.docker.tfe.sh.tpl", + podman = "${path.module}/templates/azurerm.rhel.podman.tfe.sh.tpl", + } + } + google = { + ubuntu = { + docker = "${path.module}/templates/google.ubuntu.docker.tfe.sh.tpl", + podman = null + }, + rhel = { + docker = "${path.module}/templates/google.rhel.docker.tfe.sh.tpl", + podman = "${path.module}/templates/google.rhel.podman.tfe.sh.tpl", + } + } + } + tfe_user_data = templatefile( + local.user_data_template[var.cloud][var.distribution][var.container_runtime_engine], + { + get_base64_secrets = local.get_base64_secrets + install_packages = local.install_packages + install_jq = local.install_jq + install_monitoring_agents = local.install_monitoring_agents + retry = local.retry + quadlet_unit = local.quadlet_unit + azurerm_database_init = local.azurerm_database_init + get_unmounted_disk = local.get_unmounted_disk + + active_active = var.operational_mode == "active-active" + cloud = var.cloud + custom_image_tag = try(var.custom_image_tag, null) + disk_path = var.disk_path + disk_device_name = var.disk_device_name + distribution = var.distribution + docker_config = filebase64("${path.module}/files/daemon.json") + docker_version = var.distribution == "rhel" ? var.docker_version_rhel : null + enable_monitoring = var.enable_monitoring != null ? var.enable_monitoring : false + tls_bootstrap_cert_pathname = local.tls_bootstrap_cert_pathname + tls_bootstrap_key_pathname = local.tls_bootstrap_key_pathname + tls_bootstrap_ca_pathname = local.tls_bootstrap_ca_pathname + docker_compose = var.docker_compose_yaml + podman_kube_config = var.podman_kube_yaml + + ca_certificate_secret_id = var.ca_certificate_secret_id + certificate_secret_id = var.certificate_secret_id + key_secret_id = var.key_secret_id + + postgres_bootstrap_cert_pathname = local.postgres_bootstrap_cert_pathname + postgres_bootstrap_key_pathname = local.postgres_bootstrap_key_pathname + postgres_bootstrap_ca_pathname = local.postgres_bootstrap_ca_pathname + + enable_redis_mtls = var.enable_redis_mtls + enable_sentinel_mtls = var.enable_sentinel_mtls + redis_ca_certificate_secret_id = var.redis_ca_certificate_secret_id + redis_certificate_secret_id = var.redis_client_certificate_secret_id + redis_client_key_secret_id = var.redis_client_key_secret_id + + enable_postgres_mtls = var.enable_postgres_mtls + postgres_ca_certificate_secret_id = var.postgres_ca_certificate_secret_id + postgres_certificate_secret_id = var.postgres_client_certificate_secret_id + postgres_client_key_secret_id = var.postgres_client_key_secret_id + + redis_bootstrap_cert_pathname = local.redis_bootstrap_cert_pathname + redis_bootstrap_key_pathname = local.redis_bootstrap_key_pathname + redis_bootstrap_ca_pathname = local.redis_bootstrap_ca_pathname + + database_azure_msi_auth_enabled = var.database_passwordless_azure_use_msi + + proxy_ip = var.proxy_ip + proxy_port = var.proxy_port + no_proxy = var.extra_no_proxy != null ? join(",", var.extra_no_proxy) : null + + registry = var.registry + registry_password = var.registry_password + registry_username = var.registry_username + registry_credential = base64encode("${var.registry_username}:${var.registry_password}") + + tfe_image = var.tfe_image + }) +} diff --git a/modules/tfe_init/outputs.tf b/modules/tfe_init/outputs.tf new file mode 100644 index 00000000..a0682cc3 --- /dev/null +++ b/modules/tfe_init/outputs.tf @@ -0,0 +1,7 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +output "tfe_userdata_base64_encoded" { + value = base64encode(local.tfe_user_data) + description = "The Base64 encoded TFE init script built from modules/tfe_init/templates/tfe.sh.tpl" +} diff --git a/modules/tfe_init/templates/aws.rhel.docker.tfe.sh.tpl b/modules/tfe_init/templates/aws.rhel.docker.tfe.sh.tpl new file mode 100644 index 00000000..66b5ee09 --- /dev/null +++ b/modules/tfe_init/templates/aws.rhel.docker.tfe.sh.tpl @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +set -eu pipefail + +${get_base64_secrets} +${install_packages} +%{ if enable_monitoring ~} +${install_monitoring_agents} +%{ endif ~} +${get_unmounted_disk} +${install_jq} + +log_pathname="/var/log/startup.log" + +install_packages $log_pathname +install_jq $log_pathname + +%{ if proxy_ip != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure proxy" | tee -a $log_pathname +proxy_ip="${proxy_ip}" +proxy_port="${proxy_port}" +/bin/cat <>/etc/environment +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/profile.d/proxy.sh +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +export http_proxy="${proxy_ip}:${proxy_port}" +export https_proxy="${proxy_ip}:${proxy_port}" +export no_proxy="${no_proxy}" +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping proxy configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapCert" | tee -a $log_pathname +certificate_data_b64=$(get_base64_secrets ${certificate_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_cert_pathname}) +echo $certificate_data_b64 | base64 --decode > ${tls_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapCert configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if key_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapKey" | tee -a $log_pathname +key_data_b64=$(get_base64_secrets ${key_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_key_pathname}) +echo $key_data_b64 | base64 --decode > ${tls_bootstrap_key_pathname} +chmod 0600 ${tls_bootstrap_key_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} +ca_certificate_directory="/dev/null" +ca_certificate_directory=/usr/share/pki/ca-trust-source/anchors + +ca_cert_filepath="$ca_certificate_directory/tfe-ca-certificate.crt" +%{ if ca_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure CA cert" | tee -a $log_pathname +ca_certificate_data_b64=$(get_base64_secrets ${ca_certificate_secret_id}) +mkdir -p $ca_certificate_directory +echo $ca_certificate_data_b64 | base64 --decode > $ca_cert_filepath +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping CA certificate configuration" | tee -a $log_pathname +%{ endif ~} + +if [ -f "$ca_cert_filepath" ] +then + update-ca-trust + system_ca_certificate_file="/etc/pki/tls/certs/ca-bundle.crt" + cp $ca_cert_filepath ${tls_bootstrap_ca_pathname} + tr -d "\\r" < "$ca_cert_filepath" >> "$system_ca_certificate_file" +fi + +%{ if disk_path != null ~} +device=/dev/$(get_unmounted_disk) +echo "[Terraform Enterprise] Checking disk at '$device' for EXT4 filesystem" | tee -a $log_pathname +if lsblk --fs $device | grep ext4 +then + echo "[Terraform Enterprise] EXT4 filesystem detected on disk at '$device'" | tee -a $log_pathname +else + echo "[Terraform Enterprise] Creating EXT4 filesystem on disk at '$device'" | tee -a $log_pathname + mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard $device -F +fi +echo "[Terraform Enterprise] Creating mounted disk directory at '${disk_path}'" | tee -a $log_pathname +mkdir --parents ${disk_path} +echo "[Terraform Enterprise] Mounting disk '$device' to directory at '${disk_path}'" | tee -a $log_pathname +mount --options discard,defaults $device ${disk_path} +chmod og+rw ${disk_path} +echo "[Terraform Enterprise] Configuring automatic mounting of '$device' to directory at '${disk_path}' on reboot" | tee -a $log_pathname +echo "UUID=$(lsblk --noheadings --output uuid $device) ${disk_path} ext4 discard,defaults 0 2" >> /etc/fstab +%{ endif ~} + +%{ if enable_monitoring ~} +install_monitoring_agents $log_pathname +%{ endif ~} + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing Docker Engine from Repository" | tee -a $log_pathname +/bin/cat < /etc/yum/pluginconf.d/subscription-manager.conf +[main] +enabled=0 +EOF +yum install --assumeyes yum-utils +yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +os_release=$(cat /etc/os-release | grep VERSION_ID | sed "s/VERSION_ID=\"\(.*\)\"/\1/g") +if (( $(echo "$os_release < 8.0" | bc -l ) )); then +/bin/cat <>/etc/yum.repos.d/docker-ce.repo +[centos-extras] +name=Centos extras - \$basearch +baseurl=http://mirror.centos.org/centos/7/extras/x86_64 +enabled=1 +gpgcheck=1 +gpgkey=http://centos.org/keys/RPM-GPG-KEY-CentOS-7 +EOF +fi +yum install --assumeyes docker-ce-${docker_version} docker-ce-cli-${docker_version} containerd.io docker-buildx-plugin docker-compose-plugin +systemctl start docker + + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing TFE FDO" | tee -a $log_pathname +hostname > /var/log/tfe-fdo.log +docker login -u="${registry_username}" -p="${registry_password}" ${registry} + +export HOST_IP=$(hostname -i) + +tfe_dir="/etc/tfe" +mkdir -p $tfe_dir + +echo ${docker_compose} | base64 -d > $tfe_dir/compose.yaml + +docker compose -f /etc/tfe/compose.yaml up -d diff --git a/modules/tfe_init/templates/aws.rhel.podman.tfe.sh.tpl b/modules/tfe_init/templates/aws.rhel.podman.tfe.sh.tpl new file mode 100644 index 00000000..89b779fe --- /dev/null +++ b/modules/tfe_init/templates/aws.rhel.podman.tfe.sh.tpl @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +set -eu pipefail + +${retry} +${get_base64_secrets} +${install_packages} +%{ if enable_monitoring ~} +${install_monitoring_agents} +%{ endif ~} +${get_unmounted_disk} +${install_jq} + +log_pathname="/var/log/startup.log" + +install_packages $log_pathname +install_jq $log_pathname + +%{ if proxy_ip != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure proxy" | tee -a $log_pathname +proxy_ip="${proxy_ip}" +proxy_port="${proxy_port}" +/bin/cat <>/etc/environment +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/profile.d/proxy.sh +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +export http_proxy="${proxy_ip}:${proxy_port}" +export https_proxy="${proxy_ip}:${proxy_port}" +export no_proxy="${no_proxy}" +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping proxy configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapCert" | tee -a $log_pathname +certificate_data_b64=$(get_base64_secrets ${certificate_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_cert_pathname}) +echo $certificate_data_b64 | base64 --decode > ${tls_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapCert configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if key_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapKey" | tee -a $log_pathname +key_data_b64=$(get_base64_secrets ${key_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_key_pathname}) +echo $key_data_b64 | base64 --decode > ${tls_bootstrap_key_pathname} +chmod 0600 ${tls_bootstrap_key_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} +ca_certificate_directory="/dev/null" +ca_certificate_directory=/usr/share/pki/ca-trust-source/anchors +ca_cert_filepath="$ca_certificate_directory/tfe-ca-certificate.crt" +%{ if ca_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure CA cert" | tee -a $log_pathname +ca_certificate_data_b64=$(get_base64_secrets ${ca_certificate_secret_id}) +mkdir -p $ca_certificate_directory +echo $ca_certificate_data_b64 | base64 --decode > $ca_cert_filepath +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping CA certificate configuration" | tee -a $log_pathname +%{ endif ~} + +if [ -f "$ca_cert_filepath" ] +then + update-ca-trust + system_ca_certificate_file="/etc/pki/tls/certs/ca-bundle.crt" + cp $ca_cert_filepath ${tls_bootstrap_ca_pathname} + tr -d "\\r" < "$ca_cert_filepath" >> "$system_ca_certificate_file" +fi + +%{ if disk_path != null ~} +device=/dev/$(get_unmounted_disk) +echo "[Terraform Enterprise] Checking disk at '$device' for EXT4 filesystem" | tee -a $log_pathname +if lsblk --fs $device | grep ext4 +then + echo "[Terraform Enterprise] EXT4 filesystem detected on disk at '$device'" | tee -a $log_pathname +else + echo "[Terraform Enterprise] Creating EXT4 filesystem on disk at '$device'" | tee -a $log_pathname + mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard $device -F +fi +echo "[Terraform Enterprise] Creating mounted disk directory at '${disk_path}'" | tee -a $log_pathname +mkdir --parents ${disk_path} +echo "[Terraform Enterprise] Mounting disk '$device' to directory at '${disk_path}'" | tee -a $log_pathname +mount --options discard,defaults $device ${disk_path} +chmod og+rw ${disk_path} +echo "[Terraform Enterprise] Configuring automatic mounting of '$device' to directory at '${disk_path}' on reboot" | tee -a $log_pathname +echo "UUID=$(lsblk --noheadings --output uuid $device) ${disk_path} ext4 discard,defaults 0 2" >> /etc/fstab +%{ endif ~} + +%{ if enable_monitoring ~} +install_monitoring_agents $log_pathname +%{ endif ~} + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing Podman" | tee -a $log_pathname + +if grep -q -i "release 9" /etc/redhat-release +then + dnf install -y container-tools +elif grep -q -i "release 8" /etc/redhat-release +then + dnf module install -y container-tools + dnf install -y podman-docker +else + dnf module install -y container-tools + dnf install -y podman-docker +fi +systemctl enable --now podman.socket + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing TFE FDO" | tee -a $log_pathname +hostname > /var/log/tfe-fdo.log +export HOST_IP=$(hostname -i) +tfe_dir="/etc/tfe" +mkdir -p $tfe_dir + +echo ${podman_kube_config} | base64 -d > $tfe_dir/tfe.yaml + +cat > $tfe_dir/auth.json < $tfe_dir/terraform-enterprise.kube <>/etc/environment +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/profile.d/proxy.sh +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/apt/apt.conf +Acquire::http::Proxy "http://${proxy_ip}:${proxy_port}"; +Acquire::https::Proxy "http://${proxy_ip}:${proxy_port}"; +EOF + +export http_proxy="${proxy_ip}:${proxy_port}" +export https_proxy="${proxy_ip}:${proxy_port}" +export no_proxy="${no_proxy}" +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping proxy configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapCert" | tee -a $log_pathname +certificate_data_b64=$(retry 10 get_base64_secrets ${certificate_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_cert_pathname}) +echo $certificate_data_b64 | base64 --decode > ${tls_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapCert configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if enable_redis_mtls == true || enable_sentinel_mtls == true ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure RedisCertBootstrap" | tee -a $log_pathname +redis_certificate_data_b64=$(retry 10 get_base64_secrets ${redis_certificate_secret_id}) +mkdir -p $(dirname ${redis_bootstrap_cert_pathname}) +echo $redis_certificate_data_b64 | base64 --decode > ${redis_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping RedisCertBootstrap configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if key_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapKey" | tee -a $log_pathname +key_data_b64=$(retry 10 get_base64_secrets ${key_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_key_pathname}) +echo $key_data_b64 | base64 --decode > ${tls_bootstrap_key_pathname} +chmod 0600 ${tls_bootstrap_key_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if enable_redis_mtls == true || enable_sentinel_mtls == true ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure RedisKeyBootstrap" | tee -a $log_pathname +redis_key_data_b64=$(retry 10 get_base64_secrets ${redis_client_key_secret_id}) +mkdir -p $(dirname ${redis_bootstrap_key_pathname}) +echo $redis_key_data_b64 | base64 --decode > ${redis_bootstrap_key_pathname} +chmod 0600 ${redis_bootstrap_key_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if postgres_ca_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure Postgres CA cert" | tee -a $log_pathname +postgres_ca_certificate_data_b64=$(retry 10 get_base64_secrets ${postgres_ca_certificate_secret_id}) +mkdir -p $(dirname ${postgres_bootstrap_ca_pathname}) +echo $postgres_ca_certificate_data_b64 | base64 --decode > ${postgres_bootstrap_ca_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if postgres_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure Postgres Client cert" | tee -a $log_pathname +postgres_certificate_data_b64=$(retry 10 get_base64_secrets ${postgres_certificate_secret_id}) +mkdir -p $(dirname ${postgres_bootstrap_cert_pathname}) +echo $postgres_certificate_data_b64 | base64 --decode > ${postgres_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if postgres_client_key_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure Postgres Client key" | tee -a $log_pathname +postgres_key_data_b64=$(retry 10 get_base64_secrets ${postgres_client_key_secret_id}) +mkdir -p $(dirname ${postgres_bootstrap_key_pathname}) +echo $postgres_key_data_b64 | base64 --decode > ${postgres_bootstrap_key_pathname} +chmod 0600 ${postgres_bootstrap_key_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} + +ca_certificate_directory="/dev/null" +ca_certificate_directory=/usr/local/share/ca-certificates/extra +ca_cert_filepath="$ca_certificate_directory/tfe-ca-certificate.crt" +%{ if ca_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure CA cert" | tee -a $log_pathname +ca_certificate_data_b64=$(retry 10 get_base64_secrets ${ca_certificate_secret_id}) +mkdir -p $ca_certificate_directory +echo $ca_certificate_data_b64 | base64 --decode > $ca_cert_filepath +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping CA certificate configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if enable_redis_mtls == true || enable_sentinel_mtls == true ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure Redis CA cert" | tee -a $log_pathname +redis_ca_certificate_data_b64=$(retry 10 get_base64_secrets ${redis_ca_certificate_secret_id}) +mkdir -p $(dirname ${redis_bootstrap_ca_pathname}) +echo $redis_ca_certificate_data_b64 | base64 --decode > ${redis_bootstrap_ca_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping Redis CA certificate configuration" | tee -a $log_pathname +%{ endif ~} + +if [ -f "$ca_cert_filepath" ] +then + update-ca-certificates + system_ca_certificate_file="/etc/ssl/certs/ca-certificates.crt" + cp $ca_cert_filepath ${tls_bootstrap_ca_pathname} + tr -d "\\r" < "$ca_cert_filepath" >> "$system_ca_certificate_file" +fi + +%{ if disk_path != null ~} +device=/dev/$(get_unmounted_disk) +echo "[Terraform Enterprise] Checking disk at '$device' for EXT4 filesystem" | tee -a $log_pathname +if lsblk --fs $device | grep ext4 +then + echo "[Terraform Enterprise] EXT4 filesystem detected on disk at '$device'" | tee -a $log_pathname +else + echo "[Terraform Enterprise] Creating EXT4 filesystem on disk at '$device'" | tee -a $log_pathname + mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard $device -F +fi +echo "[Terraform Enterprise] Creating mounted disk directory at '${disk_path}'" | tee -a $log_pathname +mkdir --parents ${disk_path} +echo "[Terraform Enterprise] Mounting disk '$device' to directory at '${disk_path}'" | tee -a $log_pathname +mount --options discard,defaults $device ${disk_path} +chmod og+rw ${disk_path} +echo "[Terraform Enterprise] Configuring automatic mounting of '$device' to directory at '${disk_path}' on reboot" | tee -a $log_pathname +echo "UUID=$(lsblk --noheadings --output uuid $device) ${disk_path} ext4 discard,defaults 0 2" >> /etc/fstab +%{ endif ~} + +%{ if enable_monitoring ~} +install_monitoring_agents $log_pathname +%{ endif ~} + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing Docker Engine from Repository" | tee -a $log_pathname +curl --noproxy '*' --fail --silent --show-error --location https://download.docker.com/linux/ubuntu/gpg \ + | gpg --dearmor --output /usr/share/keyrings/docker-archive-keyring.gpg +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \ + https://download.docker.com/linux/ubuntu $(lsb_release --codename --short) stable" \ + | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +retry 10 apt-get --assume-yes update +retry 10 apt-get --assume-yes install docker-ce docker-ce-cli containerd.io +retry 10 apt-get --assume-yes autoremove + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing TFE FDO" | tee -a $log_pathname +hostname > /var/log/tfe-fdo.log +docker login -u="${registry_username}" -p="${registry_password}" ${registry} + +export HOST_IP=$(hostname -i) + +tfe_dir="/etc/tfe" +mkdir -p $tfe_dir + +echo ${docker_compose} | base64 -d > $tfe_dir/compose.yaml + +docker compose -f /etc/tfe/compose.yaml up -d diff --git a/modules/tfe_init/templates/azurerm.rhel.docker.tfe.sh.tpl b/modules/tfe_init/templates/azurerm.rhel.docker.tfe.sh.tpl new file mode 100644 index 00000000..545a71ee --- /dev/null +++ b/modules/tfe_init/templates/azurerm.rhel.docker.tfe.sh.tpl @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +set -eu pipefail + +${get_base64_secrets} +${install_packages} +%{ if enable_monitoring ~} +${install_monitoring_agents} +%{ endif ~} +%{ if database_azure_msi_auth_enabled ~} +${azurerm_database_init} +%{ endif ~} +${install_jq} + +log_pathname="/var/log/startup.log" + +install_packages $log_pathname +install_jq $log_pathname + +%{ if proxy_ip != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure proxy" | tee -a $log_pathname +proxy_ip="${proxy_ip}" +proxy_port="${proxy_port}" +/bin/cat <>/etc/environment +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/profile.d/proxy.sh +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +export http_proxy="${proxy_ip}:${proxy_port}" +export https_proxy="${proxy_ip}:${proxy_port}" +export no_proxy="${no_proxy}" +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping proxy configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapCert" | tee -a $log_pathname +certificate_data_b64=$(get_base64_secrets ${certificate_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_cert_pathname}) +echo $certificate_data_b64 | base64 --decode > ${tls_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapCert configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if key_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapKey" | tee -a $log_pathname +key_data_b64=$(get_base64_secrets ${key_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_key_pathname}) +echo $key_data_b64 | base64 --decode > ${tls_bootstrap_key_pathname} +chmod 0600 ${tls_bootstrap_key_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} +ca_certificate_directory="/dev/null" +ca_certificate_directory=/usr/share/pki/ca-trust-source/anchors +ca_cert_filepath="$ca_certificate_directory/tfe-ca-certificate.crt" +%{ if ca_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure CA cert" | tee -a $log_pathname +ca_certificate_data_b64=$(get_base64_secrets ${ca_certificate_secret_id}) +mkdir -p $ca_certificate_directory +echo $ca_certificate_data_b64 | base64 --decode > $ca_cert_filepath +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping CA certificate configuration" | tee -a $log_pathname +%{ endif ~} + +if [ -f "$ca_cert_filepath" ] +then +update-ca-trust +system_ca_certificate_file="/etc/pki/tls/certs/ca-bundle.crt" +cp $ca_cert_filepath ${tls_bootstrap_ca_pathname} +tr -d "\\r" < "$ca_cert_filepath" >> "$system_ca_certificate_file" +fi + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Resize RHEL logical volume" | tee -a $log_pathname +terminal_partition=$(parted --script /dev/disk/cloud/azure_root u s p | tail -2 | head -n 1) +terminal_partition_number=$(echo $${terminal_partition:0:3} | xargs) +terminal_partition_link=/dev/disk/cloud/azure_root-part$terminal_partition_number +# Because Microsoft is publishing only LVM-partitioned images, it is necessary to partition it to the specs that TFE requires. +# First, extend the partition to fill available space +growpart /dev/disk/cloud/azure_root $terminal_partition_number +# Resize the physical volume +pvresize $terminal_partition_link +# Then resize the logical volumes to meet TFE specs +lvresize -r -L 10G /dev/mapper/rootvg-rootlv +lvresize -r -L 40G /dev/mapper/rootvg-varlv + + +%{ if disk_path != null ~} +device="/dev/${disk_device_name}" +echo "[Terraform Enterprise] Checking disk at '$device' for EXT4 filesystem" | tee -a $log_pathname +if lsblk --fs $device | grep ext4 +then + echo "[Terraform Enterprise] EXT4 filesystem detected on disk at '$device'" | tee -a $log_pathname +else + echo "[Terraform Enterprise] Creating EXT4 filesystem on disk at '$device'" | tee -a $log_pathname + mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard $device -F +fi +echo "[Terraform Enterprise] Creating mounted disk directory at '${disk_path}'" | tee -a $log_pathname +mkdir --parents ${disk_path} +echo "[Terraform Enterprise] Mounting disk '$device' to directory at '${disk_path}'" | tee -a $log_pathname +mount --options discard,defaults $device ${disk_path} +chmod og+rw ${disk_path} +echo "[Terraform Enterprise] Configuring automatic mounting of '$device' to directory at '${disk_path}' on reboot" | tee -a $log_pathname +echo "UUID=$(lsblk --noheadings --output uuid $device) ${disk_path} ext4 discard,defaults 0 2" >> /etc/fstab +%{ endif ~} + +%{ if enable_monitoring ~} +install_monitoring_agents $log_pathname +%{ endif ~} + +%{ if database_azure_msi_auth_enabled ~} +azurerm_database_init $log_pathname +%{ endif ~} + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing Docker Engine from Repository" | tee -a $log_pathname + +/bin/cat < /etc/yum/pluginconf.d/subscription-manager.conf +[main] +enabled=0 +EOF +yum install --assumeyes yum-utils +yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +os_release=$(cat /etc/os-release | grep VERSION_ID | sed "s/VERSION_ID=\"\(.*\)\"/\1/g") +if (( $(echo "$os_release < 8.0" | bc -l ) )); then +/bin/cat <>/etc/yum.repos.d/docker-ce.repo +[centos-extras] +name=Centos extras - \$basearch +baseurl=http://mirror.centos.org/centos/7/extras/x86_64 +enabled=1 +gpgcheck=1 +gpgkey=http://centos.org/keys/RPM-GPG-KEY-CentOS-7 +EOF +fi +yum install --assumeyes docker-ce-${docker_version} docker-ce-cli-${docker_version} containerd.io docker-buildx-plugin docker-compose-plugin +systemctl start docker + + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing TFE FDO" | tee -a $log_pathname +hostname > /var/log/tfe-fdo.log +docker login -u="${registry_username}" -p="${registry_password}" ${registry} + +export HOST_IP=$(hostname -i) + +tfe_dir="/etc/tfe" +mkdir -p $tfe_dir + +echo ${docker_compose} | base64 -d > $tfe_dir/compose.yaml + +docker compose -f /etc/tfe/compose.yaml up -d diff --git a/modules/tfe_init/templates/azurerm.rhel.podman.tfe.sh.tpl b/modules/tfe_init/templates/azurerm.rhel.podman.tfe.sh.tpl new file mode 100644 index 00000000..999c56f5 --- /dev/null +++ b/modules/tfe_init/templates/azurerm.rhel.podman.tfe.sh.tpl @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +set -eu pipefail + +${get_base64_secrets} +${install_packages} +%{ if enable_monitoring ~} +${install_monitoring_agents} +%{ endif ~} +%{ if database_azure_msi_auth_enabled ~} +${azurerm_database_init} +%{ endif ~} +${install_jq} + +log_pathname="/var/log/startup.log" + +install_packages $log_pathname +install_jq $log_pathname + +%{ if proxy_ip != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure proxy" | tee -a $log_pathname +proxy_ip="${proxy_ip}" +proxy_port="${proxy_port}" +/bin/cat <>/etc/environment +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/profile.d/proxy.sh +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +export http_proxy="${proxy_ip}:${proxy_port}" +export https_proxy="${proxy_ip}:${proxy_port}" +export no_proxy="${no_proxy}" +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping proxy configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapCert" | tee -a $log_pathname +certificate_data_b64=$(get_base64_secrets ${certificate_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_cert_pathname}) +echo $certificate_data_b64 | base64 --decode > ${tls_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapCert configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if key_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapKey" | tee -a $log_pathname +key_data_b64=$(get_base64_secrets ${key_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_key_pathname}) +echo $key_data_b64 | base64 --decode > ${tls_bootstrap_key_pathname} +chmod 0600 ${tls_bootstrap_key_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} +ca_certificate_directory="/dev/null" +ca_certificate_directory=/usr/share/pki/ca-trust-source/anchors +ca_cert_filepath="$ca_certificate_directory/tfe-ca-certificate.crt" +%{ if ca_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure CA cert" | tee -a $log_pathname +ca_certificate_data_b64=$(get_base64_secrets ${ca_certificate_secret_id}) +mkdir -p $ca_certificate_directory +echo $ca_certificate_data_b64 | base64 --decode > $ca_cert_filepath +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping CA certificate configuration" | tee -a $log_pathname +%{ endif ~} + +if [ -f "$ca_cert_filepath" ] +then +update-ca-trust +system_ca_certificate_file="/etc/pki/tls/certs/ca-bundle.crt" +cp $ca_cert_filepath ${tls_bootstrap_ca_pathname} +tr -d "\\r" < "$ca_cert_filepath" >> "$system_ca_certificate_file" +fi + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Resize RHEL logical volume" | tee -a $log_pathname +terminal_partition=$(parted --script /dev/disk/cloud/azure_root u s p | tail -2 | head -n 1) +terminal_partition_number=$(echo $${terminal_partition:0:3} | xargs) +terminal_partition_link=/dev/disk/cloud/azure_root-part$terminal_partition_number +# Because Microsoft is publishing only LVM-partitioned images, it is necessary to partition it to the specs that TFE requires. +# First, extend the partition to fill available space +growpart /dev/disk/cloud/azure_root $terminal_partition_number +# Resize the physical volume +pvresize $terminal_partition_link +# Then resize the logical volumes to meet TFE specs +lvresize -r -L 10G /dev/mapper/rootvg-rootlv +lvresize -r -L 40G /dev/mapper/rootvg-varlv + + +%{ if disk_path != null ~} +device="/dev/${disk_device_name}" +echo "[Terraform Enterprise] Checking disk at '$device' for EXT4 filesystem" | tee -a $log_pathname +if lsblk --fs $device | grep ext4 +then + echo "[Terraform Enterprise] EXT4 filesystem detected on disk at '$device'" | tee -a $log_pathname +else + echo "[Terraform Enterprise] Creating EXT4 filesystem on disk at '$device'" | tee -a $log_pathname + mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard $device -F +fi +echo "[Terraform Enterprise] Creating mounted disk directory at '${disk_path}'" | tee -a $log_pathname +mkdir --parents ${disk_path} +echo "[Terraform Enterprise] Mounting disk '$device' to directory at '${disk_path}'" | tee -a $log_pathname +mount --options discard,defaults $device ${disk_path} +chmod og+rw ${disk_path} +echo "[Terraform Enterprise] Configuring automatic mounting of '$device' to directory at '${disk_path}' on reboot" | tee -a $log_pathname +echo "UUID=$(lsblk --noheadings --output uuid $device) ${disk_path} ext4 discard,defaults 0 2" >> /etc/fstab +%{ endif ~} + +%{ if enable_monitoring ~} +install_monitoring_agents $log_pathname +%{ endif ~} + +%{ if database_azure_msi_auth_enabled ~} +azurerm_database_init $log_pathname +%{ endif ~} + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing Podman" | tee -a $log_pathname + +if grep -q -i "release 9" /etc/redhat-release +then + dnf install -y container-tools +elif grep -q -i "release 8" /etc/redhat-release +then + dnf module install -y container-tools + dnf install -y podman-docker +else + dnf module install -y container-tools + dnf install -y podman-docker +fi +systemctl enable --now podman.socket + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing TFE FDO" | tee -a $log_pathname +hostname > /var/log/tfe-fdo.log +export HOST_IP=$(hostname -i) +tfe_dir="/etc/tfe" +mkdir -p $tfe_dir + +echo ${podman_kube_config} | base64 -d > $tfe_dir/tfe.yaml + +cat > $tfe_dir/auth.json < $tfe_dir/terraform-enterprise.kube <>/etc/environment +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/profile.d/proxy.sh +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/apt/apt.conf +Acquire::http::Proxy "http://${proxy_ip}:${proxy_port}"; +Acquire::https::Proxy "http://${proxy_ip}:${proxy_port}"; +EOF + +export http_proxy="${proxy_ip}:${proxy_port}" +export https_proxy="${proxy_ip}:${proxy_port}" +export no_proxy="${no_proxy}" +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping proxy configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapCert" | tee -a $log_pathname +certificate_data_b64=$(get_base64_secrets ${certificate_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_cert_pathname}) +echo $certificate_data_b64 | base64 --decode > ${tls_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapCert configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if key_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapKey" | tee -a $log_pathname +key_data_b64=$(get_base64_secrets ${key_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_key_pathname}) +echo $key_data_b64 | base64 --decode > ${tls_bootstrap_key_pathname} +chmod 0600 ${tls_bootstrap_key_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} +ca_certificate_directory="/dev/null" +ca_certificate_directory=/usr/local/share/ca-certificates/extra +ca_cert_filepath="$ca_certificate_directory/tfe-ca-certificate.crt" +%{ if ca_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure CA cert" | tee -a $log_pathname +ca_certificate_data_b64=$(get_base64_secrets ${ca_certificate_secret_id}) +mkdir -p $ca_certificate_directory +echo $ca_certificate_data_b64 | base64 --decode > $ca_cert_filepath +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping CA certificate configuration" | tee -a $log_pathname +%{ endif ~} + +if [ -f "$ca_cert_filepath" ] +then +update-ca-certificates +system_ca_certificate_file="/etc/ssl/certs/ca-certificates.crt" +cp $ca_cert_filepath ${tls_bootstrap_ca_pathname} +tr -d "\\r" < "$ca_cert_filepath" >> "$system_ca_certificate_file" +fi + +%{ if disk_path != null ~} +device="/dev/${disk_device_name}" +echo "[Terraform Enterprise] Checking disk at '$device' for EXT4 filesystem" | tee -a $log_pathname +if lsblk --fs $device | grep ext4 +then + echo "[Terraform Enterprise] EXT4 filesystem detected on disk at '$device'" | tee -a $log_pathname +else + echo "[Terraform Enterprise] Creating EXT4 filesystem on disk at '$device'" | tee -a $log_pathname + mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard $device -F +fi +echo "[Terraform Enterprise] Creating mounted disk directory at '${disk_path}'" | tee -a $log_pathname +mkdir --parents ${disk_path} +echo "[Terraform Enterprise] Mounting disk '$device' to directory at '${disk_path}'" | tee -a $log_pathname +mount --options discard,defaults $device ${disk_path} +chmod og+rw ${disk_path} +echo "[Terraform Enterprise] Configuring automatic mounting of '$device' to directory at '${disk_path}' on reboot" | tee -a $log_pathname +echo "UUID=$(lsblk --noheadings --output uuid $device) ${disk_path} ext4 discard,defaults 0 2" >> /etc/fstab +%{ endif ~} + +%{ if enable_monitoring ~} +install_monitoring_agents $log_pathname +%{ endif ~} + +%{ if database_azure_msi_auth_enabled ~} +azurerm_database_init $log_pathname +%{ endif ~} + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing Docker Engine from Repository" | tee -a $log_pathname +curl --noproxy '*' --fail --silent --show-error --location https://download.docker.com/linux/ubuntu/gpg \ + | gpg --dearmor --output /usr/share/keyrings/docker-archive-keyring.gpg +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \ + https://download.docker.com/linux/ubuntu $(lsb_release --codename --short) stable" \ + | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +apt-get --assume-yes update +apt-get --assume-yes install docker-ce docker-ce-cli containerd.io +apt-get --assume-yes autoremove + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing TFE FDO" | tee -a $log_pathname +hostname > /var/log/tfe-fdo.log +docker login -u="${registry_username}" -p="${registry_password}" ${registry} + +export HOST_IP=$(hostname -i) + +tfe_dir="/etc/tfe" +mkdir -p $tfe_dir + +echo ${docker_compose} | base64 -d > $tfe_dir/compose.yaml + +docker compose -f /etc/tfe/compose.yaml up -d diff --git a/modules/tfe_init/templates/azurerm_database_init.func.tpl b/modules/tfe_init/templates/azurerm_database_init.func.tpl new file mode 100644 index 00000000..9fa57834 --- /dev/null +++ b/modules/tfe_init/templates/azurerm_database_init.func.tpl @@ -0,0 +1,33 @@ +%{ if msi_auth_enabled ~} + +# Updates the owner of database to `azure_pg_admin` so that +# MSI identity user can perform operations in the database +function azurerm_database_init { + local log_pathname=$1 + + %{ if distribution == "ubuntu" ~} + apt-get --assume-yes update + apt-get --assume-yes install postgresql-client + %{ else ~} + yum install --assumeyes postgresql + %{ endif ~} + + # Database connection parameters + DB_HOST='${database_host}' + DB_PORT="5432" + DEFAULT_DATABASE="postgres" + DB_USER='${admin_database_username}' + DB_PASSWORD='${admin_database_password}' + + # SQL command to execute + SQL_COMMAND="ALTER DATABASE ${database_name} OWNER TO azure_pg_admin;" + + echo "[Terraform Enterprise] Changing owner of database '${database_name}' to 'azure_pg_admin'." | tee -a $log_pathname + + if PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DEFAULT_DATABASE -c "$SQL_COMMAND"; then + echo "[Terraform Enterprise] Successfully changed database owner." | tee -a $log_pathname + else + echo "[Terraform Enterprise] ERROR: Failed to change database owner." | tee -a $log_pathname + fi +} +%{ endif ~} diff --git a/modules/tfe_init/templates/get_base64_secrets.func b/modules/tfe_init/templates/get_base64_secrets.func new file mode 100644 index 00000000..8affba86 --- /dev/null +++ b/modules/tfe_init/templates/get_base64_secrets.func @@ -0,0 +1,51 @@ +%{ if cloud == "azurerm" ~} +function get_base64_secrets { + local secret_id=$1 + # OS: Agnostic + # Description: Pull the Base 64 encoded secrets from Azure Key Vault + + access_token=$(curl -s 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net' -H Metadata:true | jq -r .access_token) + secret=$(curl -s --noproxy '*' $secret_id?api-version=2016-10-01 -H "x-ms-version: 2017-11-09" -H "Authorization: Bearer $access_token" | jq -r .value) + + if [[ -z "$secret" ]]; then + echo "Error: empty secrets for $secret_id" >&2 + return 1 + fi + + echo $secret +} +%{ endif ~} + +%{ if cloud == "aws" ~} +function get_base64_secrets { + local secret_id=$1 + # OS: Agnostic + # Description: Pull the Base 64 encoded secrets from AWS Secrets Manager + + secret=$(/usr/local/bin/aws secretsmanager get-secret-value --secret-id $secret_id | jq --raw-output '.SecretBinary,.SecretString | select(. != null)') + + if [[ -z "$secret" ]]; then + echo "Error: empty secrets for $secret_id" >&2 + return 1 + fi + + echo $secret +} +%{ endif ~} + +%{ if cloud == "google" ~} +get_base64_secrets () { + local secret_id=$1 + # OS: Agnostic + # Description: Pull the Base 64 encoded secrets from Google Secrets Manager + + secret=$(http_proxy="" https_proxy="" gcloud secrets versions access latest --secret="$secret_id") + + if [[ -z "$secret" ]]; then + echo "Error: empty secrets for $secret_id" >&2 + return 1 + fi + + echo $secret +} +%{ endif ~} diff --git a/modules/tfe_init/templates/get_unmounted_disk.func b/modules/tfe_init/templates/get_unmounted_disk.func new file mode 100644 index 00000000..3a9bd2c2 --- /dev/null +++ b/modules/tfe_init/templates/get_unmounted_disk.func @@ -0,0 +1,33 @@ +# Function to get the name of an attached disk that does not have a mount point +get_unmounted_disk() { + # Get all block devices + local all_devices=$(lsblk -rno NAME,TYPE,MOUNTPOINT) + + # Find all disks + echo "$all_devices" | while read -r name type mountpoint; do + if [[ "$type" == "disk" ]]; then + # Check if the disk itself is mounted or used as swap + if [[ -n "$mountpoint" ]] || swapon --show=NAME --noheadings | grep -q "^/dev/$name$"; then + continue + fi + + # Check if any partitions of this disk are mounted or used as swap + local has_mounted_partitions=false + while read -r part_name part_type part_mountpoint; do + # Check if this is a partition of the current disk + if [[ "$part_name" =~ ^${name}p?[0-9]+$ ]] && [[ "$part_type" == "part" ]]; then + if [[ -n "$part_mountpoint" ]] || swapon --show=NAME --noheadings | grep -q "^/dev/$part_name$"; then + has_mounted_partitions=true + break + fi + fi + done <<< "$all_devices" + + # If no partitions are mounted, this disk is available + if [[ "$has_mounted_partitions" == "false" ]]; then + echo "$name" + return 0 + fi + fi + done +} diff --git a/modules/tfe_init/templates/google.rhel.docker.tfe.sh.tpl b/modules/tfe_init/templates/google.rhel.docker.tfe.sh.tpl new file mode 100644 index 00000000..3f2c5c93 --- /dev/null +++ b/modules/tfe_init/templates/google.rhel.docker.tfe.sh.tpl @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +set -eu pipefail + +${get_base64_secrets} +${install_packages} +%{ if enable_monitoring ~} +${install_monitoring_agents} +%{ endif ~} +${install_jq} + +log_pathname="/var/log/startup.log" + +echo "[Terraform Enterprise] Patching GCP Yum repo configuration" | tee -a $log_pathname +# workaround for GCP RHEL 7 known issue +# https://cloud.google.com/compute/docs/troubleshooting/known-issues#keyexpired +sed -i 's/repo_gpgcheck=1/repo_gpgcheck=0/g' /etc/yum.repos.d/google-cloud.repo + +install_packages $log_pathname +install_jq $log_pathname + +docker_directory="/etc/docker" +echo "[Terraform Enterprise] Creating Docker directory at '$docker_directory'" | tee -a $log_pathname +mkdir -p $docker_directory +docker_daemon_pathname="$docker_directory/daemon.json" +echo "[Terraform Enterprise] Writing Docker daemon to '$docker_daemon_pathname'" | tee -a $log_pathname +echo "${docker_config}" | base64 --decode > $docker_daemon_pathname + +%{ if proxy_ip != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure proxy" | tee -a $log_pathname +proxy_ip="${proxy_ip}" +proxy_port="${proxy_port}" +/bin/cat <>/etc/environment +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/profile.d/proxy.sh +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +export http_proxy="${proxy_ip}:${proxy_port}" +export https_proxy="${proxy_ip}:${proxy_port}" +export no_proxy="${no_proxy}" +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping proxy configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapCert" | tee -a $log_pathname +certificate_data_b64=$(get_base64_secrets ${certificate_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_cert_pathname}) +echo $certificate_data_b64 | base64 --decode > ${tls_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapCert configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if key_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapKey" | tee -a $log_pathname +key_data_b64=$(get_base64_secrets ${key_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_key_pathname}) +echo $key_data_b64 | base64 --decode > ${tls_bootstrap_key_pathname} +chmod 0600 ${tls_bootstrap_key_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} +ca_certificate_directory="/dev/null" +ca_certificate_directory=/usr/share/pki/ca-trust-source/anchors +ca_cert_filepath="$ca_certificate_directory/tfe-ca-certificate.crt" +%{ if ca_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure CA cert" | tee -a $log_pathname +ca_certificate_data_b64=$(get_base64_secrets ${ca_certificate_secret_id}) +mkdir -p $ca_certificate_directory +echo $ca_certificate_data_b64 | base64 --decode > $ca_cert_filepath +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping CA certificate configuration" | tee -a $log_pathname +%{ endif ~} + +if [ -f "$ca_cert_filepath" ] +then + update-ca-trust + system_ca_certificate_file="/etc/pki/tls/certs/ca-bundle.crt" + cp $ca_cert_filepath ${tls_bootstrap_ca_pathname} + tr -d "\\r" < "$ca_cert_filepath" >> "$system_ca_certificate_file" +fi + +%{ if disk_path != null ~} +device="/dev/${disk_device_name}" +echo "[Terraform Enterprise] Checking disk at '$device' for EXT4 filesystem" | tee -a $log_pathname +if lsblk --fs $device | grep ext4 +then + echo "[Terraform Enterprise] EXT4 filesystem detected on disk at '$device'" | tee -a $log_pathname +else + echo "[Terraform Enterprise] Creating EXT4 filesystem on disk at '$device'" | tee -a $log_pathname + mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard $device -F +fi +echo "[Terraform Enterprise] Creating mounted disk directory at '${disk_path}'" | tee -a $log_pathname +mkdir --parents ${disk_path} +echo "[Terraform Enterprise] Mounting disk '$device' to directory at '${disk_path}'" | tee -a $log_pathname +mount --options discard,defaults $device ${disk_path} +chmod og+rw ${disk_path} +echo "[Terraform Enterprise] Configuring automatic mounting of '$device' to directory at '${disk_path}' on reboot" | tee -a $log_pathname +echo "UUID=$(lsblk --noheadings --output uuid $device) ${disk_path} ext4 discard,defaults 0 2" >> /etc/fstab +%{ endif ~} + +%{ if enable_monitoring ~} +install_monitoring_agents $log_pathname +%{ endif ~} + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing Docker Engine from Repository" | tee -a $log_pathname +/bin/cat < /etc/yum/pluginconf.d/subscription-manager.conf +[main] +enabled=0 +EOF +yum install --assumeyes yum-utils +yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +os_release=$(cat /etc/os-release | grep VERSION_ID | sed "s/VERSION_ID=\"\(.*\)\"/\1/g") +if (( $(echo "$os_release < 8.0" | bc -l ) )); then +/bin/cat <>/etc/yum.repos.d/docker-ce.repo +[centos-extras] +name=Centos extras - \$basearch +baseurl=http://mirror.centos.org/centos/7/extras/x86_64 +enabled=1 +gpgcheck=1 +gpgkey=http://centos.org/keys/RPM-GPG-KEY-CentOS-7 +EOF +fi +yum install --assumeyes docker-ce-${docker_version} docker-ce-cli-${docker_version} containerd.io docker-buildx-plugin docker-compose-plugin +systemctl start docker + + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing TFE FDO" | tee -a $log_pathname +hostname > /var/log/tfe-fdo.log +docker login -u="${registry_username}" -p="${registry_password}" ${registry} + +export HOST_IP=$(hostname -i) + +tfe_dir="/etc/tfe" +mkdir -p $tfe_dir + +echo ${docker_compose} | base64 -d > $tfe_dir/compose.yaml + +docker compose -f /etc/tfe/compose.yaml up -d + +if (( $(echo "$os_release < 8.0" | bc -l ) )); then + echo "[$(date +"%FT%T")] [Terraform Enterprise] Disable SELinux (temporary)" | tee -a $log_pathname + setenforce 0 + echo "[$(date +"%FT%T")] [Terraform Enterprise] Add docker0 to firewalld" | tee -a $log_pathname + firewall-cmd --permanent --zone=trusted --change-interface=docker0 + firewall-cmd --reload + echo "[$(date +"%FT%T")] [Terraform Enterprise] Enable SELinux" | tee -a $log_pathname + setenforce 1 +fi + + +%{ if custom_image_tag != null ~} +%{ if length(regexall("^.+-docker\\.pkg\\.dev|^.*\\.?gcr\\.io", custom_image_tag)) > 0 ~} +echo "[Terraform Enterprise] Registering gcloud as a Docker credential helper" | tee -a +gcloud auth configure-docker --quiet ${split("/", custom_image_tag)[0]} + +%{ endif ~} +echo "[Terraform Enterprise] Pulling custom worker image '${custom_image_tag}'" | tee -a +docker pull ${custom_image_tag} +%{ endif ~} diff --git a/modules/tfe_init/templates/google.rhel.podman.tfe.sh.tpl b/modules/tfe_init/templates/google.rhel.podman.tfe.sh.tpl new file mode 100644 index 00000000..1388d29a --- /dev/null +++ b/modules/tfe_init/templates/google.rhel.podman.tfe.sh.tpl @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +set -eu pipefail + +${get_base64_secrets} +${install_packages} +%{ if enable_monitoring ~} +${install_monitoring_agents} +%{ endif ~} +${install_jq} + +log_pathname="/var/log/startup.log" + +echo "[Terraform Enterprise] Patching GCP Yum repo configuration" | tee -a $log_pathname +# workaround for GCP RHEL 7 known issue +# https://cloud.google.com/compute/docs/troubleshooting/known-issues#keyexpired +sed -i 's/repo_gpgcheck=1/repo_gpgcheck=0/g' /etc/yum.repos.d/google-cloud.repo + + +install_packages $log_pathname +install_jq $log_pathname + +docker_directory="/etc/docker" +echo "[Terraform Enterprise] Creating Docker directory at '$docker_directory'" | tee -a $log_pathname +mkdir -p $docker_directory +docker_daemon_pathname="$docker_directory/daemon.json" +echo "[Terraform Enterprise] Writing Docker daemon to '$docker_daemon_pathname'" | tee -a $log_pathname +echo "${docker_config}" | base64 --decode > $docker_daemon_pathname + +%{ if proxy_ip != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure proxy" | tee -a $log_pathname +proxy_ip="${proxy_ip}" +proxy_port="${proxy_port}" +/bin/cat <>/etc/environment +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/profile.d/proxy.sh +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +export http_proxy="${proxy_ip}:${proxy_port}" +export https_proxy="${proxy_ip}:${proxy_port}" +export no_proxy="${no_proxy}" +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping proxy configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapCert" | tee -a $log_pathname +certificate_data_b64=$(get_base64_secrets ${certificate_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_cert_pathname}) +echo $certificate_data_b64 | base64 --decode > ${tls_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapCert configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if key_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapKey" | tee -a $log_pathname +key_data_b64=$(get_base64_secrets ${key_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_key_pathname}) +echo $key_data_b64 | base64 --decode > ${tls_bootstrap_key_pathname} +chmod 0600 ${tls_bootstrap_key_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} +ca_certificate_directory="/dev/null" +ca_certificate_directory=/usr/share/pki/ca-trust-source/anchors +ca_cert_filepath="$ca_certificate_directory/tfe-ca-certificate.crt" +%{ if ca_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure CA cert" | tee -a $log_pathname +ca_certificate_data_b64=$(get_base64_secrets ${ca_certificate_secret_id}) +mkdir -p $ca_certificate_directory +echo $ca_certificate_data_b64 | base64 --decode > $ca_cert_filepath +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping CA certificate configuration" | tee -a $log_pathname +%{ endif ~} + +if [ -f "$ca_cert_filepath" ] +then + update-ca-trust + system_ca_certificate_file="/etc/pki/tls/certs/ca-bundle.crt" + cp $ca_cert_filepath ${tls_bootstrap_ca_pathname} + tr -d "\\r" < "$ca_cert_filepath" >> "$system_ca_certificate_file" +fi + +%{ if disk_path != null ~} +device="/dev/${disk_device_name}" +echo "[Terraform Enterprise] Checking disk at '$device' for EXT4 filesystem" | tee -a $log_pathname +if lsblk --fs $device | grep ext4 +then + echo "[Terraform Enterprise] EXT4 filesystem detected on disk at '$device'" | tee -a $log_pathname +else + echo "[Terraform Enterprise] Creating EXT4 filesystem on disk at '$device'" | tee -a $log_pathname + mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard $device -F +fi +echo "[Terraform Enterprise] Creating mounted disk directory at '${disk_path}'" | tee -a $log_pathname +mkdir --parents ${disk_path} +echo "[Terraform Enterprise] Mounting disk '$device' to directory at '${disk_path}'" | tee -a $log_pathname +mount --options discard,defaults $device ${disk_path} +chmod og+rw ${disk_path} +echo "[Terraform Enterprise] Configuring automatic mounting of '$device' to directory at '${disk_path}' on reboot" | tee -a $log_pathname +echo "UUID=$(lsblk --noheadings --output uuid $device) ${disk_path} ext4 discard,defaults 0 2" >> /etc/fstab +%{ endif ~} + +%{ if enable_monitoring ~} +install_monitoring_agents $log_pathname +%{ endif ~} + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing Podman" | tee -a $log_pathname + +if grep -q -i "release 9" /etc/redhat-release +then + dnf install -y container-tools +elif grep -q -i "release 8" /etc/redhat-release +then + dnf module install -y container-tools + dnf install -y podman-docker +else + dnf module install -y container-tools + dnf install -y podman-docker +fi +systemctl enable --now podman.socket + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing TFE FDO" | tee -a $log_pathname +hostname > /var/log/tfe-fdo.log +export HOST_IP=$(hostname -i) +tfe_dir="/etc/tfe" +mkdir -p $tfe_dir + +echo ${podman_kube_config} | base64 -d > $tfe_dir/tfe.yaml + +cat > $tfe_dir/auth.json < $tfe_dir/terraform-enterprise.kube < $docker_daemon_pathname + +%{ if proxy_ip != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure proxy" | tee -a $log_pathname +proxy_ip="${proxy_ip}" +proxy_port="${proxy_port}" +/bin/cat <>/etc/environment +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/profile.d/proxy.sh +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/apt/apt.conf +Acquire::http::Proxy "http://${proxy_ip}:${proxy_port}"; +Acquire::https::Proxy "http://${proxy_ip}:${proxy_port}"; +EOF + +export http_proxy="${proxy_ip}:${proxy_port}" +export https_proxy="${proxy_ip}:${proxy_port}" +export no_proxy="${no_proxy}" +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping proxy configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapCert" | tee -a $log_pathname +certificate_data_b64=$(get_base64_secrets ${certificate_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_cert_pathname}) +echo $certificate_data_b64 | base64 --decode > ${tls_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapCert configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if key_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapKey" | tee -a $log_pathname +key_data_b64=$(get_base64_secrets ${key_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_key_pathname}) +echo $key_data_b64 | base64 --decode > ${tls_bootstrap_key_pathname} +chmod 0600 ${tls_bootstrap_key_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} +ca_certificate_directory="/dev/null" +ca_certificate_directory=/usr/local/share/ca-certificates/extra +ca_cert_filepath="$ca_certificate_directory/tfe-ca-certificate.crt" +%{ if ca_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure CA cert" | tee -a $log_pathname +ca_certificate_data_b64=$(get_base64_secrets ${ca_certificate_secret_id}) +mkdir -p $ca_certificate_directory +echo $ca_certificate_data_b64 | base64 --decode > $ca_cert_filepath +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping CA certificate configuration" | tee -a $log_pathname +%{ endif ~} + +if [ -f "$ca_cert_filepath" ] +then + update-ca-certificates + system_ca_certificate_file="/etc/ssl/certs/ca-certificates.crt" + cp $ca_cert_filepath ${tls_bootstrap_ca_pathname} + tr -d "\\r" < "$ca_cert_filepath" >> "$system_ca_certificate_file" +fi + +%{ if disk_path != null ~} +device="/dev/${disk_device_name}" +echo "[Terraform Enterprise] Checking disk at '$device' for EXT4 filesystem" | tee -a $log_pathname +if lsblk --fs $device | grep ext4 +then + echo "[Terraform Enterprise] EXT4 filesystem detected on disk at '$device'" | tee -a $log_pathname +else + echo "[Terraform Enterprise] Creating EXT4 filesystem on disk at '$device'" | tee -a $log_pathname + mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard $device -F +fi +echo "[Terraform Enterprise] Creating mounted disk directory at '${disk_path}'" | tee -a $log_pathname +mkdir --parents ${disk_path} +echo "[Terraform Enterprise] Mounting disk '$device' to directory at '${disk_path}'" | tee -a $log_pathname +mount --options discard,defaults $device ${disk_path} +chmod og+rw ${disk_path} +echo "[Terraform Enterprise] Configuring automatic mounting of '$device' to directory at '${disk_path}' on reboot" | tee -a $log_pathname +echo "UUID=$(lsblk --noheadings --output uuid $device) ${disk_path} ext4 discard,defaults 0 2" >> /etc/fstab +%{ endif ~} + +%{ if enable_monitoring ~} +install_monitoring_agents $log_pathname +%{ endif ~} + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing Docker Engine from Repository" | tee -a $log_pathname +curl --noproxy '*' --fail --silent --show-error --location https://download.docker.com/linux/ubuntu/gpg \ + | gpg --dearmor --output /usr/share/keyrings/docker-archive-keyring.gpg +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \ + https://download.docker.com/linux/ubuntu $(lsb_release --codename --short) stable" \ + | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +apt-get --assume-yes update +apt-get --assume-yes install docker-ce docker-ce-cli containerd.io +apt-get --assume-yes autoremove + + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing TFE FDO" | tee -a $log_pathname +hostname > /var/log/tfe-fdo.log +docker login -u="${registry_username}" -p="${registry_password}" ${registry} + +export HOST_IP=$(hostname -i) + +tfe_dir="/etc/tfe" +mkdir -p $tfe_dir + +echo ${docker_compose} | base64 -d > $tfe_dir/compose.yaml + +docker compose -f /etc/tfe/compose.yaml up -d + + +%{ if custom_image_tag != null ~} +%{ if length(regexall("^.+-docker\\.pkg\\.dev|^.*\\.?gcr\\.io", custom_image_tag)) > 0 ~} +echo "[Terraform Enterprise] Registering gcloud as a Docker credential helper" | tee -a +gcloud auth configure-docker --quiet ${split("/", custom_image_tag)[0]} + +%{ endif ~} +echo "[Terraform Enterprise] Pulling custom worker image '${custom_image_tag}'" | tee -a +docker pull ${custom_image_tag} +%{ endif ~} diff --git a/modules/tfe_init/templates/install_jq.func b/modules/tfe_init/templates/install_jq.func new file mode 100644 index 00000000..73724df5 --- /dev/null +++ b/modules/tfe_init/templates/install_jq.func @@ -0,0 +1,20 @@ +%{ if distribution == "rhel" ~} +function install_jq { + local log_pathname=$1 + # OS: RHEL/CentOS + # Description: Install jq JSON processor using yum package manager + + echo "[$(date +"%FT%T")] [Terraform Enterprise] Install JQ" | tee -a $log_pathname + yum install --assumeyes jq +} +%{ else ~} +function install_jq { + local log_pathname=$1 + # OS: Ubuntu/Debian + # Description: Install jq JSON processor using apt package manager + + echo "[$(date +"%FT%T")] [Terraform Enterprise] Install JQ" | tee -a $log_pathname + retry 10 apt-get --assume-yes update + retry 10 apt-get --assume-yes install jq +} +%{ endif ~} \ No newline at end of file diff --git a/modules/tfe_init/templates/install_monitoring_agents.func b/modules/tfe_init/templates/install_monitoring_agents.func new file mode 100644 index 00000000..4e88f47f --- /dev/null +++ b/modules/tfe_init/templates/install_monitoring_agents.func @@ -0,0 +1,45 @@ +%{ if enable_monitoring && cloud == "azurerm" ~} +function install_monitoring_agents { + local log_pathname=$1 + : +} +%{ endif ~} + +%{ if enable_monitoring && cloud == "aws" ~} +function install_monitoring_agents { + local log_pathname=$1 + : +} +%{ endif ~} + +%{ if enable_monitoring && cloud == "google" ~} +function install_monitoring_agents { + local log_pathname=$1 + + monitoring_agent_url="https://dl.google.com/cloudagents/add-monitoring-agent-repo.sh" + monitoring_agent_pathname="/tmp/add-monitoring-agent-repo.sh" + + echo "[Terraform Enterprise] Downloading Cloud Monitoring agent script from '$monitoring_agent_url' to '$monitoring_agent_pathname'" | tee -a $log_pathname + monitoring_agent_url="https://dl.google.com/cloudagents/add-monitoring-agent-repo.sh" + monitoring_agent_pathname="/tmp/add-monitoring-agent-repo.sh" + + echo "[Terraform Enterprise] Downloading Cloud Monitoring agent script from '$monitoring_agent_url' to '$monitoring_agent_pathname'" | tee -a $log_pathname + curl -sS -o $monitoring_agent_pathname $monitoring_agent_url + + echo "[Terraform Enterprise] Executing Cloud Monitoring agent script at '$monitoring_agent_pathname'" | tee -a $log_pathname + chmod +x $monitoring_agent_pathname + bash $monitoring_agent_pathname + + echo "[Terraform Enterprise] Installing Cloud Monitoring agent" | tee -a $log_pathname + %{ if distribution == "ubuntu" ~} + apt-get --assume-yes update + apt-get --assume-yes install stackdriver-agent + apt-get --assume-yes autoremove + %{ else ~} + yum install --assumeyes stackdriver-agent + %{ endif ~} + + echo "[Terraform Enterprise] Starting Cloud Monitoring agent service" | tee -a $log_pathname + service stackdriver-agent start +} +%{ endif ~} \ No newline at end of file diff --git a/modules/tfe_init/templates/install_packages.func b/modules/tfe_init/templates/install_packages.func new file mode 100644 index 00000000..00fd6bcb --- /dev/null +++ b/modules/tfe_init/templates/install_packages.func @@ -0,0 +1,42 @@ +%{ if cloud == "aws" ~} +function install_packages { + local log_pathname=$1 + # OS: Agnostic + # Description: Install AWS packages + + %{ if distribution == "rhel" ~} + echo "[$(date +"%FT%T")] [Terraform Enterprise] Install unzip and SSMAgent with yum" | tee -a $log_pathname + yum install -y \ + firewalld \ + unzip \ + https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm + systemctl enable amazon-ssm-agent + systemctl start amazon-ssm-agent + systemctl enable firewalld + systemctl start firewalld + %{ else ~} + echo "[$(date +"%FT%T")] [Terraform Enterprise] Install unzip with apt-get" | tee -a $log_pathname + retry 10 apt-get update -y + retry 10 apt-get install -y unzip + %{ endif ~} + + echo "[$(date +"%FT%T")] [Terraform Enterprise] Install AWS CLI" | tee -a $log_pathname + curl --noproxy '*' "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m | grep -q "arm\|aarch" && echo "aarch64" || echo "x86_64").zip" -o "awscliv2.zip" + unzip awscliv2.zip + ./aws/install + rm -f ./awscliv2.zip + rm -rf ./aws +} +%{ endif ~} + +%{ if cloud == "azurerm" ~} +function install_packages { + : +} +%{ endif ~} + +%{ if cloud == "google" ~} +install_packages () { + : +} +%{ endif ~} diff --git a/modules/tfe_init/templates/retry.func b/modules/tfe_init/templates/retry.func new file mode 100644 index 00000000..62302dba --- /dev/null +++ b/modules/tfe_init/templates/retry.func @@ -0,0 +1,25 @@ +# Retry a command up to a specific numer of times until it exits successfully, +# with exponential back off. +# +# $ retry 5 echo Hello + +function retry { + local retries=$1 + shift + + local count=0 + until "$@"; do + exit=$? + wait=$((2 ** $count)) + count=$(($count + 1)) + if [ $count -lt $retries ]; then + echo "Retry $count/$retries exited $exit, retrying in $wait seconds..." + sleep $wait + else + echo "Retry $count/$retries exited $exit, no more retries left." + return $exit + fi + done + return 0 + +} diff --git a/modules/tfe_init/templates/terraform-enterprise.kube.tpl b/modules/tfe_init/templates/terraform-enterprise.kube.tpl new file mode 100644 index 00000000..2f91d0e1 --- /dev/null +++ b/modules/tfe_init/templates/terraform-enterprise.kube.tpl @@ -0,0 +1,8 @@ +[Install] +WantedBy=default.target + +[Service] +Restart=always + +[Kube] +Yaml=tfe.yaml diff --git a/modules/tfe_init/templates/tfe.sh.tpl b/modules/tfe_init/templates/tfe.sh.tpl new file mode 100644 index 00000000..f7a4fbca --- /dev/null +++ b/modules/tfe_init/templates/tfe.sh.tpl @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +set -eu pipefail + +${retry} +${get_base64_secrets} +${install_packages} +%{ if enable_monitoring ~} +${install_monitoring_agents} +%{ endif ~} +${install_jq} + +log_pathname="/var/log/startup.log" + +%{ if cloud == "google" && distribution == "rhel" ~} +echo "[Terraform Enterprise] Patching GCP Yum repo configuration" | tee -a $log_pathname +# workaround for GCP RHEL 7 known issue +# https://cloud.google.com/compute/docs/troubleshooting/known-issues#keyexpired +sed -i 's/repo_gpgcheck=1/repo_gpgcheck=0/g' /etc/yum.repos.d/google-cloud.repo +%{ endif ~} + +install_packages $log_pathname +install_jq $log_pathname + +%{ if cloud == "google" ~} +docker_directory="/etc/docker" +echo "[Terraform Enterprise] Creating Docker directory at '$docker_directory'" | tee -a $log_pathname +mkdir -p $docker_directory +docker_daemon_pathname="$docker_directory/daemon.json" +echo "[Terraform Enterprise] Writing Docker daemon to '$docker_daemon_pathname'" | tee -a $log_pathname +echo "${docker_config}" | base64 --decode > $docker_daemon_pathname +%{ endif ~} + +%{ if proxy_ip != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure proxy" | tee -a $log_pathname +proxy_ip="${proxy_ip}" +proxy_port="${proxy_port}" +/bin/cat <>/etc/environment +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/profile.d/proxy.sh +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/apt/apt.conf +Acquire::http::Proxy "http://${proxy_ip}:${proxy_port}"; +Acquire::https::Proxy "http://${proxy_ip}:${proxy_port}"; +EOF + +export http_proxy="${proxy_ip}:${proxy_port}" +export https_proxy="${proxy_ip}:${proxy_port}" +export no_proxy="${no_proxy}" +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping proxy configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapCert" | tee -a $log_pathname +certificate_data_b64=$(get_base64_secrets ${certificate_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_cert_pathname}) +echo $certificate_data_b64 | base64 --decode > ${tls_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapCert configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if key_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapKey" | tee -a $log_pathname +key_data_b64=$(get_base64_secrets ${key_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_key_pathname}) +echo $key_data_b64 | base64 --decode > ${tls_bootstrap_key_pathname} +chmod 0600 ${tls_bootstrap_key_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} +ca_certificate_directory="/dev/null" +%{ if distribution == "rhel" ~} +ca_certificate_directory=/usr/share/pki/ca-trust-source/anchors +%{ else ~} +ca_certificate_directory=/usr/local/share/ca-certificates/extra +%{ endif ~} +ca_cert_filepath="$ca_certificate_directory/tfe-ca-certificate.crt" +%{ if ca_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure CA cert" | tee -a $log_pathname +ca_certificate_data_b64=$(get_base64_secrets ${ca_certificate_secret_id}) +mkdir -p $ca_certificate_directory +echo $ca_certificate_data_b64 | base64 --decode > $ca_cert_filepath +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping CA certificate configuration" | tee -a $log_pathname +%{ endif ~} + +if [ -f "$ca_cert_filepath" ] +then +%{ if distribution == "rhel" ~} +update-ca-trust +system_ca_certificate_file="/etc/pki/tls/certs/ca-bundle.crt" +%{ else ~} +update-ca-certificates +system_ca_certificate_file="/etc/ssl/certs/ca-certificates.crt" +%{ endif ~} +cp $ca_cert_filepath ${tls_bootstrap_ca_pathname} +tr -d "\\r" < "$ca_cert_filepath" >> "$system_ca_certificate_file" +fi + +%{ if cloud == "azurerm" && distribution == "rhel" ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Resize RHEL logical volume" | tee -a $log_pathname +terminal_partition=$(parted --script /dev/disk/cloud/azure_root u s p | tail -2 | head -n 1) +terminal_partition_number=$(echo $${terminal_partition:0:3} | xargs) +terminal_partition_link=/dev/disk/cloud/azure_root-part$terminal_partition_number +# Because Microsoft is publishing only LVM-partitioned images, it is necessary to partition it to the specs that TFE requires. +# First, extend the partition to fill available space +growpart /dev/disk/cloud/azure_root $terminal_partition_number +# Resize the physical volume +pvresize $terminal_partition_link +# Then resize the logical volumes to meet TFE specs +lvresize -r -L 10G /dev/mapper/rootvg-rootlv +lvresize -r -L 40G /dev/mapper/rootvg-varlv +%{ endif ~} + +%{ if disk_path != null ~} +device="/dev/${disk_device_name}" +echo "[Terraform Enterprise] Checking disk at '$device' for EXT4 filesystem" | tee -a $log_pathname +if lsblk --fs $device | grep ext4 +then + echo "[Terraform Enterprise] EXT4 filesystem detected on disk at '$device'" | tee -a $log_pathname +else + echo "[Terraform Enterprise] Creating EXT4 filesystem on disk at '$device'" | tee -a $log_pathname + mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard $device -F +fi +echo "[Terraform Enterprise] Creating mounted disk directory at '${disk_path}'" | tee -a $log_pathname +mkdir --parents ${disk_path} +echo "[Terraform Enterprise] Mounting disk '$device' to directory at '${disk_path}'" | tee -a $log_pathname +mount --options discard,defaults $device ${disk_path} +chmod og+rw ${disk_path} +echo "[Terraform Enterprise] Configuring automatic mounting of '$device' to directory at '${disk_path}' on reboot" | tee -a $log_pathname +echo "UUID=$(lsblk --noheadings --output uuid $device) ${disk_path} ext4 discard,defaults 0 2" >> /etc/fstab +%{ endif ~} + +%{ if enable_monitoring ~} +install_monitoring_agents $log_pathname +%{ endif ~} + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing Docker Engine from Repository" | tee -a $log_pathname +%{ if distribution == "rhel" ~} +/bin/cat < /etc/yum/pluginconf.d/subscription-manager.conf +[main] +enabled=0 +EOF +yum install --assumeyes yum-utils +yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +os_release=$(cat /etc/os-release | grep VERSION_ID | sed "s/VERSION_ID=\"\(.*\)\"/\1/g") +if (( $(echo "$os_release < 8.0" | bc -l ) )); then +/bin/cat <>/etc/yum.repos.d/docker-ce.repo +[centos-extras] +name=Centos extras - \$basearch +baseurl=http://mirror.centos.org/centos/7/extras/x86_64 +enabled=1 +gpgcheck=1 +gpgkey=http://centos.org/keys/RPM-GPG-KEY-CentOS-7 +EOF +fi +yum install --assumeyes docker-ce-${docker_version} docker-ce-cli-${docker_version} containerd.io docker-buildx-plugin docker-compose-plugin +systemctl start docker +%{ else ~} +curl --noproxy '*' --fail --silent --show-error --location https://download.docker.com/linux/ubuntu/gpg \ + | gpg --dearmor --output /usr/share/keyrings/docker-archive-keyring.gpg +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \ + https://download.docker.com/linux/ubuntu $(lsb_release --codename --short) stable" \ + | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +apt-get --assume-yes update +apt-get --assume-yes install docker-ce docker-ce-cli containerd.io +apt-get --assume-yes autoremove +%{ endif ~} + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Installing TFE FDO" | tee -a $log_pathname +hostname > /var/log/tfe-fdo.log +docker login -u="${registry_username}" -p="${registry_password}" ${registry} + +export HOST_IP=$(hostname -i) + +tfe_dir="/etc/tfe" +mkdir -p $tfe_dir + +echo ${docker_compose} | base64 -d > $tfe_dir/compose.yaml + +docker compose -f /etc/tfe/compose.yaml up -d + +%{ if distribution == "rhel" && cloud != "google" ~} +if (( $(echo "$os_release < 8.0" | bc -l ) )); then + echo "[$(date +"%FT%T")] [Terraform Enterprise] Disable SELinux (temporary)" | tee -a $log_pathname + setenforce 0 + echo "[$(date +"%FT%T")] [Terraform Enterprise] Add docker0 to firewalld" | tee -a $log_pathname + firewall-cmd --permanent --zone=trusted --change-interface=docker0 + firewall-cmd --reload + echo "[$(date +"%FT%T")] [Terraform Enterprise] Enable SELinux" | tee -a $log_pathname + setenforce 1 +fi +%{ endif ~} + +%{ if custom_image_tag != null && cloud == "google" ~} +%{ if length(regexall("^.+-docker\\.pkg\\.dev|^.*\\.?gcr\\.io", custom_image_tag)) > 0 ~} +echo "[Terraform Enterprise] Registering gcloud as a Docker credential helper" | tee -a +gcloud auth configure-docker --quiet ${split("/", custom_image_tag)[0]} + +%{ endif ~} +echo "[Terraform Enterprise] Pulling custom worker image '${custom_image_tag}'" | tee -a +docker pull ${custom_image_tag} +%{ endif ~} diff --git a/modules/tfe_init/variables.tf b/modules/tfe_init/variables.tf new file mode 100644 index 00000000..6c2918eb --- /dev/null +++ b/modules/tfe_init/variables.tf @@ -0,0 +1,220 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "enable_redis_mtls" { + default = false + type = bool + description = "Should Redis mTLS be enabled? This requires the redis_ca_certificate_secret_id, redis_client_key_secret_id and redis_client_certificate_secret_id variables to be set." +} + +variable "enable_postgres_mtls" { + default = false + type = bool + description = "Should postgres mTLS be enabled? This requires the postgres_ca_certificate_secret_id, postgres_client_key_secret_id and postgres_client_certificate_secret_id variables to be set." +} + +variable "enable_sentinel_mtls" { + type = bool + description = "Whether or not to use mutual TLS to access Redis Sentinel. Defaults to false if no value is given." + default = false +} + +variable "ca_certificate_secret_id" { + default = null + type = string + description = "A secret ID which contains the Base64 encoded version of a PEM encoded public certificate of a certificate authority (CA) to be trusted by the TFE instance(s)." +} + +variable "certificate_secret_id" { + default = null + type = string + description = "A secret ID which contains the Base64 encoded version of a PEM encoded public certificate for the TFE instance(s)." +} + +variable "redis_ca_certificate_secret_id" { + default = null + type = string + description = "A secret ID which contains the Base64 encoded version of a PEM encoded public certificate of a certificate authority (CA) to be trusted by the redis instance" +} + +variable "redis_client_certificate_secret_id" { + default = null + type = string + description = "A secret ID which contains the Base64 encoded version of a PEM encoded public certificate for redis instance." +} + +variable "postgres_ca_certificate_secret_id" { + default = null + type = string + description = "A secret ID which contains the Base64 encoded version of a PEM encoded public certificate of a certificate authority (CA) to be trusted by the database instance" +} + +variable "postgres_client_certificate_secret_id" { + default = null + type = string + description = "A secret ID which contains the Base64 encoded version of a PEM encoded public certificate for database instance." +} + +variable "postgres_client_key_secret_id" { + default = null + type = string + description = "A secret ID which contains the Base64 encoded version of a PEM encoded private key for the database instance" +} + +variable "cloud" { + default = null + type = string + description = "(Required) On which cloud is this Terraform Enterprise installation being deployed?" + validation { + condition = contains(["aws", "azurerm", "google"], var.cloud) + error_message = "Supported values for cloud are 'aws', 'azurerm', or 'google'." + } +} + +variable "container_runtime_engine" { + default = "docker" + type = string + description = "The container runtime engine to run the FDO container on. Default is docker." + validation { + condition = contains(["docker", "podman"], var.container_runtime_engine) + error_message = "Supported values for container_runtime_enginer are docker and podman." + } +} + +variable "custom_image_tag" { + default = null + type = string + description = "(Required if tbw_image is 'custom_image'.) The name and tag for your alternative Terraform build worker image in the format :. Default is 'hashicorp/build-worker:now'. If this variable is used, the 'tbw_image' variable must be 'custom_image'." +} + +variable "disk_device_name" { + default = null + description = "The name of the disk device on which Terraform Enterprise will store data in Mounted Disk mode." + type = string +} + +variable "disk_path" { + default = null + description = "The pathname of the directory in which Terraform Enterprise will store data in Mounted Disk mode." + type = string +} + +variable "distribution" { + default = null + type = string + description = "(Required) What is the OS distribution of the instance on which Terraoform Enterprise will be deployed?" + validation { + condition = contains(["rhel", "ubuntu"], var.distribution) + error_message = "Supported values for distribution are 'rhel', or 'ubuntu'." + } +} + +variable "docker_compose_yaml" { + default = null + description = "The yaml encoded contents of what make up a docker compose file, to be run with docker compose in the user data script" +} + +variable "docker_version_rhel" { + default = "24.0.2" + description = "When you run `yum list docker-ce --showduplicates | sort -r`, the version comes from the center column. All you need is the format major.minor.patch format." +} + +variable "enable_monitoring" { + default = null + type = bool + description = "Should cloud appropriate monitoring agents be installed as a part of the TFE installation script?" +} + +variable "extra_no_proxy" { + default = null + type = list(string) + description = "When configured to use a proxy, a list of hosts to exclude from proxying. Please note that this list does not support whitespace characters." +} + +variable "key_secret_id" { + default = null + type = string + description = "A secret ID which contains the Base64 encoded version of a PEM encoded private key for the TFE instance(s)." +} + +variable "operational_mode" { + default = null + description = "A special string to control the operational mode of Terraform Enterprise. Valid values are: 'external' for External Services mode; 'disk' for Mounted Disk mode; 'active-active' for Active/Active mode." +} + +variable "podman_kube_yaml" { + default = null + description = "The yaml encoded contents of what makes up a podman kube yaml file, to be run with podman play kube in the user data script" +} + +variable "proxy_ip" { + default = null + type = string + description = "IP Address of the proxy server" +} + +variable "proxy_port" { + default = null + type = string + description = "Port that the proxy server will use" +} + +variable "redis_client_key_secret_id" { + default = null + type = string + description = "A secret ID which contains the Base64 encoded version of a PEM encoded private key for the redis instance" +} + +variable "registry" { + default = null + type = string + description = "The docker registry from which to source the terraform_enterprise container images." +} + +variable "registry_password" { + default = null + description = "The password for the docker registry from which to pull the terraform_enterprise container images." + type = string +} + +variable "registry_username" { + default = null + description = "The username for the docker registry from which to pull the terraform_enterprise container images." + type = string +} + +variable "tfe_image" { + type = string + description = "The registry path, image name, and image version (e.g. \"quay.io/hashicorp/terraform-enterprise:1234567\")" +} + +### Database details +variable "database_host" { + default = null + type = string + description = "The PostgreSQL server to connect to. Required when Azure PostgreSQL MSI auth is enabled" +} + +variable "database_name" { + default = null + type = string + description = "Name of the PostgreSQL database to store application data in." +} + +variable "admin_database_username" { + default = null + type = string + description = "PostgreSQL user." +} + +variable "admin_database_password" { + default = null + type = string + description = "PostgreSQL password." +} + +variable "database_passwordless_azure_use_msi" { + default = false + type = bool + description = "Whether or not to use Azure Managed Service Identity (MSI) to connect to the PostgreSQL database. Defaults to false if no value is given." +} diff --git a/modules/tfe_init/versions.tf b/modules/tfe_init/versions.tf new file mode 100644 index 00000000..162d942b --- /dev/null +++ b/modules/tfe_init/versions.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_version = ">= 0.13" + + required_providers {} +} diff --git a/modules/tfe_init_replicated/README.md b/modules/tfe_init_replicated/README.md new file mode 100644 index 00000000..88473704 --- /dev/null +++ b/modules/tfe_init_replicated/README.md @@ -0,0 +1,37 @@ +# TFE Init Module (Replicated) + +This module is used to create the script that will install Terraform Enterprise (TFE) via Replicated on a virtual machine. + +## Required variables + +* `tfe_license_secret_id` - string value for the TFE license secret ID +* `replicated_configuration` - output object from the [`settings` module](../settings) of the Replicated configuration +* `tfe_configuration` - output object from the [`settings` module](../settings) of the TFE configuration + +## Example usage + +This example illustrates how it may be used by a Terraform Enterprise module, consuming outputs from other submodules. + +```hcl +module "tfe_init_replicated" { + source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/tfe_init?ref=main" + + # Replicated Configuration data + enable_active_active = true + + tfe_configuration = module.settings.tfe_configuration + replicated_configuration = module.settings.replicated_configuration + + # Secrets + ca_certificate_secret_id = var.ca_certificate_secret_id + certificate_secret_id = var.vm_certificate_secret_id + key_secret_id = var.vm_key_secret_id + tfe_license_secret_id = var.tfe_license_secret_id +} +``` + +## Resources + +This module does not create any Terraform resources, but rather uses the [`templatefile` function](https://www.terraform.io/language/functions/templatefile) +to render a template of the Terraform Enterprise installation script. The module will then output the +rendered script so that it can be used in a TFE installation. diff --git a/modules/tfe_init_replicated/files/daemon.json b/modules/tfe_init_replicated/files/daemon.json new file mode 100644 index 00000000..d9b0d0a4 --- /dev/null +++ b/modules/tfe_init_replicated/files/daemon.json @@ -0,0 +1,4 @@ +{ + "storage-driver": "overlay2", + "mtu": 1460 +} \ No newline at end of file diff --git a/modules/tfe_init_replicated/functions.tf b/modules/tfe_init_replicated/functions.tf new file mode 100644 index 00000000..b4f3f2a4 --- /dev/null +++ b/modules/tfe_init_replicated/functions.tf @@ -0,0 +1,23 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + get_base64_secrets = templatefile("${path.module}/templates/get_base64_secrets.func", { + cloud = var.cloud + }) + + install_packages = templatefile("${path.module}/templates/install_packages.func", { + cloud = var.cloud + distribution = var.distribution + }) + + install_monitoring_agents = templatefile("${path.module}/templates/install_monitoring_agents.func", { + cloud = var.cloud + distribution = var.distribution + enable_monitoring = var.enable_monitoring != null ? var.enable_monitoring : false + }) + + retry = templatefile("${path.module}/templates/retry.func", { + cloud = var.cloud + }) +} diff --git a/modules/tfe_init_replicated/main.tf b/modules/tfe_init_replicated/main.tf new file mode 100644 index 00000000..f2239ff1 --- /dev/null +++ b/modules/tfe_init_replicated/main.tf @@ -0,0 +1,46 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + + # Build TFE user data / custom data / cloud init + tfe_replicated_user_data = templatefile( + "${path.module}/templates/tfe_replicated.sh.tpl", + { + # Functions + get_base64_secrets = local.get_base64_secrets + install_packages = local.install_packages + install_monitoring_agents = local.install_monitoring_agents + retry = local.retry + + # Configuration data + active_active = var.tfe_configuration != null ? var.tfe_configuration.enable_active_active.value == "1" ? true : false : null + airgap_url = var.airgap_url + airgap_pathname = try(var.replicated_configuration.LicenseBootstrapAirgapPackagePath, null) + cloud = var.cloud + custom_image_tag = try(var.tfe_configuration.custom_image_tag.value, null) + disk_path = var.disk_path + disk_device_name = var.disk_device_name + distribution = var.distribution + docker_config = filebase64("${path.module}/files/daemon.json") + enable_monitoring = var.enable_monitoring != null ? var.enable_monitoring : false + replicated = base64encode(jsonencode(var.replicated_configuration)) + settings = base64encode(jsonencode(var.tfe_configuration)) + tls_bootstrap_cert_pathname = try(var.replicated_configuration.TlsBootstrapCert, null) + tls_bootstrap_key_pathname = try(var.replicated_configuration.TlsBootstrapKey, null) + + # Secrets + ca_certificate_secret_id = var.ca_certificate_secret_id + certificate_secret_id = var.certificate_secret_id + key_secret_id = var.key_secret_id + tfe_license_file_location = var.replicated_configuration != null ? var.replicated_configuration.LicenseFileLocation : null + tfe_license_secret_id = var.tfe_license_secret_id + + # Proxy information + proxy_ip = var.proxy_ip + proxy_port = var.proxy_port + no_proxy = var.tfe_configuration != null ? var.tfe_configuration.extra_no_proxy.value : null + } + ) +} + diff --git a/modules/tfe_init_replicated/outputs.tf b/modules/tfe_init_replicated/outputs.tf new file mode 100644 index 00000000..1e6d4e63 --- /dev/null +++ b/modules/tfe_init_replicated/outputs.tf @@ -0,0 +1,7 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +output "tfe_userdata_base64_encoded" { + value = base64encode(local.tfe_replicated_user_data) + description = "The Base64 encoded TFE init script built from modules/tfe_init/templates/tfe.sh.tpl" +} diff --git a/modules/tfe_init_replicated/templates/get_base64_secrets.func b/modules/tfe_init_replicated/templates/get_base64_secrets.func new file mode 100644 index 00000000..b438bc01 --- /dev/null +++ b/modules/tfe_init_replicated/templates/get_base64_secrets.func @@ -0,0 +1,30 @@ +%{ if cloud == "azurerm" ~} +function get_base64_secrets { + local secret_id=$1 + # OS: Agnostic + # Description: Pull the Base 64 encoded secrets from Azure Key Vault + + access_token=$(curl -s 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net' -H Metadata:true | jq -r .access_token) + curl -s --noproxy '*' $secret_id?api-version=2016-10-01 -H "x-ms-version: 2017-11-09" -H "Authorization: Bearer $access_token" | jq -r .value +} +%{ endif ~} + +%{ if cloud == "aws" ~} +function get_base64_secrets { + local secret_id=$1 + # OS: Agnostic + # Description: Pull the Base 64 encoded secrets from AWS Secrets Manager + + /usr/local/bin/aws secretsmanager get-secret-value --secret-id $secret_id | jq --raw-output '.SecretBinary,.SecretString | select(. != null)' +} +%{ endif ~} + +%{ if cloud == "google" ~} +get_base64_secrets () { + local secret_id=$1 + # OS: Agnostic + # Description: Pull the Base 64 encoded secrets from Google Secrets Manager + + http_proxy="" https_proxy="" gcloud secrets versions access latest --secret="$secret_id" +} +%{ endif ~} diff --git a/modules/tfe_init_replicated/templates/install_monitoring_agents.func b/modules/tfe_init_replicated/templates/install_monitoring_agents.func new file mode 100644 index 00000000..4e88f47f --- /dev/null +++ b/modules/tfe_init_replicated/templates/install_monitoring_agents.func @@ -0,0 +1,45 @@ +%{ if enable_monitoring && cloud == "azurerm" ~} +function install_monitoring_agents { + local log_pathname=$1 + : +} +%{ endif ~} + +%{ if enable_monitoring && cloud == "aws" ~} +function install_monitoring_agents { + local log_pathname=$1 + : +} +%{ endif ~} + +%{ if enable_monitoring && cloud == "google" ~} +function install_monitoring_agents { + local log_pathname=$1 + + monitoring_agent_url="https://dl.google.com/cloudagents/add-monitoring-agent-repo.sh" + monitoring_agent_pathname="/tmp/add-monitoring-agent-repo.sh" + + echo "[Terraform Enterprise] Downloading Cloud Monitoring agent script from '$monitoring_agent_url' to '$monitoring_agent_pathname'" | tee -a $log_pathname + monitoring_agent_url="https://dl.google.com/cloudagents/add-monitoring-agent-repo.sh" + monitoring_agent_pathname="/tmp/add-monitoring-agent-repo.sh" + + echo "[Terraform Enterprise] Downloading Cloud Monitoring agent script from '$monitoring_agent_url' to '$monitoring_agent_pathname'" | tee -a $log_pathname + curl -sS -o $monitoring_agent_pathname $monitoring_agent_url + + echo "[Terraform Enterprise] Executing Cloud Monitoring agent script at '$monitoring_agent_pathname'" | tee -a $log_pathname + chmod +x $monitoring_agent_pathname + bash $monitoring_agent_pathname + + echo "[Terraform Enterprise] Installing Cloud Monitoring agent" | tee -a $log_pathname + %{ if distribution == "ubuntu" ~} + apt-get --assume-yes update + apt-get --assume-yes install stackdriver-agent + apt-get --assume-yes autoremove + %{ else ~} + yum install --assumeyes stackdriver-agent + %{ endif ~} + + echo "[Terraform Enterprise] Starting Cloud Monitoring agent service" | tee -a $log_pathname + service stackdriver-agent start +} +%{ endif ~} \ No newline at end of file diff --git a/modules/tfe_init_replicated/templates/install_packages.func b/modules/tfe_init_replicated/templates/install_packages.func new file mode 100644 index 00000000..c470560c --- /dev/null +++ b/modules/tfe_init_replicated/templates/install_packages.func @@ -0,0 +1,60 @@ +%{ if cloud == "aws" ~} +function install_packages { + local log_pathname=$1 + # OS: Agnostic + # Description: Install AWS packages + + %{ if distribution == "rhel" || distribution == "amazon-linux-2023" ~} + echo "[$(date +"%FT%T")] [Terraform Enterprise] Install unzip with yum" | tee -a $log_pathname + yum install -y unzip + + %{ if distribution == "rhel" ~} + echo "[$(date +"%FT%T")] [Terraform Enterprise] Install firewalld with yum" | tee -a $log_pathname + yum install -y firewalld + + # Amazon Linux 2023 already has amazon-ssm-agent so install on rhel only + echo "[$(date +"%FT%T")] [Terraform Enterprise] Install ssm-agent with yum" | tee -a $log_pathname + yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm + + systemctl enable firewalld + systemctl start firewalld + %{ endif ~} + + %{ if distribution == "amazon-linux-2023" ~} + echo "[$(date +"%FT%T")] [Terraform Enterprise] Remove existing docker and install v24 with yum" | tee -a $log_pathname + yum -y remove docker + yum clean packages + yum -y install docker-24.0.5-1.amzn2023.0.3 bc + + systemctl start docker + systemctl enable docker + %{ endif ~} + + systemctl enable amazon-ssm-agent + systemctl start amazon-ssm-agent + %{ else ~} + echo "[$(date +"%FT%T")] [Terraform Enterprise] Install unzip with apt-get" | tee -a $log_pathname + retry 10 apt-get update -y + retry 10 apt-get install -y unzip + %{ endif ~} + + echo "[$(date +"%FT%T")] [Terraform Enterprise] Install AWS CLI" | tee -a $log_pathname + curl --noproxy '*' "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m | grep -q "arm\|aarch" && echo "aarch64" || echo "x86_64").zip" -o "awscliv2.zip" + unzip awscliv2.zip + ./aws/install + rm -f ./awscliv2.zip + rm -rf ./aws +} +%{ endif ~} + +%{ if cloud == "azurerm" ~} +function install_packages { + : +} +%{ endif ~} + +%{ if cloud == "google" ~} +install_packages () { + : +} +%{ endif ~} diff --git a/modules/tfe_init_replicated/templates/retry.func b/modules/tfe_init_replicated/templates/retry.func new file mode 100644 index 00000000..62302dba --- /dev/null +++ b/modules/tfe_init_replicated/templates/retry.func @@ -0,0 +1,25 @@ +# Retry a command up to a specific numer of times until it exits successfully, +# with exponential back off. +# +# $ retry 5 echo Hello + +function retry { + local retries=$1 + shift + + local count=0 + until "$@"; do + exit=$? + wait=$((2 ** $count)) + count=$(($count + 1)) + if [ $count -lt $retries ]; then + echo "Retry $count/$retries exited $exit, retrying in $wait seconds..." + sleep $wait + else + echo "Retry $count/$retries exited $exit, no more retries left." + return $exit + fi + done + return 0 + +} diff --git a/modules/tfe_init_replicated/templates/tfe_replicated.sh.tpl b/modules/tfe_init_replicated/templates/tfe_replicated.sh.tpl new file mode 100644 index 00000000..2d02c47e --- /dev/null +++ b/modules/tfe_init_replicated/templates/tfe_replicated.sh.tpl @@ -0,0 +1,315 @@ +#!/usr/bin/env bash +set -euo pipefail + +${retry} +${get_base64_secrets} +${install_packages} +${install_monitoring_agents} + +log_pathname="/var/log/ptfe.log" +tfe_settings_file="ptfe-settings.json" +tfe_settings_path="/etc/$tfe_settings_file" + +# ----------------------------------------------------------------------------- +# Patching GCP Yum repo configuration (if GCP environment) +# ----------------------------------------------------------------------------- +%{ if cloud == "google" && distribution == "rhel" ~} +echo "[Terraform Enterprise] Patching GCP Yum repo configuration" | tee -a $log_pathname +# workaround for GCP RHEL 7 known issue +# https://cloud.google.com/compute/docs/troubleshooting/known-issues#keyexpired +sed -i 's/repo_gpgcheck=1/repo_gpgcheck=0/g' /etc/yum.repos.d/google-cloud.repo +%{ endif ~} + +# ----------------------------------------------------------------------------- +# Install jq and cloud specific packages (if not an airgapped environment) +# ----------------------------------------------------------------------------- +%{ if (airgap_url == null && airgap_pathname == null) || (airgap_url != null && airgap_pathname != null) ~} +install_packages $log_pathname + +echo "[$(date +"%FT%T")] [Terraform Enterprise] Install JQ" | tee -a $log_pathname +sudo curl --noproxy '*' -Lo /bin/jq https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-$(uname -m | grep -q "arm\|aarch" && echo "arm64" || echo "amd64") +sudo chmod +x /bin/jq + +%{ endif ~} + +# ----------------------------------------------------------------------------- +# Create TFE & Replicated Settings Files +# ----------------------------------------------------------------------------- +echo "[$(date +"%FT%T")] [Terraform Enterprise] Create configuration files" | tee -a $log_pathname +sudo echo "${settings}" | sudo base64 -d > $tfe_settings_path +echo "${replicated}" | base64 -d > /etc/replicated.conf + +# ----------------------------------------------------------------------------- +# Configure Docker (if GCP environment) +# ----------------------------------------------------------------------------- +%{ if cloud == "google" ~} +docker_directory="/etc/docker" +echo "[Terraform Enterprise] Creating Docker directory at '$docker_directory'" | tee -a $log_pathname +mkdir -p $docker_directory +docker_daemon_pathname="$docker_directory/daemon.json" +echo "[Terraform Enterprise] Writing Docker daemon to '$docker_daemon_pathname'" | tee -a $log_pathname +echo "${docker_config}" | base64 --decode > $docker_daemon_pathname +%{ endif ~} + +# ----------------------------------------------------------------------------- +# Configure the proxy (if applicable) +# ----------------------------------------------------------------------------- +%{ if proxy_ip != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure proxy" | tee -a $log_pathname +proxy_ip="${proxy_ip}" +proxy_port="${proxy_port}" +/bin/cat <>/etc/environment +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +/bin/cat </etc/profile.d/proxy.sh +http_proxy="${proxy_ip}:${proxy_port}" +https_proxy="${proxy_ip}:${proxy_port}" +no_proxy="${no_proxy}" +EOF + +export http_proxy="${proxy_ip}:${proxy_port}" +export https_proxy="${proxy_ip}:${proxy_port}" +export no_proxy="${no_proxy}" +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping proxy configuration" | tee -a $log_pathname +%{ endif ~} + +# ----------------------------------------------------------------------------- +# Configure TLS (if not an airgapped environment) +# ----------------------------------------------------------------------------- +%{ if certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapCert" | tee -a $log_pathname +certificate_data_b64=$(get_base64_secrets ${certificate_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_cert_pathname}) +echo $certificate_data_b64 | base64 --decode > ${tls_bootstrap_cert_pathname} +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapCert configuration" | tee -a $log_pathname +%{ endif ~} + +%{ if key_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure TlsBootstrapKey" | tee -a $log_pathname +key_data_b64=$(get_base64_secrets ${key_secret_id}) +mkdir -p $(dirname ${tls_bootstrap_key_pathname}) +echo $key_data_b64 | base64 --decode > ${tls_bootstrap_key_pathname} +chmod 0600 ${tls_bootstrap_key_pathname} + +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping TlsBootstrapKey configuration" | tee -a $log_pathname +%{ endif ~} + +#------------------------------------------------------------------------------ +# Configure CA Certificate (if not an airgapped environment) +#------------------------------------------------------------------------------ +ca_certificate_directory="/dev/null" + +%{ if distribution == "rhel" || distribution == "amazon-linux-2023" ~} +ca_certificate_directory=/usr/share/pki/ca-trust-source/anchors +%{ else ~} +ca_certificate_directory=/usr/local/share/ca-certificates/extra +%{ endif ~} +ca_cert_filepath="$ca_certificate_directory/tfe-ca-certificate.crt" + +%{ if ca_certificate_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Configure CA cert" | tee -a $log_pathname +ca_certificate_data_b64=$(get_base64_secrets ${ca_certificate_secret_id}) + +mkdir -p $ca_certificate_directory +echo $ca_certificate_data_b64 | base64 --decode > $ca_cert_filepath +%{ else ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Skipping CA certificate configuration" | tee -a $log_pathname +%{ endif ~} + +if [ -f "$ca_cert_filepath" ] +then + %{ if distribution == "rhel" || distribution == "amazon-linux-2023" ~} + update-ca-trust + + %{ else ~} + update-ca-certificates + %{ endif ~} + + jq ". + { ca_certs: { value: \"$(/bin/cat $ca_cert_filepath)\" } }" -- $tfe_settings_path > $tfe_settings_file.updated + cp ./$tfe_settings_file.updated $tfe_settings_path +fi + +# ----------------------------------------------------------------------------- +# Resize RHEL logical volume (if Azure environment) +# ----------------------------------------------------------------------------- +%{ if cloud == "azurerm" && distribution == "rhel" ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Resize RHEL logical volume" | tee -a $log_pathname + +terminal_partition=$(parted --script /dev/disk/cloud/azure_root u s p | tail -2 | head -n 1) +terminal_partition_number=$(echo $${terminal_partition:0:3} | xargs) +terminal_partition_link=/dev/disk/cloud/azure_root-part$terminal_partition_number +# Because Microsoft is publishing only LVM-partitioned images, it is necessary to partition it to the specs that TFE requires. +# First, extend the partition to fill available space +growpart /dev/disk/cloud/azure_root $terminal_partition_number +# Resize the physical volume +pvresize $terminal_partition_link +# Then resize the logical volumes to meet TFE specs +lvresize -r -L 10G /dev/mapper/rootvg-rootlv +lvresize -r -L 40G /dev/mapper/rootvg-varlv +%{ endif ~} + +# ----------------------------------------------------------------------------- +# Configure Mounted Disk Installation +# ----------------------------------------------------------------------------- +%{ if disk_path != null ~} +device="/dev/${disk_device_name}" +echo "[Terraform Enterprise] Checking disk at '$device' for EXT4 filesystem" | tee -a $log_pathname + +if lsblk --fs $device | grep ext4 +then + echo "[Terraform Enterprise] EXT4 filesystem detected on disk at '$device'" | tee -a $log_pathname +else + echo "[Terraform Enterprise] Creating EXT4 filesystem on disk at '$device'" | tee -a $log_pathname + + mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard $device -F +fi + +echo "[Terraform Enterprise] Creating mounted disk directory at '${disk_path}'" | tee -a $log_pathname +mkdir --parents ${disk_path} + +echo "[Terraform Enterprise] Mounting disk '$device' to directory at '${disk_path}'" | tee -a $log_pathname +mount --options discard,defaults $device ${disk_path} +chmod og+rw ${disk_path} + +echo "[Terraform Enterprise] Configuring automatic mounting of '$device' to directory at '${disk_path}' on reboot" | tee -a $log_pathname +echo "UUID=$(lsblk --noheadings --output uuid $device) ${disk_path} ext4 discard,defaults 0 2" >> /etc/fstab + +%{ endif ~} + +# ----------------------------------------------------------------------------- +# Install Monitoring Agents +# ----------------------------------------------------------------------------- +%{ if enable_monitoring ~} +install_monitoring_agents $log_pathname +%{ endif ~} + +# ----------------------------------------------------------------------------- +# Retrieve TFE license (if not an airgapped environment) +# ----------------------------------------------------------------------------- +%{ if tfe_license_secret_id != null ~} +echo "[$(date +"%FT%T")] [Terraform Enterprise] Retrieve TFE license" | tee -a $log_pathname +license=$(get_base64_secrets ${tfe_license_secret_id}) +echo $license | base64 -d > ${tfe_license_file_location} +%{ endif ~} + +# ----------------------------------------------------------------------------- +# Download Replicated +# ----------------------------------------------------------------------------- +replicated_directory="/etc/replicated" + +%{ if airgap_url != null && airgap_pathname != null ~} +# Bootstrap airgapped environment with prerequisites (for dev/test environments) +echo "[Terraform Enterprise] Installing Docker Engine from Repository for Bootstrapping an Airgapped Installation" | tee -a $log_pathname + + %{ if distribution == "rhel" ~} + yum install --assumeyes yum-utils + yum-config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo + yum install --assumeyes docker-ce docker-ce-cli containerd.io + %{ else ~} + apt-get --assume-yes update + apt-get --assume-yes install \ + ca-certificates \ + curl \ + gnupg \ + lsb-release + curl --noproxy '*' --fail --silent --show-error --location https://download.docker.com/linux/ubuntu/gpg \ + | gpg --dearmor --output /usr/share/keyrings/docker-archive-keyring.gpg + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \ + https://download.docker.com/linux/ubuntu $(lsb_release --codename --short) stable" \ + | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + apt-get --assume-yes update + apt-get --assume-yes install docker-ce docker-ce-cli containerd.io + apt-get --assume-yes autoremove + %{ endif ~} + +replicated_filename="replicated.tar.gz" +replicated_url="https://s3.amazonaws.com/replicated-airgap-work/$replicated_filename" +replicated_pathname="$replicated_directory/$replicated_filename" + +echo "[Terraform Enterprise] Downloading Replicated from '$replicated_url' to '$replicated_pathname'" | tee -a $log_pathname +curl --noproxy '*' --create-dirs --output "$replicated_pathname" "$replicated_url" +echo "[Terraform Enterprise] Extracting Replicated in '$replicated_directory'" | tee -a $log_pathname +tar --directory "$replicated_directory" --extract --file "$replicated_pathname" + +echo "[Terraform Enterprise] Copying airgap package '${airgap_url}' to '${airgap_pathname}'" | tee -a $log_pathname +curl --noproxy '*' --create-dirs --output "${airgap_pathname}" "${airgap_url}" +%{ else ~} +echo "[Terraform Enterprise] Skipping Airgapped Replicated download" | tee -a $log_pathname +%{ endif ~} + +# ----------------------------------------------------------------------------- +# Install Terraform Enterprise +# ----------------------------------------------------------------------------- +echo "[$(date +"%FT%T")] [Terraform Enterprise] Install TFE" | tee -a $log_pathname + +%{ if cloud == "azurerm" ~} +instance_ip=$(curl -s -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq '.network.interface[0].ipv4.ipAddress[0].privateIpAddress' -r) +%{ else ~} +instance_ip=$(hostname -i) +%{ endif ~} + +install_pathname="$replicated_directory/install.sh" + +%{ if airgap_pathname == null ~} +curl --noproxy '*' --create-dirs --output $install_pathname https://get.replicated.com/docker/terraformenterprise/active-active +%{ endif ~} + +chmod +x $install_pathname +cd $replicated_directory +$install_pathname \ + fast-timeouts \ + bypass-firewalld-warning \ + %{ if proxy_ip != null ~} + http-proxy="${proxy_ip}:${proxy_port}" \ + additional-no-proxy="${no_proxy}" \ + %{ else ~} + no-proxy \ + %{ endif ~} + %{if active_active ~} + disable-replicated-ui \ + %{ endif ~} + private-address="$instance_ip" \ + public-address="$instance_ip" \ + %{ if airgap_pathname != null ~} + airgap \ + %{ endif ~} + %{ if distribution == "amazon-linux-2023" ~} + no-docker \ + %{ endif ~} + | tee -a $log_pathname + +# ----------------------------------------------------------------------------- +# Add docker0 to firewalld (for Red Hat instances only) +# ----------------------------------------------------------------------------- +%{ if distribution == "amazon-linux-2023" || distribution == "rhel" && cloud != "google" ~} +os_release=$(cat /etc/os-release | grep VERSION_ID | sed "s/VERSION_ID=\"\(.*\)\"/\1/g") +if (( $(echo "$os_release < 8.0" | bc -l ) )); then + echo "[$(date +"%FT%T")] [Terraform Enterprise] Disable SELinux (temporary)" | tee -a $log_pathname + setenforce 0 + echo "[$(date +"%FT%T")] [Terraform Enterprise] Add docker0 to firewalld" | tee -a $log_pathname + firewall-cmd --permanent --zone=trusted --change-interface=docker0 + firewall-cmd --reload + echo "[$(date +"%FT%T")] [Terraform Enterprise] Enable SELinux" | tee -a $log_pathname + setenforce 1 +fi +%{ endif ~} + +# ----------------------------------------------------------------------------- +# Pulling custom worker image (currently for GCP environments only) +# ----------------------------------------------------------------------------- +%{ if custom_image_tag != null && cloud == "google" ~} +%{ if length(regexall("^.+-docker\\.pkg\\.dev|^.*\\.?gcr\\.io", custom_image_tag)) > 0 ~} +echo "[Terraform Enterprise] Registering gcloud as a Docker credential helper" | tee -a +gcloud auth configure-docker --quiet ${split("/", custom_image_tag)[0]} + +%{ endif ~} +echo "[Terraform Enterprise] Pulling custom worker image '${custom_image_tag}'" | tee -a +docker pull ${custom_image_tag} +%{ endif ~} diff --git a/modules/tfe_init_replicated/variables.tf b/modules/tfe_init_replicated/variables.tf new file mode 100644 index 00000000..27df0e84 --- /dev/null +++ b/modules/tfe_init_replicated/variables.tf @@ -0,0 +1,98 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# General +# ------- +variable "cloud" { + default = null + type = string + description = "(Required) On which cloud is this Terraform Enterprise installation being deployed?" + validation { + condition = contains(["aws", "azurerm", "google"], var.cloud) + error_message = "Supported values for cloud are 'aws', 'azurerm', or 'google'." + } +} + +variable "distribution" { + default = null + type = string + description = "(Required) What is the OS distribution of the instance on which Terraoform Enterprise will be deployed?" + validation { + condition = contains(["rhel", "ubuntu", "amazon-linux-2023"], var.distribution) + error_message = "Supported values for distribution are 'rhel', 'ubuntu' or amazon-linux-2023." + } +} + +variable "tfe_license_secret_id" { + default = null + type = string + description = "The secrets manager secret ID under which the Base64 encoded Terraform Enterprise license is stored. NOTE: If this is an airgapped installation, then it is expected that the TFE license will be put on the path defined by tfe_license_file_location prior to running this module (i.e. on the virtual machine image)." +} + +variable "airgap_url" { + default = null + description = "The URL of a Replicated airgap package for Terraform Enterprise. NOTE: If this value is given, then this script will install the airgap installation prerequisites. The airgap bundle should already be on the virtual machine image, and you would not use this variable if this were a truly airgapped environment." + type = string +} + +variable "ca_certificate_secret_id" { + default = null + type = string + description = "A secret ID which contains the Base64 encoded version of a PEM encoded public certificate of a certificate authority (CA) to be trusted by the TFE instance(s)." +} + +variable "certificate_secret_id" { + default = null + type = string + description = "A secret ID which contains the Base64 encoded version of a PEM encoded public certificate for the TFE instance(s)." +} + +variable "key_secret_id" { + default = null + type = string + description = "A secret ID which contains the Base64 encoded version of a PEM encoded private key for the TFE instance(s)." +} + +# Proxy +# ----- +variable "proxy_ip" { + default = null + type = string + description = "IP Address of the proxy server" +} + +variable "proxy_port" { + default = null + type = string + description = "Port that the proxy server will use" +} + +variable "tfe_configuration" { + default = null + description = "The settings that will be used to configure Terraform Enterprise." +} + +variable "enable_monitoring" { + default = null + type = bool + description = "Should cloud appropriate monitoring agents be installed as a part of the TFE installation script?" +} + +# Mounted Disk +# ------------ +variable "disk_device_name" { + default = null + description = "The name of the disk device on which Terraform Enterprise will store data in Mounted Disk mode." + type = string +} + +variable "disk_path" { + default = null + description = "The pathname of the directory in which Terraform Enterprise will store data in Mounted Disk mode." + type = string +} + +variable "replicated_configuration" { + default = null + description = "The settings that will be used to configure Replicated." +} diff --git a/modules/tfe_init_replicated/versions.tf b/modules/tfe_init_replicated/versions.tf new file mode 100644 index 00000000..162d942b --- /dev/null +++ b/modules/tfe_init_replicated/versions.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_version = ">= 0.13" + + required_providers {} +} From 3acf557b5761a10460b3a60308bd5ad2fe9ac622 Mon Sep 17 00:00:00 2001 From: RAVI PRAKASH Date: Fri, 26 Sep 2025 14:09:30 +0530 Subject: [PATCH 5/6] Fix syntax error in main.tf - Correct corrupted line in data.aws_kms_key resource - Update settings module source to use local version --- main.tf | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/main.tf b/main.tf index 9ef552bd..c17cc307 100644 --- a/main.tf +++ b/main.tf @@ -20,9 +20,7 @@ data "aws_ami" "ubuntu" { } data "aws_kms_key" "main" { - kemodule "settings" { - source = "./modules/settings" - count = var.is_replicated_deployment ? 0 : 1d = var.kms_key_arn + key_id = var.kms_key_arn } # ----------------------------------------------------------------------------- @@ -409,7 +407,7 @@ module "tfe_init_fdo" { # TFE and Replicated settings to pass to the tfe_init_replicated module for replicated deployment # -------------------------------------------------------------------------------------------- module "settings" { - source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/settings?ref=main" + source = "./modules/settings" count = var.is_replicated_deployment ? 1 : 0 # TFE Base Configuration From 0353aa708c8ee83516e5fe4e4573b346d857c2ba Mon Sep 17 00:00:00 2001 From: RAVI PRAKASH Date: Mon, 29 Sep 2025 01:36:52 +0530 Subject: [PATCH 6/6] Fix AWS IAM PostgreSQL authentication by using RDS instead of EC2 - Remove postgres_passwordless module that used EC2+Docker (incompatible with AWS IAM auth) - Use standard database module with RDS for all scenarios including passwordless auth - Add iam_database_authentication_enabled parameter to RDS instance - Update database selection logic to use RDS for IAM authentication - Fix 502 Bad Gateway error when TFE tries to connect with AWS IAM credentials AWS IAM database authentication only works with RDS/Aurora, not EC2-based PostgreSQL containers. --- locals.tf | 4 +--- main.tf | 18 +----------------- modules/database/main.tf | 3 +++ modules/database/variables.tf | 6 ++++++ 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/locals.tf b/locals.tf index 58821baf..5125c980 100644 --- a/locals.tf +++ b/locals.tf @@ -6,7 +6,7 @@ locals { enable_airgap = var.airgap_url == null && var.tfe_license_bootstrap_airgap_package_path != null enable_external = var.operational_mode == "external" || var.operational_mode == "active-active" enable_disk = var.operational_mode == "disk" - enable_database_module = local.enable_external && var.enable_aurora == false && var.db_use_mtls == false && var.enable_edb == false && !(var.postgres_enable_iam_auth && !var.postgres_use_password_auth) + enable_database_module = local.enable_external && var.enable_aurora == false && var.db_use_mtls == false && var.enable_edb == false enable_explorer_database_module = local.enable_external && var.db_use_mtls == false && var.explorer_db_name != null enable_object_storage_module = local.enable_external enable_redis_module = var.operational_mode == "active-active" @@ -33,7 +33,6 @@ locals { aurora_database = try(module.aurora_database[0], local.default_database) mtls_database = try(module.database_mtls[0], local.default_database) - postgres_passwordless = try(module.postgres_passwordless[0], local.default_database) enterprise_db = try(module.edb[0], local.default_database) standard_db = try(module.database[0], local.default_database) @@ -43,7 +42,6 @@ locals { var.db_use_mtls && var.postgres_enable_iam_auth ? error("Both db_use_mtls and postgres_enable_iam_auth cannot be true.") : var.enable_aurora ? local.aurora_database : var.db_use_mtls ? local.mtls_database : - var.postgres_enable_iam_auth && !var.postgres_use_password_auth ? local.postgres_passwordless : var.enable_edb ? local.enterprise_db : local.standard_db ) diff --git a/main.tf b/main.tf index c17cc307..49bb293b 100644 --- a/main.tf +++ b/main.tf @@ -176,6 +176,7 @@ module "database" { kms_key_arn = local.kms_key_arn allow_major_version_upgrade = var.allow_major_version_upgrade allow_multiple_azs = var.allow_multiple_azs + enable_iam_database_authentication = var.postgres_enable_iam_auth && !var.postgres_use_password_auth } # ----------------------------------------------------------------------------- @@ -222,23 +223,6 @@ module "database_mtls" { network_public_subnets = local.network_public_subnets } -# ----------------------------------------------------------------------------- -# EC2 PostgreSQL container with passwordless authentication -# ----------------------------------------------------------------------------- -module "postgres_passwordless" { - source = "./modules/postgres-passwordless" - count = var.postgres_enable_iam_auth && !var.postgres_use_password_auth ? 1 : 0 - - domain_name = var.domain_name - db_name = var.db_name - db_username = var.db_username - db_parameters = var.db_parameters - friendly_name_prefix = var.friendly_name_prefix - network_id = local.network_id - aws_iam_instance_profile = module.service_accounts.iam_instance_profile.name - network_public_subnets = local.network_public_subnets -} - # ----------------------------------------------------------------------------- # AWS Aurora PostgreSQL Database Cluster # ----------------------------------------------------------------------------- diff --git a/modules/database/main.tf b/modules/database/main.tf index 84ca0292..9a84b036 100644 --- a/modules/database/main.tf +++ b/modules/database/main.tf @@ -83,4 +83,7 @@ resource "aws_db_instance" "postgresql" { kms_key_id = var.kms_key_arn storage_type = "gp2" vpc_security_group_ids = [aws_security_group.postgresql.id] + + # Enable IAM database authentication if requested + iam_database_authentication_enabled = var.enable_iam_database_authentication } diff --git a/modules/database/variables.tf b/modules/database/variables.tf index 5361416e..fbd55bba 100644 --- a/modules/database/variables.tf +++ b/modules/database/variables.tf @@ -77,3 +77,9 @@ variable "allow_multiple_azs" { description = "Determine Amazon RDS Postgres deployment strategy." default = true } + +variable "enable_iam_database_authentication" { + type = bool + description = "Enable IAM database authentication for the RDS instance." + default = false +}