ลองสร้าง Active Directory บน EC2 Instance (Infrastructure Edition)

ลองสร้าง Active Directory บน EC2 Instance (Infrastructure Edition)

บทความนี้เป็นส่วนหนึ่งของซีรีส์บทเรียนที่จะพาทุกท่านเรียนรู้การจัดการทรัพยากรด้วยแนวคิด Infrastructure as Code (IaC) ผ่าน Terraform ซึ่งช่วยให้สามารถ Deploy โครงสร้างพื้นฐานได้อย่างรวดเร็วและนำไปปรับใช้กับหลากหลายสถาปัตยกรรมได้อย่างยืดหยุ่น
2026.09.30

สวัสดีครับ เบิร์ด จาก ClassMethod Thailand ครับ

บทความนี้เป็นส่วนหนึ่งของซีรีส์บทเรียน "สร้าง Windows AD Server และ AD User ด้วยแนวคิด Infrastructure as Code (IaC) โดยใช้ Terraform" ซึ่งผมได้แปลมาจากบทความภาษาญี่ปุ่น จากนั้นได้ลงมือทดลองทำตามจริง และอยากนำมาแบ่งปันให้เพื่อนๆ ได้อ่านและทำตามกันครับ

https://dev.classmethod.jp/articles/ec2-active-directory-aws-infra/

ในการสร้างเซิร์ฟเวอร์ Active Directory (AD) บน AWS มีตัวเลือกหลากหลาย เช่น AWS Managed Microsoft AD, Simple AD หรือ AD Connector (สำหรับเชื่อมต่อกับสภาพแวดล้อมแบบ On-premises) แต่ในครั้งนี้ เพื่อเป็นการเรียนรู้ระบบ AD ไปด้วย เราจะทำการสร้างเซิร์ฟเวอร์ AD บน EC2 Instance ด้วยตัวเอง นอกจากนี้ เรายังจะนำ EC2 Instance อีกเครื่องหนึ่งเข้าร่วมโดเมนนี้ และทดลองเข้าใช้งานด้วยบัญชีผู้ใช้ AD (AD User) อีกด้วย

ลำดับขั้นตอนทั้งหมดนี้ จะแบ่งนำเสนอเป็น 3 ตอน ได้แก่:

  • การสร้าง AWS Infrastructure (บทความนี้)
  • การตั้งค่า AD Server บน EC2 Instance (เครื่องที่ 1)
  • การนำ EC2 Instance (เครื่องที่ 2) เข้าร่วมโดเมน และการทดสอบเข้าสู่ระบบด้วย AD User

นอกจากนี้ ในครั้งนี้ไม่ได้สร้างผ่านการกดสั่งงานบนหน้าคอนโซล (Console) แต่เป็นการจัดการโครงสร้างพื้นฐานด้วยโค้ด (Infrastructure as Code) โดยใช้ Terraform ซึ่งสะดวกอย่างมากสำหรับการสร้างเซิร์ฟเวอร์ใหม่ซ้ำๆ เพื่อทดสอบ หรือเมื่อต้องการสร้างโครงสร้างแบบเดิมอีกในอนาคต

แผนภาพการกำหนดค่า

d1-t

ข้อกำหนดเบื้องต้น

  • สร้างระบบด้วย Terraform
  • EC2 Instance ที่ใช้คือ Microsoft Windows Server 2022 Base / t3.medium
  • ต้องสร้าง Key Pair สำหรับเชื่อมต่อ RDP เข้าไปยัง EC2 ไว้ล่วงหน้า (จำเป็นต้องใช้เพื่อขอรับรหัสผ่านสำหรับเข้าสู่ระบบ Windows)
  • ในครั้งนี้ตั้งชื่อ Key Pair ว่า bird-windows-ad-server

ลองทำแล้ว

โครงสร้างไดเรกทอรี

.
├── envs
│   └── dev
│       ├── backend.tf
│       ├── main.tf
│       ├── providers.tf
│       └── variables.tf
└── modules
    ├── dhcp
    │   ├── main.tf
    │   └── variables.tf
    ├── ec2
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── iam
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── security_group
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── vpc
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    └── vpc_endpoint
        ├── main.tf
        └── variables.tf

โครงสร้างพื้นฐาน

■ main

envs/dev/main.tf

      locals {

         product_name = "bird-ec2-windows-ad"
         environment  = "dev"
      }

      module "terraform_state_backend" {
      source     = "cloudposse/tfstate-backend/aws"
      namespace  = local.product_name
      stage      = local.environment
      name       = "terraform"
      attributes = ["states"]

      terraform_backend_config_file_path = "."
      terraform_backend_config_file_name = "backend.tf"
      force_destroy = false
      }

      module "vpc" {
        source = "../../modules/vpc"

        project_name = local.product_name
        environment = local.environment

        vpc_cidr = var.vpc_cidr
        subnet_a_cidr = var.subnet_a_cidr
        subnet_c_cidr = var.subnet_c_cidr
        az_a = "ap-southeast-7a"
        az_c = "ap-southeast-7b"
      }

      module "security_group" {
       source = "../../modules/security_group"

       project_name = local.product_name
       environment = local.environment

       vpc_id = module.vpc.vpc_id
       vpc_cidr = var.vpc_cidr
       subnet_a_cidr = var.subnet_a_cidr
      }

      module "vpc_endpoint" {
       source = "../../modules/vpc_endpoint"

       project_name = local.product_name
       environment = local.environment

       vpc_id = module.vpc.vpc_id
       subnet_ids = [module.vpc.subnet_a_id]
       security_group_ids = [module.security_group.windows_server_security_group_id]
      }

      module "iam" {
       source = "../../modules/iam"

       project_name = local.product_name
       environment = local.environment
      }

      module "ec2" {
       source = "../../modules/ec2"

       project_name = local.product_name
       environment = local.environment

       subnet_id = module.vpc.subnet_a_id
       security_group_ids = [module.security_group.windows_server_security_group_id]
       instance_role = module.iam.windows_server_ssm_role_name
      }

      module "dhcp" {
       source = "../../modules/dhcp"

       project_name = local.product_name
       environment = local.environment

       ad_server_private_id = module.ec2.ad_server_private_ip
       vpc_id               = module.vpc.vpc_id
      }

envs/dev/providers.tf

     terraform {
        required_version = ">= 1.5.0"

     required_providers {
        aws = {
        source  = "hashicorp/aws"
        version = "~> 6.0.0"
           }
        }
     }

     provider "aws" {
        region = "ap-southeast-7"   # Thailand 

        default_tags {
        tags = {
        Project     = local.product_name
        Environment = local.environment
        ManagedBy   = "Terraform"
             }
           }
     }

envs/dev/variables.tf

    variable "vpc_cidr" {
      default = "10.16.0.0/16"
    }

    variable "subnet_a_cidr" {
      default = "10.16.1.0/24"
    }

    variable "subnet_c_cidr" {
      default = "10.16.2.0/24"
    }

■ dhcp

ลงทะเบียนชื่อโดเมนที่ต้องการตั้งค่าให้กับ AD Domain Controller ลงใน DHCP Option Set
ครั้งนี้ผมตั้งค่าเป็น shinyats.com แต่คุณสามารถเปลี่ยนได้ตามต้องการ (เช่น test.local เป็นต้น)
เมื่อถึงขั้นตอนการตั้งค่า AD คุณจะต้องใช้ชื่อโดเมนเดียวกันนี้ในการตั้งค่า

modules/dhcp/main.tf

   resource "aws_vpc_dhcp_options" "dhcp_options" {
   domain_name         = "shinyats.com"
   domain_name_servers = ["AmazonProvidedDNS", var.ad_server_private_id]

     tags = {
        Name = "${var.project_name}-${var.environment}-dhcp"
        }
     }

 resource "aws_vpc_dhcp_options_association" "dhcp_options_assoc" {
    vpc_id          = var.vpc_id
    dhcp_options_id = aws_vpc_dhcp_options.dhcp_options.id
 }

modules/dhcp/variables.tf

variable "project_name" {
    type = string
}

variable "environment" {
   type = string
}

variable "vpc_id" {
  type = string
}

variable "ad_server_private_id" {
  type = string
}

■ ec2

การเข้าสู่ระบบ (Login) ยัง Windows Instance จะใช้สิทธิ์ Administrator ดังนั้นจึงจำเป็นต้องสร้าง Key Pair ไว้ก่อนสร้าง EC2 Instance

ในครั้งนี้ ผมได้สร้างและดาวน์โหลด Key Pair ที่ชื่อว่า "bird-windows-ad-server" ไว้ล่วงหน้าแล้ว และได้ทำการตั้งค่าโดยใช้ Key Pair ตัวนี้ครับ

modules/ec2/main.tf

data "aws_ami" "windows_2022" {
most_recent = true
owners      = ["amazon"]

filter {
   name   = "name"
   values = ["Windows_Server-2022-English-Full-Base-*"]
   }
}

resource "aws_iam_instance_profile" "ssm_profile" {
  name = "${var.project_name}-${var.environment}-ssm-profile"
  role = var.instance_role
}

resource "aws_instance" "ec2" {
  ami = data.aws_ami.windows_2022.id
  instance_type = "t3.medium"
  subnet_id              = var.subnet_id
  security_groups        = var.security_group_ids
  iam_instance_profile   = aws_iam_instance_profile.ssm_profile.name
  key_name = "bird-windows-ad-server"
  associate_public_ip_address = false

metadata_options {
  http_tokens = "required"
}

tags = {
     Name = "${var.project_name}-${var.environment}-ad-server"

   }
}

resource "aws_instance" "ec2_user" {
ami = data.aws_ami.windows_2022.id
instance_type = "t3.medium"
subnet_id              = var.subnet_id
security_groups        = var.security_group_ids
iam_instance_profile   = aws_iam_instance_profile.ssm_profile.name
key_name = "bird-windows-ad-server"
associate_public_ip_address = false

metadata_options {
   http_tokens = "required"
}

tags = {
     Name = "${var.project_name}-${var.environment}-ad-user"
  }
}

modules/ec2/variables.tf

variable "project_name" {
   type = string
}

variable "environment" {
  type = string
}

variable "subnet_id" {
  type = string
}

variable "security_group_ids" {
   type = list(string)
}

variable "instance_role" {
  type = string
}

modules/ec2/outputs.tf

output "ad_server_private_ip" {
   value = aws_instance.ec2.private_ip
}

■ iam

modules/iam/main.tf

resource "aws_iam_role" "windows_server_ssm_role" {
     name = "${var.project_name}-${var.environment}-windows-server-ssm-role"

       assume_role_policy = jsonencode({
       Version = "2012-10-17"
       Statement = [
       {
         Effect = "Allow"
         Principal = {
         Service = "ec2.amazonaws.com"
         }
         Action = "sts:AssumeRole"
       }
      ]
    })
 }

     resource "aws_iam_role_policy_attachment" "ssm_policy_attachment" {
         role       = aws_iam_role.windows_server_ssm_role.name
         policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
 }

modules/iam/variables.tf

 variable "project_name" {
      type = string
 }

 variable "environment" {
      type = string
 }

modules/iam/outputs.tf

   output "windows_server_ssm_role_name" {
       value = aws_iam_role.windows_server_ssm_role.name
  }

■ security_group

modules/security_group/main.tf

   resource "aws_security_group" "windows_server_security_group" {
       vpc_id = var.vpc_id

   egress {
       from_port   = 0
       to_port     = 0
       protocol    = "-1"
       cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
      from_port   = 3389
      to_port     = 3389
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = [var.vpc_cidr]
  }

   #อนุญาตทุกการเชื่อมต่อจาก private subnet เดียวกัน
   ingress {
      from_port   = 0
      to_port     = 0
      protocol    = "-1"
      cidr_blocks = [var.subnet_a_cidr]
   }

  tags = {
     Name = "${var.project_name}-${var.environment}-windows-server-sg"
      }
  }

modules/security_group/variables.tf

  variable "project_name" {
     type = string
  }

  variable "environment" {
     type = string
  }

  variable "vpc_id" {
    type = string
  }

  variable "vpc_cidr" {
    type = string
  }

  variable "subnet_a_cidr" {
    type = string
  }

modules/security_group/outputs.tf

  output "windows_server_security_group_id" {
      value = aws_security_group.windows_server_security_group.id
  }

■ vpc

modules/vpc/main.tf

  resource "aws_vpc" "vpc" {
     cidr_block = var.vpc_cidr
     enable_dns_support   = true
     enable_dns_hostnames = true

  tags = {
     Name = "${var.project_name}-${var.environment}-vpc"
    }
  }

  resource "aws_subnet" "private_subnet_a" {
      vpc_id            = aws_vpc.vpc.id
      cidr_block        = var.subnet_a_cidr
      availability_zone = var.az_a

      tags = {
         Name = "${var.project_name}-${var.environment}-private-subnet-a"
    }
  }

  resource "aws_subnet" "public_subnet_c" {
       vpc_id            = aws_vpc.vpc.id
       cidr_block        = var.subnet_c_cidr
       availability_zone = var.az_c

     tags = {
     Name = "${var.project_name}-${var.environment}-public-subnet-c"
     }
   }

  resource "aws_internet_gateway" "igw" {
     vpc_id = aws_vpc.vpc.id

     tags = {
     Name = "${var.project_name}-${var.environment}-igw"
    }
  }

 resource "aws_eip" "nat_eip" {
    domain = "vpc"
  }

  resource "aws_nat_gateway" "nat" {
     allocation_id = aws_eip.nat_eip.id
     subnet_id     = aws_subnet.public_subnet_c.id

     tags = {
     Name = "${var.project_name}-${var.environment}-nat"
      }
  }

  resource "aws_route_table" "private_route" {
     vpc_id = aws_vpc.vpc.id

     tags = {
     Name = "${var.project_name}-${var.environment}-private-route"
     }
  }

  resource "aws_route" "private_route" {
       route_table_id         = aws_route_table.private_route.id
       destination_cidr_block = "0.0.0.0/0"
       nat_gateway_id         = aws_nat_gateway.nat.id
  }

  resource "aws_route_table_association" "private_rt_assoc" {
       subnet_id      = aws_subnet.private_subnet_a.id
       route_table_id = aws_route_table.private_route.id
  }

  resource "aws_route_table" "public_route" {
       vpc_id = aws_vpc.vpc.id

       route {
       cidr_block = "0.0.0.0/0"
       gateway_id = aws_internet_gateway.igw.id
  }

       tags = {
       Name = "${var.project_name}-${var.environment}-public-route"
       }
  }

  resource "aws_route_table_association" "public_rt_assoc" {
      subnet_id      = aws_subnet.public_subnet_c.id
      route_table_id = aws_route_table.public_route.id
  }

modules/vpc/variables.tf

  variable "project_name" {
     type = string
  }

  variable "environment" {
     type = string
  }

  variable "vpc_cidr" {
     type = string
  }

  variable "subnet_a_cidr" {
    type = string
  }

  variable "subnet_c_cidr" {
    type = string
  }

  variable "az_a" {
    type = string
  }

  variable "az_c" {
    type = string
  }

modules/vpc/outputs.tf

  output "vpc_id" {
      value = aws_vpc.vpc.id
  }

  output "subnet_a_id" {
      value = aws_subnet.private_subnet_a.id
  }

  output "route_table_id" {
      value = aws_route_table.private_route.id
  }

■ vpc_endpoint

modules/vpc_endpoint/main.tf

  resource "aws_vpc_endpoint" "ssm_endpoint" {
    vpc_id              = var.vpc_id
    service_name        = "com.amazonaws.ap-southeast-7.ssm"
    vpc_endpoint_type   = "Interface"
    subnet_ids          = var.subnet_ids
    private_dns_enabled = true

    security_group_ids = var.security_group_ids

     tags = {
     Name = "${var.project_name}-${var.environment}-ssm-endpoint"
     }
   }

  resource "aws_vpc_endpoint" "ssm_messages_endpoint" {
    vpc_id              = var.vpc_id
    service_name        = "com.amazonaws.ap-southeast-7.ssmmessages"
    vpc_endpoint_type   = "Interface"
    subnet_ids          = var.subnet_ids
    private_dns_enabled = true

    security_group_ids = var.security_group_ids

    tags = {
       Name = "${var.project_name}-${var.environment}-ssm-messages-endpoint"
       }
    }

# resource "aws_vpc_endpoint" "ec2_messages" {
#   vpc_id              = var.vpc_id
#   service_name        = "com.amazonaws.ap-southeast-7.ec2messages"
#   vpc_endpoint_type   = "Interface"
#   subnet_ids          = var.subnet_ids
#   private_dns_enabled = true
#
#   security_group_ids = var.security_group_ids
#
#   tags = {
#     Name = "${var.project_name}-${var.environment}-ec2-messages-endpoint"
#   }
# }

modules/vpc_endpoint/variables.tf

  variable "project_name" {
    type = string
  }

  variable "environment" {
    type = string
  }

  variable "vpc_id" {
    type = string
  }

  variable "subnet_ids" {
    type = list(string)
  }

  variable "security_group_ids" {
    type = list(string)
  }

ตัวอย่างคำสั่งสำหรับรัน Terraform
ไม่มีการอธิบายรายละเอียดเกี่ยวกับสภาพแวดล้อมในการรัน Terraform

  terraform init
  terraform plan
  terraform apply

การตรวจสอบการทำงาน

เลือกอินสแตนซ์ที่สร้างขึ้นจากส่วน EC2 ใน AWS Management Console แล้วคลิก "Connect"
sj1

เลือก RDP Client > Fleet Manager Remote Desktop

et-2

คลิก Key Pair > Select File
*หมายเหตุ: Key pair จะต้องถูกสร้างขึ้นเองก่อนที่จะดำเนินการรัน Terraform

et-3

หลังจากกดเชื่อมต่อแล้วเราจะเห็น ภาพ window ขึ้นมา
เชื่อมต่อสำเร็จ
et-4_new

สุดท้ายนี้

หากท่านต้องการนำโค้ดนี้ไปใช้งานตามที่เป็นอยู่ กรุณาใช้เพื่อการทดสอบ (Verification) เท่านั้นครับ
หวังว่าบทความนี้จะเป็นประโยชน์กับใครสักคนไม่มากก็น้อยครับ

ลิงก์

บล็อกนี้มีชื่อว่า "ทดลองสร้าง Active Directory ด้วย EC2 Instance" ซึ่งจะนำเสนอเป็นทั้งหมด 3 ตอนครับ

บทความอื่นๆที่เกี่ยวข้อง

(Japanese)

https://dev.classmethod.jp/articles/windows-server-speed-deploy-fromzero/

https://dev.classmethod.jp/articles/krsk-terraform-simple-validation-environment-setup-20250731/

https://dev.classmethod.jp/articles/active-directory-domain-joined-windows-server-ec2-vpc-migration/

https://dev.classmethod.jp/articles/command-ad-env-quick-build/

(Thai)

https://dev.classmethod.jp/articles/windows-server-2022-in-amazon-ec2/

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事