AWSで内側に宛先は存在するのに外にDNSがあるせいで名前解決できない問題をRoute 53 Resolverで解決する(terraformつきで)

# シナリオ
例えばオンプレミスネットワークとAWS側のVPCが接続してリーチャビリティはあるとします。
そしてVPC内にあるインスタンスA(client)から同じサブネットのインスタンスB(web)に通信しようとしています。

しかし、インスタンスAはインスタンスBにネットワーク的に問題はなくても名前解決ができないので通信できないとします。
そしてインスタンスBの名前はオンプレミス側のDNSが知っているとします。

図示するとこうです。

![d1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230043/d69a0090-f1da-2624-424f-b9c298bfb19a.png)


<details>
    <summary>d2 ソース</summary>
<pre>
network: {
  onpre: オンプレミス環境 {
    dns: DNS server
  }

  vpc: AWS VPC {
    client: client
    web: web

    client -> web: ここにアクセスしたいけど名前解決できない {
      style: {
        stroke-dash: 3
      }
    }
  }
  onpre.dns -> vpc.web: オンプレミスのDNSは名前を知っている
}
</pre>
</details>


今回は検証用にすべて同じサブネットにインスタンスを立てますが、実際のシナリオでも流れは同じです。

# 実際にやってみる

## 環境の再現
検証用にterraformコードを用意しました。
まずはこれでterraform applyします。


```hcl:main.tf
variable "aws_access_key" {}
variable "aws_secret_key" {}
variable "region" {
    default = "ap-northeast-1"
}

provider "aws" {
    access_key = "${var.aws_access_key}"
    secret_key = "${var.aws_secret_key}"
    region = "${var.region}"
}
```

```hcl:variables.tf
variable "images" {
  default = {
    us-east-1      = "ami-02396cdd13e9a1257"
    us-east-2      = "ami-0578f2b35d0328762"
    us-west-1      = "ami-0127b2e6f3b9b94d5"
    us-west-2      = "ami-009c5f630e96948cb"
    ap-south-1     = "ami-0c768662cc797cd75"
    ap-northeast-3 = "ami-08334eeab9e50ade9"
    ap-northeast-2 = "ami-0a306845f8cfbd41a"
    ap-southeast-1 = "ami-03e312c9b09e29831"
    ap-southeast-2 = "ami-0f6ad051716c81af1"
    ap-northeast-1 = "ami-01b32aa8589df6208"
    ca-central-1   = "ami-07e17b58812e0c7c2"
    eu-central-1   = "ami-0b7fd829e7758b06d"
    eu-west-1      = "ami-04f1014c8adcfa670"
    eu-west-2      = "ami-0d76271a8a1525c1a"
    eu-west-3      = "ami-05eb678ed1ab021c7"
    eu-north-1     = "ami-0577c11149d377ab7"
    sa-east-1      = "ami-0668aa31594136bd1"
  }
}

variable "cidr" {
  default = {
    vpc           = "192.168.0.0/24"
    public_subnet = "192.168.0.0/25"
    second_subnet = "192.168.0.128/25"
  }
}

variable "ip_address" {
  default = {
    web    = "192.168.0.10"
    dns    = "192.168.0.11"
    client = "192.168.0.100"
  }
}
```

```hcl:network.tf
resource "aws_vpc" "test-vpc" {
  cidr_block           = var.cidr.vpc
  enable_dns_support   = "true"
  enable_dns_hostnames = "true"
  tags = {
    Name = "test-vpc"
  }
}

resource "aws_internet_gateway" "test-igw" {
  vpc_id = aws_vpc.test-vpc.id
  tags = {
    Name = "test-igw"
  }
}

resource "aws_subnet" "test-public-subnet" {
  vpc_id     = aws_vpc.test-vpc.id
  cidr_block = var.cidr.public_subnet
  tags = {
    Name = "test-public-subnet"
  }
}

resource "aws_route_table" "test-public-subnet-route" {
  vpc_id = aws_vpc.test-vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.test-igw.id
  }
}

resource "aws_route_table_association" "test-public-subnet-association" {
  subnet_id      = aws_subnet.test-public-subnet.id
  route_table_id = aws_route_table.test-public-subnet-route.id
}

resource "aws_subnet" "test-second-subnet" {
  vpc_id     = aws_vpc.test-vpc.id
  cidr_block = var.cidr.second_subnet
  tags = {
    Name = "test-second-subnet"
  }
}

resource "aws_route_table" "test-second-subnet-route" {
  vpc_id = aws_vpc.test-vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.test-igw.id
  }
}

resource "aws_route_table_association" "test-second-subnet-association" {
  subnet_id      = aws_subnet.test-second-subnet.id
  route_table_id = aws_route_table.test-second-subnet-route.id
}
```

```hcl:ec2.tf
resource "aws_security_group" "web-sg" {
  name        = "web-sg"
  description = "Security group to pass HTTP traffics"
  vpc_id      = aws_vpc.test-vpc.id
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = [var.cidr.vpc]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_security_group" "dns-sg" {
  name        = "dns-sg"
  description = "Security group to pass DNS traffics"
  vpc_id      = aws_vpc.test-vpc.id
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 53
    to_port     = 53
    protocol    = "tcp"
    cidr_blocks = [var.cidr.vpc]
  }
  ingress {
    from_port   = 53
    to_port     = 53
    protocol    = "udp"
    cidr_blocks = [var.cidr.vpc]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_security_group" "client-sg" {
  name        = "client-sg"
  description = "Security group with ssh rule"
  vpc_id      = aws_vpc.test-vpc.id
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "web-instance" {
  ami           = var.images[var.region]
  instance_type = "t2.micro"
  subnet_id     = aws_subnet.test-public-subnet.id
  private_ip    = var.ip_address.web
  vpc_security_group_ids = [
    "${aws_security_group.web-sg.id}"
  ]
  associate_public_ip_address = "true"
  user_data                   = <<EOF
#!/bin/bash
sudo yum update -y
sudo yum install nginx -y 
sudo systemctl start nginx
EOF
  tags = {
    Name = "web-instance"
  }
}
resource "aws_instance" "dns-instance" {
  ami           = var.images[var.region]
  instance_type = "t2.micro"
  subnet_id     = aws_subnet.test-public-subnet.id
  private_ip    = var.ip_address.dns
  vpc_security_group_ids = [
    "${aws_security_group.dns-sg.id}"
  ]
  associate_public_ip_address = "true"
  user_data                   = file("dns_init.sh")
  tags = {
    Name = "dns-instance"
  }
}

resource "aws_instance" "client-instance" {
  ami           = var.images[var.region]
  instance_type = "t2.micro"
  subnet_id     = aws_subnet.test-public-subnet.id
  private_ip    = var.ip_address.client
  vpc_security_group_ids = [
    "${aws_security_group.client-sg.id}"
  ]
  associate_public_ip_address = "true"
  tags = {
    Name = "client-instance"
  }
}

```

```bash:dns_init.sh
#!/bin/bash
sudo yum update -y
sudo yum install bind -y 
cat << 'eof' | sudo tee /etc/named/local
; BIND data file for local loopback interface;
$TTL    604800
@       IN      SOA     web.local. web.local. (
                              2         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                         604800 )       ; Negative Cache TTL
;
@       IN      NS      web.local.
web     IN      A       192.168.0.10
eof

cat << 'eof' | sudo tee /etc/named.conf
//
// named.conf
//
// Provided by Red Hat bind package to configure the ISC BIND named(8) DNS
// server as a caching only nameserver (as a localhost DNS resolver only).
//
// See /usr/share/doc/bind*/sample/ for example named configuration files.
//

options {
        listen-on port 53 { any; };
        listen-on-v6 port 53 { ::1; };
        directory       "/var/named";
        dump-file       "/var/named/data/cache_dump.db";
        statistics-file "/var/named/data/named_stats.txt";
        memstatistics-file "/var/named/data/named_mem_stats.txt";
        secroots-file   "/var/named/data/named.secroots";
        recursing-file  "/var/named/data/named.recursing";
        allow-query     { any; };

        /* 
         - If you are building an AUTHORITATIVE DNS server, do NOT enable recursion.
         - If you are building a RECURSIVE (caching) DNS server, you need to enable 
           recursion. 
         - If your recursive DNS server has a public IP address, you MUST enable access 
           control to limit queries to your legitimate users. Failing to do so will
           cause your server to become part of large scale DNS amplification 
           attacks. Implementing BCP38 within your network would greatly
           reduce such attack surface 
        */
        recursion yes;

        dnssec-validation yes;

        managed-keys-directory "/var/named/dynamic";
        geoip-directory "/usr/share/GeoIP";

        pid-file "/run/named/named.pid";
        session-keyfile "/run/named/session.key";

        /* https://fedoraproject.org/wiki/Changes/CryptoPolicy */
        include "/etc/crypto-policies/back-ends/bind.config";
};

logging {
        channel default_debug {
                file "data/named.run";
                severity dynamic;
        };
};

zone "." IN {
        type hint;
        file "named.ca";
};

zone "local" {
        type master;
        file "/etc/named/local";
};


include "/etc/named.rfc1912.zones";
include "/etc/named.root.key";

eof
sudo systemctl start named
```

## 再現
この状態では名前解決ができません。

```
[ec2-user@ip-192-168-0-100 ~]$ nslookup web.local
Server:         192.168.0.2
Address:        192.168.0.2#53

** server can't find web.local: NXDOMAIN
```

一応DNSサーバーを直接指定すれば名前解決できますが、もしclientが各個人のラップトップPCであったりめったに触れられないものである場合は大変です。

```
[ec2-user@ip-192-168-0-100 ~]$ nslookup web.local 192.168.0.11
Server:         192.168.0.11
Address:        192.168.0.11#53

Name:   web.local
Address: 192.168.0.10
```

そこでRoute 53 Resolverを使います。

## Route 53 Resolverの立ち上げ

コンソールやCLIでも構いませんが、以下のコードを試してみてください。Route 53 ResolverがVPCに反映されるはずです。

```hcl:route53.tf
resource "aws_route53_resolver_endpoint" "test-rslvr" {
  name      = "test-rslvr"
  direction = "OUTBOUND"

  security_group_ids = [
    aws_security_group.dns-sg.id,
  ]

  ip_address {
    subnet_id = aws_subnet.test-public-subnet.id
  }
  ip_address {
    subnet_id = aws_subnet.test-second-subnet.id
  }

}

resource "aws_route53_resolver_rule" "test-rslvr-rule" {
  domain_name          = "local"
  name                 = "test-rslvr-rule"
  rule_type            = "FORWARD"
  resolver_endpoint_id = aws_route53_resolver_endpoint.test-rslvr.id

  target_ip {
    ip = var.ip_address.dns
  }
}

resource "aws_route53_resolver_rule_association" "test-rslvr-rule-association" {
  resolver_rule_id = aws_route53_resolver_rule.test-rslvr-rule.id
  vpc_id           = aws_vpc.test-vpc.id
}

```

## 結果

DNSサーバーを指定しなくても名前解決ができるようになります。

```
[ec2-user@ip-192-168-0-100 ~]$ nslookup web.local
Server:         192.168.0.2
Address:        192.168.0.2#53

Non-authoritative answer:
Name:   web.local
Address: 192.168.0.10
```

もちろんcurlも通ります。

```
[ec2-user@ip-192-168-0-100 ~]$ curl web.local
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
```


# 最後に

terraform destroyでリソースを消すことを忘れないでください