Launching a Windows EC2 Instance using Terraform

Launching a Windows EC2 Instance using Terraform

Terraform by HashiCorp is a popular Infrastructure as Code (IaC) tool for provisioning and managing cloud infrastructure. This blog post demonstrates how to launch a Windows server EC2 instance in AWS using Terraform.

Features

  • Windows Server 2019 EC2 instance
  • The latest AMI
  • KMS encrypted EBS volume
  • SSM managed EC2 instance
  • User data automation: Google Chrome, Anaconda, etc.

Terraform module

Download an entire Terraform Windows EC2 instance module code from GitHub. I’m using it whenever I need a Windows Jumpbox in AWS:

variable "prefix" {
  type        = string
  description = "Prefix for resources created by this module"
}
variable "tags" {
  description = "A map of tags to add to all resources"
  type        = map(string)
  default     = {
    Terraform   = "true"
    Environment = "dev"
  }
}
variable "vpc_id" {
  type        = string
  description = "VPC ID"
}
variable "vpc_subnet_id" {
  type        = string
  description = "VPC Subnet ID"
}
variable "ec2_policy_name" {
  type        = string
  description = "EC2 Policy Name"
  default     = "AdministratorAccess"
}
variable "ec2_instance_type" {
  type        = string
  description = "EC2 Instance Type"
  default     = "m5.large"
}
variable "ec2_volume_size" {
  type        = number
  description = "EC2 Volume Size"
  default     = 50
}
variable "ec2_enhanced_monitoring" {
  type        = bool
  description = "Enable enhanced monitoring"
  default     = false
}
variable "ec2_associate_public_ip_address" {
  type        = bool
  description = "Associate public IP address"
  default     = false
}
locals {
  aws_account_id  = data.aws_caller_identity.current.account_id
  current_identity = data.aws_caller_identity.current.arn
  ec2_policy_arn = "arn:aws:iam::aws:policy/${var.ec2_policy_name}"
  prefix      = "${var.prefix}-windows-jumphost"
  tags        = merge(
    var.tags,
    {
      Name = local.prefix
    }
  )
  vpc_id      = var.vpc_id
  vpc_subnet_id = var.vpc_subnet_id
}
data "aws_ami" "latest_amazon_linux" {
  most_recent = true
  filter {
    name   = "name"
    values = ["Windows_Server-2019-English-Full-Base-*"]
  }
  filter {
    name   = "owner-alias"
    values = ["amazon"]
  }
}
data "aws_caller_identity" "current" {}
resource "aws_security_group" "ec2_sg" {
  name        = "${local.prefix}-sg"
  description = "Security group for EC2 instance"
  vpc_id      = local.vpc_id
  egress {
    description = "Allow outbound traffic"
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = [
      "0.0.0.0/0"
    ]
  }
}
resource "aws_instance" "jumphost" {
  ami           = data.aws_ami.latest_amazon_linux.id
  instance_type = "m5.large"
  iam_instance_profile      = aws_iam_instance_profile.this.name
  subnet_id                 = local.vpc_subnet_id
  vpc_security_group_ids    = [aws_security_group.ec2_sg.id]
  ebs_optimized = true
  root_block_device {
    volume_size = 100
    encrypted = true
    kms_key_id = module.kms.key_arn
  }
  monitoring = true
  metadata_options {
    http_endpoint = "enabled"
    http_tokens   = "required"
  }
  user_data = <<-EOF
    <powershell>
    Set-ExecutionPolicy Bypass -Scope Process -Force;
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072;
    Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'));
    choco install googlechrome -y --force --ignore-checksums -y;
    # pgAdmin4
    # choco install pgadmin4 -y;
    # Download and install Anaconda
    # ATTENTION: This action might take a while!
    # choco install anaconda3 -y;
    </powershell>
  EOF
  associate_public_ip_address = false
  tags = merge(
    local.tags,
    {
      Name = local.prefix
    }
  )
}
module "kms" {
  source = "git::https://github.com/terraform-aws-modules/terraform-aws-kms.git?ref=5508c9cdd6fdb0ed4dcf399f54ba02fb8c31bd4b"  # commit hash of version 2.1.0
  description = "EC2 AutoScaling key usage"
  key_usage   = "ENCRYPT_DECRYPT"
  # Policy
  key_owners                              = [local.current_identity]
  key_administrators                      = [local.current_identity]
  key_service_users                       = [aws_iam_role.this.arn]
  # Aliases
  aliases = ["${local.prefix}/ebs"]
  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}
resource "aws_iam_policy" "kms_policy" {
  name        = "${local.prefix}-kms-policy"
  description = "Policy for KMS key"
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action = [
          "kms:Encrypt",
          "kms:Decrypt",
          "kms:ReEncrypt*",
          "kms:GenerateDataKey*",
          "kms:DescribeKey"
        ],
        Resource = [
          module.kms.key_arn
        ],
        Effect = "Allow"
      }
    ]
  })
}
data "aws_iam_policy_document" "trust_policy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}
resource "aws_iam_role" "this" {
  name = "${local.prefix}-role"
  assume_role_policy = data.aws_iam_policy_document.trust_policy.json
}
resource "aws_iam_role_policy_attachment" "ssm_kms_policy_attachment" {
  role       = aws_iam_role.this.name
  policy_arn = aws_iam_policy.kms_policy.arn
}
resource "aws_iam_role_policy_attachment" "policy_attachment" {
  # This Jumphost is used for demo dev projects, allowing Administrator permissions for flexibility
  # checkov:skip=CKV_AWS_274: "Disallow IAM roles, users, and groups from using the AWS AdministratorAccess policy"
  role       = aws_iam_role.this.name
  policy_arn = local.ec2_policy_arn
}
resource "aws_iam_instance_profile" "this" {
  name = "${local.prefix}-instance-profile"
  role = aws_iam_role.this.name
}
output "jumphost_id" {
  value = aws_instance.jump_host.id
}
output "ssm_command_jumphost_pwd_reset" {
  value = "aws ssm start-session --target ${aws_instance.jump_host.id} --document-name 'AWS-PasswordReset' --parameters username='Administrator'"
}
output "ssm_command_jumphost_port_forward" {
  value = "aws ssm start-session --target ${aws_instance.jump_host.id} --document-name 'AWS-StartPortForwardingSession' --parameters portNumber='3389',localPortNumber='53389'"
}
output "rdp_jumphost_fqdn" {
  value = "localhost:53389"
}
output "rdp_jumphost_user" {
  value = "${aws_instance.jump_host.id}\\Administrator"
}
output "security_group_id" {
  value = aws_security_group.ec2_sg.id
}

Understanding the Terraform Code

The provided Terraform script launches a Windows Server 2019 EC2 instance in AWS. Let’s break down the key components:

  1. Local Variables: Sets up various local variables, including AWS account details and tags.
  2. Data Sources:
  • aws_ami: Fetches the latest Amazon Machine Image (AMI) for Windows Server 2019.
  • aws_caller_identity: Retrieves information about the AWS account and the user making the request.
  1. Resource – Security Group: Defines a security group (aws_security_group.ec2_sg) for the EC2 instance with outbound traffic rules.
  2. Resource – EC2 Instance: The aws_instance.jump_host resource launches the EC2 instance using the Windows AMI. Key configurations include:
  • Instance type (m5.large).
  • EBS optimized setting.
  • Encrypted root block device.
  • User data script for initial setup, including installing Google Chrome (and optionally other software).
  1. KMS Module: Utilizes a Terraform module to create a KMS key for encryption purposes.
  2. IAM Resources: Sets up IAM roles, policies, and instance profiles for the EC2 instance, ensuring proper permissions.

Steps to Launch the Instance

  1. Configuration: Replace the placeholders in the script (like var.vpc_id, var.ec2_policy_name, etc.) with your specific AWS infrastructure values.
  2. Initialization: Open your terminal and navigate to the Terraform script’s directory. Run terraform init to initialize the Terraform environment.
  3. Plan: Execute terraform plan to see the AWS account changes.
  4. Apply: Run terraform apply to launch the resources. Confirm the action by typing yes when prompted.
  5. Verification: After the script execution, log into your AWS console and navigate to the EC2 dashboard. You should see your new Windows instance running.
  6. Access: To access your instance, you’ll need to reset the password for the Administrator account. Use the Session Manager plugin for the AWS CLI to forward the Remote Desktop Protocol (RDP) port to your local machine. I provided all commands in the module output.

Clean Up

Destroy the resources when you don’t need them anymore. Run terraform destroy and confirm with yes.

Conclusion

Using Terraform to manage AWS resources provides a scalable and efficient way to handle cloud infrastructure. With this script, you can easily launch and manage a Windows EC2 instance, ensuring consistent and repeatable deployments.

Similar Posts