diff --git a/.gitignore b/.gitignore index 97fe116..b555a12 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ - Local .terraform directories +# Local .terraform directories **/.terraform/* +.terraform # .tfstate files *.tfstate @@ -8,12 +9,12 @@ # Crash log files crash.log -# Exclude all .tfvars files, which are likely to contain sentitive data, such as +# Exclude secrets.tfvars files, which are likely to contain sentitive data, such as # password, private keys, and other secrets. These should not be part of version # control as they are data points which are potentially sensitive and subject # to change depending on the environment. # -*.tfvars +secrets.tfvars # Ignore override files as they are usually used to override resources locally and so # are not checked in diff --git a/aws/multi_region/README.md b/aws/multi_region/README.md index dab13d3..3ccc915 100644 --- a/aws/multi_region/README.md +++ b/aws/multi_region/README.md @@ -18,8 +18,6 @@ is deployed. [Image_Diagram]: https://f.hubspotusercontent30.net/hubfs/5856039/terraform/diagrams/aws-multi-region-int-gw.png "Diagram" - - # Pre-Configuration Consider creating a special sub account for the Kasm deployment. @@ -27,12 +25,10 @@ Consider creating a special sub account for the Kasm deployment. In your AWS account create a DNS Public zone that matches the desired domain name for the deployment. e.g `kasm.contoso.com` ### SSH Key Pair -In the desired AWS region create an aws Key pair. This will be configured as the SSH key for the deployed EC2 machines +In the each AWS region where you will deploy Kasm, create an aws Key pair with the same name. The key name will be value used in the `aws_key_pair` variable and it will be configured as the SSH key for the deployed EC2 machines. ### AWS API Keys -Create a user via the IAM console that will be used for the terraform deployment. Give the user **Programatic Access** -and attach the existing policy **AdministratorAccess**. Save the key and key secret - +Create a user via the IAM console that will be used for the terraform deployment. Give the user **Programatic Access** and attach the existing policy **AdministratorAccess**. Save the key and key secret. # Terraform Configuration @@ -41,22 +37,61 @@ and attach the existing policy **AdministratorAccess**. Save the key and key sec terraform init -2. Open `variables.tf` and update the global variables. The variable definitions and descriptions -can be found in `/variables.tf` - -3. Open `deployment.tf` and update the module level variables as desired. - +2. Open `settings.tfvars` and update the variable values. The variable definitions, descriptions, and validation expectations can be found in the `variables.tf` file. -4. Verify the configuration +> ***NOTE:*** This document assumes you are using a separate file named `secrets.tfvars` for the AWS credentials generated in the [AWS API Keys](#aws-api-keys) section above. The .gitignore file in this repository will ignore any files named `secrets.tfvars` since they are expected to have sensitive values in them. This will prevent you from accidentally committing them to source control. - terraform plan +3. If you are deploying more than 2 regions, you will need to modify the `provider.tf` and the `deployment.tf` files. There are commented sections in both files indicating how to deploy additional regions. -5. Deploy +3. Verify the configuration - terraform apply + terraform plan -var-file settings.tfvars -var-file secrets.tfvars + +4. Deploy + + terraform apply -var-file settings.tfvars -var-file secrets.tfvars + +5. Login to the Deployment as an Admin via the domain defined e.g `https://kasm.contoso.com` + +6. Navigate to the Agents tab, and enable each Agent after it checks in. (May take a few minutes) -6. Login to the Deployment as an Admin via the domain defined e.g `https://kasm.contoso.com` +# AWS Terraform Variable definitions -7. Navigate to the Agents tab, and enable each Agent after it checks in. (May take a few minutes) +| Variable | Description | Variable type | Example | +|:--------:|-------------|---------------|---------| +| `aws_access_key` | The AWS access key used for deployment. | String | `"AKIAJSIE27KKMHXI3BJQ"` | +| `aws_secret_key` | The AWS secret key used for deployment. | String | `"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"` | +| `aws_primary_region` | The AWS Region to deploy all Kasm Management resources. | String | `"us-east-1"` | +| `project_name` | The name of the deployment (e.g dev, staging). A short single word of up to 15 characters. | String | `"kasm"` | +| `aws_domain_name` | The Route53 Zone used for the dns entries. This must already exist in the AWS account. (e.g dev.kasm.contoso.com). The deployment will be accessed via this zone name via https. | String | `"kasm.contoso.com"` | +| `kasm_zone_name` | A name given to the kasm deployment Zone. | String | `"default"` | +| `primary_vpc_subnet_cidr` | The subnet CIDR to use for the Primary region's VPC. | String | `"10.0.0.0/16"` | +| `aws_key_pair` | The name of an aws keypair to use. | String | `"kasm_ssh_key"` | +| `primary_region_ec2_ami_id` | The AMI used for the EC2 nodes in the Primary (Management) region. Recommended Ubuntu 20.04 LTS. | String | `"ami-09cd747c78a9add63"` | +| `swap_size` | The amount of swap (in MB) to configure inside the Kasm servers. | Number | `2048` | +| `webapp_instance_type` | The instance type for the Kasm WebApps. | String | `"t3.small"` | +| `webapp_hdd_size_gb` | The HDD size for the WebApp EC2s in GB. | Number | `40` | +| `db_instance_type` | The instance type for the Kasm Database. | String | `"t3.medium"` | +| `db_hdd_size_gb` | The HDD size for the DB EC2 in GB. | Number | `40` | +| `agent_instance_type` | The instance type for the Kasm Agents in the Primary region. | String | `"t3.medium"` | +| `agent_hdd_size_gb` | The HDD size for the Agent EC2s in GB. | Number | `120` | +| `num_webapps` | The number of WebApp role servers to create in this deployment. Acceptable ranges from 1-3. | Number | `2` | +| `num_agents` | The number of static Kasm Agents to create in the primary region. Acceptable ranges from 0-100. | Number | `2` | +| `allow_ssh_cidrs` | A list of subnets in CIDR notation allowed to SSH into your kasm servers (use `["0.0.0.0/0]"` to allow SSH from any IP). | List(String) | `["1.1.1.1/32","172.217.22.14/32"]` | +| `web_access_cidrs` | A list of subnets in CIDR notation allowed Web access to your kasm servers (use `["0.0.0.0/0]"` to allow HTTP/HTTPS from any IP). | List(String) | `["0.0.0.0/0"]` | +| `secondary_regions_settings` | A map of AWS environment settings for secondary regions. The Primary region is considered "region1", thus all secondary regions should be labeled "region2", "region3", etc. Refer to the commented settings in the `secondary_regions_settings` variable in the `settings.tf` for an example. | Map(any) |
{
  region2 = {
    agent_region = "eu-central-1"
    agent_ec2_ami_id = "ami-0e067cc8a2b58de59"
    agent_instance_type = "t3.medium"
    num_agents = 2
    agent_vpc_cidr = "10.1.0.0/16"
  }
}
+| `database_password` | The Kasm PostgreSQL database password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `redis_password` | The Kasm Redis password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `admin_password` | The Kasm Administrative user login password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `user_password` | A Kasm standard (non-administrator) user password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `manager_token` | The manager token value used by Kasm agents to authenticate to the Kasm WebApps. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `kasm_build` | The download URL for the desired Kasm Workspaces version. | String | `"https://kasm-static-content.s3.amazonaws.com/kasm_release_1.12.0.d4fd8a.tar.gz"` | +| `aws_default_tags` | A Map of all tags you wish to apply to all TF created resources in this deployment. | Map(Any) |
{
  Service_name = "Kasm Workspaces"
  Kasm_version = "1.12"
}
| + +# Detailed Terraform Deployment Diagram + +![Detailed Diagram][Detailed_Diagram] + +[Detailed_Diagram]: ./diagram/aws_multi_region.png "Detailed Diagram" \ No newline at end of file diff --git a/aws/multi_region/agents/agent.tf b/aws/multi_region/agents/agent.tf index 98e4e8e..b32beb2 100644 --- a/aws/multi_region/agents/agent.tf +++ b/aws/multi_region/agents/agent.tf @@ -1,29 +1,25 @@ resource "aws_instance" "kasm-agent" { - count = "${var.num_agents}" - ami = "${var.ec2_ami}" - instance_type = "${var.agent_instance_type}" - vpc_security_group_ids = ["${aws_security_group.kasm-agent-sg.id}"] - subnet_id = "${aws_subnet.kasm-agent-subnet.id}" - key_name = "${var.aws_key_pair}" + count = var.num_agents + ami = var.ec2_ami + instance_type = var.agent_instance_type + vpc_security_group_ids = [data.aws_security_group.data-kasm_agent_sg.id] + subnet_id = data.aws_subnet.data-kasm_agent_subnet.id + key_name = var.aws_key_pair root_block_device { - volume_size = "50" + volume_size = var.agent_hdd_size_gb } - user_data = <<-EOF - #!/bin/bash - fallocate -l 4g /mnt/kasm.swap - chmod 600 /mnt/kasm.swap - mkswap /mnt/kasm.swap - swapon /mnt/kasm.swap - echo '/mnt/kasm.swap swap swap defaults 0 0' | tee -a /etc/fstab - cd /tmp - wget ${var.kasm_build} - tar xvf kasm_*.tar.gz - PUBLIC_DNS=(`curl -s http://169.254.169.254/latest/meta-data/public-ipv4`) - bash kasm_release/install.sh -S agent -e -p $PUBLIC_DNS -m ${var.zone_name}-lb.${var.aws_domain_name} -M ${var.manager_token} - EOF + user_data = templatefile("${path.module}/../userdata/agent_bootstrap.sh", + { + kasm_build_url = var.kasm_build + swap_size = var.swap_size + manager_address = var.aws_domain_name + manager_token = var.manager_token + } + ) + tags = { Name = "${var.project_name}-${var.zone_name}-kasm-agent" } -} \ No newline at end of file +} diff --git a/aws/multi_region/agents/availability_zones.tf b/aws/multi_region/agents/availability_zones.tf new file mode 100644 index 0000000..87d8f48 --- /dev/null +++ b/aws/multi_region/agents/availability_zones.tf @@ -0,0 +1,3 @@ +data "aws_availability_zones" "available" { + state = "available" +} diff --git a/aws/multi_region/agents/main.tf b/aws/multi_region/agents/main.tf deleted file mode 100644 index 83275fb..0000000 --- a/aws/multi_region/agents/main.tf +++ /dev/null @@ -1,8 +0,0 @@ -provider "aws" { - region = "${var.aws_region}" - access_key = "${var.aws_access_key}" - secret_key = "${var.aws_secret_key}" -} -data "aws_availability_zones" "available" { - state = "available" -} \ No newline at end of file diff --git a/aws/multi_region/agents/provider.tf b/aws/multi_region/agents/provider.tf new file mode 100644 index 0000000..ea82995 --- /dev/null +++ b/aws/multi_region/agents/provider.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + #version = "4.56.0" + } + } +} \ No newline at end of file diff --git a/aws/multi_region/agents/routes.tf b/aws/multi_region/agents/routes.tf new file mode 100644 index 0000000..3d48abb --- /dev/null +++ b/aws/multi_region/agents/routes.tf @@ -0,0 +1,21 @@ +resource "aws_route_table" "internet_access" { + vpc_id = data.aws_vpc.data-kasm_agent_vpc.id + + route { + cidr_block = var.anywhere + gateway_id = data.aws_internet_gateway.data-kasm_agent_default_ig.id + } + + tags = { + Name = "${var.project_name}-kasm-agent-default-route" + } +} + +data "aws_route_table" "data-agent_internet_gateway_route_table" { + route_table_id = aws_route_table.internet_access.id +} + +resource "aws_route_table_association" "agent_table_association" { + subnet_id = data.aws_subnet.data-kasm_agent_subnet.id + route_table_id = data.aws_route_table.data-agent_internet_gateway_route_table.id +} diff --git a/aws/multi_region/agents/security_group.tf b/aws/multi_region/agents/security_group.tf index ec7d79c..4fe5d53 100644 --- a/aws/multi_region/agents/security_group.tf +++ b/aws/multi_region/agents/security_group.tf @@ -1,26 +1,30 @@ resource "aws_security_group" "kasm-agent-sg" { name = "${var.project_name}-${var.zone_name}-kasm-agent-access" description = "Allow access to agents" - vpc_id = "${aws_vpc.kasm-default-vpc.id}" + vpc_id = data.aws_vpc.data-kasm_agent_vpc.id ingress { from_port = 22 to_port = 22 protocol = "tcp" - cidr_blocks = ["${var.ssh_access_cidr}"] + cidr_blocks = var.ssh_access_cidrs } ingress { from_port = 443 to_port = 443 protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] + cidr_blocks = [var.anywhere] } egress { from_port = 0 to_port = 0 protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] + cidr_blocks = [var.anywhere] } -} \ No newline at end of file +} + +data "aws_security_group" "data-kasm_agent_sg" { + id = aws_security_group.kasm-agent-sg.id +} diff --git a/aws/multi_region/agents/subnet.tf b/aws/multi_region/agents/subnet.tf index e8fce6d..598d6f8 100644 --- a/aws/multi_region/agents/subnet.tf +++ b/aws/multi_region/agents/subnet.tf @@ -1,9 +1,19 @@ +locals { + kasm_agent_vpc_subnet_cidr_mask = split("/", var.agent_vpc_cidr)[1] + kasm_agent_subnet_cidr_calculation = (8 - (local.kasm_agent_vpc_subnet_cidr_mask - 16)) + kasm_agent_subnet_cidr_size = local.kasm_agent_subnet_cidr_calculation < 0 ? 0 : local.kasm_agent_subnet_cidr_calculation +} + resource "aws_subnet" "kasm-agent-subnet" { - vpc_id = "${aws_vpc.kasm-default-vpc.id}" - cidr_block = "10.0.40.0/24" + vpc_id = data.aws_vpc.data-kasm_agent_vpc.id + cidr_block = cidrsubnet(var.agent_vpc_cidr, local.kasm_agent_subnet_cidr_size, 0) availability_zone = data.aws_availability_zones.available.names[0] map_public_ip_on_launch = true tags = { Name = "${var.project_name}-${var.zone_name}-kasm-agent-subnet" } -} \ No newline at end of file +} + +data "aws_subnet" "data-kasm_agent_subnet" { + id = aws_subnet.kasm-agent-subnet.id +} diff --git a/aws/multi_region/agents/variables.tf b/aws/multi_region/agents/variables.tf index b4075f6..5d59afa 100644 --- a/aws/multi_region/agents/variables.tf +++ b/aws/multi_region/agents/variables.tf @@ -1,51 +1,76 @@ -variable "aws_access_key" { - description = "The AWS access key used for deployment" -} - -variable "aws_secret_key" { - description = "The AWS secret key used for deployment" -} - variable "project_name" { description = "The name of the deployment (e.g dev, staging). A short single word" + type = string } variable "aws_domain_name" { description = "The Route53 Zone used for the dns entries. This must already exist in the AWS account. (e.g dev.kasm.contoso.com). The deployment will be accessed via this zone name via https" + type = string +} + +variable "agent_vpc_cidr" { + description = "Subnet CIDR range for Agent VPC" + type = string +} + +variable "swap_size" { + description = "The amount of swap (in MB) to configure inside the compute instances" + type = number } variable "num_agents" { description = "The number of Agent Role Servers to create in the deployment" + type = number } variable "agent_instance_type" { - description = "the instance type for the agents" + description = "The instance type for the agents" + type = string +} + +variable "agent_hdd_size_gb" { + description = "The HDD size for agents" + type = number } variable "aws_region" { description = "The AWS region for the deployment. (e.g us-east-1)" + type = string } variable "kasm_build" { description = "The URL for the Kasm Workspaces build" + type = string } variable "zone_name" { description = "A name given to the Kasm deployment Zone" + type = string } variable "aws_key_pair" { description = "The name of an aws keypair to use." + type = string } variable "ec2_ami" { - description = "The AMI used for the EC2 nodes. Recommended Ubuntu 18.04 LTS." + description = "The AMI used for the EC2 nodes. Recommended Ubuntu 20.04 LTS." + type = string } variable "manager_token" { description = "The password for the database. No special characters" + type = string + sensitive = true } -variable "ssh_access_cidr" { +variable "ssh_access_cidrs" { description = "CIDR notation of the bastion host allowed to SSH in to the machines" + type = list(string) +} + +variable "anywhere" { + description = "Anywhere subnet for routing and load ingress from all IPs" + type = string + default = "0.0.0.0/0" } diff --git a/aws/multi_region/agents/vpc.tf b/aws/multi_region/agents/vpc.tf index 383913c..4f5a78d 100644 --- a/aws/multi_region/agents/vpc.tf +++ b/aws/multi_region/agents/vpc.tf @@ -1,19 +1,23 @@ -resource "aws_vpc" "kasm-default-vpc" { - cidr_block = "10.0.0.0/16" +resource "aws_vpc" "kasm-agent-vpc" { + cidr_block = var.agent_vpc_cidr + enable_dns_hostnames = true + enable_dns_support = true tags = { Name = "${var.project_name}-${var.zone_name}-kasm-vpc" } } +data "aws_vpc" "data-kasm_agent_vpc" { + id = aws_vpc.kasm-agent-vpc.id +} + resource "aws_internet_gateway" "kasm-default-ig" { - vpc_id = "${aws_vpc.kasm-default-vpc.id}" + vpc_id = data.aws_vpc.data-kasm_agent_vpc.id tags = { Name = "${var.project_name}-${var.zone_name}-kasm-ig" } } -resource "aws_route" "internet_access" { - route_table_id = "${aws_vpc.kasm-default-vpc.main_route_table_id}" - destination_cidr_block = "0.0.0.0/0" - gateway_id = "${aws_internet_gateway.kasm-default-ig.id}" -} \ No newline at end of file +data "aws_internet_gateway" "data-kasm_agent_default_ig" { + internet_gateway_id = aws_internet_gateway.kasm-default-ig.id +} diff --git a/aws/multi_region/deployment.tf b/aws/multi_region/deployment.tf index d10789f..9d4b835 100644 --- a/aws/multi_region/deployment.tf +++ b/aws/multi_region/deployment.tf @@ -3,88 +3,175 @@ # This will house the Kasm Workspaces DB, and a set of # agents/webapps that map to this region. ########################################################### - module "primary_region" { - source = "./primary" - aws_region = "us-east-1" - zone_name = "us-east-1" - ec2_ami = "ami-013f17f36f8b1fefb" - db_instance_type = "t3.small" - num_agents = 2 - agent_instance_type = "t3.medium" - num_webapps = 2 - webapp_instance_type = "t3.small" - - - - aws_access_key = "${var.aws_access_key}" - aws_secret_key = "${var.aws_secret_key}" - project_name = "${var.project_name}" - kasm_build = "${var.kasm_build}" - database_password = "${var.database_password}" - redis_password = "${var.redis_password}" - user_password = "${var.user_password}" - admin_password = "${var.admin_password}" - manager_token = "${var.manager_token}" - aws_key_pair = "${var.aws_key_pair}" - aws_domain_name = "${var.aws_domain_name}" - ssh_access_cidr = "${var.ssh_access_cidr}" + source = "./primary" + aws_region = var.aws_primary_region + zone_name = var.aws_primary_region + vpc_subnet_cidr = var.primary_vpc_subnet_cidr + ec2_ami = var.primary_region_ec2_ami_id + db_instance_type = var.db_instance_type + num_webapps = var.num_webapps + project_name = var.project_name + kasm_build = var.kasm_build + db_hdd_size_gb = var.db_hdd_size_gb + swap_size = var.swap_size + database_password = var.database_password + redis_password = var.redis_password + user_password = var.user_password + admin_password = var.admin_password + manager_token = var.manager_token + service_registration_token = var.service_registration_token + aws_key_pair = var.aws_key_pair + aws_domain_name = var.aws_domain_name + ssh_access_cidrs = var.ssh_access_cidrs + web_access_cidrs = var.web_access_cidrs } - -########################################################### -# Add a pair of webapp and agent modules -# for each additional region desired. -########################################################### - -module "us-west-1-webapps" { - source = "./webapps" - faux_aws_region = "us-west-1" - zone_name = "us-west-1" - num_webapps = 2 - webapp_instance_type = "t3.small" - ec2_ami = "ami-013f17f36f8b1fefb" - - - - primary_aws_region = "${module.primary_region.primary_aws_region}" - webapp_subnet_id_1 = "${module.primary_region.webapp_subnet_1_id}" - webapp_subnet_id_2 = "${module.primary_region.webapp_subnet_2_id}" - agent_subnet_id = "${module.primary_region.agent_subnet_id}" - aws_access_key = "${var.aws_access_key}" - aws_secret_key = "${var.aws_secret_key}" - aws_domain_name = "${var.aws_domain_name}" - project_name = "${var.project_name}" - kasm_build = "${var.kasm_build}" - database_password = "${var.database_password}" - redis_password = "${var.redis_password}" - manager_token = "${var.manager_token}" - aws_key_pair = "${var.aws_key_pair}" - kasm_db_ip = "${module.primary_region.kasm_db_ip}" - primary_vpc_id = "${module.primary_region.primary_vpc_id}" - certificate_arn = "${module.primary_region.certificate_arn}" - ssh_access_cidr = "${var.ssh_access_cidr}" +module "primary_region-webapps-and-agents" { + source = "./webapps" + faux_aws_region = var.aws_primary_region + zone_name = var.aws_primary_region + primary_aws_region = var.aws_primary_region + num_webapps = var.num_webapps + num_agents = var.num_agents + ec2_ami = var.primary_region_ec2_ami_id + swap_size = var.swap_size + webapp_subnet_ids = module.primary_region.webapp_subnet_ids + webapp_security_group_id = module.primary_region.webapp_security_group_id + agent_subnet_id = module.primary_region.agent_subnet_id + agent_security_group_id = module.primary_region.agent_security_group_id + load_balancer_security_group_id = module.primary_region.lb_security_group_id + webapp_instance_type = var.webapp_instance_type + webapp_hdd_size_gb = var.webapp_hdd_size_gb + agent_instance_type = var.agent_instance_type + agent_hdd_size_gb = var.agent_hdd_size_gb + aws_domain_name = var.aws_domain_name + project_name = var.project_name + kasm_build = var.kasm_build + database_password = var.database_password + redis_password = var.redis_password + manager_token = var.manager_token + aws_key_pair = var.aws_key_pair + kasm_db_ip = module.primary_region.kasm_db_ip + primary_vpc_id = module.primary_region.primary_vpc_id + certificate_arn = module.primary_region.certificate_arn + ssh_access_cidrs = var.ssh_access_cidrs + load_balancer_log_bucket = module.primary_region.lb_log_bucket } - -module "us-west-1-agents" { - source = "./agents" - aws_region = "us-west-1" - zone_name = "us-west-1" - num_agents = 2 - agent_instance_type = "t3.medium" - ec2_ami = "ami-08d0eee5e00da8a9b" - - - - aws_access_key = "${var.aws_access_key}" - aws_secret_key = "${var.aws_secret_key}" - aws_domain_name = "${var.aws_domain_name}" - project_name = "${var.project_name}" - kasm_build = "${var.kasm_build}" - manager_token = "${var.manager_token}" - aws_key_pair = "${var.aws_key_pair}" - ssh_access_cidr = "${var.ssh_access_cidr}" - +##################################################################### +# +# Add a webapp and agent module for each additional region desired. +# +##################################################################### +module "region2-webapps" { + source = "./webapps" + faux_aws_region = var.secondary_regions_settings.region2.agent_region + zone_name = var.secondary_regions_settings.region2.agent_region + primary_aws_region = var.aws_primary_region + num_webapps = var.num_webapps + webapp_instance_type = var.webapp_instance_type + webapp_hdd_size_gb = var.webapp_hdd_size_gb + swap_size = var.swap_size + ec2_ami = var.primary_region_ec2_ami_id + webapp_subnet_ids = module.primary_region.webapp_subnet_ids + webapp_security_group_id = module.primary_region.webapp_security_group_id + load_balancer_security_group_id = module.primary_region.lb_security_group_id + aws_domain_name = var.aws_domain_name + project_name = var.project_name + kasm_build = var.kasm_build + database_password = var.database_password + redis_password = var.redis_password + manager_token = var.manager_token + aws_key_pair = var.aws_key_pair + kasm_db_ip = module.primary_region.kasm_db_ip + primary_vpc_id = module.primary_region.primary_vpc_id + certificate_arn = module.primary_region.certificate_arn + ssh_access_cidrs = var.ssh_access_cidrs + load_balancer_log_bucket = module.primary_region.lb_log_bucket } +module "region2-agents" { + source = "./agents" + aws_region = var.secondary_regions_settings.region2.agent_region + zone_name = var.secondary_regions_settings.region2.agent_region + num_agents = var.secondary_regions_settings.region2.num_agents + agent_instance_type = var.secondary_regions_settings.region2.agent_instance_type + ec2_ami = var.secondary_regions_settings.region2.agent_ec2_ami_id + agent_vpc_cidr = var.secondary_regions_settings.region2.agent_vpc_cidr + agent_hdd_size_gb = var.secondary_regions_settings.region2.agent_hdd_size_gb + swap_size = var.swap_size + aws_domain_name = var.aws_domain_name + project_name = var.project_name + kasm_build = var.kasm_build + manager_token = var.manager_token + aws_key_pair = var.aws_key_pair + ssh_access_cidrs = var.ssh_access_cidrs + + providers = { + aws = aws.region2 + } +} + +######################################################################### +# +# Uncomment the below section and update the provider and the settings +# in the secondary_regions_settings variable in the settings.tfvars +# file for your desired region. +# +######################################################################### +# module "region3-webapps" { +# source = "./webapps" +# ##### Update the values below to reference the appropriate region number +# faux_aws_region = var.secondary_regions_settings.region3.agent_region +# zone_name = var.secondary_regions_settings.region3.agent_region +# +# ##### The values below should not change +# primary_aws_region = var.aws_primary_region +# num_webapps = var.num_webapps +# webapp_instance_type = var.webapp_instance_type +# webapp_hdd_size_gb = var.webapp_hdd_size_gb +# swap_size = var.swap_size +# ec2_ami = var.primary_region_ec2_ami_id +# webapp_subnet_ids = module.primary_region.webapp_subnet_ids +# webapp_security_group_id = module.primary_region.webapp_security_group_id +# load_balancer_security_group_id = module.primary_region.lb_security_group_id +# aws_domain_name = var.aws_domain_name +# project_name = var.project_name +# kasm_build = var.kasm_build +# database_password = var.database_password +# redis_password = var.redis_password +# manager_token = var.manager_token +# aws_key_pair = var.aws_key_pair +# kasm_db_ip = module.primary_region.kasm_db_ip +# primary_vpc_id = module.primary_region.primary_vpc_id +# certificate_arn = module.primary_region.certificate_arn +# ssh_access_cidrs = var.ssh_access_cidrs +# load_balancer_log_bucket = module.primary_region.lb_log_bucket +# } + +# module "region3-agents" { +# source = "./agents" +# ##### Update the values below to reference the appropriate region number +# aws_region = var.secondary_regions_settings.region3.agent_region +# zone_name = var.secondary_regions_settings.region3.agent_region +# num_agents = var.secondary_regions_settings.region3.num_agents +# agent_instance_type = var.secondary_regions_settings.region3.agent_instance_type +# ec2_ami = var.secondary_regions_settings.region3.agent_ec2_ami_id +# agent_vpc_cidr = var.secondary_regions_settings.region3.agent_vpc_cidr +# agent_hdd_size_gb = var.secondary_regions_settings.region3.agent_hdd_size_gb +# swap_size = var.swap_size +# +# ##### The values below should not change +# aws_domain_name = var.aws_domain_name +# project_name = var.project_name +# kasm_build = var.kasm_build +# manager_token = var.manager_token +# aws_key_pair = var.aws_key_pair +# ssh_access_cidrs = var.ssh_access_cidrs +# +# ##### Update the provider to reference the settings in the provder.tf file +# providers = { +# aws = aws.region3 +# } +# } diff --git a/aws/multi_region/diagram/aws_multi_region.drawio b/aws/multi_region/diagram/aws_multi_region.drawio new file mode 100644 index 0000000..084fa9f --- /dev/null +++ b/aws/multi_region/diagram/aws_multi_region.drawio @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws/multi_region/diagram/aws_multi_region.png b/aws/multi_region/diagram/aws_multi_region.png new file mode 100644 index 0000000..61de799 Binary files /dev/null and b/aws/multi_region/diagram/aws_multi_region.png differ diff --git a/aws/multi_region/primary/availability_zones.tf b/aws/multi_region/primary/availability_zones.tf new file mode 100644 index 0000000..87d8f48 --- /dev/null +++ b/aws/multi_region/primary/availability_zones.tf @@ -0,0 +1,3 @@ +data "aws_availability_zones" "available" { + state = "available" +} diff --git a/aws/multi_region/primary/cert.tf b/aws/multi_region/primary/cert.tf index eb2c3a1..9881f83 100644 --- a/aws/multi_region/primary/cert.tf +++ b/aws/multi_region/primary/cert.tf @@ -1,12 +1,12 @@ data "aws_route53_zone" "kasm-route53-zone" { - name = "${var.aws_domain_name}" + name = var.aws_domain_name private_zone = false } resource "aws_acm_certificate" "kasm-alb-cert" { - domain_name = "${var.aws_domain_name}" + domain_name = var.aws_domain_name subject_alternative_names = ["*.${var.aws_domain_name}"] - validation_method = "DNS" + validation_method = "DNS" lifecycle { @@ -15,8 +15,8 @@ resource "aws_acm_certificate" "kasm-alb-cert" { } resource "aws_route53_record" "kasm-route53-cert-validation-record" { - for_each = { - for dvo in aws_acm_certificate.kasm-alb-cert.domain_validation_options: dvo.domain_name => { + for_each = { + for dvo in aws_acm_certificate.kasm-alb-cert.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name record = dvo.resource_record_value type = dvo.resource_record_type @@ -27,16 +27,12 @@ resource "aws_route53_record" "kasm-route53-cert-validation-record" { records = [each.value.record] zone_id = data.aws_route53_zone.kasm-route53-zone.id - ttl = 30 + ttl = 30 allow_overwrite = true } resource "aws_acm_certificate_validation" "kasm-elb-certificate-validation" { certificate_arn = aws_acm_certificate.kasm-alb-cert.arn - validation_record_fqdns = [for record in aws_route53_record.kasm-route53-cert-validation-record: record.fqdn] + validation_record_fqdns = [for record in aws_route53_record.kasm-route53-cert-validation-record : record.fqdn] } - -output "certificate_arn" { - value = "${aws_acm_certificate_validation.kasm-elb-certificate-validation.certificate_arn}" -} \ No newline at end of file diff --git a/aws/multi_region/primary/db.tf b/aws/multi_region/primary/db.tf index d6e60f2..6f7b8e4 100644 --- a/aws/multi_region/primary/db.tf +++ b/aws/multi_region/primary/db.tf @@ -1,33 +1,31 @@ resource "aws_instance" "kasm-db" { - ami = "${var.ec2_ami}" - instance_type = "${var.db_instance_type}" - vpc_security_group_ids = ["${aws_security_group.kasm-default-sg.id}"] - subnet_id = "${aws_subnet.kasm-database-subnet.id}" - key_name = "${var.aws_key_pair}" + ami = var.ec2_ami + instance_type = var.db_instance_type + vpc_security_group_ids = [data.aws_security_group.data-kasm_db_sg.id] + subnet_id = data.aws_subnet.data-kasm_db_subnet.id + key_name = var.aws_key_pair root_block_device { - volume_size = "40" + volume_size = var.db_hdd_size_gb } - user_data = <<-EOF - #!/bin/bash - fallocate -l 4g /mnt/kasm.swap - chmod 600 /mnt/kasmswap - mkswap /mnt/kasm.swap - swapon /mnt/kasm.swap - echo '/mnt/kasm.swap swap swap defaults 0 0' | tee -a /etc/fstab - cd /tmp - wget ${var.kasm_build} - tar xvf kasm_*.tar.gz - bash kasm_release/install.sh -S db -e -Q ${var.database_password} -R ${var.redis_password} -U ${var.user_password} -P ${var.admin_password} -M ${var.manager_token} - EOF + user_data = templatefile("${path.module}/../userdata/db_bootstrap.sh", + { + kasm_build_url = var.kasm_build + user_password = var.user_password + admin_password = var.admin_password + redis_password = var.redis_password + database_password = var.database_password + manager_token = var.manager_token + swap_size = var.swap_size + } + ) + tags = { Name = "${var.project_name}-kasm-db" } } - -output "kasm_db_ip" { - value = "${aws_instance.kasm-db.private_ip}" +data "aws_instance" "data-kasm_db" { + instance_id = aws_instance.kasm-db.id } - diff --git a/aws/multi_region/primary/lb_s3_log_bucket.tf b/aws/multi_region/primary/lb_s3_log_bucket.tf new file mode 100644 index 0000000..b43677f --- /dev/null +++ b/aws/multi_region/primary/lb_s3_log_bucket.tf @@ -0,0 +1,56 @@ +data "aws_elb_service_account" "main" {} + +resource "aws_s3_bucket" "kasm_s3_logs" { + bucket_prefix = "${var.project_name}-${var.zone_name}-" + force_destroy = true +} + +data "aws_s3_bucket" "data-kasm_s3_logs_bucket" { + bucket = aws_s3_bucket.kasm_s3_logs.bucket +} + +resource "aws_s3_bucket_acl" "kasm_s3_acl" { + bucket = data.aws_s3_bucket.data-kasm_s3_logs_bucket.id + acl = "private" +} + +resource "aws_s3_bucket_policy" "kasm_s3_logs_policy" { + bucket = data.aws_s3_bucket.data-kasm_s3_logs_bucket.id + + policy = jsonencode({ + Id = "Policy" + Version = "2012-10-17" + Statement = [ + { + Action = [ + "s3:PutObject" + ] + Effect = "Allow" + Resource = "${aws_s3_bucket.kasm_s3_logs.arn}/AWSLogs/*" + Principal = { + AWS = [ + data.aws_elb_service_account.main.arn + ] + } + } + ] + }) +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "encrypt_elb_bucket" { + bucket = data.aws_s3_bucket.data-kasm_s3_logs_bucket.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_public_access_block" "s3_log_public_access" { + bucket = data.aws_s3_bucket.data-kasm_s3_logs_bucket.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} diff --git a/aws/multi_region/primary/main.tf b/aws/multi_region/primary/main.tf deleted file mode 100644 index 0c7c31a..0000000 --- a/aws/multi_region/primary/main.tf +++ /dev/null @@ -1,14 +0,0 @@ -provider "aws" { - region = "${var.aws_region}" - access_key = "${var.aws_access_key}" - secret_key = "${var.aws_secret_key}" -} - -data "aws_availability_zones" "available" { - state = "available" -} - -output "primary_aws_region" { - value = "${var.aws_region}" -} - diff --git a/aws/multi_region/primary/output.tf b/aws/multi_region/primary/output.tf new file mode 100644 index 0000000..f735419 --- /dev/null +++ b/aws/multi_region/primary/output.tf @@ -0,0 +1,35 @@ +output "certificate_arn" { + value = aws_acm_certificate_validation.kasm-elb-certificate-validation.certificate_arn +} + +output "webapp_subnet_ids" { + value = data.aws_subnet.data-kasm_webapp_subnets[*].id +} + +output "agent_subnet_id" { + value = data.aws_subnet.data-kasm_agent_subnet.id +} + +output "kasm_db_ip" { + value = data.aws_instance.data-kasm_db.private_ip +} + +output "primary_vpc_id" { + value = data.aws_vpc.data-kasm-default-vpc.id +} + +output "lb_log_bucket" { + value = data.aws_s3_bucket.data-kasm_s3_logs_bucket.bucket +} + +output "lb_security_group_id" { + value = data.aws_security_group.data-kasm_default_elb_sg.id +} + +output "webapp_security_group_id" { + value = data.aws_security_group.data-kasm_webapp_sg.id +} + +output "agent_security_group_id" { + value = data.aws_security_group.data-kasm_agent_sg.id +} diff --git a/aws/multi_region/primary/provider.tf b/aws/multi_region/primary/provider.tf new file mode 100644 index 0000000..ea82995 --- /dev/null +++ b/aws/multi_region/primary/provider.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + #version = "4.56.0" + } + } +} \ No newline at end of file diff --git a/aws/multi_region/primary/routes.tf b/aws/multi_region/primary/routes.tf new file mode 100644 index 0000000..50c62e8 --- /dev/null +++ b/aws/multi_region/primary/routes.tf @@ -0,0 +1,32 @@ +resource "aws_route_table" "internet_access" { + vpc_id = data.aws_vpc.data-kasm-default-vpc.id + + route { + cidr_block = var.anywhere + gateway_id = data.aws_internet_gateway.data-kasm_default_ig.id + } + + tags = { + Name = "${var.project_name}-kasm-default-route" + } +} + +data "aws_route_table" "data-internet_gateway_route_table" { + route_table_id = aws_route_table.internet_access.id +} + +resource "aws_route_table_association" "webapp_route_association" { + count = var.num_webapps + subnet_id = data.aws_subnet.data-kasm_webapp_subnets[count.index].id + route_table_id = data.aws_route_table.data-internet_gateway_route_table.id +} + +resource "aws_route_table_association" "db_route_association" { + subnet_id = data.aws_subnet.data-kasm_db_subnet.id + route_table_id = data.aws_route_table.data-internet_gateway_route_table.id +} + +resource "aws_route_table_association" "agent_table_association" { + subnet_id = data.aws_subnet.data-kasm_agent_subnet.id + route_table_id = data.aws_route_table.data-internet_gateway_route_table.id +} diff --git a/aws/multi_region/primary/security_group.tf b/aws/multi_region/primary/security_group.tf index 7447042..fe1cc95 100644 --- a/aws/multi_region/primary/security_group.tf +++ b/aws/multi_region/primary/security_group.tf @@ -1,37 +1,145 @@ -resource "aws_security_group" "kasm-default-sg" { - name = "${var.project_name}-kasm-allow-db-access" - description = "Allow access to db" - vpc_id = "${aws_vpc.kasm-default-vpc.id}" +resource "aws_security_group" "kasm-default-elb-sg" { + name = "${var.project_name}-${var.zone_name}-kasm-allow-elb-access" + description = "Security Group for ELB" + vpc_id = data.aws_vpc.data-kasm-default-vpc.id ingress { - from_port = 22 - to_port = 22 + from_port = 443 + to_port = 443 protocol = "tcp" - cidr_blocks = ["${var.ssh_access_cidr}"] + cidr_blocks = var.web_access_cidrs } ingress { - from_port = 5432 - to_port = 5432 + from_port = 80 + to_port = 80 protocol = "tcp" - cidr_blocks = [aws_subnet.kasm-webapp-subnet-1.cidr_block, aws_subnet.kasm-webapp-subnet-2.cidr_block] - } - - ingress { - from_port = 6379 - to_port = 6379 - protocol = "tcp" - cidr_blocks = [aws_subnet.kasm-webapp-subnet-1.cidr_block, aws_subnet.kasm-webapp-subnet-2.cidr_block] + cidr_blocks = var.web_access_cidrs } egress { from_port = 0 to_port = 0 protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] + cidr_blocks = [var.anywhere] + } + + tags = { + Name = "${var.project_name}-${var.zone_name}-kasm-allow-access" + } +} + +data "aws_security_group" "data-kasm_default_elb_sg" { + id = aws_security_group.kasm-default-elb-sg.id +} + +resource "aws_security_group" "kasm-db-sg" { + name = "${var.project_name}-kasm-allow-db-access" + description = "Allow access to db" + vpc_id = data.aws_vpc.data-kasm-default-vpc.id + + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = var.ssh_access_cidrs + } + + ingress { + from_port = 5432 + to_port = 5432 + protocol = "tcp" + cidr_blocks = data.aws_subnet.data-kasm_webapp_subnets[*].id + } + + ingress { + from_port = 6379 + to_port = 6379 + protocol = "tcp" + cidr_blocks = data.aws_subnet.data-kasm_webapp_subnets[*].id + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = [var.anywhere] } tags = { Name = "${var.project_name}-kasm-allow-db-access" } } + +data "aws_security_group" "data-kasm_db_sg" { + id = aws_security_group.kasm-db-sg.id +} + +resource "aws_security_group" "kasm-webapp-sg" { + name = "${var.project_name}-${var.zone_name}-kasm-webapp-access" + description = "Allow access to webapps" + vpc_id = data.aws_vpc.data-kasm-default-vpc.id + + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = var.ssh_access_cidrs + } + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + security_groups = [data.aws_security_group.data-kasm_db_sg.id] + } + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + security_groups = [data.aws_security_group.data-kasm_agent_sg.id] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = [var.anywhere] + } +} + +data "aws_security_group" "data-kasm_webapp_sg" { + id = aws_security_group.kasm-webapp-sg.id +} + +resource "aws_security_group" "kasm-agent-sg" { + name = "${var.project_name}-${var.zone_name}-kasm-agent-access" + description = "Allow access to agents" + vpc_id = data.aws_vpc.data-kasm-default-vpc.id + + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = var.ssh_access_cidrs + } + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = [var.anywhere] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = [var.anywhere] + } +} + +data "aws_security_group" "data-kasm_agent_sg" { + id = aws_security_group.kasm-agent-sg.id +} diff --git a/aws/multi_region/primary/subnet.tf b/aws/multi_region/primary/subnet.tf index 0f45679..a626318 100644 --- a/aws/multi_region/primary/subnet.tf +++ b/aws/multi_region/primary/subnet.tf @@ -1,54 +1,51 @@ -resource "aws_subnet" "kasm-database-subnet" { - vpc_id = "${aws_vpc.kasm-default-vpc.id}" - cidr_block = "10.0.0.0/24" +locals { + kasm_vpc_subnet_cidr_mask = split("/", var.vpc_subnet_cidr)[1] + kasm_server_subnet_cidr_calculation = (8 - (local.kasm_vpc_subnet_cidr_mask - 16)) + kasm_server_subnet_cidr_size = local.kasm_server_subnet_cidr_calculation < 2 ? 2 : local.kasm_server_subnet_cidr_calculation + kasm_agent_subnet_id = (var.num_webapps + 1) +} + +## Will create Agent subnet x.x.0.0/24 (assuming a VPC Subnet CIDR between x.x.0.0/16 and x.x.0.0/22) +resource "aws_subnet" "kasm-db-subnet" { + vpc_id = data.aws_vpc.data-kasm-default-vpc.id + cidr_block = cidrsubnet(var.vpc_subnet_cidr, local.kasm_server_subnet_cidr_size, 0) map_public_ip_on_launch = true tags = { - Name = "${var.project_name}-kasm-subnet" + Name = "${var.project_name}-kasm-db-subnet" } } -resource "aws_subnet" "kasm-webapp-subnet-1" { - vpc_id = "${aws_vpc.kasm-default-vpc.id}" - cidr_block = "10.0.20.0/24" - availability_zone = data.aws_availability_zones.available.names[2] +data "aws_subnet" "data-kasm_db_subnet" { + id = aws_subnet.kasm-db-subnet.id +} + +## Will create WebApp subnets x.x.1.0/24 and x.x.2.0/24 (assuming a VPC Subnet CIDR between x.x.0.0/16 and x.x.0.0/22 and 2 webapps) +resource "aws_subnet" "kasm-webapp-subnets" { + count = var.num_webapps + vpc_id = data.aws_vpc.data-kasm-default-vpc.id + cidr_block = cidrsubnet(var.vpc_subnet_cidr, local.kasm_server_subnet_cidr_size, (count.index + 1)) + availability_zone = data.aws_availability_zones.available.names[count.index] map_public_ip_on_launch = true tags = { - Name = "${var.project_name}-kasm-webapp-subnet-1" + Name = "${var.project_name}-kasm-webapp-subnet" } } - - -resource "aws_subnet" "kasm-webapp-subnet-2" { - vpc_id = "${aws_vpc.kasm-default-vpc.id}" - cidr_block = "10.0.30.0/24" - availability_zone = data.aws_availability_zones.available.names[1] - map_public_ip_on_launch = true - tags = { - Name = "${var.project_name}-kasm-webapp-subnet-2" - } +data "aws_subnet" "data-kasm_webapp_subnets" { + count = var.num_webapps + id = aws_subnet.kasm-webapp-subnets[count.index].id } +## Will create Agent subnet x.x.3.0/24 (assuming a VPC Subnet CIDR between x.x.0.0/16 and x.x.0.0/22) resource "aws_subnet" "kasm-agent-subnet" { - vpc_id = "${aws_vpc.kasm-default-vpc.id}" - cidr_block = "10.0.40.0/24" - availability_zone = data.aws_availability_zones.available.names[1] + vpc_id = data.aws_vpc.data-kasm-default-vpc.id + cidr_block = cidrsubnet(var.vpc_subnet_cidr, local.kasm_server_subnet_cidr_size, local.kasm_agent_subnet_id) map_public_ip_on_launch = true tags = { - Name = "${var.project_name}-kasm-webapp-subnet-2" + Name = "${var.project_name}-agent-natgw-subnet" } } - -output "webapp_subnet_1_id" { - value = "${aws_subnet.kasm-webapp-subnet-1.id}" +data "aws_subnet" "data-kasm_agent_subnet" { + id = aws_subnet.kasm-agent-subnet.id } - - -output "webapp_subnet_2_id" { - value = "${aws_subnet.kasm-webapp-subnet-2.id}" -} - -output "agent_subnet_id" { - value = "${aws_subnet.kasm-agent-subnet.id}" -} \ No newline at end of file diff --git a/aws/multi_region/primary/variables.tf b/aws/multi_region/primary/variables.tf index 2b162c4..dca6b3b 100644 --- a/aws/multi_region/primary/variables.tf +++ b/aws/multi_region/primary/variables.tf @@ -1,82 +1,113 @@ variable "aws_region" { description = "The AWS region for the deployment. (e.g us-east-1)" + type = string } -variable "aws_access_key" { - description = "The AWS access key used for deployment" -} - -variable "aws_secret_key" { - description = "The AWS secret key used for deployment" -} variable "project_name" { description = "The name of the deployment (e.g dev, staging). A short single word" + type = string +} + +variable "vpc_subnet_cidr" { + description = "The subnet CIDR to use for the Primary VPC" + type = string } variable "aws_domain_name" { description = "The Route53 Zone used for the dns entries. This must already exist in the AWS account. (e.g dev.kasm.contoso.com). The deployment will be accessed via this zone name via https" + type = string +} + +variable "db_hdd_size_gb" { + description = "The HDD size in GB to configure for the Kasm Database instances" + type = number +} + +variable "db_instance_type" { + description = "The instance type for the Database" + type = string + default = "t3.small" +} + +variable "swap_size" { + description = "The amount of swap (in MB) to configure inside the compute instances" + type = number } variable "kasm_build" { description = "The URL for the Kasm Workspaces build" + type = string } variable "database_password" { description = "The password for the database. No special characters" + type = string + sensitive = true } variable "redis_password" { - description = "The password for the database. No special characters" + description = "The password for the Redis server. No special characters" + type = string + sensitive = true } variable "user_password" { - description = "The password for the database. No special characters" + description = "The standard (non administrator) user password. No special characters" + type = string + sensitive = true } variable "admin_password" { - description = "The password for the database. No special characters" + description = "The administrative user password. No special characters" + type = string + sensitive = true } variable "manager_token" { - description = "The password for the database. No special characters" + description = "The manager token value for Agents to authenticate to webapps. No special characters" + type = string + sensitive = true +} + +variable "service_registration_token" { + description = "The guac token value for Guac RDP servers to authenticate to webapps. No special characters" + type = string + sensitive = true } variable "zone_name" { - default = "default" - description="A name given to the kasm deployment Zone" + description = "A name given to the kasm deployment Zone" + type = string + default = "default" } variable "aws_key_pair" { description = "The name of an aws keypair to use." -} - -variable "db_instance_type" { - default = "t3.small" - description = "The instance type for the Database" + type = string } variable "ec2_ami" { - description = "The AMI used for the EC2 nodes. Recommended Ubuntu 18.04 LTS." + description = "The AMI used for the EC2 nodes. Recommended Ubuntu 20.04 LTS." + type = string } -variable "ssh_access_cidr" { +variable "ssh_access_cidrs" { description = "CIDR notation of the bastion host allowed to SSH in to the machines" + type = list(string) } -variable "num_agents" { - default = "2" - description = "The number of Agent Role Servers to create in the deployment" +variable "web_access_cidrs" { + description = "List of Networks in CIDR notation for IPs allowed to access the Kasm Web interface" + type = list(string) } variable "num_webapps" { description = "The number of WebApp role servers to create in the deployment" + type = number } -variable "agent_instance_type" { - default = "t3.medium" -} - -variable "webapp_instance_type" { - default = "t3.small" - description = "The instance type for the webapps" +variable "anywhere" { + description = "Anywhere subnet for routing and load ingress from all IPs" + type = string + default = "0.0.0.0/0" } diff --git a/aws/multi_region/primary/vpc.tf b/aws/multi_region/primary/vpc.tf index 2baf711..133c952 100644 --- a/aws/multi_region/primary/vpc.tf +++ b/aws/multi_region/primary/vpc.tf @@ -1,25 +1,23 @@ resource "aws_vpc" "kasm-default-vpc" { - cidr_block = "10.0.0.0/16" + cidr_block = var.vpc_subnet_cidr + enable_dns_hostnames = true + enable_dns_support = true tags = { Name = "${var.project_name}-kasm-db-vpc" } } +data "aws_vpc" "data-kasm-default-vpc" { + id = aws_vpc.kasm-default-vpc.id +} + resource "aws_internet_gateway" "kasm-default-ig" { - vpc_id = "${aws_vpc.kasm-default-vpc.id}" + vpc_id = aws_vpc.kasm-default-vpc.id tags = { Name = "${var.project_name}-kasm-ig" } } -resource "aws_route" "internet_access" { - route_table_id = "${aws_vpc.kasm-default-vpc.main_route_table_id}" - destination_cidr_block = "0.0.0.0/0" - gateway_id = "${aws_internet_gateway.kasm-default-ig.id}" +data "aws_internet_gateway" "data-kasm_default_ig" { + internet_gateway_id = aws_internet_gateway.kasm-default-ig.id } - - -output "primary_vpc_id" { - value = "${aws_vpc.kasm-default-vpc.id}" -} - diff --git a/aws/multi_region/primary/webapps.tf b/aws/multi_region/primary/webapps.tf deleted file mode 100644 index 81e08d2..0000000 --- a/aws/multi_region/primary/webapps.tf +++ /dev/null @@ -1,27 +0,0 @@ -module "primary-webapps" { - source = "../webapps" - primary_aws_region = "${var.aws_region}" - faux_aws_region = "${var.aws_region}" - zone_name = "${var.zone_name}" - num_agents = "${var.num_agents}" - agent_instance_type = "${var.agent_instance_type}" - num_webapps = "${var.num_webapps}" - webapp_instance_type = "${var.webapp_instance_type}" - ec2_ami = "${var.ec2_ami}" - webapp_subnet_id_1 = "${aws_subnet.kasm-webapp-subnet-1.id}" - webapp_subnet_id_2 = "${aws_subnet.kasm-webapp-subnet-2.id}" - agent_subnet_id = "${aws_subnet.kasm-agent-subnet.id}" - aws_access_key = "${var.aws_access_key}" - aws_secret_key = "${var.aws_secret_key}" - aws_domain_name = "${var.aws_domain_name}" - project_name = "${var.project_name}" - kasm_build = "${var.kasm_build}" - database_password = "${var.database_password}" - redis_password = "${var.redis_password}" - manager_token = "${var.manager_token}" - aws_key_pair = "${var.aws_key_pair}" - kasm_db_ip = "${aws_instance.kasm-db.private_ip}" - primary_vpc_id = "${aws_vpc.kasm-default-vpc.id}" - certificate_arn = "${aws_acm_certificate_validation.kasm-elb-certificate-validation.certificate_arn}" - ssh_access_cidr = "${var.ssh_access_cidr}" -} \ No newline at end of file diff --git a/aws/multi_region/provider.tf b/aws/multi_region/provider.tf new file mode 100644 index 0000000..041dd35 --- /dev/null +++ b/aws/multi_region/provider.tf @@ -0,0 +1,49 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + #version = "4.56.0" + } + } +} + +provider "aws" { + access_key = var.aws_access_key + secret_key = var.aws_secret_key + region = var.aws_primary_region + + default_tags { + tags = var.aws_default_tags + } +} + +provider "aws" { + access_key = var.aws_access_key + secret_key = var.aws_secret_key + region = var.secondary_regions_settings.region2.agent_region + alias = "region2" + + default_tags { + tags = var.aws_default_tags + } +} + +############################################################################## +### +### Uncomment the below provider section if you want to deploy a 3rd region. +### +### Copy/paste the provider below to deploy additional regions, then refer +### to the README.md, the deployment.tf file, and the settings.tfvars file for +### code blocks to copy/paste/configure to deploy the new regions. +### +############################################################################## +# provider "aws" { +# access_key = var.aws_access_key +# secret_key = var.aws_secret_key +# region = var.secondary_regions_settings.region3.agent_region +# alias = "region3" + +# default_tags { +# tags = var.aws_default_tags +# } +# } diff --git a/aws/multi_region/settings.tfvars b/aws/multi_region/settings.tfvars new file mode 100644 index 0000000..1dd8776 --- /dev/null +++ b/aws/multi_region/settings.tfvars @@ -0,0 +1,62 @@ +aws_domain_name = "kasm.contoso.com" +project_name = "contoso" +aws_key_pair = "" +aws_primary_region = "us-east-1" +primary_vpc_subnet_cidr = "10.0.0.0/16" + +database_password = "changeme" +redis_password = "changeme" +user_password = "changeme" +admin_password = "changeme" +manager_token = "changeme" + +kasm_build = "https://kasm-static-content.s3.amazonaws.com/kasm_release_1.12.0.d4fd8a.tar.gz" +ssh_access_cidrs = ["0.0.0.0/0"] +web_access_cidrs = ["0.0.0.0/0"] + +swap_size = 2048 +primary_region_ec2_ami_id = "ami-09cd747c78a9add63" +webapp_instance_type = "t3.small" +db_instance_type = "t3.small" +agent_instance_type = "t3.medium" +webapp_hdd_size_gb = 40 +db_hdd_size_gb = 40 +agent_hdd_size_gb = 40 + +## Settings for all additional Agent regions +secondary_regions_settings = { + region2 = { + agent_region = "us-west-1" + agent_ec2_ami_id = "ami-0d221cb540e0015f4" + agent_instance_type = "t3.medium" + agent_hdd_size_gb = 120 + num_agents = 2 + agent_vpc_cidr = "10.1.0.0/16" + } + + ####################################################################### + ### + ### Uncomment and update the settings below for an third region. + ### Copy/paste the settings below (changing the region number) for any + ### additional regions. + ### + ### Make sure to add a provider section for each additional region in + ### the providers.tf file. + ### + ####################################################################### + # region3 = { + # agent_region = "eu-central-1" + # agent_ec2_ami_id = "ami-0e067cc8a2b58de59" + # agent_instance_type = "t3.medium" + # num_agents = 2 + # agent_vpc_cidr = "10.2.0.0/16" + # } +} + +## Default tags for all AWS resources +aws_default_tags = { + Deployed_by = "Terraform" + Deployment_type = "Multi-Region" + Service_name = "Kasm Workspaces" + Kasm_version = "1.12" +} diff --git a/aws/multi_region/userdata/agent_bootstrap.sh b/aws/multi_region/userdata/agent_bootstrap.sh new file mode 100644 index 0000000..ca26186 --- /dev/null +++ b/aws/multi_region/userdata/agent_bootstrap.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -ex +echo "Starting Kasm Workspaces Agent Install" + +/bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=${swap_size} +/sbin/mkswap /var/swap.1 +chmod 600 /var/swap.1 +/sbin/swapon /var/swap.1 + +echo '/var/swap.1 swap swap defaults 0 0' | tee -a /etc/fstab + +cd /tmp + +PRIVATE_IP=(`hostname -I | cut -d' ' -f1 | tr -d '\\n'`) + +wget ${kasm_build_url} -O kasm_workspaces.tar.gz +tar -xf kasm_workspaces.tar.gz + +echo "Waiting for Kasm WebApp availability..." +while ! (curl -k https://${manager_address}/api/__healthcheck 2>/dev/null | grep -q true) +do + echo "Waiting for API server..." + sleep 5 +done +echo "WebApp is alive" + +bash kasm_release/install.sh -S agent -e -p $PRIVATE_IP -m ${manager_address} -M ${manager_token} + +echo "Done" diff --git a/aws/multi_region/userdata/db_bootstrap.sh b/aws/multi_region/userdata/db_bootstrap.sh new file mode 100644 index 0000000..ac7b403 --- /dev/null +++ b/aws/multi_region/userdata/db_bootstrap.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -ex +echo "Starting Kasm Workspaces Install" + +/bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=${swap_size} +/sbin/mkswap /var/swap.1 +chmod 600 /var/swap.1 +/sbin/swapon /var/swap.1 + +echo '/var/swap.1 swap swap defaults 0 0' | tee -a /etc/fstab + +cd /tmp + +PRIVATE_IP=(`hostname -I | cut -d ' ' -f1 | tr -d '\\n'`) + +wget ${kasm_build_url} -O kasm_workspaces.tar.gz +tar -xf kasm_workspaces.tar.gz +bash kasm_release/install.sh -S db -e -Q ${database_password} -R ${redis_password} -U ${user_password} -P ${admin_password} -M ${manager_token} + +echo "Done" diff --git a/aws/multi_region/userdata/webapp_bootstrap.sh b/aws/multi_region/userdata/webapp_bootstrap.sh new file mode 100644 index 0000000..5f781f5 --- /dev/null +++ b/aws/multi_region/userdata/webapp_bootstrap.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -ex +echo "Starting Kasm Workspaces Install" + +/bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=${swap_size} +/sbin/mkswap /var/swap.1 +chmod 600 /var/swap.1 +/sbin/swapon /var/swap.1 + +echo '/var/swap.1 swap swap defaults 0 0' | tee -a /etc/fstab + +cd /tmp + +PRIVATE_IP=(`hostname -I | cut -d ' ' -f1 | tr -d '\\n'`) + +wget ${kasm_build_url} -O kasm_workspaces.tar.gz +tar -xf kasm_workspaces.tar.gz + +echo "Checking for Kasm DB and Redis..." +apt-get update && apt-get install -y netcat +while ! nc -w 1 -z ${db_ip} 5432; do + echo "Database not ready..." + sleep 5 +done +echo "DB is alive" + +while ! nc -w 1 -z ${db_ip} 6379; do + echo "Redis not ready..." + sleep 5 +done +echo "Redis is alive" + + +bash kasm_release/install.sh -S app -e -z ${zone_name} -q "${db_ip}" -Q ${database_password} -R ${redis_password} + +echo "Done" diff --git a/aws/multi_region/variables.tf b/aws/multi_region/variables.tf index faf5d67..0d3c352 100644 --- a/aws/multi_region/variables.tf +++ b/aws/multi_region/variables.tf @@ -1,47 +1,297 @@ -variable "aws_domain_name" { - default = "kasm.contoso.com" -} - -variable "project_name" { - default = "contoso" -} - -variable "aws_key_pair" { - default = "" -} - variable "aws_access_key" { - default = "" + description = "The AWS access key used for deployment" + type = string + sensitive = true + + validation { + condition = can(regex("^([A-Z0-9]{20})", var.aws_access_key)) + error_message = "The aws_access_key variable must be a valid AWS Access Key (e.g. AKIAJSIE27KKMHXI3BJQ)." + } } variable "aws_secret_key" { - default = "" + description = "The AWS secret key used for deployment" + type = string + sensitive = true + + validation { + condition = can(regex("^([a-zA-Z0-9+\\/-]{40})", var.aws_secret_key)) + error_message = "The aws_secret_key variable must be a valid AWS Secret Key value (e.g. wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY)" + } +} + +variable "aws_key_pair" { + description = "The name of an aws keypair to use." + type = string + + validation { + condition = can(regex("^[a-zA-Z0-9-_]{4,15}", var.aws_key_pair)) + error_message = "The aws_key_pair variable contains invalid characters. Allowed values are between 4-15 characters consisting of letters, numbers, and dashes (-)." + } +} + +variable "project_name" { + description = "The name of the deployment (e.g dev, staging). A short single word" + type = string + + validation { + condition = can(regex("^[a-z]{4,15}", var.project_name)) + error_message = "The project_name variable can only be one word between 4 and 15 lower-case letters since it is a seed value in multiple object names." + } +} + +variable "aws_domain_name" { + description = "The Route53 Zone used for the dns entries. This must already exist in the AWS account. (e.g dev.kasm.contoso.com). The deployment will be accessed via this zone name via https" + type = string + + validation { + condition = can(regex("(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]", var.aws_domain_name)) + error_message = "There are invalid characters in the aws_domain_name - it must be a valid domain name." + } +} + +variable "primary_vpc_subnet_cidr" { + description = "The subnet CIDR to use for the VPC" + type = string + default = "10.0.0.0/16" + + validation { + condition = can(cidrhost(var.primary_vpc_subnet_cidr, 0)) + error_message = "The VPC subnet must be valid IPv4 CIDR." + } +} + +variable "num_agents" { + description = "The number of Agent Role Servers to create in the deployment" + type = number + default = 2 + + validation { + condition = var.num_agents >= 0 && var.num_agents <= 100 && floor(var.num_agents) == var.num_agents + error_message = "Acceptable number of Kasm Agents range between 0-100." + } +} + +variable "num_webapps" { + description = "The number of WebApp role servers to create in the deployment" + type = number + default = 2 + + validation { + condition = var.num_webapps >= 1 && var.num_webapps <= 3 && floor(var.num_webapps) == var.num_webapps + error_message = "Acceptable number of webapps range between 1-3." + } +} + +variable "webapp_instance_type" { + description = "The instance type for the webapps" + type = string + default = "t3.small" + + validation { + condition = can(regex("^(([a-z-]{1,3})(\\d{1,2})?(\\w{1,4})?)\\.(nano|micro|small|medium|metal|large|(2|3|4|6|8|9|10|12|16|18|24|32|48|56|112)?xlarge)", var.webapp_instance_type)) + error_message = "Check the webapp_instance_type variable and ensure it is a valid AWS Instance type (https://aws.amazon.com/ec2/instance-types/)." + } +} + +variable "db_instance_type" { + description = "The instance type for the Database" + type = string + default = "t3.small" + + validation { + condition = can(regex("^(([a-z-]{1,3})(\\d{1,2})?(\\w{1,4})?)\\.(nano|micro|small|medium|metal|large|(2|3|4|6|8|9|10|12|16|18|24|32|48|56|112)?xlarge)", var.db_instance_type)) + error_message = "Check the db_instance_type variable and ensure it is a valid AWS Instance type (https://aws.amazon.com/ec2/instance-types/)." + } +} + +variable "agent_instance_type" { + description = "The instance type for the Agents" + type = string + default = "t3.medium" + + validation { + condition = can(regex("^(([a-z-]{1,3})(\\d{1,2})?(\\w{1,4})?)\\.(nano|micro|small|medium|metal|large|(2|3|4|6|8|9|10|12|16|18|24|32|48|56|112)?xlarge)", var.agent_instance_type)) + error_message = "Check the agent_instance_type variable and ensure it is a valid AWS Instance type (https://aws.amazon.com/ec2/instance-types/)." + } +} + +variable "webapp_hdd_size_gb" { + description = "The HDD size in GB to configure for the Kasm WebApp instances" + type = number + + validation { + condition = can(var.webapp_hdd_size_gb >= 40) + error_message = "Kasm Webapps should have at least a 40 GB HDD to ensure enough space for Kasm services." + } +} + +variable "db_hdd_size_gb" { + description = "The HDD size in GB to configure for the Kasm Database instances" + type = number + + validation { + condition = can(var.db_hdd_size_gb >= 40) + error_message = "Kasm Database should have at least a 40 GB HDD to ensure enough space for Kasm services." + } +} + +variable "agent_hdd_size_gb" { + description = "The HDD size in GB to configure for the Kasm Agent instances" + type = number + + validation { + condition = can(var.agent_hdd_size_gb >= 120) + error_message = "Kasm Agents should have at least a 120 GB HDD to ensure enough space for Kasm services." + } +} + +variable "primary_region_ec2_ami_id" { + description = "AMI Id of Kasm EC2 image in the primary region. Recommended AMI OS Version is Ubuntu 20.04 LTS." + type = string + default = "ami-09cd747c78a9add63" + + validation { + condition = can(regex("^(ami-[a-f0-9]{17})", var.primary_region_ec2_ami_id)) + error_message = "Please verify that your AMI is in the correct format for AWS (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html)." + } +} + +variable "secondary_regions_settings" { + description = "Map of Kasm settings for secondary regions" + type = map(any) + + validation { + condition = can([for region in var.secondary_regions_settings : regex("^([a-z]{2}-[a-z]{4,}-[\\d]{1})$", region.agent_region)]) + error_message = "Verify the regions in the secondary_regions_settings variable and ensure they are valid AWS regions in a valid format (e.g. us-east-1)." + } + validation { + condition = can([for ami_id in var.secondary_regions_settings : regex("^(ami-[a-f0-9]{17})", ami_id.agent_ec2_ami_id)]) + error_message = "Please verify that all of your Region's AMI IDs are in the correct format for AWS (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html)." + } + validation { + condition = can([for instance_type in var.secondary_regions_settings : regex("^(([a-z-]{1,3})(\\d{1,2})?(\\w{1,4})?)\\.(nano|micro|small|medium|metal|large|(2|3|4|6|8|9|10|12|16|18|24|32|48|56|112)?xlarge)", instance_type.agent_instance_type)]) + error_message = "Check the Instance types used in your secondary_regions_settings and ensure they are valid AWS Instance types (https://aws.amazon.com/ec2/instance-types/)." + } + validation { + condition = can([for number_of_agents in var.secondary_regions_settings : number_of_agents.num_agents >= 0 && number_of_agents.num_agents <= 100 && floor(number_of_agents.num_agents) == number_of_agents.num_agents]) + error_message = "Check the number of agents in the secondary_regions_settings variable. Acceptable number of Kasm Agents range between 0-100." + } + validation { + condition = can([for subnet in var.secondary_regions_settings : cidrhost(subnet.agent_vpc_cidr, 0)]) + error_message = "Verify the VPC subnet in your secondary_regions_settings. They must all be valid IPv4 CIDRs." + } +} + +variable "aws_primary_region" { + description = "The AWS Region used for deployment" + type = string + default = "us-east-1" + + validation { + condition = can(regex("^([a-z]{2}-[a-z]{4,}-[\\d]{1})$", var.aws_primary_region)) + error_message = "The aws_primary_region must be a valid Amazon Web Services (AWS) Region name, e.g. us-east-1" + } +} + +variable "swap_size" { + description = "The amount of swap (in MB) to configure inside the compute instances" + type = number + + validation { + condition = var.swap_size >= 1024 && var.swap_size <= 8192 && floor(var.swap_size) == var.swap_size + error_message = "Swap size is the amount of disk space to use for Kasm in MB and must be an integer between 1024 and 8192." + } } variable "database_password" { - default = "changeme" + description = "The password for the database. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.database_password)) + error_message = "The Database Password should be a string between 12 and 30 letters and numbers with no special characters." + } } variable "redis_password" { - default = "changeme" + description = "The password for the Redis server. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.redis_password)) + error_message = "The Redis Password should be a string between 12 and 30 letters and numbers with no special characters." + } } variable "user_password" { - default = "changeme" + description = "The standard (non administrator) user password. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.user_password)) + error_message = "The User Password should be a string between 12 and 30 letters and numbers with no special characters." + } } variable "admin_password" { - default = "changeme" + description = "The administrative user password. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.admin_password)) + error_message = "The Admin password should be a string between 12 and 30 letters and numbers with no special characters." + } } variable "manager_token" { - default = "changeme" + description = "The manager token value for Agents to authenticate to webapps. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.manager_token)) + error_message = "The Manager Token should be a string between 12 and 30 letters and numbers with no special characters." + } } +variable "ssh_access_cidrs" { + description = "CIDR notation of the bastion host allowed to SSH in to the machines" + type = list(string) + default = ["0.0.0.0/0"] + + validation { + condition = can([for subnet in var.ssh_access_cidrs : cidrhost(subnet, 0)]) + error_message = "One of the subnets provided in the ssh_access_cidr variable is invalid." + } +} + +variable "web_access_cidrs" { + description = "CIDR notation of the bastion host allowed to SSH in to the machines" + type = list(string) + default = ["0.0.0.0/0"] + + validation { + condition = can([for subnet in var.web_access_cidrs : cidrhost(subnet, 0)]) + error_message = "One of the subnets provided in the web_access_cidrs variable is invalid." + } +} + +## Non-validated variables variable "kasm_build" { - default = "https://kasm-static-content.s3.amazonaws.com/kasm_release_1.9.0.077388.tar.gz" + description = "Download URL for Kasm Workspaces" + type = string } -variable "ssh_access_cidr" { - default = "0.0.0.0/0" +variable "aws_default_tags" { + description = "Default tags to apply to all AWS resources for this deployment" + type = map(any) + default = { + Service_name = "Kasm Workspaces" + Kasm_version = "1.12" + } } diff --git a/aws/multi_region/webapps/agent.tf b/aws/multi_region/webapps/agent.tf index d833ac9..299f515 100644 --- a/aws/multi_region/webapps/agent.tf +++ b/aws/multi_region/webapps/agent.tf @@ -1,29 +1,25 @@ resource "aws_instance" "kasm-agent" { - count = "${var.num_agents}" - ami = "${var.ec2_ami}" - instance_type = "${var.agent_instance_type}" - vpc_security_group_ids = ["${aws_security_group.kasm-agent-sg.id}"] - subnet_id = "${var.webapp_subnet_id_1}" - key_name = "${var.aws_key_pair}" + count = var.num_agents + ami = var.ec2_ami + instance_type = var.agent_instance_type + vpc_security_group_ids = [var.agent_security_group_id] + subnet_id = var.agent_subnet_id + key_name = var.aws_key_pair root_block_device { - volume_size = "50" + volume_size = var.agent_hdd_size_gb } - user_data = <<-EOF - #!/bin/bash - fallocate -l 4g /mnt/kasm.swap - chmod 600 /mnt/kasm.swap - mkswap /mnt/kasm.swap - swapon /mnt/kasm.swap - echo '/mnt/kasm.swap swap swap defaults 0 0' | tee -a /etc/fstab - cd /tmp - wget ${var.kasm_build} - tar xvf kasm_*.tar.gz - PUBLIC_DNS=(`curl -s http://169.254.169.254/latest/meta-data/public-ipv4`) - bash kasm_release/install.sh -S agent -e -p $PUBLIC_DNS -m ${var.zone_name}-lb.${var.aws_domain_name} -M ${var.manager_token} - EOF + user_data = templatefile("${path.module}/../userdata/agent_bootstrap.sh", + { + kasm_build_url = var.kasm_build + swap_size = var.swap_size + manager_address = var.aws_domain_name + manager_token = var.manager_token + } + ) + tags = { Name = "${var.project_name}-${var.zone_name}-kasm-agent" } -} \ No newline at end of file +} diff --git a/aws/multi_region/webapps/availability_zones.tf b/aws/multi_region/webapps/availability_zones.tf new file mode 100644 index 0000000..87d8f48 --- /dev/null +++ b/aws/multi_region/webapps/availability_zones.tf @@ -0,0 +1,3 @@ +data "aws_availability_zones" "available" { + state = "available" +} diff --git a/aws/multi_region/webapps/elb.tf b/aws/multi_region/webapps/elb.tf index 9471d40..994e22a 100644 --- a/aws/multi_region/webapps/elb.tf +++ b/aws/multi_region/webapps/elb.tf @@ -1,150 +1,103 @@ -resource "aws_security_group" "kasm-default-elb-sg" { - name = "${var.project_name}-${var.zone_name}-kasm-allow-elb-access" - description = "Security Group for ELB" - vpc_id = "${var.primary_vpc_id}" - - ingress { - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - ingress { - from_port = 80 - to_port = 80 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { - Name = "${var.project_name}-${var.zone_name}-kasm-allow-access" - } +data "aws_route53_zone" "kasm-route53-zone" { + name = var.aws_domain_name + private_zone = false } -resource "aws_s3_bucket" "kasm-s3-logs" { - bucket_prefix = "${var.project_name}-${var.zone_name}-" - acl = "private" - force_destroy = true - -} - -resource "aws_s3_bucket_policy" "kasm-s3-logs-policy" { - bucket = aws_s3_bucket.kasm-s3-logs.id - - policy = < ***NOTE:*** This document assumes you are using a separate file named `secrets.tfvars` for the AWS credentials generated in the [AWS API Keys](#aws-api-keys) section above. The .gitignore file in this repository will ignore any files named `secrets.tfvars` since they are expected to have sensitive values in them. This will prevent you from accidentally committing them to source control. 3. Verify the configuration - terraform plan + terraform plan -var-file settings.tfvars -var-file secrets.tfvars 4. Deploy - terraform apply - + terraform apply -var-file settings.tfvars -var-file secrets.tfvars 5. Login to the Deployment as an Admin via the domain defined e.g `https://kasm.contoso.com` 6. Navigate to the Agents tab, and enable each Agent after it checks in. (May take a few minutes) +# AWS Terraform Variable definitions + +| Variable | Description | Variable type | Example | +|:--------:|-------------|---------------|---------| +| `aws_access_key` | The AWS access key used for deployment. | String | `"AKIAJSIE27KKMHXI3BJQ"` | +| `aws_secret_key` | The AWS secret key used for deployment. | String | `"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"` | +| `aws_region` | The AWS Region used for deployment. | String | `"us-east-1"` | +| `project_name` | The name of the deployment (e.g dev, staging). A short single word of up to 15 characters. | String | `"kasm"` | +| `aws_domain_name` | The Route53 Zone used for the dns entries. This must already exist in the AWS account. (e.g dev.kasm.contoso.com). The deployment will be accessed via this zone name via https. | String | `"kasm.contoso.com"` | +| `kasm_zone_name` | A name given to the kasm deployment Zone. | String | `"default"` | +| `vpc_subnet_cidr` | The subnet CIDR to use for the VPC | String | `"10.0.0.0/16"` | +| `aws_key_pair` | The name of an aws keypair to use. | String | `"kasm_ssh_key"` | +| `ec2_ami` | The AMI used for the EC2 nodes. Recommended Ubuntu 20.04 LTS. | String | `"ami-09cd747c78a9add63"` | +| `swap_size` | The amount of swap (in MB) to configure inside the Kasm servers. | Number | `2048` | +| `webapp_instance_type` | The instance type for the webapps. | String | `"t3.small"` | +| `db_instance_type` | The instance type for the webapps. | String | `"t3.medium"` | +| `agent_instance_type` | The instance type for the webapps. | String | `"t3.medium"` | +| `guac_instance_type` | The instance type for the webapps. | String | `"t3.medium"` | +| `num_webapps` | The number of WebApp role servers to create in this deployment. Acceptable ranges from 1-3. | Number | `2` | +| `num_agents` | The number of static Kasm Agents to create in this deploymenbt. Acceptable ranges from 0-100. | Number | `2` | +| `num_guac_rdp_nodes` | The number of Guacamole RDP access servers to create in this deployment. Acceptable ranges from 0-100. | Number | `1` | +| `allow_ssh_cidrs` | A list of subnets in CIDR notation allowed to SSH into your kasm servers | List(String) | `["10.0.0.0/16","172.217.22.14/32"]` | +| `web_access_cidrs` | A list of subnets in CIDR notation allowed Web access to your kasm servers | List(String) | `["0.0.0.0/0"]` | +| `database_password` | The Kasm PostgreSQL database password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `redis_password` | The Kasm Redis password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `admin_password` | The Kasm Administrative user login password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `user_password` | A Kasm standard (non-administrator) user password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `manager_token` | The manager token value used by Kasm agents to authenticate to the Kasm WebApps. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `service_registration_token` | The service registration token value used by Guac RDP servers to authenticate to the Kasm Webapps. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `kasm_build` | The download URL for the desired Kasm Workspaces version. | String | `"https://kasm-static-content.s3.amazonaws.com/kasm_release_1.12.0.d4fd8a.tar.gz"` | +| `aws_default_tags` | A Map of all tags you wish to apply to all TF created resources in this deployment. | Map(Any) |
{
  Service_name = "Kasm Workspaces"
  Kasm_version = "1.12"
}
| + + +# Detailed Terraform Deployment Diagram + +![Detailed Diagram][Detailed_Diagram] + +[Detailed_Diagram]: ./diagram/aws_multi_server.png "Detailed Diagram" \ No newline at end of file diff --git a/aws/standard/deployment.tf b/aws/standard/deployment.tf index 3d1000f..b3c978f 100644 --- a/aws/standard/deployment.tf +++ b/aws/standard/deployment.tf @@ -1,30 +1,30 @@ module "standard" { - source = "./module" - aws_access_key = "" - aws_secret_key = "" - aws_key_pair = "" - aws_region = "us-east-1" - aws_domain_name = "kasm.contoso.com" - project_name = "contoso" - num_agents = "2" - num_webapps = "2" + source = "./module" + aws_key_pair = var.aws_key_pair + aws_region = var.aws_region + aws_domain_name = var.aws_domain_name + project_name = var.project_name + num_agents = var.num_agents + num_webapps = var.num_webapps + num_guac_nodes = var.num_guac_nodes + vpc_subnet_cidr = var.vpc_subnet_cidr + ## Kasm Server settings + agent_instance_type = var.agent_instance_type + guac_instance_type = var.guac_instance_type + webapp_instance_type = var.webapp_instance_type + db_instance_type = var.db_instance_type + ec2_ami = var.ec2_ami + swap_size = var.swap_size - - agent_instance_type = "t3.medium" - webapp_instance_type = "t3.small" - db_instance_type = "t3.small" - ec2_ami = "ami-0747bdcabd34c712a" - - - ssh_access_cidr = "0.0.0.0/0" - database_password = "changeme" - redis_password = "changeme" - user_password = "changeme" - admin_password = "changeme" - manager_token = "changeme" - zone_name = "default" - kasm_build = "https://kasm-static-content.s3.amazonaws.com/kasm_release_1.9.0.077388.tar.gz" + ssh_access_cidrs = var.ssh_access_cidrs + web_access_cidrs = var.web_access_cidrs + database_password = var.database_password + redis_password = var.redis_password + user_password = var.user_password + admin_password = var.admin_password + manager_token = var.manager_token + service_registration_token = var.service_registration_token + kasm_zone_name = var.kasm_zone_name + kasm_build = var.kasm_build } - - diff --git a/aws/standard/diagram/aws_multi_server.drawio b/aws/standard/diagram/aws_multi_server.drawio new file mode 100644 index 0000000..1edf218 --- /dev/null +++ b/aws/standard/diagram/aws_multi_server.drawio @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws/standard/diagram/aws_multi_server.png b/aws/standard/diagram/aws_multi_server.png new file mode 100644 index 0000000..6098881 Binary files /dev/null and b/aws/standard/diagram/aws_multi_server.png differ diff --git a/aws/standard/module/agent.tf b/aws/standard/module/agent.tf index b01e296..4914007 100644 --- a/aws/standard/module/agent.tf +++ b/aws/standard/module/agent.tf @@ -1,30 +1,26 @@ resource "aws_instance" "kasm-agent" { - count = "${var.num_agents}" - ami = "${var.ec2_ami}" - instance_type = "${var.agent_instance_type}" - vpc_security_group_ids = ["${aws_security_group.kasm-agent-sg.id}"] - subnet_id = "${aws_subnet.kasm-use-natgw-subnet.id}" - key_name = "${var.aws_key_pair}" + count = var.num_agents + ami = var.ec2_ami + instance_type = var.agent_instance_type + vpc_security_group_ids = [data.aws_security_group.data-kasm_agent_sg.id] + subnet_id = data.aws_subnet.data-kasm_agent_subnet.id + key_name = var.aws_key_pair associate_public_ip_address = false root_block_device { - volume_size = "50" + volume_size = 120 } - user_data = <<-EOF - #!/bin/bash - fallocate -l 5g /mnt/kasm.swap - chmod 600 /mnt/kasm.swap - mkswap /mnt/kasm.swap - swapon /mnt/kasm.swap - echo '/mnt/kasm.swap swap swap defaults 0 0' | tee -a /etc/fstab - cd /tmp - wget ${var.kasm_build} - tar xvf kasm_*.tar.gz - PRIVATE_IP=(`curl -s http://169.254.169.254/latest/meta-data/local-ipv4`) - bash kasm_release/install.sh -S agent -e -p $PRIVATE_IP -m ${var.zone_name}-lb.${var.aws_domain_name} -M ${var.manager_token} - EOF + user_data = templatefile("${path.module}/userdata/agent_bootstrap.sh", + { + kasm_build_url = var.kasm_build + swap_size = var.swap_size + manager_address = local.private_lb_hostname + manager_token = var.manager_token + } + ) + tags = { - Name = "${var.project_name}-${var.zone_name}-kasm-agent" + Name = "${var.project_name}-${var.kasm_zone_name}-kasm-agent" } } diff --git a/aws/standard/module/alb_logs_s3_bucket.tf b/aws/standard/module/alb_logs_s3_bucket.tf new file mode 100644 index 0000000..bbfa0e0 --- /dev/null +++ b/aws/standard/module/alb_logs_s3_bucket.tf @@ -0,0 +1,60 @@ +data "aws_route53_zone" "kasm-route53-zone" { + name = var.aws_domain_name +} + +data "aws_elb_service_account" "main" {} + +resource "aws_s3_bucket" "kasm_s3_logs" { + bucket_prefix = "${var.project_name}-${var.kasm_zone_name}-" + force_destroy = true +} + +resource "aws_s3_bucket_acl" "kasm_s3_acl" { + bucket = aws_s3_bucket.kasm_s3_logs.id + acl = "private" +} + +resource "aws_s3_bucket_policy" "kasm_s3_logs_policy" { + bucket = aws_s3_bucket.kasm_s3_logs.id + + policy = jsonencode({ + Id = "Policy" + Version = "2012-10-17" + Statement = [ + { + Action = [ + "s3:PutObject" + ] + Effect = "Allow" + Resource = "${aws_s3_bucket.kasm_s3_logs.arn}/AWSLogs/*" + Principal = { + AWS = [ + data.aws_elb_service_account.main.arn + ] + } + } + ] + }) +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "encrypt_elb_bucket" { + bucket = aws_s3_bucket.kasm_s3_logs.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +data "aws_s3_bucket" "data-kasm_s3_logs_bucket" { + bucket = aws_s3_bucket.kasm_s3_logs.bucket +} + +resource "aws_s3_bucket_public_access_block" "s3_log_public_access" { + bucket = aws_s3_bucket.kasm_s3_logs.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} diff --git a/aws/standard/module/availability_zones.tf b/aws/standard/module/availability_zones.tf new file mode 100644 index 0000000..87d8f48 --- /dev/null +++ b/aws/standard/module/availability_zones.tf @@ -0,0 +1,3 @@ +data "aws_availability_zones" "available" { + state = "available" +} diff --git a/aws/standard/module/cert.tf b/aws/standard/module/cert.tf index 13dc8e9..2126e66 100644 --- a/aws/standard/module/cert.tf +++ b/aws/standard/module/cert.tf @@ -1,7 +1,7 @@ resource "aws_acm_certificate" "kasm-alb-cert" { - domain_name = "${var.aws_domain_name}" + domain_name = var.aws_domain_name subject_alternative_names = ["*.${var.aws_domain_name}"] - validation_method = "DNS" + validation_method = "DNS" lifecycle { create_before_destroy = true @@ -10,7 +10,7 @@ resource "aws_acm_certificate" "kasm-alb-cert" { resource "aws_route53_record" "kasm-route53-cert-validation-record" { for_each = { - for dvo in aws_acm_certificate.kasm-alb-cert.domain_validation_options: dvo.domain_name => { + for dvo in aws_acm_certificate.kasm-alb-cert.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name record = dvo.resource_record_value type = dvo.resource_record_type @@ -19,14 +19,13 @@ resource "aws_route53_record" "kasm-route53-cert-validation-record" { name = each.value.name type = each.value.type records = [each.value.record] - zone_id = "${data.aws_route53_zone.kasm-route53-zone.id}" + zone_id = data.aws_route53_zone.kasm-route53-zone.id - ttl = 30 + ttl = 30 allow_overwrite = true } - resource "aws_acm_certificate_validation" "kasm-elb-certificate-validation" { certificate_arn = aws_acm_certificate.kasm-alb-cert.arn - validation_record_fqdns = [for record in aws_route53_record.kasm-route53-cert-validation-record: record.fqdn] + validation_record_fqdns = [for record in aws_route53_record.kasm-route53-cert-validation-record : record.fqdn] } diff --git a/aws/standard/module/db.tf b/aws/standard/module/db.tf index 5b77d6c..e802b50 100644 --- a/aws/standard/module/db.tf +++ b/aws/standard/module/db.tf @@ -1,28 +1,32 @@ resource "aws_instance" "kasm-db" { - ami = "${var.ec2_ami}" - instance_type = "${var.db_instance_type}" - vpc_security_group_ids = ["${aws_security_group.kasm-db-sg.id}"] - subnet_id = "${aws_subnet.kasm-db-subnet.id}" - key_name = "${var.aws_key_pair}" + ami = var.ec2_ami + instance_type = var.db_instance_type + vpc_security_group_ids = [data.aws_security_group.data-kasm_db_sg.id] + subnet_id = data.aws_subnet.data-kasm_db_subnet.id + key_name = var.aws_key_pair root_block_device { - volume_size = "40" - volume_type = "gp2" + volume_size = 40 } - user_data = <<-EOF - #!/bin/bash - fallocate -l 4g /mnt/kasm.swap - chmod 600 /mnt/kasm.swap - mkswap /mnt/kasm.swap - swapon /mnt/kasm.swap - echo '/mnt/kasm.swap swap swap defaults 0 0' | tee -a /etc/fstab - cd /tmp - wget ${var.kasm_build} - tar xvf kasm_*.tar.gz - bash kasm_release/install.sh -S db -e -Q ${var.database_password} -R ${var.redis_password} -U ${var.user_password} -P ${var.admin_password} -M ${var.manager_token} - EOF + user_data = templatefile("${path.module}/userdata/db_bootstrap.sh", + { + kasm_build_url = var.kasm_build + user_password = var.user_password + admin_password = var.admin_password + redis_password = var.redis_password + database_password = var.database_password + service_registration_token = var.service_registration_token + manager_token = var.manager_token + swap_size = var.swap_size + } + ) + tags = { Name = "${var.project_name}-kasm-db" } -} \ No newline at end of file +} + +data "aws_instance" "data-kasm_db" { + instance_id = aws_instance.kasm-db.id +} diff --git a/aws/standard/module/elb.tf b/aws/standard/module/elb.tf deleted file mode 100644 index 87953f4..0000000 --- a/aws/standard/module/elb.tf +++ /dev/null @@ -1,130 +0,0 @@ -data "aws_route53_zone" "kasm-route53-zone" { - name = "${var.aws_domain_name}" -} - -resource "aws_s3_bucket" "kasm-s3-logs" { - bucket_prefix = "${var.project_name}-${var.zone_name}-" - acl = "private" - force_destroy = true -} - - -resource "aws_s3_bucket_policy" "kasm-s3-logs-policy" { - bucket = aws_s3_bucket.kasm-s3-logs.id - - policy = </dev/null | grep -q true) +do + echo "Waiting for API server..." + sleep 5 +done +echo "WebApp is alive" + +bash kasm_release/install.sh -S agent -e -p $PRIVATE_IP -m ${manager_address} -M ${manager_token} + +echo "Done" diff --git a/aws/standard/module/userdata/db_bootstrap.sh b/aws/standard/module/userdata/db_bootstrap.sh new file mode 100644 index 0000000..5317314 --- /dev/null +++ b/aws/standard/module/userdata/db_bootstrap.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -ex +echo "Starting Kasm Workspaces Install" + +/bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=${swap_size} +/sbin/mkswap /var/swap.1 +chmod 600 /var/swap.1 +/sbin/swapon /var/swap.1 + +echo '/var/swap.1 swap swap defaults 0 0' | tee -a /etc/fstab + +cd /tmp + +PRIVATE_IP=(`hostname -I | cut -d ' ' -f1 | tr -d '\\n'`) + +wget ${kasm_build_url} -O kasm_workspaces.tar.gz +tar -xf kasm_workspaces.tar.gz +bash kasm_release/install.sh -S db -e -Q ${database_password} -R ${redis_password} -U ${user_password} -P ${admin_password} -M ${manager_token} -k ${service_registration_token} + +echo "Done" diff --git a/aws/standard/module/userdata/guac_bootstrap.sh b/aws/standard/module/userdata/guac_bootstrap.sh new file mode 100644 index 0000000..484d063 --- /dev/null +++ b/aws/standard/module/userdata/guac_bootstrap.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -ex +echo "Starting Kasm Workspaces Agent Install" + +/bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=${swap_size} +/sbin/mkswap /var/swap.1 +chmod 600 /var/swap.1 +/sbin/swapon /var/swap.1 + +echo '/var/swap.1 swap swap defaults 0 0' | tee -a /etc/fstab + +cd /tmp + +PRIVATE_IP=(`hostname -I | cut -d ' ' -f1 | tr -d '\\n'`) + +wget ${kasm_build_url} -O kasm_workspaces.tar.gz +tar -xf kasm_workspaces.tar.gz + +echo "Waiting for Kasm WebApp availability..." +while ! (curl -k https://${manager_address}/api/__healthcheck 2>/dev/null | grep -q true) +do + echo "Waiting for API server..." + sleep 5 +done +echo "WebApp is alive" + +bash kasm_release/install.sh -S guac -e -p $PRIVATE_IP -n ${manager_address} -k ${service_registration_token} + +echo "Done" diff --git a/aws/standard/module/userdata/webapp_bootstrap.sh b/aws/standard/module/userdata/webapp_bootstrap.sh new file mode 100644 index 0000000..5f781f5 --- /dev/null +++ b/aws/standard/module/userdata/webapp_bootstrap.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -ex +echo "Starting Kasm Workspaces Install" + +/bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=${swap_size} +/sbin/mkswap /var/swap.1 +chmod 600 /var/swap.1 +/sbin/swapon /var/swap.1 + +echo '/var/swap.1 swap swap defaults 0 0' | tee -a /etc/fstab + +cd /tmp + +PRIVATE_IP=(`hostname -I | cut -d ' ' -f1 | tr -d '\\n'`) + +wget ${kasm_build_url} -O kasm_workspaces.tar.gz +tar -xf kasm_workspaces.tar.gz + +echo "Checking for Kasm DB and Redis..." +apt-get update && apt-get install -y netcat +while ! nc -w 1 -z ${db_ip} 5432; do + echo "Database not ready..." + sleep 5 +done +echo "DB is alive" + +while ! nc -w 1 -z ${db_ip} 6379; do + echo "Redis not ready..." + sleep 5 +done +echo "Redis is alive" + + +bash kasm_release/install.sh -S app -e -z ${zone_name} -q "${db_ip}" -Q ${database_password} -R ${redis_password} + +echo "Done" diff --git a/aws/standard/module/variables.tf b/aws/standard/module/variables.tf index 2af9eaa..b9e0e7d 100644 --- a/aws/standard/module/variables.tf +++ b/aws/standard/module/variables.tf @@ -1,89 +1,147 @@ variable "project_name" { description = "The name of the deployment (e.g dev, staging). A short single word" + type = string } variable "aws_domain_name" { description = "The Route53 Zone used for the dns entries. This must already exist in the AWS account. (e.g dev.kasm.contoso.com). The deployment will be accessed via this zone name via https" + type = string } variable "aws_key_pair" { description = "The name of an aws keypair to use." + type = string } -variable "aws_access_key" { - description = "The AWS access key used for deployment" +variable "vpc_subnet_cidr" { + description = "The subnet CIDR to use for the VPC" + type = string + default = "10.0.0.0/16" } -variable "aws_secret_key" { - description = "The AWS secret key used for deployment" +variable "num_webapps" { + description = "The number of WebApp role servers to create in the deployment" + type = number + default = 2 } variable "num_agents" { - default = "2" description = "The number of Agent Role Servers to create in the deployment" + type = number + default = 2 } -variable "agent_instance_type" { - default = "t3.medium" - description = "The instance type for the Agents" -} - - -variable "num_webapps" { - default = "2" - description = "The number of WebApp role servers to create in the deployment" +variable "num_guac_nodes" { + description = "The number of Agent Role Servers to create in the deployment" + type = number + default = 2 } variable "webapp_instance_type" { - default = "t3.small" description = "The instance type for the webapps" + type = string + default = "t3.small" } - variable "db_instance_type" { - default = "t3.small" description = "The instance type for the Database" + type = string + default = "t3.small" } -variable "ssh_access_cidr" { - default = "0.0.0.0/0" +variable "agent_instance_type" { + description = "The instance type for the Agents" + type = string + default = "t3.medium" +} + +variable "guac_instance_type" { + description = "The instance type for the Guacamole RDP nodes" + type = string + default = "t3.medium" +} + +variable "ssh_access_cidrs" { description = "CIDR notation of the bastion host allowed to SSH in to the machines" + type = list(string) + default = ["0.0.0.0/0"] +} + +variable "web_access_cidrs" { + description = "CIDR notation of the bastion host allowed to SSH in to the machines" + type = list(string) + default = ["0.0.0.0/0"] } variable "aws_region" { - default = "us-east-1" description = "The AWS region for the deployment. (e.g us-east-1)" + type = string } + variable "ec2_ami" { - default = "ami-0747bdcabd34c712a" - description = "The AMI used for the EC2 nodes. Recommended Ubuntu 18.04 LTS." + description = "The AMI used for the EC2 nodes. Recommended Ubuntu 20.04 LTS." + type = string } variable "kasm_build" { description = "The URL for the Kasm Workspaces build" + type = string } -variable "master_subnet_id" { - default = "0" - description = "The 2nd octect of VPC subnet" +variable "swap_size" { + description = "The amount of swap (in MB) to configure inside the compute instances" + type = number } variable "database_password" { description = "The password for the database. No special characters" + type = string + sensitive = true } + variable "redis_password" { - description = "The password for the database. No special characters" + description = "The password for the Redis server. No special characters" + type = string + sensitive = true } + variable "user_password" { - description = "The password for the database. No special characters" + description = "The standard (non administrator) user password. No special characters" + type = string + sensitive = true } + variable "admin_password" { - description = "The password for the database. No special characters" + description = "The administrative user password. No special characters" + type = string + sensitive = true } + variable "manager_token" { - description = "The password for the database. No special characters" + description = "The manager token value for Agents to authenticate to webapps. No special characters" + type = string + sensitive = true } -variable "zone_name" { - default = "default" - description="A name given to the kasm deployment Zone" + +variable "service_registration_token" { + description = "The service registration token value for Guac RDP servers to authenticate to webapps. No special characters" + type = string + sensitive = true +} + +variable "kasm_zone_name" { + description = "A name given to the kasm deployment Zone" + type = string + default = "default" +} + +variable "anywhere" { + description = "Anywhere route subnet" + type = string + default = "0.0.0.0/0" + + validation { + condition = can(cidrhost(var.anywhere, 0)) + error_message = "Anywhere variable must be valid IPv4 CIDR - usually 0.0.0.0/0 for all default routes and default Security Group access." + } } diff --git a/aws/standard/module/vpc.tf b/aws/standard/module/vpc.tf index 56a8047..27442b5 100644 --- a/aws/standard/module/vpc.tf +++ b/aws/standard/module/vpc.tf @@ -1,22 +1,23 @@ resource "aws_vpc" "kasm-default-vpc" { - cidr_block = "10.${var.master_subnet_id}.0.0/16" + cidr_block = var.vpc_subnet_cidr + enable_dns_hostnames = true + enable_dns_support = true tags = { Name = "${var.project_name}-kasm-db-vpc" } } +data "aws_vpc" "data-kasm-default-vpc" { + id = aws_vpc.kasm-default-vpc.id +} + resource "aws_internet_gateway" "kasm-default-ig" { - vpc_id = "${aws_vpc.kasm-default-vpc.id}" + vpc_id = data.aws_vpc.data-kasm-default-vpc.id tags = { Name = "${var.project_name}-kasm-ig" } } -resource "aws_route" "internet_access" { - route_table_id = "${aws_vpc.kasm-default-vpc.main_route_table_id}" - destination_cidr_block = "0.0.0.0/0" - gateway_id = "${aws_internet_gateway.kasm-default-ig.id}" +data "aws_internet_gateway" "data-kasm-default-ig" { + internet_gateway_id = aws_internet_gateway.kasm-default-ig.id } - - - diff --git a/aws/standard/module/webapp.tf b/aws/standard/module/webapp.tf index 2f2e96b..1361856 100644 --- a/aws/standard/module/webapp.tf +++ b/aws/standard/module/webapp.tf @@ -1,38 +1,33 @@ resource "aws_instance" "kasm-web-app" { - count = "${var.num_webapps}" - ami = "${var.ec2_ami}" - instance_type = "${var.webapp_instance_type}" - vpc_security_group_ids = ["${aws_security_group.kasm-webapp-sg.id}"] - subnet_id = "${aws_subnet.kasm-webapp-subnet.id}" - key_name = "${var.aws_key_pair}" + count = var.num_webapps + ami = var.ec2_ami + instance_type = var.webapp_instance_type + vpc_security_group_ids = [data.aws_security_group.data-kasm_webapp_sg.id] + subnet_id = data.aws_subnet.data-kasm_webapp_subnets[count.index].id + key_name = var.aws_key_pair associate_public_ip_address = true root_block_device { - volume_size = "40" + volume_size = 40 } - user_data = <<-EOF - #!/bin/bash - set -x - fallocate -l 4g /mnt/kasm.swap - chmod 600 /mnt/kasm.swap - mkswap /mnt/kasm.swap - swapon /mnt/kasm.swap - echo '/mnt/kasm.swap swap swap defaults 0 0' | tee -a /etc/fstab - cd /tmp - wget ${var.kasm_build} - tar xvf kasm_*.tar.gz + user_data = templatefile("${path.module}/userdata/webapp_bootstrap.sh", + { + kasm_build_url = var.kasm_build + db_ip = data.aws_instance.data-kasm_db.private_ip + database_password = var.database_password + redis_password = var.redis_password + swap_size = var.swap_size + zone_name = "default" + } + ) - echo "Checking for Kasm DB..." - while ! nc -w 1 -z ${aws_instance.kasm-db.private_ip} 5432; do - echo "Not Ready..." - sleep 5 - done - echo "DB is alive" - - bash kasm_release/install.sh -S app -e -z ${var.zone_name} -q "${aws_instance.kasm-db.private_ip}" -Q ${var.database_password} -R ${var.redis_password} - EOF tags = { - Name = "${var.project_name}-${var.zone_name}-kasm-webapp" + Name = "${var.project_name}-${var.kasm_zone_name}-kasm-webapp" } } + +data "aws_instance" "data-kasm_web_app" { + count = var.num_webapps + instance_id = aws_instance.kasm-web-app[count.index].id +} diff --git a/aws/standard/provider.tf b/aws/standard/provider.tf new file mode 100644 index 0000000..f85cc71 --- /dev/null +++ b/aws/standard/provider.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + #version = "4.56.0" + } + } +} + +provider "aws" { + access_key = var.aws_access_key + secret_key = var.aws_secret_key + region = var.aws_region + + default_tags { + tags = var.aws_default_tags + } +} diff --git a/aws/standard/settings.tfvars b/aws/standard/settings.tfvars new file mode 100644 index 0000000..e46bbcb --- /dev/null +++ b/aws/standard/settings.tfvars @@ -0,0 +1,43 @@ +## AWS Environment settings +aws_key_pair = "troy-common-ssh" +aws_region = "us-east-1" +aws_domain_name = "bryan-test2.sandbox1.kasmweb.net" +vpc_subnet_cidr = "10.0.0.0/16" + +## Kasm deployment settings +kasm_zone_name = "default" +project_name = "contoso" +num_agents = 2 +num_webapps = 2 +num_guac_nodes = 1 + +## Kasm Server settings +agent_instance_type = "t3.medium" +guac_instance_type = "t3.medium" +webapp_instance_type = "t3.small" +db_instance_type = "t3.small" +ec2_ami = "ami-09cd747c78a9add63" +swap_size = 2048 + +## VM Access subnets +ssh_access_cidrs = ["0.0.0.0/0"] +web_access_cidrs = ["0.0.0.0/0"] + +## Kasm passwords +database_password = "changeme" +redis_password = "changeme" +user_password = "changeme" +admin_password = "changeme" +manager_token = "changeme" +service_registration_token = "changeme" + +## Kasm download URL +kasm_build = "https://kasm-static-content.s3.amazonaws.com/kasm_release_1.12.0.d4fd8a.tar.gz" + +## Default tags for all AWS resources +aws_default_tags = { + Deployed_by = "Terraform" + Deployment_type = "Multi-Server" + Service_name = "Kasm Workspaces" + Kasm_version = "1.12" +} diff --git a/aws/standard/variables.tf b/aws/standard/variables.tf new file mode 100644 index 0000000..8abb128 --- /dev/null +++ b/aws/standard/variables.tf @@ -0,0 +1,284 @@ +variable "aws_access_key" { + description = "The AWS access key used for deployment" + type = string + sensitive = true + + validation { + condition = can(regex("^([A-Z0-9]{20})", var.aws_access_key)) + error_message = "The aws_access_key variable must be a valid AWS Access Key (e.g. AKIAJSIE27KKMHXI3BJQ)." + } +} + +variable "aws_secret_key" { + description = "The AWS secret key used for deployment" + type = string + sensitive = true + + validation { + condition = can(regex("^([a-zA-Z0-9+\\/-]{40})", var.aws_secret_key)) + error_message = "The aws_secret_key variable must be a valid AWS Secret Key value (e.g. wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY)" + } +} + +variable "aws_region" { + description = "The AWS Region used for deployment" + type = string + default = "us-east-1" + + validation { + condition = can(regex("^([a-z]{2}-[a-z]{4,}-[\\d]{1})$", var.aws_region)) + error_message = "The aws_region must be a valid Amazon Web Services (AWS) Region name, e.g. us-east-1" + } +} + +variable "project_name" { + description = "The name of the deployment (e.g dev, staging). A short single word" + type = string + + validation { + condition = can(regex("^[a-z]{4,15}", var.project_name)) + error_message = "The project_name variable can only be one word between 4 and 15 lower-case letters since it is a seed value in multiple object names." + } +} + +variable "kasm_zone_name" { + description = "A name given to the kasm deployment Zone" + type = string + default = "default" + + validation { + condition = can(regex("^[a-z0-9A-Z-_]{4,15}", var.kasm_zone_name)) + error_message = "The kasm_zone_name variable can only be one word between 4 and 15 characters consisting of letters, numbers, dash (-), and underscore (_)." + } +} + +variable "aws_domain_name" { + description = "The Route53 Zone used for the dns entries. This must already exist in the AWS account. (e.g dev.kasm.contoso.com). The deployment will be accessed via this zone name via https" + type = string + + validation { + condition = can(regex("(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]", var.aws_domain_name)) + error_message = "There are invalid characters in the aws_domain_name - it must be a valid domain name." + } +} + +variable "aws_key_pair" { + description = "The name of an aws keypair to use." + type = string + + validation { + condition = can(regex("^[a-zA-Z0-9-_]{4,15}", var.aws_key_pair)) + error_message = "The aws_key_pair variable contains invalid characters. Allowed values are between 4-15 characters consisting of letters, numbers, and dashes (-)." + } +} + +variable "vpc_subnet_cidr" { + description = "The subnet CIDR to use for the VPC" + type = string + default = "10.0.0.0/16" + + validation { + condition = can(cidrhost(var.vpc_subnet_cidr, 0)) + error_message = "The VPC subnet must be valid IPv4 CIDR." + } +} + +variable "swap_size" { + description = "The amount of swap (in MB) to configure inside the compute instances" + type = number + + validation { + condition = var.swap_size >= 1024 && var.swap_size <= 8192 && floor(var.swap_size) == var.swap_size + error_message = "Swap size is the amount of disk space to use for Kasm in MB and must be an integer between 1024 and 8192." + } +} + +variable "webapp_instance_type" { + description = "The instance type for the webapps" + type = string + default = "t3.small" + + validation { + condition = can(regex("^(([a-z-]{1,3})(\\d{1,2})?(\\w{1,4})?)\\.(nano|micro|small|medium|metal|large|(2|3|4|6|8|9|10|12|16|18|24|32|48|56|112)?xlarge)", var.webapp_instance_type)) + error_message = "Check the webapp_instance_type variable and ensure it is a valid AWS Instance type (https://aws.amazon.com/ec2/instance-types/)." + } +} + +variable "db_instance_type" { + description = "The instance type for the Database" + type = string + default = "t3.small" + + validation { + condition = can(regex("^(([a-z-]{1,3})(\\d{1,2})?(\\w{1,4})?)\\.(nano|micro|small|medium|metal|large|(2|3|4|6|8|9|10|12|16|18|24|32|48|56|112)?xlarge)", var.db_instance_type)) + error_message = "Check the db_instance_type variable and ensure it is a valid AWS Instance type (https://aws.amazon.com/ec2/instance-types/)." + } +} + +variable "agent_instance_type" { + description = "The instance type for the Agents" + type = string + default = "t3.medium" + + validation { + condition = can(regex("^(([a-z-]{1,3})(\\d{1,2})?(\\w{1,4})?)\\.(nano|micro|small|medium|metal|large|(2|3|4|6|8|9|10|12|16|18|24|32|48|56|112)?xlarge)", var.agent_instance_type)) + error_message = "Check the agent_instance_type variable and ensure it is a valid AWS Instance type (https://aws.amazon.com/ec2/instance-types/)." + } +} + +variable "guac_instance_type" { + description = "The instance type for the Guacamole RDP nodes" + type = string + default = "t3.medium" + + validation { + condition = can(regex("^(([a-z-]{1,3})(\\d{1,2})?(\\w{1,4})?)\\.(nano|micro|small|medium|metal|large|(2|3|4|6|8|9|10|12|16|18|24|32|48|56|112)?xlarge)", var.guac_instance_type)) + error_message = "Check the guac_instance_type variable and ensure it is a valid AWS Instance type (https://aws.amazon.com/ec2/instance-types/)." + } +} + +variable "num_webapps" { + description = "The number of WebApp role servers to create in the deployment" + type = number + default = 2 + + validation { + condition = var.num_webapps >= 1 && var.num_webapps <= 3 && floor(var.num_webapps) == var.num_webapps + error_message = "Acceptable number of webapps range between 1-3." + } +} + +variable "num_agents" { + description = "The number of Agent Role Servers to create in the deployment" + type = number + default = 2 + + validation { + condition = var.num_agents >= 0 && var.num_agents <= 100 && floor(var.num_agents) == var.num_agents + error_message = "Acceptable number of Kasm Agents range between 0-100." + } +} + +variable "num_guac_nodes" { + description = "The number of Agent Role Servers to create in the deployment" + type = number + default = 1 + + validation { + condition = var.num_guac_nodes >= 0 && var.num_guac_nodes <= 100 && floor(var.num_guac_nodes) == var.num_guac_nodes + error_message = "Acceptable number of Kasm Agents range between 0-100." + } +} + +variable "ssh_access_cidrs" { + description = "CIDR notation of the bastion host allowed to SSH in to the machines" + type = list(string) + default = ["0.0.0.0/0"] + + validation { + condition = can([for subnet in var.ssh_access_cidrs : cidrhost(subnet, 0)]) + error_message = "One of the subnets provided in the ssh_access_cidr variable is invalid." + } +} + +variable "web_access_cidrs" { + description = "CIDR notation of the bastion host allowed to SSH in to the machines" + type = list(string) + default = ["0.0.0.0/0"] + + validation { + condition = can([for subnet in var.web_access_cidrs : cidrhost(subnet, 0)]) + error_message = "One of the subnets provided in the load_balancer_public_access variable is invalid." + } +} + +variable "ec2_ami" { + description = "The AMI used for the EC2 nodes. Recommended Ubuntu 20.04 LTS." + type = string + + validation { + condition = can(regex("^(ami-[a-f0-9]{17})", var.ec2_ami)) + error_message = "Please verify that your AMI is in the correct format for AWS (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html)." + } +} + +variable "database_password" { + description = "The password for the database. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.database_password)) + error_message = "The Database Password should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "redis_password" { + description = "The password for the Redis server. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.redis_password)) + error_message = "The Redis Password should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "user_password" { + description = "The standard (non administrator) user password. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.user_password)) + error_message = "The User Password should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "admin_password" { + description = "The administrative user password. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.admin_password)) + error_message = "The Admin password should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "manager_token" { + description = "The manager token value for Agents to authenticate to webapps. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.manager_token)) + error_message = "The Manager Token should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "service_registration_token" { + description = "The service registration token value for Guac RDP servers to authenticate to webapps. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.service_registration_token)) + error_message = "The Service Registration Token should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +## Non-validated variables +variable "kasm_build" { + description = "The URL for the Kasm Workspaces build" + type = string +} + +variable "aws_default_tags" { + description = "Default tags to apply to all AWS resources for this deployment" + type = map(any) + default = { + Service_name = "Kasm Workspaces" + Kasm_version = "1.12" + } +} diff --git a/digitalocean/single_server/README.md b/digitalocean/single_server/README.md index 7e9a478..4004c34 100644 --- a/digitalocean/single_server/README.md +++ b/digitalocean/single_server/README.md @@ -23,18 +23,40 @@ This project will launch a droplet and allow connections using the ssh keys defi terraform init -2. Open `deployment.tf` and update the variables. The variable definitions and descriptions -can be found in `module/variables.tf` - +2. Open `settings.tf` and update the variables. The variable definitions and descriptions can be found in `variables.tf`. + +> ***NOTE:*** This document assumes you are using a separate file named `secrets.tfvars` for the DigitalOcean token credential. The .gitignore file in this repository will ignore any files named `secrets.tfvars` since they are expected to have sensitive values in them. This will prevent you from accidentally committing them to source control. Refer to the [Generating a DigitalOcean Access Token](https://docs.digitalocean.com/reference/api/create-personal-access-token/) document if you need help with this process. 3. Verify the configuration - terraform plan + terraform plan -var-file settings.tfvars -var-file secrets.tfvars 4. Deploy - terraform apply + terraform apply -var-file settings.tfvars -var-file secrets.tfvars -5. Login to the Deployment as an Admin via the domain defined e.g `https://kasm.contoso.com`. Single server installs -download all workspaces images during the install process so it may take ~15 minutes for the server to fully come online. +5. Login to the Deployment as an Admin via the domain defined e.g `https://kasm.contoso.com`. Single server installs download all workspaces images during the install process so it may take ~15 minutes for the server to fully come online. + + + +# OCI Terraform Variable definitions + +| Variable | Description | Variable type | Example | +|:--------:|-------------|---------------|---------| +| `digital_ocean_token` | The DigitalOcean authentication token. | String | `"dop_v1_EXAMPLEb8f85b081895f489921abbf26e64d7f3a0e581f8a1d8d532a5ba553"` | +| `digital_ocean_region` | The DigitalOcean region where you wish to deploy Kasm | String | `"nyc3"` | +| `do_domain_name` | The domain name that users will use to access kasm. | String | `"kasm.contoso.com"` | +| `ssh_key_fingerprints` | A list of DigitalOcean SSH fingerprints to use for SSH access to your Kasm server. | List(String) | `["66:e5:d1:85:cd:ba:ca:6a:d0:76:86:ef:1c:11:63:97"]` | +| `project_name` | The name of the deployment (e.g dev, staging). A short single word of up to 15 characters. | String | `"kasm"` | +| `oci_domain_name` | The public Zone used for the dns entries. This must already exist in the OCI account. (e.g kasm.contoso.com). The deployment will be accessed via this zone name using https. | String | `"kasm.contoso.com"` | +| `vpc_subnet_cidr` | The VPC Subnet CIDR where you wish to deploy Kasm | String | `"10.0.0.0/24"` | +| `digital_ocean_droplet_slug` | The Default Digital Ocean Droplet Slug: https://slugs.do-api.dev/ | String | `"s-2vcpu-4gb-intel"` | +| `digital_ocean_image` | Default Image for Ubuntu 20.04 LTS with Docker | String | `"docker-20-04"` | +| `kasm_build_url` | The download URL for the desired Kasm Workspaces version. | String | `"https://kasm-static-content.s3.amazonaws.com/kasm_release_1.12.0.d4fd8a.tar.gz"` | +| `admin_password` | The Kasm Administrative user login password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `user_password` | A Kasm standard (non-administrator) user password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `allow_ssh_cidrs` | A list of subnets in CIDR notation allowed to SSH into your kasm servers | List(String) | `["10.0.0.0/16","172.217.22.14/32"]` | +| `allow_web_cidrs` | A list of subnets in CIDR notation allowed Web access to your kasm servers | List(String) | `["0.0.0.0/0"]` | +| `swap_size` | The amount of swap (in MB) to configure inside the Kasm servers. | Number | `2048` | +| `instance_shape` | The OCI instance shape to use for Kasm deployment. Kasm recommends using a Flex instance type. | String | `"VM.Standard.E4.Flex"` | diff --git a/digitalocean/single_server/deployment.tf b/digitalocean/single_server/deployment.tf index 371ca49..a981c09 100644 --- a/digitalocean/single_server/deployment.tf +++ b/digitalocean/single_server/deployment.tf @@ -1,19 +1,16 @@ module "kasm" { - source = "./module" - - digital_ocean_token = "" - - do_domain_name = "kasm.contoso.com" - project_name = "contoso" - - digital_ocean_region = "nyc3" - digital_ocean_image = "docker-18-04" - digital_ocean_droplet_slug = "s-2vcpu-4gb-intel" - swap_size = 2048 - - kasm_build_url = "https://kasm-static-content.s3.amazonaws.com/kasm_release_1.9.0.077388.tar.gz" - user_password = "changeme" - admin_password = "changeme" - allow_ssh_cidrs = ["0.0.0.0/0"] - ssh_key_fingerprints = [] + source = "./module" + project_name = var.project_name + do_domain_name = var.do_domain_name + vpc_subnet_cidr = var.vpc_subnet_cidr + digital_ocean_region = var.digital_ocean_region + digital_ocean_image = var.digital_ocean_image + digital_ocean_droplet_slug = var.digital_ocean_droplet_slug + swap_size = var.swap_size + kasm_build_url = var.kasm_build_url + user_password = var.user_password + admin_password = var.admin_password + allow_ssh_cidrs = var.allow_ssh_cidrs + allow_kasm_web_cidrs = var.allow_kasm_web_cidrs + ssh_key_fingerprints = var.ssh_key_fingerprints } diff --git a/digitalocean/single_server/module/dns.tf b/digitalocean/single_server/module/dns.tf index 901061d..ec051c2 100644 --- a/digitalocean/single_server/module/dns.tf +++ b/digitalocean/single_server/module/dns.tf @@ -1,5 +1,9 @@ resource "digitalocean_domain" "default" { - name = "${var.do_domain_name}" + name = var.do_domain_name +} + +data "digitalocean_domain" "data-default" { + name = digitalocean_domain.default.name } resource "digitalocean_record" "static" { @@ -12,32 +16,13 @@ resource "digitalocean_record" "static" { resource "digitalocean_certificate" "cert" { name = "${var.project_name}-cert" type = "lets_encrypt" - domains = ["${digitalocean_domain.default.id}"] + domains = [digitalocean_domain.default.id] lifecycle { create_before_destroy = true } } -resource "digitalocean_loadbalancer" "www-lb" { - name = "${var.project_name}-lb" - region = "${var.digital_ocean_region}" - - forwarding_rule { - entry_port = 443 - entry_protocol = "https" - - target_port = 443 - target_protocol = "https" - - certificate_name = digitalocean_certificate.cert.name - } - - healthcheck { - port = 443 - protocol = "https" - path = "/" - } - - droplet_ids = digitalocean_droplet.kasm-server.*.id +data "digitalocean_certificate" "data-cert" { + name = digitalocean_certificate.cert.name } diff --git a/digitalocean/single_server/module/firewall.tf b/digitalocean/single_server/module/firewall.tf index 3559969..b8853dd 100644 --- a/digitalocean/single_server/module/firewall.tf +++ b/digitalocean/single_server/module/firewall.tf @@ -4,31 +4,37 @@ resource "digitalocean_firewall" "workspaces-fw" { tags = ["${digitalocean_tag.project.id}"] inbound_rule { - protocol = "tcp" - port_range = "22" - source_addresses = "${var.allow_ssh_cidrs}" + protocol = "tcp" + port_range = "22" + source_addresses = var.allow_ssh_cidrs } inbound_rule { - protocol = "tcp" - port_range = "443" - source_addresses = ["0.0.0.0/0", "::/0"] + protocol = "tcp" + port_range = "443" + source_addresses = var.allow_kasm_web_cidrs + } + + inbound_rule { + protocol = "tcp" + port_range = "80" + source_addresses = var.allow_kasm_web_cidrs } outbound_rule { - protocol = "tcp" - port_range = "1-65535" - destination_addresses = ["0.0.0.0/0", "::/0"] + protocol = "tcp" + port_range = "1-65535" + destination_addresses = var.anywhere } outbound_rule { - protocol = "udp" - port_range = "1-65535" - destination_addresses = ["0.0.0.0/0", "::/0"] + protocol = "udp" + port_range = "1-65535" + destination_addresses = var.anywhere } outbound_rule { - protocol = "icmp" - destination_addresses = ["0.0.0.0/0", "::/0"] + protocol = "icmp" + destination_addresses = var.anywhere } } diff --git a/digitalocean/single_server/module/load_balancer.tf b/digitalocean/single_server/module/load_balancer.tf new file mode 100644 index 0000000..a60c29f --- /dev/null +++ b/digitalocean/single_server/module/load_balancer.tf @@ -0,0 +1,24 @@ +resource "digitalocean_loadbalancer" "www-lb" { + name = "${var.project_name}-lb" + region = var.digital_ocean_region + vpc_uuid = data.digitalocean_vpc.data-kasm_vpc.id + redirect_http_to_https = true + + forwarding_rule { + entry_port = 443 + entry_protocol = "https" + + target_port = 443 + target_protocol = "https" + + certificate_name = data.digitalocean_certificate.data-cert.id + } + + healthcheck { + port = 443 + protocol = "https" + path = "/" + } + + droplet_ids = [data.digitalocean_droplet.data-kasm_server.id] +} diff --git a/digitalocean/single_server/module/output.tf b/digitalocean/single_server/module/output.tf new file mode 100644 index 0000000..6e70e92 --- /dev/null +++ b/digitalocean/single_server/module/output.tf @@ -0,0 +1,3 @@ +output "kasm_server_ip" { + value = digitalocean_droplet.kasm-server.ipv4_address +} diff --git a/digitalocean/single_server/module/project.tf b/digitalocean/single_server/module/project.tf index 7b47382..09b152a 100644 --- a/digitalocean/single_server/module/project.tf +++ b/digitalocean/single_server/module/project.tf @@ -1,10 +1,18 @@ resource "digitalocean_project" "project" { - name = "${var.project_name}" + name = var.project_name description = "Deployment for ${var.project_name}" - purpose = "Kasm Workspaces" + purpose = "Kasm Workspaces" environment = "Development" resources = [ - digitalocean_droplet.kasm-server.urn, - digitalocean_domain.default.urn + data.digitalocean_droplet.data-kasm_server.urn, + data.digitalocean_domain.data-default.urn ] } + +resource "digitalocean_tag" "project" { + name = var.project_name +} + +data "digitalocean_tag" "data-project" { + name = digitalocean_tag.project.name +} diff --git a/digitalocean/single_server/module/provider.tf b/digitalocean/single_server/module/provider.tf new file mode 100644 index 0000000..68aba8c --- /dev/null +++ b/digitalocean/single_server/module/provider.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = "~> 2.0" + } + } +} diff --git a/digitalocean/single_server/module/server.tf b/digitalocean/single_server/module/server.tf index 4b98f9f..a4a0652 100644 --- a/digitalocean/single_server/module/server.tf +++ b/digitalocean/single_server/module/server.tf @@ -1,43 +1,23 @@ -data "template_file" "user_data" { - template = "${file("${path.module}/files/kasm_server_init.sh")}" - vars = { - kasm_build_url = "${var.kasm_build_url}" - user_password = "${var.user_password}" - admin_password = "${var.admin_password}" - swap_size = "${var.swap_size}" - } -} - resource "digitalocean_droplet" "kasm-server" { - ssh_keys = "${var.ssh_key_fingerprints}" - image = "${var.digital_ocean_image}" - region = "${var.digital_ocean_region}" - size = "${var.digital_ocean_droplet_slug}" - private_networking = false - backups = false - ipv6 = false - name = "${var.project_name}-workspaces" - tags = ["${digitalocean_tag.project.id}"] - user_data = "${data.template_file.user_data.rendered}" -} - -output "kasm_server_ip" { - value = "${digitalocean_droplet.kasm-server.ipv4_address}" -} - -terraform { - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = "~> 2.0" + ssh_keys = var.ssh_key_fingerprints + image = var.digital_ocean_image + region = var.digital_ocean_region + size = var.digital_ocean_droplet_slug + vpc_uuid = data.digitalocean_vpc.data-kasm_vpc.id + backups = false + ipv6 = false + name = "${var.project_name}-workspaces" + tags = [data.digitalocean_tag.data-project.id] + user_data = templatefile("${path.module}/userdata/kasm_server_init.sh", + { + kasm_build_url = var.kasm_build_url + user_password = var.user_password + admin_password = var.admin_password + swap_size = var.swap_size } - } + ) } -provider "digitalocean" { - token = "${var.digital_ocean_token}" -} - -resource "digitalocean_tag" "project" { - name = "${var.project_name}" +data "digitalocean_droplet" "data-kasm_server" { + id = digitalocean_droplet.kasm-server.id } diff --git a/digitalocean/single_server/module/files/kasm_server_init.sh b/digitalocean/single_server/module/userdata/kasm_server_init.sh similarity index 86% rename from digitalocean/single_server/module/files/kasm_server_init.sh rename to digitalocean/single_server/module/userdata/kasm_server_init.sh index 7d79aea..45483a9 100644 --- a/digitalocean/single_server/module/files/kasm_server_init.sh +++ b/digitalocean/single_server/module/userdata/kasm_server_init.sh @@ -9,7 +9,7 @@ chmod 600 /var/swap.1 cd /tmp -PRIVATE_IP=(`hostname -I | cut -d ' ' -f1 | tr -d '\\n'`) +PRIVATE_IP=(`hostname -I | cut -d' ' -f1 | tr -d '\\n'`) wget ${kasm_build_url} -O kasm_workspaces.tar.gz tar -xf kasm_workspaces.tar.gz diff --git a/digitalocean/single_server/module/variables.tf b/digitalocean/single_server/module/variables.tf index 13fa31b..311fce7 100644 --- a/digitalocean/single_server/module/variables.tf +++ b/digitalocean/single_server/module/variables.tf @@ -1,45 +1,83 @@ variable "project_name" { description = "The name of the project/deployment/company eg (acme). Lower case all one word as this will be used in a domain name" + type = string } -variable "digital_ocean_token" { - description = "Authentication Token For Digital Ocean" -} + variable "digital_ocean_region" { description = "The Default Digital Ocean Region Slug: https://docs.digitalocean.com/products/platform/availability-matrix/" - default = "nyc3" + type = string } + variable "digital_ocean_droplet_slug" { description = "The Default Digital Ocean Droplet Slug: https://slugs.do-api.dev/" - default = "s-2vcpu-4gb-intel" + type = string } + variable "digital_ocean_image" { description = "Default Image for Ubuntu LTS" - default = "docker-18-04" + type = string } + +variable "vpc_subnet_cidr" { + description = "VPC Subnet CIDR to deploy Kasm" + type = string +} + variable "kasm_build_url" { - description = "The Build file to install" - default = "https://kasm-static-content.s3.amazonaws.com/kasm_release_1.9.0.077388.tar.gz" + description = "The Kasm build file to install" + type = string } + variable "user_password" { - default = "changeme" description = "The default password to be used for the default user@kasm.local account. Only use alphanumeric characters" + type = string + sensitive = true } + variable "admin_password" { - default = "changeme" description = "The default password to be used for the default admin@kasm.local account. Only use alphanumeric characters" + type = string + sensitive = true } + variable "allow_ssh_cidrs" { - description = "CIDR notation for hosts allowed to SSH" + description = "List of Subnets in CIDR notation for hosts allowed to SSH" + type = list(string) } + +variable "allow_kasm_web_cidrs" { + description = "CIDR notation of the bastion host allowed to SSH in to the machines" + type = list(string) +} + variable "do_domain_name" { description = "The domain name that users will use to access kasm" + type = string } + variable "ssh_key_fingerprints" { # The ssh key fingerprints from uploaded keys can be obtained at https://cloud.digitalocean.com/account/security description = "Keys used for sshing into kasm hosts" + type = list(string) } -variable swap_size { +variable "swap_size" { description = "The amount of swap (in MB) to configure inside the compute instances" - default = 2048 + type = number + + validation { + condition = var.swap_size >= 1024 && var.swap_size <= 8192 && floor(var.swap_size) == var.swap_size + error_message = "Swap size is the amount of disk space to use for Kasm in MB and must be an integer between 1024 and 8192." + } +} + +variable "anywhere" { + description = "Anywhere route subnet" + type = list(string) + default = ["0.0.0.0/0", "::/0"] + + validation { + condition = can([for subnet in var.anywhere : cidrhost(subnet, 0)]) + error_message = "Anywhere variable must be valid IPv4 CIDR - usually 0.0.0.0/0 for all default routes and default Security Group access." + } } diff --git a/digitalocean/single_server/module/vpc.tf b/digitalocean/single_server/module/vpc.tf new file mode 100644 index 0000000..8114c9b --- /dev/null +++ b/digitalocean/single_server/module/vpc.tf @@ -0,0 +1,10 @@ +resource "digitalocean_vpc" "kasm_vpc" { + name = "${var.project_name}-vpc" + description = "Kasm deployment VPC for ${var.project_name}" + region = var.digital_ocean_region + ip_range = var.vpc_subnet_cidr +} + +data "digitalocean_vpc" "data-kasm_vpc" { + name = digitalocean_vpc.kasm_vpc.name +} diff --git a/digitalocean/single_server/provider.tf b/digitalocean/single_server/provider.tf new file mode 100644 index 0000000..6225189 --- /dev/null +++ b/digitalocean/single_server/provider.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = "~> 2.0" + } + } +} + +provider "digitalocean" { + token = var.digital_ocean_token +} diff --git a/digitalocean/single_server/settings.tfvars b/digitalocean/single_server/settings.tfvars new file mode 100644 index 0000000..2e8a629 --- /dev/null +++ b/digitalocean/single_server/settings.tfvars @@ -0,0 +1,24 @@ +## Kasm deployment settings +project_name = "contoso" +do_domain_name = "kasm.contoso.com" +digital_ocean_region = "nyc3" +vpc_subnet_cidr = "10.0.0.0/24" + +## DO Authentication variables +ssh_key_fingerprints = ["c2:4c:f2:5d:0c:59:5a:3f:93:a2:9d:25:94:24:9a:b1"] + +## VM Settings +digital_ocean_image = "docker-20-04" +digital_ocean_droplet_slug = "s-2vcpu-4gb-intel" +swap_size = 2048 + +## Kasm passwords +user_password = "changeme" +admin_password = "changeme" + +## VM Access subnets +allow_ssh_cidrs = ["0.0.0.0/0"] +allow_kasm_web_cidrs = ["0.0.0.0/0"] + +## Kasm download URL +kasm_build_url = "https://kasm-static-content.s3.amazonaws.com/kasm_release_1.12.0.d4fd8a.tar.gz" \ No newline at end of file diff --git a/digitalocean/single_server/variables.tf b/digitalocean/single_server/variables.tf new file mode 100644 index 0000000..b8c95ad --- /dev/null +++ b/digitalocean/single_server/variables.tf @@ -0,0 +1,135 @@ +variable "digital_ocean_token" { + description = "Authentication Token For Digital Ocean" + type = string + sensitive = true + + validation { + condition = can(regex("^(dop_v1_[a-f0-9]{64})", var.digital_ocean_token)) + error_message = "The digital_ocean_token must be a valid API Token (https://docs.digitalocean.com/reference/api/create-personal-access-token/)." + } +} + +variable "digital_ocean_region" { + description = "The Digital Ocean region where you wish to deploy Kasm" + type = string + default = "nyc3" + + validation { + condition = can(regex("^[a-zA-Z]{3}\\d", var.digital_ocean_region)) + error_message = "The DigitalOcean region format is always 3 letters and a number (e.g. nyc3) - check out https://docs.digitalocean.com/products/platform/availability-matrix/ for available regions." + } +} + +variable "do_domain_name" { + description = "The domain name that users will use to access Kasm" + type = string + + validation { + condition = can(regex("^[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,6}", var.do_domain_name)) + error_message = "There are invalid characters in the do_domain_name - it must be a valid domain name." + } +} + +variable "ssh_key_fingerprints" { + # The ssh key fingerprints from uploaded keys can be obtained at https://cloud.digitalocean.com/account/security + description = "Keys used for sshing into kasm hosts" + type = list(string) + + validation { + condition = can([for fingerprint in var.ssh_key_fingerprints : regex("^([a-f0-9]{2}:?){16}$", fingerprint)]) + error_message = "One of the SSH Key fingerprints is incorrectly formatted. It should be 16 colon-delimited hex bytes (e.g. 12:34:56:78:90:ab:cd:ef:12:34:56:78:90:ab:cd:ef)." + } +} + +variable "project_name" { + description = "The name of the project/deployment/company eg (acme)." + type = string + + validation { + condition = can(regex("^[a-z]{1,15}", var.project_name)) + error_message = "The project_name variable can only be one word between 1 and 15 lower-case letters since it is a seed value in multiple object names." + } +} + +variable "vpc_subnet_cidr" { + description = "VPC Subnet CIDR where you wish to deploy Kasm" + type = string + default = "10.0.0.0/24" + + validation { + condition = can(cidrhost(var.vpc_subnet_cidr, 0)) + error_message = "The vpc_subnet_cidr must be a valid IPv4 Subnet in CIDR notation (e.g. 10.0.0.0/24)" + } +} + +variable "digital_ocean_droplet_slug" { + description = "The Default Digital Ocean Droplet Slug: https://slugs.do-api.dev/" + type = string + default = "s-2vcpu-4gb-intel" +} + +variable "digital_ocean_image" { + description = "Default Image for Ubuntu 20.04 LTS with Docker" + type = string + default = "docker-20-04" +} + +variable "kasm_build_url" { + description = "The Kasm build file to install" + type = string + default = "https://kasm-static-content.s3.amazonaws.com/kasm_release_1.12.0.d4fd8a.tar.gz" +} + +variable "user_password" { + description = "The default password to be used for the default user@kasm.local account. Only use alphanumeric characters" + type = string + default = "changeme" + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.user_password)) + error_message = "The User Password should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "admin_password" { + description = "The default password to be used for the default admin@kasm.local account. Only use alphanumeric characters" + default = "changeme" + type = string + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.admin_password)) + error_message = "The Admin Password should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "allow_ssh_cidrs" { + description = "CIDR notation of the bastion host allowed to SSH in to the machines" + type = list(string) + default = ["0.0.0.0/0"] + + validation { + condition = can([for subnet in var.allow_ssh_cidrs : cidrhost(subnet, 0)]) + error_message = "One of the subnets provided in the allow_ssh_cidrs list is invalid." + } +} + +variable "allow_kasm_web_cidrs" { + description = "CIDR notation of the bastion host allowed to SSH in to the machines" + type = list(string) + default = ["0.0.0.0/0"] + + validation { + condition = can([for subnet in var.allow_kasm_web_cidrs : cidrhost(subnet, 0)]) + error_message = "One of the subnets provided in the allow_ssh_cidrs list is invalid." + } +} + +variable "swap_size" { + description = "The amount of swap (in MB) to configure inside the compute instances" + default = 2048 + + validation { + condition = var.swap_size >= 1024 && var.swap_size <= 8192 && floor(var.swap_size) == var.swap_size + error_message = "Swap size is the amount of disk space to use for Kasm in MB and must be an integer between 1024 and 8192." + } +} diff --git a/oci/single_server/README.md b/oci/single_server/README.md index db060db..f14cbb5 100644 --- a/oci/single_server/README.md +++ b/oci/single_server/README.md @@ -8,7 +8,6 @@ This project will deploy Kasm Workspaces in a single-server deployment in OCI. - # Pre-Configuration Consider creating a new Compartment for the Kasm Workspaces deployment. @@ -16,40 +15,70 @@ Consider creating a new Compartment for the Kasm Workspaces deployment. In OCI create a public DNS zone that matches the desired domain name for the deployment. e.g `kasm.contoso.com`. ### API Keys -Create an administative user in the OCI console that will be used for the terraform deployment. Add the user to the -**Administrators** Group. Generate an API Key for the user. The API Key Fingerprint will be used as a variable -in the deployment configuration. Save the private key to the local directory replacing `oci-private-key.pem`. +Create an administative user in the OCI console that will be used for the terraform deployment. Add the user to the **Administrators** Group. Generate an API Key for the user. The API Key Fingerprint will be used as a variable in the deployment configuration. Save the private key to the local directory replacing `oci-private-key.pem`. -### SSH Authorized Keys -The project will install an SSH key(s) inside the OCI compute instance(s). Update `authorized_keys` in this directory -with the desired SSH public keys. - -### SSL Certificate -Create an SSL certificate that matches the desired domain for the deployment. e.g (kasm.contoso.com). Place the pem encoded -cert and key in this directory overwriting `kasm_ssl.crt` and `kasm_ssl.key`. +### SSL Certificate Options +#### Terraform-generated Let's Encrypt Certificate +To use Terraform to generate a Let's Encrypt certificate automatically, set the `letsencrypt_cert_support_email` to a valid email address and set the `letsencrypt_server_type` to either "staging" or "prod" and leave the `kasm_ssl_crt_path` and `kasm_ssl_key_path` variables empty. +***NOTE:*** +- Staging generates certificates that a browser will not trust, but are formatted correctly and are designed for testing and validating the system configuraiton and deployment and has a limit of hundreds of certificates per domain per week. +- Prod generates valid Let's Encrypt certificates but is limited to 5 certificates per week per domain. +#### Bring Your Own Certificates +Create an SSL certificate that matches the desired domain for the deployment. e.g (kasm.contoso.com). Place the pem encoded cert and key in this directory overwriting `kasm_ssl.crt` and `kasm_ssl.key`. # Terraform Configuration - 1. Initialize the project terraform init -2. Open `deployment.tf` and update the variables. The variable definitions and descriptions -can be found in `module/variables.tf` +2. Open `settings.tfvars` and update the variables. The variable definitions, descriptions, and validation requirements can be found in `variables.tf`, or in the [table](#oci-terraform-variable-definitions) below. - 3. Verify the configuration - terraform plan + terraform plan -var-file settings.tfvars 4. Deploy - terraform apply + terraform apply -var-file settings.tfvars + +5. Login to the Deployment as an Admin via the domain defined e.g `https://kasm.contoso.com`. Single server installs download all workspaces images during the install process so it may take ~15 minutes for the server to fully come online. -5. Login to the Deployment as an Admin via the domain defined e.g `https://kasm.contoso.com` . Single server installs -download all workspaces images during the install process so it may take ~15 minutes for the server to fully come online. +# OCI Terraform Variable definitions + +| Variable | Description | Variable type | Example | +|:--------:|-------------|---------------|---------| +| `tenancy_ocid` | The OCI Tenancy OCID | String | `"ocid1.tenancy.oc1..aaaaaaaaai06vvcguozt39d4ilmwtpdovl998wsxpyn0hjkab2kuh7z16po7"` | +| `compartment_ocid` | The OCI Compartment OCID | String | `"ocid1.compartment.oc1..aaaaaaaauepg1z967huiazuiwjt80rtbszp64x9oxaidkoi7wz0pgr950bzb"` | +| `region` | The OCI Region name | String | `"us-ashburn-1"` | +| `user_ocid` | The OCI User OCID | String | `"ocid1.user.oc1..aaaaaaaau3me8nojmdjrbj2vzfxeouscc1i7cf9w0aoy0iyv9b38t2y0a1ba"` | +| `fingerprint` | The OCI User API Key fingerprint | String | `"66:e5:d1:85:cd:ba:ca:6a:d0:76:86:ef:1c:11:63:97"` | +| `private_key_path` | The path for the API Key PEM encoded Private Key for the OCI User. ***NOTE:*** *Ensure the API Key contents are a valid PEM encoded RSA key file. You can tell this by ensuring that the value `-----BEGIN RSA PRIVATE KEY-----` is the first line in the key file. Otherwise, you can validate the key file by running the `openssl rsa -in oci-private-key.pem -check` command.* | String | `"./oci-private-key.pem"` | +| `project_name` | The name of the deployment (e.g dev, staging). A short single word of up to 15 characters. | String | `"kasm"` | +| `oci_domain_name` | The public Zone used for the dns entries. This must already exist in the OCI account. (e.g kasm.contoso.com). The deployment will be accessed via this zone name using https. | String | `"kasm.contoso.com"` | +| `letsencrypt_cert_support_email` | Email address to use for Terraform-generated Let's Encrypt SSL certificates | String | `"support@contoso.com"` | +| `letsencrypt_server_type` | SSL Server type for certificate generation. Valid options are staging, prod, and empty string (""). Refer to [SSL Certificate Options](#ssl-certificate-options) section of this document for more information. | String | "prod" | +| `kasm_ssl_crt_path` | Bring Your own Certificate - The file path fo the PEM encoded SSL Certificate file generated outside of Terraform. Copy/paste the contents of your generated SSL Certificate to the file designated in this path variable. | String | `"./kasm_ssl.crt"` | +| `kasm_ssl_key_path` | Bring Your own Certificate - The file path to the PEM encoded SSL Private Key file generated outside of Terraform. Copy/paste the contents of your generated SSL Private Key to the file designated in this path variable. | String | `"./kasm_ssl.key"` | +| `vcn_subnet_cidr` | The OCI VCN Subnet CIDR of the VCN where you wish to deploy Kasm | String | `"10.0.0.0/16"` | +| `ssh_authorized_keys` | The SSH Public key to be installed on the Kasm servers for SSH access | String | `"ssh-rsa some_base64_encoded_ssh_public_key_data"` | +| `instance_image_ocid` | The OCI Image OCID value of the OS to use. Kasm recommends using lates Ubuntu 20.04 LTS-Minimal for speed and efficiency. | String | `"ocid1.image.oc1.iad.aaaaaaaahiz6xym3a76xhwkmwmhrz6luyiehho7dpxpkphxhsq5q6z4m3nlq"` | +| `allow_ssh_cidrs` | A list of subnets in CIDR notation allowed to SSH into your kasm servers | List(String) | `["10.0.0.0/16","172.217.22.14/32"]` | +| `allow_web_cidrs` | A list of subnets in CIDR notation allowed Web access to your kasm servers | List(String) | `["0.0.0.0/0"]` | +| `admin_password` | The Kasm Administrative user login password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `user_password` | A Kasm standard (non-administrator) user password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `kasm_build_url` | The download URL for the desired Kasm Workspaces version. | String | `"https://kasm-static-content.s3.amazonaws.com/kasm_release_1.12.0.d4fd8a.tar.gz"` | +| `swap_size` | The amount of swap (in MB) to configure inside the Kasm servers. | Number | `2048` | +| `instance_shape` | The OCI instance shape to use for Kasm deployment. Kasm recommends using a Flex instance type. | String | `"VM.Standard.E4.Flex"` | +| `kasm_server_cpus` | The number of CPUs, memory in GB, and HDD size to use for Kasm WebApps. | Number | `4` | +| `kasm_server_memory` | The number of CPUs, memory in GB, and HDD size to use for the Kasm Database server. | Number | `8` | +| `kasm_server_hdd_size` | The number of CPUs, memory in GB, and HDD size to use for the Kasm Agent server(s). | Number | `120` | +# Detailed Terraform Deployment Diagram + +![Detailed Diagram][Detailed_Diagram] + +[Detailed_Diagram]: ./diagram/oci_single_server.png "Detailed Diagram" diff --git a/oci/single_server/authorized_keys b/oci/single_server/authorized_keys deleted file mode 100644 index 5d6af95..0000000 --- a/oci/single_server/authorized_keys +++ /dev/null @@ -1 +0,0 @@ -replaceme \ No newline at end of file diff --git a/oci/single_server/deployment.tf b/oci/single_server/deployment.tf index c3440a4..b4bf79e 100644 --- a/oci/single_server/deployment.tf +++ b/oci/single_server/deployment.tf @@ -1,30 +1,36 @@ module "kasm" { - source = "./module" + source = "./module" + oci_domain_name = var.oci_domain_name + project_name = var.project_name + kasm_build_url = var.kasm_build_url + vcn_subnet_cidr = var.vcn_subnet_cidr - oci_domain_name = "kasm.contoso.com" - project_name = "contoso" + ## OCI Auth information + tenancy_ocid = var.tenancy_ocid + compartment_ocid = var.compartment_ocid + user_ocid = var.user_ocid + fingerprint = var.fingerprint + private_key_path = var.private_key_path + region = var.region - tenancy_ocid = "" - user_ocid = "" - compartment_ocid = "" + ## SSL Certificate values + # Let TF generate Let's Encrypt SSL Certificates automatically + letsencrypt_cert_support_email = var.letsencrypt_cert_support_email + letsencrypt_server_type = var.letsencrypt_server_type + # Bring your own SSL Certificates + kasm_ssl_crt_path = var.kasm_ssl_crt_path + kasm_ssl_key_path = var.kasm_ssl_key_path - fingerprint = "" - private_key_path = "./oci-private-key.pem" + instance_image_ocid = var.instance_image_ocid + instance_shape = var.instance_shape + swap_size = var.swap_size + kasm_server_cpus = var.kasm_server_cpus + kasm_server_memory = var.kasm_server_memory + kasm_server_hdd_size = var.kasm_server_hdd_size + allow_ssh_cidrs = var.allow_ssh_cidrs + allow_web_cidrs = var.allow_web_cidrs + ssh_authorized_keys = var.ssh_authorized_keys - ssh_authorized_keys = "./authorized_keys" - kasm_ssl_crt_path = "./kasm_ssl.crt" - kasm_ssl_key_path = "./kasm_ssl.key" - - region = "us-ashburn-1" - instance_image_ocid = "ocid1.image.oc1.iad.aaaaaaaafg6lg7dejwjebjqontwzyvutgf6qs5awyze6fgoiqepyj5qkvcuq" - instance_shape = "VM.Standard.E4.Flex" - instance_ocpus = 2 - shape_memory_in_gb = 4 - instance_boot_size_gb = 60 - - kasm_build_url = "https://kasm-static-content.s3.amazonaws.com/kasm_release_1.9.0.077388.tar.gz" - user_password = "changeme" - admin_password = "changeme" - allow_ssh_cidr = "0.0.0.0/0" - allow_web_cidr = "0.0.0.0/0" -} \ No newline at end of file + admin_password = var.admin_password + user_password = var.user_password +} diff --git a/oci/single_server/diagram/oci_single_server.drawio b/oci/single_server/diagram/oci_single_server.drawio new file mode 100644 index 0000000..49eb0a3 --- /dev/null +++ b/oci/single_server/diagram/oci_single_server.drawio @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/oci/single_server/diagram/oci_single_server.png b/oci/single_server/diagram/oci_single_server.png new file mode 100644 index 0000000..d8fb340 Binary files /dev/null and b/oci/single_server/diagram/oci_single_server.png differ diff --git a/oci/single_server/module/dns.tf b/oci/single_server/module/dns.tf index f49066f..af075bf 100644 --- a/oci/single_server/module/dns.tf +++ b/oci/single_server/module/dns.tf @@ -1,12 +1,17 @@ +data "oci_dns_zones" "kasm_dns_zone" { + compartment_id = var.compartment_ocid + name = var.oci_domain_name +} + resource "oci_dns_rrset" "kasm_a_record" { + domain = var.oci_domain_name + rtype = "A" + zone_name_or_id = var.oci_domain_name + compartment_id = var.compartment_ocid + items { domain = var.oci_domain_name - rtype = "A" - zone_name_or_id = var.oci_domain_name - compartment_id = var.compartment_ocid - items { - domain = var.oci_domain_name - rdata = oci_core_instance.kasm_instance.public_ip - rtype = "A" - ttl = 300 - } -} \ No newline at end of file + rdata = oci_core_instance.kasm_instance.public_ip + rtype = "A" + ttl = 300 + } +} diff --git a/oci/single_server/module/instance.tf b/oci/single_server/module/instance.tf index 23c5d08..578feed 100644 --- a/oci/single_server/module/instance.tf +++ b/oci/single_server/module/instance.tf @@ -1,15 +1,3 @@ -data "template_file" "user_data" { - template = "${file("${path.module}/userdata/bootstrap.sh")}" - vars = { - kasm_build_url = "${var.kasm_build_url}" - user_password = "${var.user_password}" - admin_password = "${var.admin_password}" - swap_size = "${var.swap_size}" - nginx_cert_in = file("${var.kasm_ssl_crt_path}") - nginx_key_in = file("${var.kasm_ssl_key_path}") - } -} - resource "oci_core_instance" "kasm_instance" { availability_domain = data.oci_identity_availability_domain.ad.name compartment_id = var.compartment_ocid @@ -17,8 +5,8 @@ resource "oci_core_instance" "kasm_instance" { shape = var.instance_shape shape_config { - ocpus = var.instance_ocpus - memory_in_gbs = var.shape_memory_in_gb + ocpus = var.kasm_server_cpus + memory_in_gbs = var.kasm_server_memory } create_vnic_details { @@ -30,17 +18,24 @@ resource "oci_core_instance" "kasm_instance" { } source_details { - source_type = "image" - source_id = var.instance_image_ocid - boot_volume_size_in_gbs = var.instance_boot_size_gb + source_type = "image" + source_id = var.instance_image_ocid + boot_volume_size_in_gbs = var.kasm_server_hdd_size } - metadata = { - ssh_authorized_keys = file("${var.ssh_authorized_keys}") - user_data = base64encode("${data.template_file.user_data.rendered}") + ssh_authorized_keys = var.ssh_authorized_keys + user_data = base64encode(templatefile("${path.module}/userdata/bootstrap.sh", + { + kasm_build_url = var.kasm_build_url + user_password = var.user_password + admin_password = var.admin_password + swap_size = var.swap_size + nginx_cert_in = var.letsencrypt_server_type == "" ? file(var.kasm_ssl_crt_path) : acme_certificate.certificate.certificate_pem + nginx_key_in = var.letsencrypt_server_type == "" ? file(var.kasm_ssl_key_path) : tls_private_key.certificate_private_key.private_key_pem + } + )) } - } diff --git a/oci/single_server/module/letsencrypt.tf b/oci/single_server/module/letsencrypt.tf new file mode 100644 index 0000000..b9bf330 --- /dev/null +++ b/oci/single_server/module/letsencrypt.tf @@ -0,0 +1,48 @@ +resource "tls_private_key" "registration_private_key" { + algorithm = "RSA" +} + +resource "tls_private_key" "certificate_private_key" { + algorithm = "RSA" +} + +resource "acme_registration" "registration" { + account_key_pem = tls_private_key.registration_private_key.private_key_pem + email_address = var.letsencrypt_cert_support_email +} + +resource "tls_cert_request" "kasm_certificate_request" { + private_key_pem = tls_private_key.certificate_private_key.private_key_pem + dns_names = [data.oci_dns_zones.kasm_dns_zone.zones[0].name, "*.${data.oci_dns_zones.kasm_dns_zone.zones[0].name}"] + + subject { + common_name = data.oci_dns_zones.kasm_dns_zone.zones[0].name + } +} + +resource "acme_certificate" "certificate" { + account_key_pem = acme_registration.registration.account_key_pem + certificate_request_pem = tls_cert_request.kasm_certificate_request.cert_request_pem + recursive_nameservers = [ + "8.8.8.8:53", + "4.4.2.2:53" + ] + + dns_challenge { + provider = "oraclecloud" + + config = { + OCI_COMPARTMENT_OCID = var.compartment_ocid + OCI_PRIVKEY_FILE = var.private_key_path + OCI_TENANCY_OCID = var.tenancy_ocid + OCI_REGION = var.region + OCI_PUBKEY_FINGERPRINT = var.fingerprint + OCI_USER_OCID = var.user_ocid + OCI_PROPOGATION_TIMEOUT = 600 + OCI_POLLING_INTERVAL = 60 + OCI_TTL = 300 + } + } + + depends_on = [acme_registration.registration] +} diff --git a/oci/single_server/module/main.tf b/oci/single_server/module/main.tf deleted file mode 100644 index 586a8cb..0000000 --- a/oci/single_server/module/main.tf +++ /dev/null @@ -1,8 +0,0 @@ -provider "oci" { - tenancy_ocid = var.tenancy_ocid - user_ocid = var.user_ocid - fingerprint = var.fingerprint - private_key_path = var.private_key_path - region = var.region -} - diff --git a/oci/single_server/module/provider.tf b/oci/single_server/module/provider.tf new file mode 100644 index 0000000..7539cab --- /dev/null +++ b/oci/single_server/module/provider.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + oci = { + source = "oracle/oci" + version = ">= 4.0.0" + } + acme = { + source = "vancluever/acme" + version = ">= 2.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0.0" + } + } +} diff --git a/oci/single_server/module/security_list.tf b/oci/single_server/module/security_list.tf index 3fae3cb..827d823 100644 --- a/oci/single_server/module/security_list.tf +++ b/oci/single_server/module/security_list.tf @@ -1,42 +1,47 @@ - resource "oci_core_security_list" "allow_web" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.kasm_vcn.id - display_name = "allow_web" + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.kasm_vcn.id + display_name = "allow_web" - egress_security_rules { - destination = "0.0.0.0/0" - protocol = "all" - stateless = "false" - } + egress_security_rules { + destination = var.anywhere + protocol = "all" + stateless = "false" + } - ingress_security_rules { - protocol = "6" - source = "${var.allow_web_cidr}" - tcp_options { - max = "443" - min = "443" - } + dynamic "ingress_security_rules" { + for_each = var.allow_web_cidrs + content { + protocol = "6" + source = ingress_security_rules.value + tcp_options { + max = "443" + min = "443" + } } + } } resource "oci_core_security_list" "allow_ssh" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.kasm_vcn.id - display_name = "allow_ssh" + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.kasm_vcn.id + display_name = "allow_ssh" - egress_security_rules { - destination = "0.0.0.0/0" - protocol = "all" - stateless = "false" - } + egress_security_rules { + destination = var.anywhere + protocol = "all" + stateless = "false" + } - ingress_security_rules { - protocol = "6" - source = "${var.allow_ssh_cidr}" - tcp_options { - max = "22" - min = "22" - } + dynamic "ingress_security_rules" { + for_each = var.allow_ssh_cidrs + content { + protocol = "6" + source = ingress_security_rules.value + tcp_options { + max = "22" + min = "22" + } } + } } diff --git a/oci/single_server/module/userdata/bootstrap.sh b/oci/single_server/module/userdata/bootstrap.sh index dfe4da9..0ff4b69 100644 --- a/oci/single_server/module/userdata/bootstrap.sh +++ b/oci/single_server/module/userdata/bootstrap.sh @@ -9,17 +9,18 @@ chmod 600 /var/swap.1 cd /tmp -PRIVATE_IP=(`hostname -I | cut -d ' ' -f1 | tr -d '\\n'`) +PRIVATE_IP=(`hostname -I | cut -d ' ' -f1 | tr -d '\\n'`) wget ${kasm_build_url} -O kasm_workspaces.tar.gz tar -xf kasm_workspaces.tar.gz bash kasm_release/install.sh -e -U ${user_password} -P ${admin_password} -p $PRIVATE_IP -m $PRIVATE_IP echo -e "${nginx_cert_in}" > /opt/kasm/current/certs/kasm_nginx.crt - echo -e "${nginx_key_in}" > /opt/kasm/current/certs/kasm_nginx.key -docker exec kasm_proxy nginx -s reload +echo "Stopping and restarting Kasm services to apply certificates..." +/opt/kasm/bin/stop +docker rm $(docker ps -aq) +/opt/kasm/bin/start echo "Done" - diff --git a/oci/single_server/module/variables.tf b/oci/single_server/module/variables.tf index 29fb5f5..847373c 100644 --- a/oci/single_server/module/variables.tf +++ b/oci/single_server/module/variables.tf @@ -1,94 +1,139 @@ variable "project_name" { description = "The name of the deployment (e.g dev, staging). A short single word" + type = string } variable "oci_domain_name" { description = "The public Zone used for the dns entries. This must already exist in the OCI account. (e.g kasm.contoso.com). The deployment will be accessed via this zone name via https" -} - -variable kasm_build_url { - description = "The URL for the Kasm Workspaces build" -} - -variable swap_size { - description = "The amount of swap (in MB) to configure inside the compute instances" - default = 2048 + type = string } variable "tenancy_ocid" { description = "The Tenancy OCID." -} - -variable "user_ocid" { - description = "The User OCID." + type = string } variable "compartment_ocid" { description = "The Compartment OCID" + type = string } variable "region" { description = "The OCI Region eg: (us-ashburn-1)" + type = string +} + +variable "user_ocid" { + description = "The User OCID." + type = string } variable "fingerprint" { description = "API Key Fingerprint" + type = string } variable "private_key_path" { description = "The path to the API Key PEM encoded Private Key" + type = string + sensitive = true +} + +variable "letsencrypt_cert_support_email" { + description = "Email address to use for Let's Encrypt SSL certificates for OCI Deployment" + type = string +} +variable "letsencrypt_server_type" { + description = "SSL Server type to generate. Valid options are staging, prod, and empty string. Prod certificates are limited to 5 per week per domain." + type = string +} + +variable "vcn_subnet_cidr" { + description = "VPC Subnet CIDR where you wish to deploy Kasm" + type = string } variable "ssh_authorized_keys" { - description = "The file path to the authorized_keys file that contains SSH public keys to be installed on the OCI compute instance" + description = "The SSH Public Keys to be installed on the OCI compute instance" + type = string } variable "instance_image_ocid" { - description = "The OCID for the instance image , such as ubuntu 18.04, to use." - default = "ocid1.image.oc1.iad.aaaaaaaafg6lg7dejwjebjqontwzyvutgf6qs5awyze6fgoiqepyj5qkvcuq" + description = "The OCID for the instance image , such as ubuntu 20.04, to use." + type = string } -variable "instance_boot_size_gb" { - description = "The size, in GB, of the instance drive" - default = 60 -} - -variable "instance_shape" { - description = "The instance shape to use. Should be a Flex type." - default = "VM.Standard.E3.Flex" -} - -variable "instance_ocpus" { - description = "The number of CPUs to configure for the instance" - default = 2 -} - -variable "shape_memory_in_gb" { - description = "The amount of memory, in GB, to configure for the instance" - default = 4 -} - -variable "allow_ssh_cidr" { +variable "allow_ssh_cidrs" { description = "The CIDR notation to allow SSH access to the systems." - default = "0.0.0.0/0" + type = list(string) } -variable "allow_web_cidr" { +variable "allow_web_cidrs" { description = "The CIDR notation to allow HTTPS access to the systems." - default = "0.0.0.0/0" + type = list(string) } variable "kasm_ssl_crt_path" { description = "The file path to the PEM encoded SSL Certificate" + type = string } variable "kasm_ssl_key_path" { description = "The file path to the PEM encoded SSL Certificate Key" + type = string + sensitive = true } variable "user_password" { - description = "The password for the database. No special characters" + description = "The standard (non administrator) user password. No special characters" + type = string + sensitive = true } + variable "admin_password" { - description = "The password for the database. No special characters" -} \ No newline at end of file + description = "The administrative user password. No special characters" + type = string + sensitive = true +} + +variable "kasm_build_url" { + description = "The URL for the Kasm Workspaces build" + type = string +} + +variable "swap_size" { + description = "The amount of swap (in MB) to configure inside the compute instances" + type = number +} + +variable "instance_shape" { + description = "The instance shape to use. Should be a Flex type." + type = string +} + +variable "kasm_server_cpus" { + description = "The number of CPUs to configure for the Kasm instance" + type = number +} + +variable "kasm_server_memory" { + description = "The amount of memory to configure for the Kasm instance" + type = number +} + +variable "kasm_server_hdd_size" { + description = "The size in GBs of the Kasm instance HDD" + type = number +} + +## Pre-set values +variable "anywhere" { + description = "Anywhere route subnet" + type = string + default = "0.0.0.0/0" + + validation { + condition = can(cidrhost(var.anywhere, 0)) + error_message = "Anywhere variable must be valid IPv4 CIDR - usually 0.0.0.0/0 for all default routes and default Security Group access." + } +} diff --git a/oci/single_server/module/vcn.tf b/oci/single_server/module/vcn.tf index a068830..23958c5 100644 --- a/oci/single_server/module/vcn.tf +++ b/oci/single_server/module/vcn.tf @@ -1,12 +1,16 @@ +locals { + kasm_vcn_subnet_cidr_mask = split("/", var.vcn_subnet_cidr)[1] + kasm_server_subnet_cidr_calculation = (8 - (local.kasm_vcn_subnet_cidr_mask - 16)) + kasm_server_subnet_cidr_size = local.kasm_server_subnet_cidr_calculation < 0 ? 0 : local.kasm_server_subnet_cidr_calculation +} resource "oci_core_vcn" "kasm_vcn" { - cidr_block = "10.0.0.0/16" + cidr_block = var.vcn_subnet_cidr compartment_id = var.compartment_ocid display_name = "${var.project_name}-VCN" dns_label = "${var.project_name}vcn" } - resource "oci_core_internet_gateway" "kasm_internet_gateway" { compartment_id = var.compartment_ocid display_name = "${var.project_name}-Gateway" @@ -18,7 +22,7 @@ resource "oci_core_default_route_table" "default_route_table" { display_name = "DefaultRouteTable" route_rules { - destination = "0.0.0.0/0" + destination = var.anywhere destination_type = "CIDR_BLOCK" network_entity_id = oci_core_internet_gateway.kasm_internet_gateway.id } @@ -26,7 +30,7 @@ resource "oci_core_default_route_table" "default_route_table" { resource "oci_core_subnet" "kasm_subnet" { availability_domain = data.oci_identity_availability_domain.ad.name - cidr_block = "10.0.10.0/24" + cidr_block = cidrsubnet(var.vcn_subnet_cidr, local.kasm_server_subnet_cidr_size, 0) display_name = "${var.project_name}-Subnet" dns_label = "${var.project_name}subnet" security_list_ids = [oci_core_security_list.allow_web.id, oci_core_security_list.allow_ssh.id] diff --git a/oci/single_server/oci-private-key.pem b/oci/single_server/oci-private-key.pem index 5d6af95..6593da0 100644 --- a/oci/single_server/oci-private-key.pem +++ b/oci/single_server/oci-private-key.pem @@ -1 +1,28 @@ -replaceme \ No newline at end of file +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9ue4AlUo3K11g +loxg3CvAUwJdkthsPmxZzOarkel1NZw22vdFnIbZhVWeO7U/WErzBWW0gpublWrq +MVZvtwitAs+++QsUV9NkDecCfghxoOV4xO3WIGmBUx32iuNM0jzqFSodUq2JpBLB +41lBToUaUbVVvd8934GoXIaFIjgvFhZcfMnQCj+FMCc86Sm5MEOtIfdI2fv6Ih71 +Yqoeqwz85xM3O8AtXlVkTwea2MOrtaPe+FzvcyLMB+lemTlaVkZ/ZnasB1SC5nzS +eOCerdx7/E7QR8mpXCWovOUIWDvMAdJvGHZM9FzI/Ve0hcmOAHTTkVn3vplnFKdO +hXAyBbCXAgMBAAECggEAH2hFFwGfvIxIM/IYIPyxnW/gZ18kxp56z07VLLfF3upW +Btqurm5gAGhLKRuXQHo8RgixsnUgj1vnzAcI2QuCuylI/MM7a02yp9CJWmdEvJK3 +CgwHlzfcX1VEE4kgYZo4DUQdqacDJVLs6/3Xy7DU5et6ykvZRePXEejnN+jXRivn +yOXD4rAA5C7Zf+vQRqfah41CqF85el7Iwc5s7//GTOoobanpxHbrQxqftfmYJooY +jWx2SEj1IWWesOp3uRUuldgfLDu9i5jr3uY9NaCHp4Q41sf/ww2Qa9SimPJWPVyi +F/UxYmasnygere/dEwEBbF7Qt9WWAFmgWEePG5OTkQKBgQDpG11wtp/KN74S70YN +StbsCnYqWjnfjwiUF0v45uqxMlcaF/i7/M/REyQgZ4c4pimdqLQiz+M1CLoG6K6v +XetK8waskxbduXtogjTZq0IbemLyLb0aw8hxb/VLP2DTOa/y21zKVSznT5RmEHuF +mMfeYVUjg70QqbpTvSP6NcSQ+QKBgQDQW+pO0vV52i6fvcLR/wRXxHdpOKyq/7SL +ugdWPiEZBSRdRT6L4pPDA0cbASsf79sYLiwqV9WlpKh4VXePot7xwU7w59AqWWrn +M3Oed9PtLZYMyKd1Udqx8nvkgHqmDEUw8q3iG7EcwT6FYMbfiKCdfiVYcMooHOeJ +NI3EKGlCDwKBgQDfUmooi612DAU7GqSgI2zX4gXm+V2sgdmUFuWf5Yo+jJ3Jz+Fq +J//oq0/XA5gWe4ovEurQ1x0zN9MQv2Zrd6zttvpT5u7nKMXED8oWJu5EaPHWVl9u +/tZPZxU3K9bVRdT8849oAEXurNzdEzljekUxFUp9pb5B8nkhtHh9Q3m38QKBgQCQ +mVlwvsMRf7OftKgB6MNwza45XdWnDrKTzdTNq2x42rKz8BxKJEJ7skikmxj8nw8k +JtcLw81HmZA5gsbo7hVfHdLVKWu0GD4ymeiBb5OqJFrgIFjm+JmXqTsGB4wWtKlI +IQM/ygwOcqYbFmCuPTmKU0NDr3L8h6K/+7LvEUSLTQKBgB7mHVR3IS08w8rdJi3v +Ej72UYtPDbCSkEwf2/grdfwSqPFq/BGCOJjRn9D/cHIS9g/OOBkssJX/TI3PHA17 +mrtwF2F93H4wWptnmjyNnApIvndE5IoPNMZhTtsdk7+D/2QcSMaUtfv8dCi8E6K6 +OQsbnf32CLR2ef1inNhCFBuO +-----END PRIVATE KEY----- diff --git a/oci/single_server/provider.tf b/oci/single_server/provider.tf new file mode 100644 index 0000000..e65d81e --- /dev/null +++ b/oci/single_server/provider.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + oci = { + source = "oracle/oci" + version = ">= 4.0.0" + } + acme = { + source = "vancluever/acme" + version = ">= 2.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0.0" + } + } +} + +provider "oci" { + tenancy_ocid = var.tenancy_ocid + user_ocid = var.user_ocid + fingerprint = var.fingerprint + private_key_path = var.private_key_path + region = var.region +} + +provider "acme" { + server_url = local.letsencrypt_server_url +} diff --git a/oci/single_server/settings.tfvars b/oci/single_server/settings.tfvars new file mode 100644 index 0000000..4e9dbd5 --- /dev/null +++ b/oci/single_server/settings.tfvars @@ -0,0 +1,43 @@ +## Kasm deployment settings +oci_domain_name = "kasm.contoso.com" +project_name = "contoso" +vcn_subnet_cidr = "10.0.0.0/16" + +## OCI Authentication variables +tenancy_ocid = "changeme" +user_ocid = "changeme" +compartment_ocid = "changeme" +fingerprint = "changeme" +private_key_path = "./oci-private-key.pem" +region = "us-ashburn-1" + +## Load Balancer SSL Keys +# Terraform auto-generated Let's Encrypt keys +letsencrypt_cert_support_email = "" +letsencrypt_server_type = "" + +# Bring your own - Load Balancer SSL Keys +kasm_ssl_crt_path = "" +kasm_ssl_key_path = "" + +## VM Access subnets +allow_ssh_cidrs = ["0.0.0.0/0"] +allow_web_cidrs = ["0.0.0.0/0"] + +## Kasm passwords +admin_password = "changeme" +user_password = "changeme" + +## SSH Public Keys +ssh_authorized_keys = "changeme" + +## OCI VM Settings +instance_image_ocid = "ocid1.image.oc1.iad.aaaaaaaahiz6xym3a76xhwkmwmhrz6luyiehho7dpxpkphxhsq5q6z4m3nlq" +instance_shape = "VM.Standard.E4.Flex" +swap_size = 2048 +kasm_server_cpus = 2 +kasm_server_memory = 2 +kasm_server_hdd_size = 120 + +## Kasm download URL +kasm_build_url = "https://kasm-static-content.s3.amazonaws.com/kasm_release_1.12.0.d4fd8a.tar.gz" diff --git a/oci/single_server/variables.tf b/oci/single_server/variables.tf new file mode 100644 index 0000000..46e9fd3 --- /dev/null +++ b/oci/single_server/variables.tf @@ -0,0 +1,268 @@ +variable "project_name" { + description = "The name of the deployment (e.g dev, staging). A short single word" + type = string + + validation { + condition = can(regex("^[a-z]{1,15}", var.project_name)) + error_message = "The project_name variable can only be one word between 1 and 15 lower-case letters since it is a seed value in multiple object names." + } +} + +variable "oci_domain_name" { + description = "The public Zone used for the dns entries. This must already exist in the OCI account. (e.g kasm.contoso.com). The deployment will be accessed via this zone name via https" + type = string + + validation { + condition = can(regex("^[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,6}", var.oci_domain_name)) + error_message = "There are invalid characters in the oci_domain_name - it must be a valid domain name." + } +} + +variable "tenancy_ocid" { + description = "The Tenancy OCID." + type = string + + validation { + condition = can(regex("^(ocid\\d)\\.(compartment|tenancy)\\.(oc\\d)[.]{1,}[a-z0-9]{60}", var.tenancy_ocid)) + error_message = "The tenancy_ocid must be a valid Oracle Cloud Tenancy OCID value (e.g. ocid1.tenancy.oc1..aaaaaaaaba3pv6wkcr4jqae5f44n2b2m2yt2j6rx32uzr4h25vqstifsfdsq)." + } +} + +variable "user_ocid" { + description = "The User OCID." + type = string + + validation { + condition = can(regex("^(ocid\\d)\\.(user)\\.(oc\\d)[.]{1,}[a-z0-9]+", var.user_ocid)) + error_message = "The variable user_ocid must be a valid Oracle Cloud User OCID." + } +} + +variable "compartment_ocid" { + description = "The Compartment OCID" + type = string + + validation { + condition = can(regex("^(ocid\\d)\\.(compartment|tenancy)\\.(oc\\d)[.]+[a-z0-9]{60}", var.compartment_ocid)) + error_message = "The compartment_ocid must be a valid Oracle Cloud Compartment OCID value (e.g. ocid1.compartment.oc1..aaaaaaaaba3pv6wkcr4jqae5f44n2b2m2yt2j6rx32uzr4h25vqstifsfdsq)." + } +} + +variable "region" { + description = "The OCI Region eg: (us-ashburn-1)" + type = string + + validation { + condition = can(regex("^([a-z]{2}-[a-z]{5,}-[\\d]{1})$", var.region)) + error_message = "The provider-oci_deployment_region must be a valid Oracle Cloud (OCI) Region name, e.g. us-ashburn-1" + } +} + +variable "fingerprint" { + description = "API Key Fingerprint" + type = string + + validation { + condition = can(regex("^([a-f0-9]{2}:?){16}$", var.fingerprint)) + error_message = "The API fingerprint is incorrectly formatted. It should be 16 colon-delimited hex bytes (e.g. 12:34:56:78:90:ab:cd:ef:12:34:56:78:90:ab:cd:ef)." + } +} + +variable "private_key_path" { + description = "The path to the OCI API Key PEM encoded Private Key" + type = string + sensitive = true + + validation { + condition = can(fileexists(var.private_key_path)) + error_message = "The variable private_key_path must point to a valid OCI API Key file." + } + + validation { + condition = !can(regex("replaceme", file(var.private_key_path))) + error_message = "You must enter a valid OCI API Private key in the file located at the private_key_path." + } +} + +variable "vcn_subnet_cidr" { + description = "VCN Subnet CIDR where you wish to deploy Kasm" + type = string + + validation { + condition = can(cidrhost(var.vcn_subnet_cidr, 0)) + error_message = "The vcn_subnet_cidr must be a valid IPv4 Subnet in CIDR notation (e.g. 10.0.0.0/24)." + } +} + +variable "ssh_authorized_keys" { + description = "The SSH Public Keys to be installed on the OCI compute instance" + type = string + + validation { + condition = can(regex("^ssh-rsa\\s+[A-Za-z0-9+/]+[=]{0,3}(\\s+.+)?\\s*$", var.ssh_authorized_keys)) + error_message = "The ssh_authorized_keys value is not in the correct format." + } +} + +variable "instance_image_ocid" { + description = "The OCID for the instance image , such as ubuntu 20.04, to use." + type = string + + validation { + condition = can(regex("^(ocid\\d)\\.(image)\\.(oc\\d)\\.[a-z]{3,}\\.[a-z0-9]{60}", var.instance_image_ocid)) + error_message = "The compartment_ocid must be a valid Oracle Cloud Compartment OCID value (e.g. ocid1.image.oc1.iad.aaaaaaaahiz6xym3a76xhwkmwmhrz6luyiehho7dpxpkphxhsq5q6z4m3nlq)." + } +} + +variable "allow_ssh_cidrs" { + description = "The CIDR notation to allow SSH access to the systems." + type = list(string) + + validation { + condition = can([for subnet in var.allow_ssh_cidrs : cidrhost(subnet, 0)]) + error_message = "One of the subnets provided in the allow_ssh_cidrs list is invalid." + } +} + +variable "allow_web_cidrs" { + description = "The CIDR notation to allow HTTPS access to the systems." + type = list(string) + + validation { + condition = can([for subnet in var.allow_web_cidrs : cidrhost(subnet, 0)]) + error_message = "One of the subnets provided in the allow_web_cidrs list is invalid." + } +} + +variable "letsencrypt_cert_support_email" { + description = "Email address to use for Let's Encrypt SSL certificates for OCI Deployment" + type = string + default = "" + + validation { + condition = can(var.letsencrypt_cert_support_email == "" ? true : can(regex("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", var.letsencrypt_cert_support_email))) + error_message = "The ssl_cert_support_email must be a valid email address format." + } +} + +variable "letsencrypt_server_type" { + description = "SSL Server type to generate. Valid options are staging and prod, and prod certificates are limited to 5 certificates per week." + type = string + default = "" + + validation { + condition = can(contains(["staging", "prod", ""], var.letsencrypt_server_type)) + error_message = "Allowed values for the letsencrypt_server_type variable are: staging, prod, and empty string. For reference: Staging generates certificates that a browser will not trust, but are formatted correctly to apply to resources; while Prod generates valid, useable, trusted certificates. NOTE: Prod certificate generation is limited to 5 times per week, so if you are testing Kasm or intend to possibly re-deploy multiple times, it is recommended to use Staging (which has a much higher generation limit since it is intended for testing) until you are ready to deploy your 'final' version. If you have your own CA and are bringing your own certificates, then copy/paste then set this value to an empty string." + } +} + +variable "kasm_ssl_crt_path" { + description = "The file path to the PEM encoded SSL Certificate. Leave this empty if you are using Lets Encrypt to automatically generate your certificates." + type = string + default = "" + + validation { + condition = can(var.kasm_ssl_crt_path == "" ? true : can(fileexists(var.kasm_ssl_crt_path))) + error_message = "The variable kasm_ssl_crt_path must point to a valid OCI API Key file or be left empty (using Terraform-generated Lets Encrypt certificates)." + } + + validation { + condition = can(var.kasm_ssl_crt_path == "" ? true : !can(regex("replaceme", file(var.kasm_ssl_crt_path)))) + error_message = "You must enter a valid SSL Cert in the file located at the kasm_ssl_crt_path or be left empty (using Terraform-generated Lets Encrypt certificates)." + } +} + +variable "kasm_ssl_key_path" { + description = "The file path to the PEM encoded SSL Certificate Key. Leave this empty if you are using Lets Encrypt to automatically generate your certificates." + type = string + sensitive = true + default = "" + + validation { + condition = can(var.kasm_ssl_key_path == "" ? true : can(fileexists(var.kasm_ssl_key_path))) + error_message = "The variable kasm_ssl_key_path must point to a valid OCI API Key file or be left empty (using Terraform-generated Lets Encrypt certificates)." + } + + validation { + condition = can(var.kasm_ssl_key_path == "" ? true : !can(regex("replaceme", file(var.kasm_ssl_key_path)))) + error_message = "You must enter a valid SSL Cert in the file located at the kasm_ssl_key_path or be left empty (using Terraform-generated Lets Encrypt certificates)." + } +} + +variable "user_password" { + description = "The standard (non administrator) user password. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.user_password)) + error_message = "The User Password should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "admin_password" { + description = "The administrative user password. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.admin_password)) + error_message = "The Admin password should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "swap_size" { + description = "The amount of swap (in MB) to configure inside the compute instances" + type = number + + validation { + condition = var.swap_size >= 1024 && var.swap_size <= 8192 && floor(var.swap_size) == var.swap_size + error_message = "Swap size is the amount of disk space to use for Kasm in MB and must be an integer between 1024 and 8192." + } +} + +variable "kasm_server_cpus" { + description = "The number of CPUs to configure for the Kasm instance" + type = number + + validation { + condition = can(var.kasm_server_cpus >= 2) + error_message = "Kasm Webapps should have at least 2 CPUs to ensure enough resources for Kasm services." + } +} + +variable "kasm_server_memory" { + description = "The amount of memory to configure for the Kasm instance" + type = number + + validation { + condition = can(var.kasm_server_memory >= 2) + error_message = "Kasm Webapps should have at least 2 GB Memory to ensure enough resources for Kasm services." + } +} + +variable "kasm_server_hdd_size" { + description = "The size in GBs of the Kasm instance HDD" + type = number + + validation { + condition = can(var.kasm_server_hdd_size >= 120) + error_message = "Kasm Webapps should have at least a 120 GB HDD to ensure enough space Kasm Workspaces images." + } +} + +## Non-validated variables +variable "kasm_build_url" { + description = "The URL for the Kasm Workspaces build" + type = string +} + +variable "instance_shape" { + description = "The instance shape to use. Should be a Flex type." + type = string +} + +## Local for Let's Encrypt staging/prod URL selection +locals { + letsencrypt_server_url = var.letsencrypt_server_type == "prod" ? "https://acme-v02.api.letsencrypt.org/directory" : "https://acme-staging-v02.api.letsencrypt.org/directory" +} diff --git a/oci/standard/README.md b/oci/standard/README.md index 5992457..9eceb10 100644 --- a/oci/standard/README.md +++ b/oci/standard/README.md @@ -8,7 +8,6 @@ This project will deploy Kasm Workspaces in a multi-server deployment in OCI. - # Pre-Configuration Consider creating a new Compartment for the Kasm Workspaces deployment. @@ -16,18 +15,17 @@ Consider creating a new Compartment for the Kasm Workspaces deployment. In OCI create a public DNS zone that matches the desired domain name for the deployment. e.g `kasm.contoso.com`. ### API Keys -Create an administative user the OCI console that will be used for the terraform deployment. Add the user to the -**Administrators** Group. Generate an API Key for the user. The API Key Fingerprint will be used as a variable -in the deployment configuration. Save the private key to the local directory replacing `oci-private-key.pem`. +Create an administative user in the OCI console that will be used for the terraform deployment. Add the user to the **Administrators** Group. Generate an API Key for the user. The API Key Fingerprint will be used as a variable in the deployment configuration. Save the private key to the local directory replacing `oci-private-key.pem`. -### SSH Authorized Keys -The project will install an SSH key(s) inside the OCI compute instance(s). Update `authorized_keys` in this directory -with the desired SSH public keys. - -### SSL Certificate -Create an SSL certificate that matches the desired domain for the deployment. e.g (kasm.contoso.com). Place the pem encoded -cert and key in this directory overwriting `kasm_ssl.crt` and `kasm_ssl.key`. +### SSL Certificate Options +#### Terraform-generated Let's Encrypt Certificate +To use Terraform to generate a Let's Encrypt certificate automatically, set the `letsencrypt_cert_support_email` to a valid email address and set the `letsencrypt_server_type` to either "staging" or "prod" and leave the `kasm_ssl_crt_path` and `kasm_ssl_key_path` variables empty. +***NOTE:*** +- Staging generates certificates that a browser will not trust, but are formatted correctly and are designed for testing and validating the system configuraiton and deployment and has a limit of hundreds of certificates per domain per week. +- Prod generates valid Let's Encrypt certificates but is limited to 5 certificates per week per domain. +#### Bring Your Own Certificates +Create an SSL certificate that matches the desired domain for the deployment. e.g (kasm.contoso.com). Place the pem encoded cert and key in this directory overwriting `kasm_ssl.crt` and `kasm_ssl.key`. # Terraform Configuration @@ -36,20 +34,60 @@ cert and key in this directory overwriting `kasm_ssl.crt` and `kasm_ssl.key`. terraform init -2. Open `deployment.tf` and update the variables. The variable definitions and descriptions -can be found in `module/variables.tf` +2. Open `settings.tfvars` and update the variables. The variable definitions, descriptions, and validation requirements can be found in `variables.tf`, or in the [table](#oci-terraform-variable-definitions) below. - 3. Verify the configuration - terraform plan + terraform plan -var-file settings.tfvars 4. Deploy - terraform apply + terraform apply -var-file settings.tfvars + +5. Login to the Deployment as an Admin via the domain defined e.g `https://kasm.contoso.com`. It may take several minutes for the deployment to fully come online. -5. Login to the Deployment as an Admin via the domain defined e.g `https://kasm.contoso.com`. It may take several minutes -for the deployment to fully come online. +# OCI Terraform Variable definitions + +| Variable | Description | Variable type | Example | +|:--------:|-------------|---------------|---------| +| `tenancy_ocid` | The OCI Tenancy OCID | String | `"ocid1.tenancy.oc1..aaaaaaaaai06vvcguozt39d4ilmwtpdovl998wsxpyn0hjkab2kuh7z16po7"` | +| `compartment_ocid` | The OCI Compartment OCID | String | `"ocid1.compartment.oc1..aaaaaaaauepg1z967huiazuiwjt80rtbszp64x9oxaidkoi7wz0pgr950bzb"` | +| `region` | The OCI Region name | String | `"us-ashburn-1"` | +| `user_ocid` | The OCI User OCID | String | `"ocid1.user.oc1..aaaaaaaau3me8nojmdjrbj2vzfxeouscc1i7cf9w0aoy0iyv9b38t2y0a1ba"` | +| `fingerprint` | The OCI User API Key fingerprint | String | `"66:e5:d1:85:cd:ba:ca:6a:d0:76:86:ef:1c:11:63:97"` | +| `private_key_path` | The path for the API Key PEM encoded Private Key for the OCI User. ***NOTE:*** *Ensure the API Key contents are a valid PEM encoded RSA key file. You can tell this by ensuring that the value `-----BEGIN RSA PRIVATE KEY-----` is the first line in the key file. Otherwise, you can validate the key file by running the `openssl rsa -in oci-private-key.pem -check` command.* | String | `"./oci-private-key.pem"` | +| `project_name` | The name of the deployment (e.g dev, staging). A short single word of up to 15 characters. | String | `"kasm"` | +| `oci_domain_name` | The public Zone used for the dns entries. This must already exist in the OCI account. (e.g kasm.contoso.com). The deployment will be accessed via this zone name using https. | String | `"kasm.contoso.com"` | +| `letsencrypt_cert_support_email` | Email address to use for Terraform-generated Let's Encrypt SSL certificates | String | `"support@contoso.com"` | +| `letsencrypt_server_type` | SSL Server type for certificate generation. Valid options are staging, prod, and empty string (""). Refer to [SSL Certificate Options](#ssl-certificate-options) section of this document for more information. | String | "prod" | +| `kasm_ssl_crt_path` | Bring Your own Certificate - The file path fo the PEM encoded SSL Certificate file generated outside of Terraform. Copy/paste the contents of your generated SSL Certificate to the file designated in this path variable. | String | `"./kasm_ssl.crt"` | +| `kasm_ssl_key_path` | Bring Your own Certificate - The file path to the PEM encoded SSL Private Key file generated outside of Terraform. Copy/paste the contents of your generated SSL Private Key to the file designated in this path variable. | String | `"./kasm_ssl.key"` | +| `vcn_subnet_cidr` | The OCI VCN Subnet CIDR of the VCN where you wish to deploy Kasm | String | `"10.0.0.0/16"` | +| `ssh_authorized_keys` | The SSH Public key to be installed on the Kasm servers for SSH access | String | `"ssh-rsa some_base64_encoded_ssh_public_key_data"` | +| `instance_image_ocid` | The OCI Image OCID value of the OS to use. Kasm recommends using lates Ubuntu 20.04 LTS-Minimal for speed and efficiency. | String | `"ocid1.image.oc1.iad.aaaaaaaahiz6xym3a76xhwkmwmhrz6luyiehho7dpxpkphxhsq5q6z4m3nlq"` | +| `allow_ssh_cidrs` | A list of subnets in CIDR notation allowed to SSH into your kasm servers | List(String) | `["10.0.0.0/16","172.217.22.14/32"]` | +| `allow_web_cidrs` | A list of subnets in CIDR notation allowed Web access to your kasm servers | List(String) | `["0.0.0.0/0"]` | +| `num_webapps` | The number of WebApp role servers to create in this deployment. Acceptable ranges from 1-3. | Number | `2` | +| `num_agents` | The number of static Kasm Agents to create in this deploymenbt. Acceptable ranges from 0-100. | Number | `2` | +| `num_guac_rdp_nodes` | The number of Guacamole RDP access servers to create in this deployment. Acceptable ranges from 0-100. | Number | `1` | +| `database_password` | The Kasm PostgreSQL database password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `redis_password` | The Kasm Redis password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `admin_password` | The Kasm Administrative user login password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `user_password` | A Kasm standard (non-administrator) user password. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `manager_token` | The manager token value used by Kasm agents to authenticate to the Kasm WebApps. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `service_registration_token` | The service registration token value used by Guac RDP servers to authenticate to the Kasm Webapps. String from 12-30 characters in length with no special characters. | String | `"1qaz2wsx3EDC4RFV"` | +| `kasm_build_url` | The download URL for the desired Kasm Workspaces version. | String | `"https://kasm-static-content.s3.amazonaws.com/kasm_release_1.12.0.d4fd8a.tar.gz"` | +| `swap_size` | The amount of swap (in MB) to configure inside the Kasm servers. | Number | `2048` | +| `instance_shape` | The OCI instance shape to use for Kasm deployment. Kasm recommends using a Flex instance type. | String | `"VM.Standard.E4.Flex"` | +| `kasm_webapp_vm_settings` | The number of CPUs, memory in GB, and HDD size to use for Kasm WebApps. | Map(Any) |
{
  cpus = 2
  memory = 2
  hdd_size_gb = 50
}
| +| `kasm_database_vm_settings` | The number of CPUs, memory in GB, and HDD size to use for the Kasm Database server. | Map(Any) |
{
  cpus = 2
  memory = 2
  hdd_size_gb = 50
}
| +| `kasm_agent_vm_settings` | The number of CPUs, memory in GB, and HDD size to use for the Kasm Agent server(s). | Map(Any) |
{
  cpus = 4
  memory = 8
  hdd_size_gb = 120
}
| +| `kasm_guac_vm_settings` | The number of CPUs, memory in GB, and HDD size to use for the Kasm Guac RDP server(s). | Map(Any) |
{
  cpus = 4
  memory = 4
  hdd_size_gb = 50
}
| +# Detailed Terraform Deployment Diagram + +![Detailed Diagram][Detailed_Diagram] + +[Detailed_Diagram]: ./diagram/oci_multi_server.png "Detailed Diagram" diff --git a/oci/standard/authorized_keys b/oci/standard/authorized_keys deleted file mode 100644 index 5d6af95..0000000 --- a/oci/standard/authorized_keys +++ /dev/null @@ -1 +0,0 @@ -replaceme \ No newline at end of file diff --git a/oci/standard/deployment.tf b/oci/standard/deployment.tf index 6d770d8..842eeba 100644 --- a/oci/standard/deployment.tf +++ b/oci/standard/deployment.tf @@ -1,29 +1,44 @@ module "kasm" { - source = "./module" - oci_domain_name = "kasm.contoso.com" - project_name = "contoso" + source = "./module" + oci_domain_name = var.oci_domain_name + project_name = var.project_name + kasm_build_url = var.kasm_build_url + vcn_subnet_cidr = var.vcn_subnet_cidr - tenancy_ocid = "" - user_ocid = "" - compartment_ocid = "" + ## OCI Auth information + tenancy_ocid = var.tenancy_ocid + compartment_ocid = var.compartment_ocid + user_ocid = var.user_ocid + fingerprint = var.fingerprint + private_key_path = var.private_key_path + region = var.region + ssh_authorized_keys = var.ssh_authorized_keys - fingerprint = "" - private_key_path = "./oci-private-key.pem" - ssh_authorized_keys = "./authorized_keys" - kasm_ssl_crt_path = "./kasm_ssl.crt" - kasm_ssl_key_path = "./kasm_ssl.key" + ## SSL Certificate values + # Let TF generate Let's Encrypt SSL Certificates automatically + letsencrypt_cert_support_email = var.letsencrypt_cert_support_email + letsencrypt_server_type = var.letsencrypt_server_type + # Bring your own SSL Certificates + kasm_ssl_crt_path = var.kasm_ssl_crt_path + kasm_ssl_key_path = var.kasm_ssl_key_path - region = "us-ashburn-1" - instance_image_ocid = "ocid1.image.oc1.iad.aaaaaaaafg6lg7dejwjebjqontwzyvutgf6qs5awyze6fgoiqepyj5qkvcuq" - num_agents = "2" - num_webapps = "2" + instance_image_ocid = var.instance_image_ocid + instance_shape = var.instance_shape + num_agents = var.num_agents + num_webapps = var.num_webapps + num_guac_rdp_nodes = var.num_guac_rdp_nodes + kasm_agent_vm_settings = var.kasm_agent_vm_settings + kasm_database_vm_settings = var.kasm_database_vm_settings + kasm_webapp_vm_settings = var.kasm_webapp_vm_settings + kasm_guac_vm_settings = var.kasm_guac_vm_settings + allow_ssh_cidrs = var.allow_ssh_cidrs + allow_web_cidrs = var.allow_web_cidrs + swap_size = var.swap_size - kasm_build_url = "https://kasm-static-content.s3.amazonaws.com/kasm_release_1.9.0.077388.tar.gz" - allow_ssh_cidr = "0.0.0.0/0" - allow_web_cidr = "0.0.0.0/0" - manager_token = "changeme" - admin_password = "changeme" - user_password = "changeme" - redis_password = "changeme" - database_password = "changeme" -} \ No newline at end of file + manager_token = var.manager_token + admin_password = var.admin_password + user_password = var.user_password + redis_password = var.redis_password + database_password = var.database_password + service_registration_token = var.service_registration_token +} diff --git a/oci/standard/diagram/oci_multi_server.drawio b/oci/standard/diagram/oci_multi_server.drawio new file mode 100644 index 0000000..0542cc8 --- /dev/null +++ b/oci/standard/diagram/oci_multi_server.drawio @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/oci/standard/diagram/oci_multi_server.png b/oci/standard/diagram/oci_multi_server.png new file mode 100644 index 0000000..5578b24 Binary files /dev/null and b/oci/standard/diagram/oci_multi_server.png differ diff --git a/oci/standard/module/agent.tf b/oci/standard/module/agent.tf index 90cd524..c149f0d 100644 --- a/oci/standard/module/agent.tf +++ b/oci/standard/module/agent.tf @@ -1,44 +1,39 @@ -data "template_file" "agent_user_data" { - template = "${file("${path.module}/userdata/agent_bootstrap.sh")}" - vars = { - kasm_build_url = var.kasm_build_url - swap_size = var.swap_size - manager_address = var.oci_domain_name - manager_token = var.manager_token - } -} - resource "oci_core_instance" "kasm_agent_instance" { count = var.num_agents - availability_domain = data.oci_identity_availability_domain.ad.name + availability_domain = data.oci_identity_availability_domains.kasm_ads.availability_domains[0].name compartment_id = var.compartment_ocid display_name = "${var.project_name}-Kasm-Agent-${count.index}" shape = var.instance_shape shape_config { - ocpus = var.agent_ocpus - memory_in_gbs = var.agent_memory_in_gb + ocpus = var.kasm_agent_vm_settings.cpus + memory_in_gbs = var.kasm_agent_vm_settings.memory } create_vnic_details { - subnet_id = oci_core_subnet.kasm-agent-subnet.id - display_name = "${var.project_name}-Primaryvnic" + subnet_id = data.oci_core_subnet.data-kasm_agent_subnet.id + display_name = "${var.project_name}-Primaryvnic-${count.index}" assign_public_ip = true assign_private_dns_record = true hostname_label = "${var.project_name}-Kasm-Agent-${count.index}" } source_details { - source_type = "image" - source_id = var.instance_image_ocid - boot_volume_size_in_gbs = var.agent_boot_size_gb + source_type = "image" + source_id = var.instance_image_ocid + boot_volume_size_in_gbs = var.kasm_agent_vm_settings.hdd_size_gb } metadata = { - ssh_authorized_keys = file("${var.ssh_authorized_keys}") - user_data = base64encode("${data.template_file.agent_user_data.rendered}") + ssh_authorized_keys = var.ssh_authorized_keys + user_data = base64encode(templatefile("${path.module}/userdata/agent_bootstrap.sh", + { + kasm_build_url = var.kasm_build_url + swap_size = var.swap_size + manager_address = var.oci_domain_name + manager_token = var.manager_token + } + )) } - } - diff --git a/oci/standard/module/db.tf b/oci/standard/module/db.tf index fb21f6f..fb150dc 100644 --- a/oci/standard/module/db.tf +++ b/oci/standard/module/db.tf @@ -1,26 +1,12 @@ -data "template_file" "db_user_data" { - template = "${file("${path.module}/userdata/db_bootstrap.sh")}" - vars = { - kasm_build_url = var.kasm_build_url - user_password = var.user_password - admin_password = var.admin_password - redis_password = var.redis_password - database_password = var.database_password - manager_token = var.manager_token - swap_size = var.swap_size - - } -} - resource "oci_core_instance" "kasm_db_instance" { - availability_domain = data.oci_identity_availability_domain.ad.name + availability_domain = data.oci_identity_availability_domains.kasm_ads.availability_domains[0].name compartment_id = var.compartment_ocid display_name = "${var.project_name}-Kasm-DB" shape = var.instance_shape shape_config { - ocpus = var.db_ocpus - memory_in_gbs = var.db_memory_in_gb + ocpus = var.kasm_database_vm_settings.cpus + memory_in_gbs = var.kasm_database_vm_settings.memory } create_vnic_details { @@ -34,15 +20,26 @@ resource "oci_core_instance" "kasm_db_instance" { source_details { source_type = "image" source_id = var.instance_image_ocid - boot_volume_size_in_gbs = var.db_boot_size_gb + boot_volume_size_in_gbs = var.kasm_database_vm_settings.hdd_size_gb } - metadata = { - ssh_authorized_keys = file("${var.ssh_authorized_keys}") - user_data = base64encode("${data.template_file.db_user_data.rendered}") + ssh_authorized_keys = var.ssh_authorized_keys + user_data = base64encode(templatefile("${path.module}/userdata/db_bootstrap.sh", + { + kasm_build_url = var.kasm_build_url + user_password = var.user_password + admin_password = var.admin_password + redis_password = var.redis_password + database_password = var.database_password + service_registration_token = var.service_registration_token + manager_token = var.manager_token + swap_size = var.swap_size + } + )) } - } - +data "oci_core_instance" "data-kasm_db_instance" { + instance_id = oci_core_instance.kasm_db_instance.id +} diff --git a/oci/standard/module/dns.tf b/oci/standard/module/dns.tf index daaa765..fbe4c4b 100644 --- a/oci/standard/module/dns.tf +++ b/oci/standard/module/dns.tf @@ -1,12 +1,17 @@ +data "oci_dns_zones" "kasm_dns_zone" { + compartment_id = var.compartment_ocid + name = var.oci_domain_name +} + resource "oci_dns_rrset" "kasm_a_record" { + compartment_id = var.compartment_ocid + domain = var.oci_domain_name + zone_name_or_id = data.oci_dns_zones.kasm_dns_zone.zones[0].name + rtype = "A" + items { domain = var.oci_domain_name - rtype = "A" - zone_name_or_id = var.oci_domain_name - compartment_id = var.compartment_ocid - items { - domain = var.oci_domain_name - rdata = oci_load_balancer_load_balancer.kasm_load_balancer.ip_addresses[0] - rtype = "A" - ttl = 300 - } -} \ No newline at end of file + rdata = oci_load_balancer.kasm_load_balancer.ip_address_details[0].ip_address + rtype = "A" + ttl = 300 + } +} diff --git a/oci/standard/module/guac_rdp.tf b/oci/standard/module/guac_rdp.tf new file mode 100644 index 0000000..dc3d129 --- /dev/null +++ b/oci/standard/module/guac_rdp.tf @@ -0,0 +1,39 @@ +resource "oci_core_instance" "kasm_guac_instance" { + count = var.num_guac_rdp_nodes + availability_domain = data.oci_identity_availability_domains.kasm_ads.availability_domains[0].name + compartment_id = var.compartment_ocid + display_name = "${var.project_name}-Kasm-Guac-${count.index}" + shape = var.instance_shape + + shape_config { + ocpus = var.kasm_guac_vm_settings.cpus + memory_in_gbs = var.kasm_guac_vm_settings.memory + } + + create_vnic_details { + subnet_id = data.oci_core_subnet.data-kasm_guac_subnet.id + display_name = "${var.project_name}-Primaryvnic-${count.index}" + assign_public_ip = true + assign_private_dns_record = true + hostname_label = "${var.project_name}-Kasm-Guac-${count.index}" + } + + source_details { + source_type = "image" + source_id = var.instance_image_ocid + boot_volume_size_in_gbs = var.kasm_guac_vm_settings.hdd_size_gb + } + + + metadata = { + ssh_authorized_keys = var.ssh_authorized_keys + user_data = base64encode(templatefile("${path.module}/userdata/guac_bootstrap.sh", + { + kasm_build_url = var.kasm_build_url + swap_size = var.swap_size + manager_address = var.oci_domain_name + service_registration_token = var.service_registration_token + } + )) + } +} diff --git a/oci/standard/module/letsencrypt.tf b/oci/standard/module/letsencrypt.tf new file mode 100644 index 0000000..b9bf330 --- /dev/null +++ b/oci/standard/module/letsencrypt.tf @@ -0,0 +1,48 @@ +resource "tls_private_key" "registration_private_key" { + algorithm = "RSA" +} + +resource "tls_private_key" "certificate_private_key" { + algorithm = "RSA" +} + +resource "acme_registration" "registration" { + account_key_pem = tls_private_key.registration_private_key.private_key_pem + email_address = var.letsencrypt_cert_support_email +} + +resource "tls_cert_request" "kasm_certificate_request" { + private_key_pem = tls_private_key.certificate_private_key.private_key_pem + dns_names = [data.oci_dns_zones.kasm_dns_zone.zones[0].name, "*.${data.oci_dns_zones.kasm_dns_zone.zones[0].name}"] + + subject { + common_name = data.oci_dns_zones.kasm_dns_zone.zones[0].name + } +} + +resource "acme_certificate" "certificate" { + account_key_pem = acme_registration.registration.account_key_pem + certificate_request_pem = tls_cert_request.kasm_certificate_request.cert_request_pem + recursive_nameservers = [ + "8.8.8.8:53", + "4.4.2.2:53" + ] + + dns_challenge { + provider = "oraclecloud" + + config = { + OCI_COMPARTMENT_OCID = var.compartment_ocid + OCI_PRIVKEY_FILE = var.private_key_path + OCI_TENANCY_OCID = var.tenancy_ocid + OCI_REGION = var.region + OCI_PUBKEY_FINGERPRINT = var.fingerprint + OCI_USER_OCID = var.user_ocid + OCI_PROPOGATION_TIMEOUT = 600 + OCI_POLLING_INTERVAL = 60 + OCI_TTL = 300 + } + } + + depends_on = [acme_registration.registration] +} diff --git a/oci/standard/module/load_balancer.tf b/oci/standard/module/load_balancer.tf index 1daa13d..7f791e1 100644 --- a/oci/standard/module/load_balancer.tf +++ b/oci/standard/module/load_balancer.tf @@ -1,66 +1,88 @@ -resource "oci_load_balancer_load_balancer" "kasm_load_balancer" { - compartment_id = var.compartment_ocid - display_name = "Kasm-${var.project_name}-lb" - shape = "100Mbps" - subnet_ids = [oci_core_subnet.kasm-webapp-subnet.id, oci_core_subnet.kasm-webapp-subnet-2.id] - is_private = false +resource "oci_load_balancer" "kasm_load_balancer" { + shape = "flexible" + compartment_id = var.compartment_ocid + subnet_ids = [for subnet_id in data.oci_core_subnets.data-kasm_webapp_subnets : subnet_id.subnets[0].id] + + shape_details { + minimum_bandwidth_in_mbps = 10 + maximum_bandwidth_in_mbps = 100 + } + + display_name = "${var.project_name}-kasm-load_balancer" } resource "oci_load_balancer_certificate" "kasm_lb_certificate" { - certificate_name = "${var.project_name}-kasm-cert-2" - load_balancer_id = oci_load_balancer_load_balancer.kasm_load_balancer.id + certificate_name = "${var.project_name}-kasm-cert" + load_balancer_id = oci_load_balancer.kasm_load_balancer.id - ca_certificate = file(var.kasm_ssl_crt_path) - public_certificate = file(var.kasm_ssl_crt_path) - private_key = file(var.kasm_ssl_key_path) + ca_certificate = var.letsencrypt_server_type == "" ? file(var.kasm_ssl_crt_path) : acme_certificate.certificate.certificate_pem + public_certificate = var.letsencrypt_server_type == "" ? file(var.kasm_ssl_crt_path) : acme_certificate.certificate.certificate_pem + private_key = var.letsencrypt_server_type == "" ? file(var.kasm_ssl_key_path) : tls_private_key.certificate_private_key.private_key_pem - lifecycle { - create_before_destroy = true - } + lifecycle { + create_before_destroy = true + } } -resource "oci_load_balancer_backend_set" "kasm-webapps-backend-set" { +resource "oci_load_balancer_backend_set" "kasm_load_balancer_backend_set" { + name = "${var.project_name}-kasm-backend_set" + load_balancer_id = oci_load_balancer.kasm_load_balancer.id + policy = "ROUND_ROBIN" + health_checker { - interval_ms = "10000" - port = "443" + port = 443 protocol = "HTTP" response_body_regex = "" - retries = "3" - return_code = "200" - timeout_in_millis = "3000" + retries = 3 + return_code = 200 + timeout_in_millis = 3000 + interval_ms = 10000 url_path = "/api/__healthcheck" } + ssl_configuration { - certificate_name = oci_load_balancer_certificate.kasm_lb_certificate.certificate_name + protocols = [ + "TLSv1.1", + "TLSv1.2" + ] + cipher_suite_name = data.oci_load_balancer_ssl_cipher_suite.data-kasm_load_balancer_cipher_suite.name + certificate_name = oci_load_balancer_certificate.kasm_lb_certificate.certificate_name verify_peer_certificate = false } - - load_balancer_id = oci_load_balancer_load_balancer.kasm_load_balancer.id - name = "kasm-webapps-lb-backend-set" - policy = "ROUND_ROBIN" } -resource "oci_load_balancer_backend" "webapp-ld-backed" { - count = var.num_webapps - backendset_name = oci_load_balancer_backend_set.kasm-webapps-backend-set.name - backup = "false" - drain = "false" - load_balancer_id = oci_load_balancer_load_balancer.kasm_load_balancer.id - ip_address = oci_core_instance.kasm_webapp_instance[count.index].private_ip - offline = "false" - port = "443" - weight = "1" +resource "oci_load_balancer_backend" "kasm_webapp_load_balancer_backend" { + count = var.num_webapps + backendset_name = oci_load_balancer_backend_set.kasm_load_balancer_backend_set.name + backup = false + drain = false + load_balancer_id = oci_load_balancer.kasm_load_balancer.id + ip_address = data.oci_core_instance.data-kasm_webapp_instances[count.index].private_ip + offline = false + port = 443 + weight = 1 } resource "oci_load_balancer_listener" "kasm_https_ssl_listener" { - load_balancer_id = oci_load_balancer_load_balancer.kasm_load_balancer.id name = "${var.project_name}-https-ssl-listener" - default_backend_set_name = oci_load_balancer_backend_set.kasm-webapps-backend-set.name + load_balancer_id = oci_load_balancer.kasm_load_balancer.id + default_backend_set_name = oci_load_balancer_backend_set.kasm_load_balancer_backend_set.name port = "443" protocol = "HTTP" ssl_configuration { - certificate_name = oci_load_balancer_certificate.kasm_lb_certificate.certificate_name + protocols = [ + "TLSv1.1", + "TLSv1.2" + ] + server_order_preference = "ENABLED" verify_peer_certificate = false + cipher_suite_name = data.oci_load_balancer_ssl_cipher_suite.data-kasm_load_balancer_cipher_suite.name + certificate_name = oci_load_balancer_certificate.kasm_lb_certificate.certificate_name } } + +data "oci_load_balancer_ssl_cipher_suite" "data-kasm_load_balancer_cipher_suite" { + name = "oci-default-ssl-cipher-suite-v1" + load_balancer_id = oci_load_balancer.kasm_load_balancer.id +} diff --git a/oci/standard/module/main.tf b/oci/standard/module/main.tf deleted file mode 100644 index 586a8cb..0000000 --- a/oci/standard/module/main.tf +++ /dev/null @@ -1,8 +0,0 @@ -provider "oci" { - tenancy_ocid = var.tenancy_ocid - user_ocid = var.user_ocid - fingerprint = var.fingerprint - private_key_path = var.private_key_path - region = var.region -} - diff --git a/oci/standard/module/provider.tf b/oci/standard/module/provider.tf new file mode 100644 index 0000000..9e55cc0 --- /dev/null +++ b/oci/standard/module/provider.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + oci = { + source = "oracle/oci" + version = ">= 4.0.0" + } + acme = { + source = "vancluever/acme" + version = ">= 2.0.0" + } + } +} diff --git a/oci/standard/module/security_lists.tf b/oci/standard/module/security_lists.tf index 5499bb3..946824f 100644 --- a/oci/standard/module/security_lists.tf +++ b/oci/standard/module/security_lists.tf @@ -1,101 +1,143 @@ resource "oci_core_security_list" "allow_web" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.kasm_vcn.id - display_name = "allow_web" + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.kasm_vcn.id + display_name = "allow_web" - egress_security_rules { - destination = "0.0.0.0/0" - protocol = "all" - stateless = "false" - } + dynamic "egress_security_rules" { + for_each = var.anywhere - ingress_security_rules { - protocol = "6" - source = var.allow_web_cidr - tcp_options { - max = "443" - min = "443" - } + content { + destination = egress_security_rules.value + protocol = "all" + stateless = "false" } + } + + dynamic "ingress_security_rules" { + for_each = var.allow_web_cidrs + content { + protocol = "6" + source = ingress_security_rules.value + tcp_options { + max = "443" + min = "443" + } + } + } +} + +data "oci_core_security_lists" "data-allow_web" { + compartment_id = var.compartment_ocid + display_name = oci_core_security_list.allow_web.display_name } resource "oci_core_security_list" "allow_ssh" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.kasm_vcn.id - display_name = "allow_ssh" + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.kasm_vcn.id + display_name = "allow_ssh" - egress_security_rules { - destination = "0.0.0.0/0" - protocol = "all" - stateless = "false" - } + dynamic "egress_security_rules" { + for_each = var.anywhere - ingress_security_rules { - protocol = "6" - source = var.allow_ssh_cidr - tcp_options { - max = "22" - min = "22" - } + content { + destination = egress_security_rules.value + protocol = "all" + stateless = "false" } + } + + dynamic "ingress_security_rules" { + for_each = var.allow_ssh_cidrs + content { + protocol = "6" + source = ingress_security_rules.value + tcp_options { + max = "22" + min = "22" + } + } + } } +data "oci_core_security_lists" "data-allow_ssh" { + compartment_id = var.compartment_ocid + display_name = oci_core_security_list.allow_ssh.display_name +} resource "oci_core_security_list" "allow_db_redis" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.kasm_vcn.id - display_name = "allow_db_redis" + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.kasm_vcn.id + display_name = "allow_db_redis" - egress_security_rules { - destination = "0.0.0.0/0" - protocol = "all" - stateless = "false" - } + dynamic "egress_security_rules" { + for_each = var.anywhere - ingress_security_rules { - protocol = "6" - source = oci_core_subnet.kasm-webapp-subnet.cidr_block - tcp_options { - max = "5432" - min = "5432" - } + content { + destination = egress_security_rules.value + protocol = "all" + stateless = "false" } - ingress_security_rules { - protocol = "6" - source = oci_core_subnet.kasm-webapp-subnet.cidr_block - tcp_options { - max = "6379" - min = "6379" - } + } + + dynamic "ingress_security_rules" { + for_each = [for cidr_block in data.oci_core_subnets.data-kasm_webapp_subnets : cidr_block.subnets[0].cidr_block] + content { + protocol = "6" + source = ingress_security_rules.value + tcp_options { + max = "5432" + min = "5432" + } } + } + + dynamic "ingress_security_rules" { + for_each = [for cidr_block in data.oci_core_subnets.data-kasm_webapp_subnets : cidr_block.subnets[0].cidr_block] + content { + protocol = "6" + source = ingress_security_rules.value + tcp_options { + max = "6379" + min = "6379" + } + } + } } +data "oci_core_security_lists" "data-allow_db_redis" { + compartment_id = var.compartment_ocid + display_name = oci_core_security_list.allow_db_redis.display_name +} resource "oci_core_security_list" "allow_web_from_webapp" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.kasm_vcn.id - display_name = "allow_web_from_webapp" + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.kasm_vcn.id + display_name = "allow_web_from_webapp" - egress_security_rules { - destination = "0.0.0.0/0" - protocol = "all" - stateless = "false" - } + dynamic "egress_security_rules" { + for_each = var.anywhere - ingress_security_rules { - protocol = "6" - source = var.webapp_cidr_1 - tcp_options { - max = "443" - min = "443" - } + content { + destination = egress_security_rules.value + protocol = "all" + stateless = "false" } - ingress_security_rules { - protocol = "6" - source = var.webapp_cidr_2 - tcp_options { - max = "443" - min = "443" - } + } + + dynamic "ingress_security_rules" { + for_each = [for cidr_block in data.oci_core_subnets.data-kasm_webapp_subnets : cidr_block.subnets[0].cidr_block] + content { + protocol = "6" + source = ingress_security_rules.value + tcp_options { + max = "443" + min = "443" + } } + } +} + +data "oci_core_security_lists" "data-allow_web_from_webapp" { + compartment_id = var.compartment_ocid + display_name = oci_core_security_list.allow_web_from_webapp.display_name } diff --git a/oci/standard/module/subnet.tf b/oci/standard/module/subnet.tf index 371cb4b..e6ffbee 100644 --- a/oci/standard/module/subnet.tf +++ b/oci/standard/module/subnet.tf @@ -1,48 +1,90 @@ +locals { + kasm_vcn_subnet_cidr_mask = split("/", var.vcn_subnet_cidr)[1] + kasm_server_subnet_cidr_calculation = (8 - (local.kasm_vcn_subnet_cidr_mask - 16)) + kasm_server_subnet_cidr_size = local.kasm_server_subnet_cidr_calculation < 3 ? 3 : local.kasm_server_subnet_cidr_calculation + kasm_agent_subnet_id = (var.num_webapps + 1) +} + +## Will create Agent subnet x.x.0.x/24 (assuming a VPC Subnet CIDR between x.x.0.0/16 and x.x.0.0/21) resource "oci_core_subnet" "kasm-db-subnet" { - availability_domain = data.oci_identity_availability_domain.ad.name - cidr_block = "10.0.10.0/24" - display_name = "${var.project_name}-kasm-db-subnet" + compartment_id = var.compartment_ocid + vcn_id = data.oci_core_vcn.data-kasm_vcn.id + route_table_id = oci_core_route_table.default_route_table.id + dhcp_options_id = data.oci_core_vcn.data-kasm_vcn.default_dhcp_options_id + availability_domain = data.oci_identity_availability_domains.kasm_ads.availability_domains[0].name + cidr_block = cidrsubnet(var.vcn_subnet_cidr, local.kasm_server_subnet_cidr_size, 0) + display_name = "${var.project_name}-db-subnet" dns_label = "${var.project_name}db" - security_list_ids = [oci_core_security_list.allow_db_redis.id, oci_core_security_list.allow_ssh.id] - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.kasm_vcn.id - route_table_id = oci_core_vcn.kasm_vcn.default_route_table_id - dhcp_options_id = oci_core_vcn.kasm_vcn.default_dhcp_options_id + security_list_ids = [ + data.oci_core_security_lists.data-allow_db_redis.security_lists[0].id, + data.oci_core_security_lists.data-allow_ssh.security_lists[0].id + ] } -resource "oci_core_subnet" "kasm-webapp-subnet" { - availability_domain = data.oci_identity_availability_domain.ad.name - cidr_block = var.webapp_cidr_1 - display_name = "${var.project_name}-kasm-webapp-subnet" - dns_label = "${var.project_name}webapp" - security_list_ids = [oci_core_security_list.allow_web.id, oci_core_security_list.allow_ssh.id] - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.kasm_vcn.id - route_table_id = oci_core_vcn.kasm_vcn.default_route_table_id - dhcp_options_id = oci_core_vcn.kasm_vcn.default_dhcp_options_id +data "oci_core_subnet" "data-kasm_db_subnet" { + subnet_id = oci_core_subnet.kasm-db-subnet.id } -resource "oci_core_subnet" "kasm-webapp-subnet-2" { - availability_domain = data.oci_identity_availability_domain.ad2.name - cidr_block = var.webapp_cidr_2 - display_name = "${var.project_name}-kasm-webapp-subnet-2" - dns_label = "${var.project_name}webapp2" - security_list_ids = [oci_core_security_list.allow_web.id, oci_core_security_list.allow_ssh.id] +## Will create WebApp subnets x.x.1.x/24 and x.x.2.x/24 (assuming a VPC Subnet CIDR between x.x.0.0/16 and x.x.0.0/21, and 2 WebApps) +resource "oci_core_subnet" "kasm-webapp-subnets" { + count = var.num_webapps compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.kasm_vcn.id - route_table_id = oci_core_vcn.kasm_vcn.default_route_table_id - dhcp_options_id = oci_core_vcn.kasm_vcn.default_dhcp_options_id + vcn_id = data.oci_core_vcn.data-kasm_vcn.id + route_table_id = oci_core_route_table.default_route_table.id + dhcp_options_id = data.oci_core_vcn.data-kasm_vcn.default_dhcp_options_id + availability_domain = data.oci_identity_availability_domains.kasm_ads.availability_domains[count.index].name + cidr_block = cidrsubnet(var.vcn_subnet_cidr, local.kasm_server_subnet_cidr_size, (count.index + 1)) + display_name = "${var.project_name}-webapp-subnet${count.index}" + dns_label = "${var.project_name}webapp${count.index}" + security_list_ids = [ + data.oci_core_security_lists.data-allow_web.security_lists[0].id, + data.oci_core_security_lists.data-allow_ssh.security_lists[0].id + ] } +data "oci_core_subnets" "data-kasm_webapp_subnets" { + count = var.num_webapps + compartment_id = var.compartment_ocid + vcn_id = data.oci_core_vcn.data-kasm_vcn.id + display_name = oci_core_subnet.kasm-webapp-subnets[count.index].display_name +} +## Will create Agent subnet x.x.3.x/24 (assuming a VPC Subnet CIDR between x.x.0.0/16 and x.x.0.0/21) resource "oci_core_subnet" "kasm-agent-subnet" { - availability_domain = data.oci_identity_availability_domain.ad.name - cidr_block = "10.0.30.0/24" - display_name = "${var.project_name}-kasm-agent-subnet" - dns_label = "${var.project_name}agent" - security_list_ids = [oci_core_security_list.allow_web_from_webapp.id, oci_core_security_list.allow_ssh.id] compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.kasm_vcn.id - route_table_id = oci_core_vcn.kasm_vcn.default_route_table_id - dhcp_options_id = oci_core_vcn.kasm_vcn.default_dhcp_options_id -} \ No newline at end of file + vcn_id = data.oci_core_vcn.data-kasm_vcn.id + route_table_id = oci_core_route_table.default_route_table.id + dhcp_options_id = data.oci_core_vcn.data-kasm_vcn.default_dhcp_options_id + availability_domain = data.oci_identity_availability_domains.kasm_ads.availability_domains[0].name + cidr_block = cidrsubnet(var.vcn_subnet_cidr, local.kasm_server_subnet_cidr_size, local.kasm_agent_subnet_id) + display_name = "${var.project_name}-agent-subnet" + dns_label = "${var.project_name}agent" + security_list_ids = [ + data.oci_core_security_lists.data-allow_web_from_webapp.security_lists[0].id, + data.oci_core_security_lists.data-allow_ssh.security_lists[0].id + ] +} + +data "oci_core_subnet" "data-kasm_agent_subnet" { + subnet_id = oci_core_subnet.kasm-agent-subnet.id +} + +## Will create Guac subnet x.x.4.x/24 (assuming a VPC Subnet CIDR between x.x.0.0/16 and x.x.0.0/21) +resource "oci_core_subnet" "kasm-guac-subnet" { + compartment_id = var.compartment_ocid + vcn_id = data.oci_core_vcn.data-kasm_vcn.id + route_table_id = oci_core_route_table.default_route_table.id + dhcp_options_id = data.oci_core_vcn.data-kasm_vcn.default_dhcp_options_id + availability_domain = data.oci_identity_availability_domains.kasm_ads.availability_domains[0].name + cidr_block = cidrsubnet(var.vcn_subnet_cidr, local.kasm_server_subnet_cidr_size, (local.kasm_agent_subnet_id + 1)) + display_name = "${var.project_name}-guac-subnet" + dns_label = "${var.project_name}guac" + security_list_ids = [ + data.oci_core_security_lists.data-allow_web_from_webapp.security_lists[0].id, + data.oci_core_security_lists.data-allow_ssh.security_lists[0].id + ] +} + +data "oci_core_subnet" "data-kasm_guac_subnet" { + subnet_id = oci_core_subnet.kasm-guac-subnet.id +} diff --git a/oci/standard/module/userdata/agent_bootstrap.sh b/oci/standard/module/userdata/agent_bootstrap.sh index 58ebf1b..ca26186 100644 --- a/oci/standard/module/userdata/agent_bootstrap.sh +++ b/oci/standard/module/userdata/agent_bootstrap.sh @@ -11,11 +11,19 @@ echo '/var/swap.1 swap swap defaults 0 0' | tee -a /etc/fstab cd /tmp -PRIVATE_IP=(`hostname -I | cut -d ' ' -f1 | tr -d '\\n'`) +PRIVATE_IP=(`hostname -I | cut -d' ' -f1 | tr -d '\\n'`) wget ${kasm_build_url} -O kasm_workspaces.tar.gz tar -xf kasm_workspaces.tar.gz + +echo "Waiting for Kasm WebApp availability..." +while ! (curl -k https://${manager_address}/api/__healthcheck 2>/dev/null | grep -q true) +do + echo "Waiting for API server..." + sleep 5 +done +echo "WebApp is alive" + bash kasm_release/install.sh -S agent -e -p $PRIVATE_IP -m ${manager_address} -M ${manager_token} echo "Done" - diff --git a/oci/standard/module/userdata/db_bootstrap.sh b/oci/standard/module/userdata/db_bootstrap.sh index 76b1efb..5317314 100644 --- a/oci/standard/module/userdata/db_bootstrap.sh +++ b/oci/standard/module/userdata/db_bootstrap.sh @@ -15,7 +15,6 @@ PRIVATE_IP=(`hostname -I | cut -d ' ' -f1 | tr -d '\\n'`) wget ${kasm_build_url} -O kasm_workspaces.tar.gz tar -xf kasm_workspaces.tar.gz -bash kasm_release/install.sh -S db -e -Q ${database_password} -R ${redis_password} -U ${user_password} -P ${admin_password} -M ${manager_token} +bash kasm_release/install.sh -S db -e -Q ${database_password} -R ${redis_password} -U ${user_password} -P ${admin_password} -M ${manager_token} -k ${service_registration_token} echo "Done" - diff --git a/oci/standard/module/userdata/guac_bootstrap.sh b/oci/standard/module/userdata/guac_bootstrap.sh new file mode 100644 index 0000000..484d063 --- /dev/null +++ b/oci/standard/module/userdata/guac_bootstrap.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -ex +echo "Starting Kasm Workspaces Agent Install" + +/bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=${swap_size} +/sbin/mkswap /var/swap.1 +chmod 600 /var/swap.1 +/sbin/swapon /var/swap.1 + +echo '/var/swap.1 swap swap defaults 0 0' | tee -a /etc/fstab + +cd /tmp + +PRIVATE_IP=(`hostname -I | cut -d ' ' -f1 | tr -d '\\n'`) + +wget ${kasm_build_url} -O kasm_workspaces.tar.gz +tar -xf kasm_workspaces.tar.gz + +echo "Waiting for Kasm WebApp availability..." +while ! (curl -k https://${manager_address}/api/__healthcheck 2>/dev/null | grep -q true) +do + echo "Waiting for API server..." + sleep 5 +done +echo "WebApp is alive" + +bash kasm_release/install.sh -S guac -e -p $PRIVATE_IP -n ${manager_address} -k ${service_registration_token} + +echo "Done" diff --git a/oci/standard/module/userdata/webapp_bootstrap.sh b/oci/standard/module/userdata/webapp_bootstrap.sh index dfd0b23..5f781f5 100644 --- a/oci/standard/module/userdata/webapp_bootstrap.sh +++ b/oci/standard/module/userdata/webapp_bootstrap.sh @@ -16,15 +16,21 @@ PRIVATE_IP=(`hostname -I | cut -d ' ' -f1 | tr -d '\\n'`) wget ${kasm_build_url} -O kasm_workspaces.tar.gz tar -xf kasm_workspaces.tar.gz -echo "Checking for Kasm DB..." +echo "Checking for Kasm DB and Redis..." apt-get update && apt-get install -y netcat while ! nc -w 1 -z ${db_ip} 5432; do - echo "Not Ready..." + echo "Database not ready..." sleep 5 done echo "DB is alive" +while ! nc -w 1 -z ${db_ip} 6379; do + echo "Redis not ready..." + sleep 5 +done +echo "Redis is alive" + + bash kasm_release/install.sh -S app -e -z ${zone_name} -q "${db_ip}" -Q ${database_password} -R ${redis_password} echo "Done" - diff --git a/oci/standard/module/variables.tf b/oci/standard/module/variables.tf index f621f6a..f0d19ad 100644 --- a/oci/standard/module/variables.tf +++ b/oci/standard/module/variables.tf @@ -1,156 +1,183 @@ -variable "project_name" { - description = "The name of the deployment (e.g dev, staging). A short single word" -} - -variable "oci_domain_name" { - description = "The public Zone used for the dns entries. This must already exist in the OCI account. (e.g kasm.contoso.com). The deployment will be accessed via this zone name via https" -} - -variable kasm_build_url { - description = "The URL for the Kasm Workspaces build" -} - - -variable swap_size { - description = "The amount of swap (in MB) to configure inside the compute instances" - default = 2048 -} - - variable "tenancy_ocid" { description = "The Tenancy OCID." -} - -variable "user_ocid" { - description = "The User OCID." + type = string } variable "compartment_ocid" { description = "The Compartment OCID" + type = string } variable "region" { description = "The OCI Region eg: (us-ashburn-1)" + type = string +} + +variable "user_ocid" { + description = "The User OCID." + type = string } variable "fingerprint" { description = "API Key Fingerprint" + type = string } variable "private_key_path" { description = "The path to the API Key PEM encoded Private Key" + type = string + sensitive = true } -variable "ssh_authorized_keys" { - description = "The file path to the authorized_keys file that contains SSH public keys to be installed on the OCI compute instance" +variable "project_name" { + description = "The name of the deployment (e.g dev, staging). A short single word" + type = string } -variable "instance_image_ocid" { - description = "The OCID for the instance image , such as ubuntu 18.04, to use." - default = "ocid1.image.oc1.iad.aaaaaaaafg6lg7dejwjebjqontwzyvutgf6qs5awyze6fgoiqepyj5qkvcuq" +variable "oci_domain_name" { + description = "The public Zone used for the dns entries. This must already exist in the OCI account. (e.g kasm.contoso.com). The deployment will be accessed via this zone name via https" + type = string } - -variable "allow_ssh_cidr" { - description = "The CIDR notation to allow SSH access to the systems." - default = "0.0.0.0/0" +variable "letsencrypt_cert_support_email" { + description = "Email address to use for Let's Encrypt SSL certificates for OCI Deployment" + type = string } - -variable "allow_web_cidr" { - description = "The CIDR notation to allow HTTPS access to the systems." - default = "0.0.0.0/0" +variable "letsencrypt_server_type" { + description = "SSL Server type to generate. Valid options are staging and prod, and prod certificates are limited to 5 certificates per week." + type = string } variable "kasm_ssl_crt_path" { description = "The file path to the PEM encoded SSL Certificate" + type = string } variable "kasm_ssl_key_path" { description = "The file path to the PEM encoded SSL Certificate Key" + type = string + sensitive = true } - -variable "instance_shape" { - description = "The instance shape to use. Should be a Flex type." - default = "VM.Standard.E3.Flex" +variable "vcn_subnet_cidr" { + description = "VCN Subnet CIDR where you wish to deploy Kasm" + type = string } -variable "db_boot_size_gb" { - description = "The size, in GB, of the DB instance drive" - default = 50 +variable "ssh_authorized_keys" { + description = "The SSH Public Keys to be installed on the OCI compute instance" + type = string } -variable "webapp_boot_size_gb" { - description = "The size, in GB, of the Webapp instance drives" - default = 50 +variable "instance_image_ocid" { + description = "The OCID for the instance image , such as ubuntu 20.04, to use." + type = string } -variable "agent_boot_size_gb" { - description = "The size, in GB, of the Agemt instance drives" - default = 60 +variable "allow_ssh_cidrs" { + description = "The CIDR notation to allow SSH access to the systems." + type = list(string) } +variable "allow_web_cidrs" { + description = "The CIDR notation to allow HTTPS access to the systems." + type = list(string) +} variable "num_agents" { - default = "2" description = "The number of Agent Role Servers to create in the deployment" + type = number } - variable "num_webapps" { - default = "2" description = "The number of WebApp role servers to create in the deployment" + type = number } - -variable "agent_ocpus" { - description = "The number of CPUs to configure for the Agent instances" - default = 2 +variable "num_guac_rdp_nodes" { + description = "The number of WebApp role servers to create in the deployment" + type = number } -variable "db_ocpus" { - description = "The number of CPUs to configure for the DB instance" - default = 1 +variable "database_password" { + description = "The password for the database. No special characters" + type = string + sensitive = true } -variable "webapp_ocpus" { - description = "The number of CPUs to configure for the Webapp instances" - default = 1 +variable "redis_password" { + description = "The password for the Redis server. No special characters" + type = string + sensitive = true } -variable "agent_memory_in_gb" { - description = "The amount of memory, in GB, to configure for the Agent instances" - default = 4 +variable "user_password" { + description = "The standard (non administrator) user password. No special characters" + type = string + sensitive = true } -variable "webapp_memory_in_gb" { - description = "The amount of memory, in GB, to configure for the Webapp instances" - default = 2 -} - -variable "db_memory_in_gb" { - description = "The amount of memory, in GB, to configure for the DB instance" - default = 2 +variable "admin_password" { + description = "The administrative user password. No special characters" + type = string + sensitive = true } variable "manager_token" { - description = "The password for the database. No special characters" + description = "The manager token value for Agents to authenticate to webapps. No special characters" + type = string + sensitive = true } -variable "database_password" { - description = "The password for the database. No special characters" + +variable "service_registration_token" { + description = "The service registration token value for Guac RDP servers to authenticate to webapps. No special characters" + type = string + sensitive = true } -variable "redis_password" { - description = "The password for the database. No special characters" + +variable "kasm_build_url" { + description = "The URL for the Kasm Workspaces build" + type = string } -variable "user_password" { - description = "The password for the database. No special characters" + +variable "swap_size" { + description = "The amount of swap (in MB) to configure inside the compute instances" + type = number } -variable "admin_password" { - description = "The password for the database. No special characters" + +variable "instance_shape" { + description = "The instance shape to use. Should be a Flex type." + type = string } -variable "webapp_cidr_1" { - default = "10.0.20.0/24" + +variable "kasm_webapp_vm_settings" { + description = "The amount of memory, in GB, to configure for the Kasm WebApp instance" + type = map(any) +} + +variable "kasm_database_vm_settings" { + description = "The amount of memory, in GB, to configure for the Kasm DB instance" + type = map(any) +} + +variable "kasm_agent_vm_settings" { + description = "The amount of memory, in GB, to configure for the Kasm Agent instance" + type = map(any) +} + +variable "kasm_guac_vm_settings" { + description = "The number of CPUs, amount of memory in GB, and HDD size in GB to configure for the Kasm Guac RDP instance" + type = map(any) +} + +## Pre-set values +variable "anywhere" { + description = "Anywhere route subnet" + type = list(string) + default = ["0.0.0.0/0"] + + validation { + condition = can([for subnet in var.anywhere : cidrhost(subnet, 0)]) + error_message = "Anywhere variable must be valid IPv4 CIDR - usually 0.0.0.0/0 for all default routes and default Security Group access." + } } -variable "webapp_cidr_2" { - default = "10.0.25.0/24" -} \ No newline at end of file diff --git a/oci/standard/module/vcn.tf b/oci/standard/module/vcn.tf index 23a7605..de2acb8 100644 --- a/oci/standard/module/vcn.tf +++ b/oci/standard/module/vcn.tf @@ -1,4 +1,3 @@ - resource "oci_core_vcn" "kasm_vcn" { cidr_block = "10.0.0.0/16" compartment_id = var.compartment_ocid @@ -6,7 +5,9 @@ resource "oci_core_vcn" "kasm_vcn" { dns_label = "${var.project_name}vcn" } - +data "oci_core_vcn" "data-kasm_vcn" { + vcn_id = oci_core_vcn.kasm_vcn.id +} resource "oci_core_internet_gateway" "kasm_internet_gateway" { compartment_id = var.compartment_ocid @@ -14,25 +15,29 @@ resource "oci_core_internet_gateway" "kasm_internet_gateway" { vcn_id = oci_core_vcn.kasm_vcn.id } -resource "oci_core_default_route_table" "default_route_table" { - manage_default_resource_id = oci_core_vcn.kasm_vcn.default_route_table_id - display_name = "DefaultRouteTable" +data "oci_core_internet_gateways" "data-kasm_internet_gateway" { + compartment_id = var.compartment_ocid + vcn_id = data.oci_core_vcn.data-kasm_vcn.id +} + +resource "oci_core_route_table" "default_route_table" { + compartment_id = var.compartment_ocid + vcn_id = data.oci_core_vcn.data-kasm_vcn.id + display_name = "KasmRouteTable" route_rules { - destination = "0.0.0.0/0" + destination = var.anywhere[0] destination_type = "CIDR_BLOCK" - network_entity_id = oci_core_internet_gateway.kasm_internet_gateway.id + network_entity_id = oci_core_internet_gateway.kasm_internet_gateway.id #data.oci_core_internet_gateways.data-kasm_internet_gateway.gateways[0].id } } +# data "oci_core_route_tables" "data-default_route_table" { +# compartment_id = var.compartment_ocid +# vcn_id = data.oci_core_vcn.data-kasm_vcn.id +# display_name = oci_core_route_table.default_route_table.display_name +# } - -data "oci_identity_availability_domain" "ad" { +data "oci_identity_availability_domains" "kasm_ads" { compartment_id = var.tenancy_ocid - ad_number = 1 -} - -data "oci_identity_availability_domain" "ad2" { - compartment_id = var.tenancy_ocid - ad_number = 2 } diff --git a/oci/standard/module/webapp.tf b/oci/standard/module/webapp.tf index e227c31..380a120 100644 --- a/oci/standard/module/webapp.tf +++ b/oci/standard/module/webapp.tf @@ -1,29 +1,17 @@ -data "template_file" "webapp_user_data" { - template = "${file("${path.module}/userdata/webapp_bootstrap.sh")}" - vars = { - kasm_build_url = var.kasm_build_url - db_ip = oci_core_instance.kasm_db_instance.private_ip - database_password = var.database_password - redis_password = var.redis_password - swap_size = var.swap_size - zone_name = "default" - } -} - resource "oci_core_instance" "kasm_webapp_instance" { count = var.num_webapps - availability_domain = data.oci_identity_availability_domain.ad.name + availability_domain = data.oci_identity_availability_domains.kasm_ads.availability_domains[count.index].name compartment_id = var.compartment_ocid display_name = "${var.project_name}-Kasm-Webapp-${count.index}" shape = var.instance_shape shape_config { - ocpus = var.webapp_ocpus - memory_in_gbs = var.webapp_memory_in_gb + ocpus = var.kasm_webapp_vm_settings.cpus + memory_in_gbs = var.kasm_webapp_vm_settings.memory } create_vnic_details { - subnet_id = oci_core_subnet.kasm-webapp-subnet.id + subnet_id = data.oci_core_subnets.data-kasm_webapp_subnets[count.index].subnets[0].id display_name = "${var.project_name}-Primaryvnic" assign_public_ip = true assign_private_dns_record = true @@ -31,18 +19,28 @@ resource "oci_core_instance" "kasm_webapp_instance" { } source_details { - source_type = "image" - source_id = var.instance_image_ocid - boot_volume_size_in_gbs = var.webapp_boot_size_gb + source_type = "image" + source_id = var.instance_image_ocid + boot_volume_size_in_gbs = var.kasm_webapp_vm_settings.hdd_size_gb } - metadata = { - ssh_authorized_keys = file("${var.ssh_authorized_keys}") - user_data = base64encode("${data.template_file.webapp_user_data.rendered}") + ssh_authorized_keys = var.ssh_authorized_keys + user_data = base64encode(templatefile("${path.module}/userdata/webapp_bootstrap.sh", + { + kasm_build_url = var.kasm_build_url + db_ip = data.oci_core_instance.data-kasm_db_instance.private_ip + database_password = var.database_password + redis_password = var.redis_password + swap_size = var.swap_size + zone_name = "default" + } + )) } } - - +data "oci_core_instance" "data-kasm_webapp_instances" { + count = var.num_webapps + instance_id = oci_core_instance.kasm_webapp_instance[count.index].id +} diff --git a/oci/standard/provider.tf b/oci/standard/provider.tf new file mode 100644 index 0000000..e65d81e --- /dev/null +++ b/oci/standard/provider.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + oci = { + source = "oracle/oci" + version = ">= 4.0.0" + } + acme = { + source = "vancluever/acme" + version = ">= 2.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0.0" + } + } +} + +provider "oci" { + tenancy_ocid = var.tenancy_ocid + user_ocid = var.user_ocid + fingerprint = var.fingerprint + private_key_path = var.private_key_path + region = var.region +} + +provider "acme" { + server_url = local.letsencrypt_server_url +} diff --git a/oci/standard/settings.tfvars b/oci/standard/settings.tfvars new file mode 100644 index 0000000..b8dc804 --- /dev/null +++ b/oci/standard/settings.tfvars @@ -0,0 +1,69 @@ +## Kasm deployment settings +oci_domain_name = "kasm.contoso.com" +project_name = "contoso" +kasm_build_url = "https://kasm-static-content.s3.amazonaws.com/kasm_release_1.12.0.d4fd8a.tar.gz" +vcn_subnet_cidr = "10.0.0.0/16" + +## OCI Authentication variables +tenancy_ocid = "" +user_ocid = "" +compartment_ocid = "" +fingerprint = "" +region = "us-ashburn-1" +private_key_path = "./oci-private-key.pem" + +## Load Balancer SSL Keys +# Terraform auto-generated Let's Encrypt keys +letsencrypt_cert_support_email = "" +letsencrypt_server_type = "" + +# Bring your own - Load Balancer SSL Keys +kasm_ssl_crt_path = "" +kasm_ssl_key_path = "" + +## VM Access subnets +allow_ssh_cidrs = ["0.0.0.0/0"] +allow_web_cidrs = ["0.0.0.0/0"] + +## Kasm passwords +manager_token = "changeme" +admin_password = "changeme" +user_password = "changeme" +redis_password = "changeme" +database_password = "changeme" +service_registration_token = "changeme" + +## SSH Public Keys +ssh_authorized_keys = "changeme" + +## OCI VM Settings +instance_image_ocid = "ocid1.image.oc1.iad.aaaaaaaahiz6xym3a76xhwkmwmhrz6luyiehho7dpxpkphxhsq5q6z4m3nlq" +instance_shape = "VM.Standard.E4.Flex" +swap_size = 2048 +num_webapps = 2 +num_agents = 2 +num_guac_rdp_nodes = 1 + +kasm_webapp_vm_settings = { + cpus = 2 + memory = 2 + hdd_size_gb = 50 +} + +kasm_database_vm_settings = { + cpus = 2 + memory = 2 + hdd_size_gb = 50 +} + +kasm_agent_vm_settings = { + cpus = 4 + memory = 8 + hdd_size_gb = 120 +} + +kasm_guac_vm_settings = { + cpus = 4 + memory = 4 + hdd_size_gb = 50 +} \ No newline at end of file diff --git a/oci/standard/variables.tf b/oci/standard/variables.tf new file mode 100644 index 0000000..59f7e2d --- /dev/null +++ b/oci/standard/variables.tf @@ -0,0 +1,399 @@ +variable "tenancy_ocid" { + description = "The Tenancy OCID." + type = string + + validation { + condition = can(regex("^(ocid\\d)\\.(compartment|tenancy)\\.(oc\\d)[.]{1,}[a-z0-9]{60}", var.tenancy_ocid)) + error_message = "The tenancy_ocid must be a valid Oracle Cloud Tenancy OCID value (e.g. ocid1.tenancy.oc1..aaaaaaaaba3pv6wkcr4jqae5f44n2b2m2yt2j6rx32uzr4h25vqstifsfdsq)." + } +} + +variable "user_ocid" { + description = "The User OCID." + type = string + + validation { + condition = can(regex("^(ocid\\d)\\.(user)\\.(oc\\d)[.]{1,}[a-z0-9]+", var.user_ocid)) + error_message = "The variable user_ocid must be a valid Oracle Cloud User OCID." + } +} + +variable "compartment_ocid" { + description = "The Compartment OCID" + type = string + + validation { + condition = can(regex("^(ocid\\d)\\.(compartment|tenancy)\\.(oc\\d)[.]+[a-z0-9]{60}", var.compartment_ocid)) + error_message = "The compartment_ocid must be a valid Oracle Cloud Compartment OCID value (e.g. ocid1.compartment.oc1..aaaaaaaaba3pv6wkcr4jqae5f44n2b2m2yt2j6rx32uzr4h25vqstifsfdsq)." + } +} + +variable "region" { + description = "The OCI Region eg: (us-ashburn-1)" + type = string + + validation { + condition = can(regex("^([a-z]{2}-[a-z]{5,}-[\\d]{1})$", var.region)) + error_message = "The provider-oci_deployment_region must be a valid Oracle Cloud (OCI) Region name, e.g. us-ashburn-1" + } +} + +variable "fingerprint" { + description = "API Key Fingerprint" + type = string + + validation { + condition = can(regex("^([a-f0-9]{2}:?){16}$", var.fingerprint)) + error_message = "The API fingerprint is incorrectly formatted. It should be 16 colon-delimited hex bytes (e.g. 12:34:56:78:90:ab:cd:ef:12:34:56:78:90:ab:cd:ef)." + } +} + +variable "private_key_path" { + description = "The path to the API Key PEM encoded Private Key" + type = string + sensitive = true + + validation { + condition = can(fileexists(var.private_key_path)) + error_message = "The variable private_key_path must point to a valid OCI API Key file." + } + + validation { + condition = !can(regex("replaceme", file(var.private_key_path))) + error_message = "You must enter a valid OCI API Private key in the file located at the private_key_path." + } +} +variable "project_name" { + description = "The name of the deployment (e.g dev, staging). A short single word" + type = string + + validation { + condition = can(regex("^[a-z]{1,15}", var.project_name)) + error_message = "The project_name variable can only be one word between 1 and 15 lower-case letters since it is a seed value in multiple object names." + } +} + +variable "oci_domain_name" { + description = "The public Zone used for the dns entries. This must already exist in the OCI account. (e.g kasm.contoso.com). The deployment will be accessed via this zone name via https" + type = string + + validation { + condition = can(regex("^[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,6}", var.oci_domain_name)) + error_message = "There are invalid characters in the oci_domain_name - it must be a valid domain name." + } +} + +variable "vcn_subnet_cidr" { + description = "VCN Subnet CIDR where you wish to deploy Kasm" + type = string + + validation { + condition = can(cidrhost(var.vcn_subnet_cidr, 0)) + error_message = "The vcn_subnet_cidr must be a valid IPv4 Subnet in CIDR notation (e.g. 10.0.0.0/24)." + } +} + +variable "ssh_authorized_keys" { + description = "The SSH Public Keys to be installed on the OCI compute instance" + type = string + + validation { + condition = can(regex("^ssh-rsa\\s+[A-Za-z0-9+/]+[=]{0,3}(\\s+.+)?\\s*$", var.ssh_authorized_keys)) + error_message = "The ssh_authorized_keys value is not in the correct format." + } +} + +variable "instance_image_ocid" { + description = "The OCID for the instance image , such as ubuntu 20.04, to use." + default = "ocid1.image.oc1.iad.aaaaaaaafg6lg7dejwjebjqontwzyvutgf6qs5awyze6fgoiqepyj5qkvcuq" + + validation { + condition = can(regex("^(ocid\\d)\\.(image)\\.(oc\\d)\\.[a-z]{3,}\\.[a-z0-9]{60}", var.instance_image_ocid)) + error_message = "The compartment_ocid must be a valid Oracle Cloud Compartment OCID value (e.g. ocid1.image.oc1.iad.aaaaaaaafg6lg7dejwjebjqontwzyvutgf6qs5awyze6fgoiqepyj5qkvcuq)." + } +} + +variable "allow_ssh_cidrs" { + description = "The CIDR notation to allow SSH access to the systems." + type = list(string) + + validation { + condition = can([for subnet in var.allow_ssh_cidrs : cidrhost(subnet, 0)]) + error_message = "One of the subnets provided in the allow_ssh_cidrs list is invalid." + } +} + +variable "allow_web_cidrs" { + description = "The CIDR notation to allow HTTPS access to the systems." + type = list(string) + + validation { + condition = can([for subnet in var.allow_web_cidrs : cidrhost(subnet, 0)]) + error_message = "One of the subnets provided in the allow_web_cidrs list is invalid." + } +} + +variable "letsencrypt_cert_support_email" { + description = "Email address to use for Let's Encrypt SSL certificates for OCI Deployment" + type = string + default = "" + + validation { + condition = can(var.letsencrypt_cert_support_email == "" ? true : can(regex("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", var.letsencrypt_cert_support_email))) + error_message = "The ssl_cert_support_email must be a valid email address format." + } +} + +variable "letsencrypt_server_type" { + description = "SSL Server type to generate. Valid options are staging and prod, and prod certificates are limited to 5 certificates per week." + type = string + default = "" + + validation { + condition = can(contains(["staging", "prod", ""], var.letsencrypt_server_type)) + error_message = "Allowed values for the letsencrypt_server_type variable are: staging, prod. For reference: Staging generates certificates that a browser will not trust, but are formatted correctly to apply to resources; while Prod generates valid, useable, trusted certificates. NOTE: Prod certificate generation is limited to 5 times per week, so if you are testing Kasm or intend to possibly re-deploy multiple times, it is recommended to use Staging (which has a much higher generation limit since it is intended for testing) until you are ready to deploy your 'final' version." + } +} + +variable "kasm_ssl_crt_path" { + description = "The file path to the PEM encoded SSL Certificate" + type = string + default = "" + + validation { + condition = can(var.kasm_ssl_crt_path == "" ? true : can(fileexists(var.kasm_ssl_crt_path))) + error_message = "The variable kasm_ssl_crt_path must point to a valid OCI API Key file." + } + + validation { + condition = can(var.kasm_ssl_crt_path == "" ? true : !can(regex("replaceme", file(var.kasm_ssl_crt_path)))) + error_message = "You must enter a valid SSL Cert in the file located at the kasm_ssl_crt_path." + } +} + +variable "kasm_ssl_key_path" { + description = "The file path to the PEM encoded SSL Certificate Key" + type = string + sensitive = true + default = "" + + validation { + condition = can(var.kasm_ssl_key_path == "" ? true : can(fileexists(var.kasm_ssl_key_path))) + error_message = "The variable kasm_ssl_key_path must point to a valid OCI API Key file." + } + + validation { + condition = can(var.kasm_ssl_key_path == "" ? true : !can(regex("replaceme", file(var.kasm_ssl_key_path)))) + error_message = "You must enter a valid SSL Cert in the file located at the kasm_ssl_key_path." + } +} + +variable "database_password" { + description = "The password for the database. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.database_password)) + error_message = "The Database Password should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "redis_password" { + description = "The password for the Redis server. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.redis_password)) + error_message = "The Redis Password should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "user_password" { + description = "The standard (non administrator) user password. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.user_password)) + error_message = "The User Password should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "admin_password" { + description = "The administrative user password. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.admin_password)) + error_message = "The Admin password should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "manager_token" { + description = "The manager token value for Agents to authenticate to webapps. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.manager_token)) + error_message = "The Manager Token should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "service_registration_token" { + description = "The service registration token value for Guac RDP servers to authenticate to webapps. No special characters" + type = string + sensitive = true + + validation { + condition = can(regex("^[a-zA-Z0-9]{12,30}$", var.service_registration_token)) + error_message = "The Service Registration Token should be a string between 12 and 30 letters or numbers with no special characters." + } +} + +variable "num_agents" { + description = "The number of Agent Role Servers to create in the deployment" + type = number + + validation { + condition = var.num_agents >= 0 && var.num_agents <= 100 && floor(var.num_agents) == var.num_agents + error_message = "Acceptable number of Kasm Agents range between 0-100." + } +} + +variable "num_guac_rdp_nodes" { + description = "The number of Guac RDP Role Servers to create in the deployment" + type = number + + validation { + condition = var.num_guac_rdp_nodes >= 0 && var.num_guac_rdp_nodes <= 100 && floor(var.num_guac_rdp_nodes) == var.num_guac_rdp_nodes + error_message = "Acceptable number of Kasm Guac RDP nodes range between 0-100." + } +} + +variable "num_webapps" { + description = "The number of WebApp role servers to create in the deployment" + type = number + + validation { + condition = var.num_webapps >= 1 && var.num_webapps <= 3 && floor(var.num_webapps) == var.num_webapps + error_message = "Acceptable number of Kasm Agents range between 1-3." + } +} + +variable "swap_size" { + description = "The amount of swap (in MB) to configure inside the compute instances" + type = number + + validation { + condition = var.swap_size >= 1024 && var.swap_size <= 8192 && floor(var.swap_size) == var.swap_size + error_message = "Swap size is the amount of disk space to use for Kasm in MB and must be an integer between 1024 and 8192." + } +} + +variable "kasm_webapp_vm_settings" { + description = "The number of CPUs, amount of memory in GB, and HDD size in GB to configure for the Kasm WebApp instances" + type = object({ + cpus = number + memory = number + hdd_size_gb = number + }) + + validation { + condition = can(var.kasm_webapp_vm_settings.cpus >= 2) + error_message = "Kasm Webapps should have at least 2 CPUs to ensure enough resources for Kasm services." + } + validation { + condition = can(var.kasm_webapp_vm_settings.memory >= 2) + error_message = "Kasm Webapps should have at least 2 GB Memory to ensure enough resources for Kasm services." + } + validation { + condition = can(var.kasm_webapp_vm_settings.hdd_size_gb >= 50) + error_message = "Kasm Webapps should have at least a 50 GB HDD to meet OCI minimum requirements, and ensure enough space Kasm services." + } +} + +variable "kasm_database_vm_settings" { + description = "The number of CPUs, amount of memory in GB, and HDD size in GB to configure for the Kasm Database instance" + type = object({ + cpus = number + memory = number + hdd_size_gb = number + }) + + validation { + condition = can(var.kasm_database_vm_settings.cpus >= 2) + error_message = "Kasm Webapps should have at least 2 CPUs to ensure enough resources for Kasm services." + } + validation { + condition = can(var.kasm_database_vm_settings.memory >= 2) + error_message = "Kasm Webapps should have at least 2 GB Memory to ensure enough resources for Kasm services." + } + validation { + condition = can(var.kasm_database_vm_settings.hdd_size_gb >= 50) + error_message = "Kasm Webapps should have at least a 50 GB HDD to meet OCI minimum requirements, and ensure enough space Kasm services." + } +} + +variable "kasm_agent_vm_settings" { + description = "The number of CPUs, amount of memory in GB, and HDD size in GB to configure for the Kasm Agent instances" + type = object({ + cpus = number + memory = number + hdd_size_gb = number + }) + + validation { + condition = can(var.kasm_agent_vm_settings.cpus >= 2) + error_message = "Kasm Agents should have at least 2 CPUs to ensure enough resources for Kasm services. More CPU is definitely better for the Kasm Agent as it is the VM that runs your Kasm workspaces." + } + validation { + condition = can(var.kasm_agent_vm_settings.memory >= 2) + error_message = "Kasm Agents should have at least 4 GB Memory to ensure enough resources for Kasm services. More Memory is definitely better for the Kasm Agent as it is the VM that runs your Kasm workspaces." + } + validation { + condition = can(var.kasm_agent_vm_settings.hdd_size_gb >= 120) + error_message = "Kasm Agents should have at least a 120 GB HDD to meet OCI minimum requirements, and ensure enough space to pull the default Kasm workspace images." + } +} + +variable "kasm_guac_vm_settings" { + description = "The number of CPUs, amount of memory in GB, and HDD size in GB to configure for the Kasm Guac RDP instances" + type = object({ + cpus = number + memory = number + hdd_size_gb = number + }) + + validation { + condition = can(var.kasm_guac_vm_settings.cpus >= 4) + error_message = "Kasm Guac RDP servers should have at least 4 CPUs to ensure enough resources for Kasm services." + } + validation { + condition = can(var.kasm_guac_vm_settings.memory >= 4) + error_message = "Kasm Guac RDP servers should have at least 4 GB Memory to ensure enough resources for Kasm services." + } + validation { + condition = can(var.kasm_guac_vm_settings.hdd_size_gb >= 50) + error_message = "Kasm Guac RDP servers should have at least a 50 GB HDD to meet OCI minimum requirements, and ensure enough space Kasm services." + } +} + +## Non-validated variables +variable "kasm_build_url" { + description = "The URL for the Kasm Workspaces build" + type = string +} + +variable "instance_shape" { + description = "The instance shape to use. Should be a Flex type." + type = string +} + +## Local for Let's Encrypt staging/prod URL selection +locals { + letsencrypt_server_url = var.letsencrypt_server_type == "prod" ? "https://acme-v02.api.letsencrypt.org/directory" : "https://acme-staging-v02.api.letsencrypt.org/directory" +}