AWS環境でCoreDNSを使ってみた
歴史シミュレーションゲーム好きのくろすけです!
オンプレミス環境からAWSリソースへのDNS解決をする場合、Route 53 Resolver の Inbound Endpoint を使う方法がありますが、小規模なシステムではコストが難点になる場合があります。
今回は代替手段として ECS Fargate で CoreDNS を試してみました。
本記事では、Terraform で AWS 上の CoreDNS(ECS Fargate + NLB)を構成してみた結果をまとめます。
概要
CoreDNS とは
CoreDNS は、Go で書かれたオープンソースの DNS サーバーです。
プラグインで機能を拡張でき、フォワードやキャッシュ、ログ出力などを設定ファイル(Corefile)で記述します。
構成
作成した構成は下記の通りです。
検証のために作成したためシングルAZ構成にしていますが、本番運用を考える場合はマルチAZ構成が推奨されますので、NLB も設けています。
また、ECS 単体ではプライベートIPアドレスを固定できませんが、NLB と組み合わせることで固定可能です。
したがってシングルAZ構成の場合であっても、基本的には NLB と組み合わせて使用することがベターと考えています。

やってみた
1. CoreDNS
使用した CoreDNS の Config、Dockerfile を記載します。
上流のDNSサーバーに Route 53 Resolver を指定します。
Route 53 Resolver のプライベートIPアドレスは、169.254.169.253 もしくは VPC+2 (今回の場合 172.16.0.2) に設定されているようです。
今回は、タスクの定義にて環境変数(DNS_SERVERS)を設定しています。
Config
# CoreDNS設定ファイル
# Route53 Resolver Inbound Endpointの代替として使用
# すべてのDNSクエリを処理
.:53 {
# エラーログを有効化
errors
# キャッシュ設定(30秒)
cache 30
# ループ検出(無限ループを防ぐ)
loop
# クエリログを出力
log
# VPC内のRoute53 DNSにフォワード
# VPC DNSサーバー(Route53 Resolver)のアドレス
# 注意: この設定はAWS VPC内でのみ動作します
forward . {$DNS_SERVERS} {
max_concurrent 1000
# タイムアウト設定
expire 10s
# ヘルスチェック
health_check 10s
}
}
Dockerfile
# CoreDNS Dockerfile for DNS Server on ECS Fargate
# CoreDNS公式イメージを使用
FROM coredns/coredns:1.14.1
# 設定ファイルをコピー
COPY Corefile /etc/coredns/Corefile
# DNS用のポートを公開(TCP/UDP両方)
EXPOSE 53/tcp 53/udp
# CoreDNSを実行
WORKDIR /etc/coredns
ENTRYPOINT ["/coredns"]
CMD ["-conf", "/etc/coredns/Corefile"]
2. Terraform テンプレート
今回使用した Terraform テンプレートは下記になります。
VPCではカスタムモジュールを使用していますが、本題から逸れるかつ文字数の都合で紹介が難しいため割愛させていただきます。
下記のテンプレートでは ECR と ECS のタスクの定義で使用している URI が異なっています。
これはデプロイの順番が ECR => イメージビルド・プッシュ => ECS となる必要があるため、初めはダミーのイメージで ECS を起動し、その後に正式なイメージを ECR にプッシュして ECS に反映するという方法を採用しているためです。
方法としては「ECR のみテンプレートを分ける」もしくは「イメージビルド・プッシュもテンプレートに記述する」方法もあります。
ただし、個人的にはこれらの方法はライフサイクルの観点からあまり良いとは考えていないため、今回のような方法をとっています。
とはいえ今回の検証はダミーのリポジトリに CoreDNS のイメージを事前にプッシュしておいたので、更新不要で起動できています。
main.tf
################################################################################
# Data Sources #
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# VPC #
################################################################################
module "vpc" {
source = "../../modules/vpc"
name = "${var.project_name}-${var.env}-vpc"
cidr_block = var.vpc_cidr_block
igw_name = "${var.project_name}-${var.env}-igw"
subnets = {
"public_a" = {
name = "${var.project_name}-${var.env}-subnet-publ-a"
cidr_block = var.public_subnets.a.cidr_block
availability_zone = "ap-northeast-1a"
route_tables = ["public"]
network_acl = "public"
},
"protected_a" = {
name = "${var.project_name}-${var.env}-subnet-prot-a"
cidr_block = var.protected_subnets.a.cidr_block
availability_zone = "ap-northeast-1a"
route_tables = ["protected_a"]
network_acl = "protected"
}
}
route_tables = {
"public" = {
name = "${var.project_name}-${var.env}-rtb-publ",
use_internet_gateway = true,
},
"protected_a" = {
name = "${var.project_name}-${var.env}-rtb-prot-a",
nat_gateway = "ngw_a"
},
"protected_c" = {
name = "${var.project_name}-${var.env}-rtb-prot-c"
}
}
nat_gateways = {
"ngw_a" = {
name = "${var.project_name}-${var.env}-ngw-a"
subnet_id = "public_a"
}
}
network_acls = {
"public" = {
name = "${var.project_name}-${var.env}-nacl-publ"
ingress = [
{
rule_no = 100
protocol = "-1"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
]
egress = [
{
rule_no = 100
protocol = "-1"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
]
},
"protected" = {
name = "${var.project_name}-${var.env}-nacl-prot"
ingress = [
{
rule_no = 100
protocol = "-1"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
]
egress = [
{
rule_no = 100
protocol = "-1"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
]
}
}
security_groups = {
"sg_ec2" = {
name = "${var.project_name}-${var.env}-sg-ec2"
description = "Security group for EC2"
egress = [
{
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
]
},
"sg_nlb" = {
name = "${var.project_name}-${var.env}-sg-nlb"
description = "Security group for NLB"
ingress = [
{
description = "DNS TCP"
from_port = 53
to_port = 53
protocol = "tcp"
cidr_blocks = [var.protected_subnets.a.cidr_block]
},
{
description = "DNS UDP"
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = [var.protected_subnets.a.cidr_block]
}
]
egress = [
{
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
]
}
}
}
################################################################################
# NLB #
################################################################################
resource "aws_lb" "nlb" {
name = "${var.project_name}-${var.env}-nlb"
internal = true
load_balancer_type = "network"
security_groups = [module.vpc.security_groups["sg_nlb"].id]
subnet_mapping {
subnet_id = module.vpc.subnets["protected_a"].id
private_ipv4_address = var.nlb_private_ip_prot_a
}
enable_deletion_protection = false
tags = {
Name = "${var.project_name}-${var.env}-nlb"
}
}
# Target Group for DNS (TCP_UDP)
resource "aws_lb_target_group" "dns" {
name = "${var.project_name}-${var.env}-tg-dns"
port = 53
protocol = "TCP_UDP"
target_type = "ip"
vpc_id = module.vpc.vpc.id
health_check {
enabled = true
protocol = "TCP"
port = 53
interval = 30
healthy_threshold = 3
unhealthy_threshold = 3
}
tags = {
Name = "${var.project_name}-${var.env}-tg-dns"
}
}
# NLB Listener for DNS (TCP_UDP)
resource "aws_lb_listener" "dns" {
load_balancer_arn = aws_lb.nlb.arn
port = 53
protocol = "TCP_UDP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.dns.arn
}
}
################################################################################
# DNS #
################################################################################
# ECR Repository for CoreDNS
module "ecr_coredns" {
source = "terraform-aws-modules/ecr/aws"
version = "~> 3.2"
repository_name = "ecs/${var.project_name}-${var.env}/dns-srv"
# イメージタグの可変性
repository_image_tag_mutability = "IMMUTABLE"
# プッシュ時の自動スキャン
repository_image_scan_on_push = true
# 暗号化設定(デフォルトのAES256を使用)
repository_encryption_type = "AES256"
# リポジトリ削除時に画像があっても削除可能
repository_force_delete = true
# ライフサイクルポリシー(古いイメージを自動削除)
repository_lifecycle_policy = jsonencode({
rules = [
{
rulePriority = 1
description = "Keep last 10 images"
selection = {
tagStatus = "any"
countType = "imageCountMoreThan"
countNumber = 10
}
action = {
type = "expire"
}
}
]
})
tags = {
Name = "ecs/${var.project_name}-${var.env}/dns-srv"
}
}
################################################################################
# ECS(DNS) #
################################################################################
module "ecs" {
source = "terraform-aws-modules/ecs/aws"
version = "~> 7.3"
cluster_name = "${var.project_name}-${var.env}-cluster"
# Cluster設定
cluster_configuration = {
execute_command_configuration = {
logging = "DEFAULT"
}
}
cluster_setting = [{
name = "containerInsights"
value = "enabled"
}]
# Task Execution IAM Role
create_task_exec_iam_role = true
task_exec_iam_role_name = "${var.project_name}-${var.env}-task-exec-role"
task_exec_iam_role_use_name_prefix = false
task_exec_iam_role_policies = {
AmazonECSTaskExecutionRolePolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# Fargate Capacity Provider
cluster_capacity_providers = ["FARGATE"]
default_capacity_provider_strategy = {
FARGATE = {
weight = 1
}
}
# VPC ID(Security Group作成用)
vpc_id = module.vpc.vpc.id
# ECS Service定義
services = {
"${var.project_name}-${var.env}-dns-srv" = {
cpu = 256
memory = 512
track_latest = true
security_group_name = "${var.project_name}-${var.env}-sg-dns-srv"
security_group_use_name_prefix = false
# Container定義
container_definitions = {
"${var.project_name}-${var.env}-dns-srv" = {
cpu = 256
memory = 512
essential = true
image = "${data.aws_caller_identity.current.account_id}.dkr.ecr.${data.aws_region.current.id}.amazonaws.com/ecs/dummy-krsk-repo:dns-v1"
# CoreDNS forward の上流(VPC DNS Resolver)
environment = [
{ name = "DNS_SERVERS", value = "169.254.169.253" }
]
portMappings = [
{
name = "dns"
containerPort = 53
protocol = "tcp"
}
]
enable_cloudwatch_logging = true
cloudwatch_log_group_name = "/aws/ecs/${var.project_name}-${var.env}/dns-srv"
cloudwatch_log_group_retention_in_days = 7
}
}
# Network設定
subnet_ids = [module.vpc.subnets["protected_a"].id]
assign_public_ip = false
# Security Group設定
security_group_ingress_rules = {
dns_tcp = {
description = "DNS TCP from NLB"
from_port = 53
to_port = 53
ip_protocol = "tcp"
referenced_security_group_id = module.vpc.security_groups["sg_nlb"].id
}
dns_udp = {
description = "DNS UDP from NLB"
from_port = 53
to_port = 53
ip_protocol = "udp"
referenced_security_group_id = module.vpc.security_groups["sg_nlb"].id
}
}
security_group_egress_rules = {
all = {
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
}
}
# Load Balancer設定
load_balancer = {
dns = {
target_group_arn = aws_lb_target_group.dns.arn
container_name = "${var.project_name}-${var.env}-dns-srv"
container_port = 53
}
}
# Deployment設定
deployment_maximum_percent = 100
deployment_minimum_healthy_percent = 0
desired_count = 1
# タスク定義のtrack_latest相当の設定
ignore_task_definition_changes = false
# サービス固有のタグ
service_tags = {
Name = "${var.project_name}-${var.env}-svc-dns-srv"
}
# タスク定義のタグ
task_tags = {
Name = "${var.project_name}-${var.env}-dns-srv"
}
}
}
depends_on = [
aws_lb_listener.dns
]
}
################################################################################
# EC2 #
################################################################################
# SSM エージェント入りの標準 Amazon Linux 2023 AMI を Parameter Store から取得
data "aws_ssm_parameter" "amazon_linux_2023_ami" {
name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}
# EC2用のIAMロール(モジュール使用)
module "ec2_iam_role" {
source = "terraform-aws-modules/iam/aws//modules/iam-role"
version = "~> 6.4"
name = "${var.project_name}-${var.env}-role-ec2"
use_name_prefix = false
create_instance_profile = true
# Trust policy(EC2が引き受けるロール)
trust_policy_permissions = {
EC2AssumeRole = {
actions = ["sts:AssumeRole"]
principals = [
{
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
]
}
}
# マネージドポリシー(SSM接続用)
policies = {
AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
tags = {
Name = "${var.project_name}-${var.env}-role-ec2"
}
}
# EC2インスタンス
module "ec2" {
source = "terraform-aws-modules/ec2-instance/aws"
version = "~> 6.2"
name = "${var.project_name}-${var.env}-ec2"
ami = data.aws_ssm_parameter.amazon_linux_2023_ami.value
instance_type = "t3.micro"
subnet_id = module.vpc.subnets["protected_a"].id
vpc_security_group_ids = [module.vpc.security_groups["sg_ec2"].id]
create_security_group = false
iam_instance_profile = module.ec2_iam_role.instance_profile_name
# EBSボリューム設定
root_block_device = {
volume_type = "gp3"
volume_size = 8
encrypted = true
}
tags = {
Name = "${var.project_name}-${var.env}-ec2"
}
}
variables.tf
variable "env" {
description = "Environment name"
type = string
default = "dev"
}
variable "project_name" {
description = "Project name"
type = string
}
variable "vpc_cidr_block" {
description = "VPC CIDR block"
type = string
}
variable "public_subnets" {
description = "Public subnets"
type = map(object({
cidr_block = string
}))
}
variable "protected_subnets" {
description = "Protected subnets"
type = map(object({
cidr_block = string
}))
}
variable "private_subnets" {
description = "Private subnets"
type = map(object({
cidr_block = string
}))
}
variable "nlb_private_ip_prot_a" {
description = "Private IP for NLB node (must be in protected subnet CIDR)"
type = string
}
3. 動作確認
デプロイが完了後に EC2 にログインして、下記の通り動作を確認しました。
NLB のプライベートIPアドレスを指定して、NLB のDNS解決ができるか確認しています。

今回はターゲットグループおよびセキュリティグループで tcp のポートも開放しているので、tcp でもDNS解決可能です。

課題
最後に検証を通して見えてきた課題もありました。
NLB のヘルスチェックは TCP,HTTP,HTTPS のみ対応しているため、UDP のみ使用する場合は実際に使用する udp:53 が生きているのか正確にチェックできません。
コンテナ側でヘルスチェックを実施することが良さそうと思いましたが、CoreDNS の公式 Docker イメージは scratch ベースのため、シェルやパッケージマネージャが含まれておらず、コンテナ内で dig や nslookup などを後からインストールすることもできません。
コンテナ側のヘルスチェックで対応する場合は、Alpine などパッケージが使えるベースイメージで自前イメージを作成する必要がありそうです。
あとがき
CoreDNS は設定もわかりやすく、簡単に作成することができました。
もちろん Route53 Resolver Inbound Endpoint に比べて保守の手間がありますが、コストの問題がある場合には選択肢の一つとして検討してもいいかもしれません。
以上、くろすけでした!








