EC2インスタンスでActive Directoryを作成してみた(インフラ編)

EC2インスタンスでActive Directoryを作成してみた(インフラ編)

Clock Icon2024.08.24

AWS上でActive Directory(AD)サーバーを構築する場合、AWS Managed Microsoft AD、Simple AD、またはAD Connector(オンプレミス環境と連携)といった選択肢があります。しかし、今回はADの学習も兼ねて、EC2インスタンス上にADサーバーを自前で構築します。さらに、別のEC2インスタンスをこのドメインに参加させ、ADユーザーでログインする実践も行います。

この一連の手順を、全3編でお届けする予定です:

  • AWSインフラの構築(本記事)
  • EC2インスタンス(1台目)へのADサーバーの設定
  • EC2インスタンス(2台目)のドメイン参加とADユーザーによるログイン検証

また、今回はコンソール操作による構築ではなく、Terraformによるインフラコード化をして構築しています。
検証するため何度もサーバーを構築し直したり、将来に同様の構築をしたいときに便利です。

構成図

ec2_ad_aws_infra

前提

  • Terraformで構築
  • EC2インスタンスは、Microsoft Windows Server 2022 Base/t2.large
  • EC2にRDP接続するためのキーペアは事前に生成しておく(Windowsにログオンするためのパスワード取得で必要)
    • 今回は、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 = "shinyat-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-northeast-1a"
  az_c = "ap-northeast-1c"
}

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/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ドメインコントローラーに設定したいドメイン名をDHCPオプションセットに登録します。
今回私は、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

WindowsインスタンスにAdministratorでログオンするために、キーペアが必要なのでEC2インスタンス作成時に紐づけをしています。
今回私は、windows-ad-serverという名称のキーペアを事前に作成、ダウンロードをしており、それを設定しています。

modules/ec2/main.tf
resource "aws_iam_instance_profile" "ssm_profile" {
  name = "${var.project_name}-${var.environment}-ssm-profile"
  role = var.instance_role
}

resource "aws_instance" "ec2" {
  ami           = "ami-0f36f4f3d34a4df19" # Microsoft Windows 2022 Datacenter edition. [English]
  instance_type = "t2.large"
  subnet_id              = var.subnet_id
  security_groups        = var.security_group_ids
  iam_instance_profile   = aws_iam_instance_profile.ssm_profile.name
  key_name = "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           = "ami-0f36f4f3d34a4df19" # Microsoft Windows 2022 Datacenter edition. [English]
  instance_type = "t2.large"
  subnet_id              = var.subnet_id
  security_groups        = var.security_group_ids
  iam_instance_profile   = aws_iam_instance_profile.ssm_profile.name
  key_name = "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]
  }

  # 同じプライベートサブネットからのすべての通信を許可
  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-northeast-1.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-northeast-1.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-northeast-1.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の実行環境の説明は割愛します。

aws-vault exec shinyat -- terraform init
aws-vault exec shinyat -- terraform plan
aws-vault exec shinyat -- terraform apply

動作確認

AWSマネジメントコンソールのEC2から作成したインスタンスを選択し、「接続」を押下します。

ec2-active-directory-aws-infra_2

RDPクライアント > Fleet Manager を使用して接続する > Fleet Manager Remote Desktop を押下します。

ec2-active-directory-aws-infra_3

キーペア > ファイル選択 を押下します。
※キーペアはTerraform実行前に手動で作成しています。

ec2-active-directory-aws-infra_4

キーペアを選択したら、接続を押下します。

ec2-active-directory-aws-infra_5

接続できました。

ec2-active-directory-aws-infra_6

さいごに

コードをそのまま利用される際は、あくまで検証用としてご利用ください。
どなたかの参考になれば幸いです。

リンク

このブログは「EC2インスタンスでActive Directoryを作成してみた」という名称で、全3篇でお届けしています。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.