ลองสร้าง Active Directory บน EC2 Instance (Infrastructure Edition)
สวัสดีครับ เบิร์ด จาก ClassMethod Thailand ครับ
บทความนี้เป็นส่วนหนึ่งของซีรีส์บทเรียน "สร้าง Windows AD Server และ AD User ด้วยแนวคิด Infrastructure as Code (IaC) โดยใช้ Terraform" ซึ่งผมได้แปลมาจากบทความภาษาญี่ปุ่น จากนั้นได้ลงมือทดลองทำตามจริง และอยากนำมาแบ่งปันให้เพื่อนๆ ได้อ่านและทำตามกันครับ
ในการสร้างเซิร์ฟเวอร์ 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 ซึ่งสะดวกอย่างมากสำหรับการสร้างเซิร์ฟเวอร์ใหม่ซ้ำๆ เพื่อทดสอบ หรือเมื่อต้องการสร้างโครงสร้างแบบเดิมอีกในอนาคต
แผนภาพการกำหนดค่า

ข้อกำหนดเบื้องต้น
- สร้างระบบด้วย 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"

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

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

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

สุดท้ายนี้
หากท่านต้องการนำโค้ดนี้ไปใช้งานตามที่เป็นอยู่ กรุณาใช้เพื่อการทดสอบ (Verification) เท่านั้นครับ
หวังว่าบทความนี้จะเป็นประโยชน์กับใครสักคนไม่มากก็น้อยครับ
ลิงก์
บล็อกนี้มีชื่อว่า "ทดลองสร้าง Active Directory ด้วย EC2 Instance" ซึ่งจะนำเสนอเป็นทั้งหมด 3 ตอนครับ
-
การสร้างโครงสร้างพื้นฐานบน AWS
https://dev.classmethod.jp/articles/ec2-active-directory-aws-infra/ -
การกำหนดค่า AD server บน EC2 instance (instance แรก)
https://dev.classmethod.jp/articles/ec2-active-directory-ad-server/ -
การนำ EC2 instance (instance ที่สอง) เข้าร่วม Domain และการตรวจสอบสิทธิ์การเข้าสู่ระบบด้วยบัญชีผู้ใช้ AD
https://dev.classmethod.jp/articles/ec2-active-directory-ad-user/
บทความอื่นๆที่เกี่ยวข้อง
(Japanese)
(Thai)








