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:
- Local Variables: Sets up various local variables, including AWS account details and tags.
- 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.
- Resource – Security Group: Defines a security group (
aws_security_group.ec2_sg
) for the EC2 instance with outbound traffic rules. - 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).
- KMS Module: Utilizes a Terraform module to create a KMS key for encryption purposes.
- IAM Resources: Sets up IAM roles, policies, and instance profiles for the EC2 instance, ensuring proper permissions.
Steps to Launch the Instance
- Configuration: Replace the placeholders in the script (like
var.vpc_id
,var.ec2_policy_name
, etc.) with your specific AWS infrastructure values. - Initialization: Open your terminal and navigate to the Terraform script’s directory. Run
terraform init
to initialize the Terraform environment. - Plan: Execute
terraform plan
to see the AWS account changes. - Apply: Run
terraform apply
to launch the resources. Confirm the action by typingyes
when prompted. - Verification: After the script execution, log into your AWS console and navigate to the EC2 dashboard. You should see your new Windows instance running.
- 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.