VPC LambdaはDHCPオプションセットの影響を受ける件

Lambdaの処理の中で参照先のDNSサーバーを変更する方法はない認識なので、DHCPオプションセットをカスタマイズするときは気をつけよう
2023.08.13

VPC Lambdaが参照するDNSサーバーはDHCPオプションセットで指定したものなのだろうか

こんにちは、のんピ(@non____97)です。

皆さんはVPC LambdaはDHCPオプションセットの影響を受けるか気になったことはありますか? 私はあります。

DHCPオプションセットを使うことで、DHCPオプションセットを割り当てているVPC上の全てのEC2インスタンスが参照するDNSサーバーを簡単に設定することができます。

特にオンプレミス上のDNSサーバーを参照させる場合に便利です。

この場合、他にもRoute 53 Resolver Outbound Endpointを使う案もあります。私はRoute 53 Resolver Endpoint推しですが、お値段がそれなりにかかります。(0.125 USD/h/ENI)

一方、DHCPオプションセットを使えば無料で参照先のDNSサーバーを指定することが可能です。

そんな便利なDHCPオプションセットですが、EC2インスタンス以外にLamddaにも影響を与えるのでしょうか。

VPCのDHCPオプションセットには「VPC内のEC2インスタンスが」と記載があります。

DHCP オプションセットは、VPC 内の EC2 インスタンスが仮想ネットワーク経由で通信するために使用するネットワーク構成のグループです。

DHCP オプションセットの概念 - Amazon Virtual Private Cloud

一方で、VPC LambdaのトラブルシューティングにはDHCPオプションセットがLambdaの動作に影響を与えるような記載がされています。

重要:カスタムの動的ホスト構成プロトコル (DHCP) オプションセットを使用している場合は、カスタム DNS サーバーが期待どおりに動作していることを確認します。

VPC の Lambda 関数を使用したタイムアウトエラーのトラブルシューティング | AWS re:Post

 

VPC にカスタム DHCP オプションセットを使用している場合は、Amazon Route 53 Resolver クエリログを使用して DNS クエリ応答を確認してください。

AWS Lambda の DNS 関連エラーのトラブルシューティング | AWS re:Post

また、「VPCを作成するとLambdaが自動的にDHCPオプションセットを作成してVPCに関連付ける」といった記載もありました。

When you create a VPC, Lambda automatically creates a set of DHCP options and associates them with the VPC. You can configure your own DHCP options set for your VPC. For more details, refer to Amazon VPC DHCP options.

Private networking with VPC - AWS Lambda

きっとDHCPオプションセットの影響を受けるんでしょうが、気になったので確認してみます。

いきなりまとめ

  • VPC LambdaはDHCPオプションセットのDNSサーバーの設定値の影響を受ける
  • ただし、LambdaからDHCPオプションセットで指定したDNSサーバーに対して直接問い合わせは行わない
    • リンクローカルアドレスのフォワーダーを介して、指定したDNSサーバーにフォワーディングして名前解決をする
    • リンクローカルアドレスのフォワーダーからどこにフォワーディングするかはLambda内の/etc/resolv.confからも確認できる
  • Lambda実行時のCloudWatch Logsに出力されるログは、CloudWatch Logsのサービスエンドポイントへの名前解決ができなくとも行える
    • おそらくLambdaのService VPC上のHyperplane ENIで名前解決と、実際の通信が行われている
    • 一方、名前解決できない場合、Lambdaの処理でログストリームを作成しようとしても失敗する
  • Lambdaの処理の中で参照先のDNSサーバーを変更する方法はない認識
    • DHCPオプションセットの影響範囲を気につけよう

検証環境

検証環境は以下のとおりです。

VPC LambdaはDHCPオプションセットの影響を受けるよ検証環境構成図

VPC Lambda(Node.js 18)を作成しました。VPC LambdaのENIにはElastic IPアドレスを割り当てて、インターネットに抜けられるようにしています。

DHCPオプションセットはVPCのデフォルトのものと、DNSサーバーに存在しないIPアドレスを指定したものの2つを用意しました。

IAMロールはVPC Lambda作成時にデフォルトで作成されるものです。以下IAMポリシーがアタッチされていました。

AWSLambdaBasicExecutionRole-46e33b43-1130-4b8a-bb10-aed1ba70f2a9

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:us-east-1:<AWSアカウントID>:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:us-east-1:<AWSアカウントID>:log-group:/aws/lambda/vpc-lambda:*"
            ]
        }
    ]
}

AWSLambdaVPCAccessExecutionRole-15416f43-2f0d-43ff-b431-06536ae4e3a4

{
  "Version": "2012-10-17",
  "Statement": [
      {
          "Effect": "Allow",
          "Action": [
              "ec2:CreateNetworkInterface",
              "ec2:DeleteNetworkInterface",
              "ec2:DescribeNetworkInterfaces"
          ],
          "Resource": "*"
      }
  ]
}

やってみた

VPC Lambdaが参照しているDNSサーバーを確認する

まず、VPC Lambdaが参照しているDNSサーバーを確認してみます。

DHCPオプションセットはデフォルトのもの = DNSサーバー(フォワーダー & フルサービスリゾルバ)はRoute 53 Resolverです。

使用するコードは以下のとおりです。

import * as dns from 'dns';

export const handler = async (event) => {
  console.log(dns.getServers())
};

実行結果は以下のとおりです。

START RequestId: afaae791-f2ca-4ff7-a0ea-51f254e3d0a7 Version: $LATEST
2023-08-12T03:11:38.081Z	afaae791-f2ca-4ff7-a0ea-51f254e3d0a7	INFO	[ '169.254.78.1' ]
END RequestId: afaae791-f2ca-4ff7-a0ea-51f254e3d0a7
REPORT RequestId: afaae791-f2ca-4ff7-a0ea-51f254e3d0a7	Duration: 58.03 ms	Billed Duration: 59 ms	Memory Size: 128 MB	Max Memory Used: 66 MB	Init Duration: 177.40 ms

169.254.78.1というIPアドレスがDNSサーバーとして返ってきました。何回実行しても169.254.78.1でした。

Route 53 ResolverのIPアドレスは169.254.169.253もしくはfd00:ec2::253、VPCのCIDR+2です。

Route 53 Resolver (「Amazon DNS サーバー」または「AmazonProvidedDNS」とも呼ばれます) は、AWS リージョン内の各アベイラビリティーゾーンに組み込まれている DNS リゾルバーサービスです。Route 53 Resolver は 169.254.169.253 (IPv4)、fd00:ec2::253 (IPv6)、および VPC+2 にプロビジョニングされたプライマリプライベート IPV4 CIDR 範囲に配置されています。例えば、IPv4 CIDR が 10.0.0.0/16 で、IPv6 CIDR が fd00:ec2::253 の VPC がある場合、Route 53 Resolver には 169.254.169.253 (IPv4)、fd00:ec2::253 (IPv6)、または 10.0.0.2 (IPv4) でアクセスできます。

VPC の DNS 属性 - Amazon Virtual Private Cloud

169.254.78.1というIPアドレスであることから、リンクローカルアドレスであることが分かります。

VPC Lambdaではない、通常のLambdaでも同じコードで実行してみます。

START RequestId: 37a49730-5a48-4e36-8db9-62fd2d46e8c2 Version: $LATEST
2023-08-12T03:13:05.337Z	37a49730-5a48-4e36-8db9-62fd2d46e8c2	INFO	[ '169.254.78.1' ]
END RequestId: 37a49730-5a48-4e36-8db9-62fd2d46e8c2
REPORT RequestId: 37a49730-5a48-4e36-8db9-62fd2d46e8c2	Duration: 55.48 ms	Billed Duration: 56 ms	Memory Size: 128 MB	Max Memory Used: 66 MB	Init Duration: 178.67 ms

同じく169.254.78.1でした。

VPC上かどうかに関わらず同じIPアドレスが使われるということは、こちらのIPアドレスはLamdaのService VPC上で動作するフォワーダーなのでしょうか。

VPC Lambdaが参照するDNSサーバーを変更してみる

次にVPC Lambdaが参照するDNSサーバーを8.8.8.8に変更してみましょう。

以下のコードでLambdaを実行します。

import * as dns from 'dns';

export const handler = async (event) => {
  console.log(dns.getServers())
  console.log(await dns.promises.resolve4('dev.classmethod.jp'));
  
  dns.setServers(['8.8.8.8'])
  console.log(dns.getServers())
  console.log(await dns.promises.resolve4('dev.classmethod.jp'));
};

実行結果は以下のとおりです。

START RequestId: 6dc9c86f-7198-409f-9ac1-107e21042c60 Version: $LATEST
2023-08-12T03:17:28.178Z	6dc9c86f-7198-409f-9ac1-107e21042c60	INFO	[ '169.254.78.1' ]
2023-08-12T03:17:28.226Z	6dc9c86f-7198-409f-9ac1-107e21042c60	INFO	[ '13.248.175.13', '76.223.57.58' ]
2023-08-12T03:17:28.243Z	6dc9c86f-7198-409f-9ac1-107e21042c60	INFO	[ '169.254.78.1' ]
2023-08-12T03:17:28.263Z	6dc9c86f-7198-409f-9ac1-107e21042c60	INFO	[ '13.248.175.13', '76.223.57.58' ]
END RequestId: 6dc9c86f-7198-409f-9ac1-107e21042c60
REPORT RequestId: 6dc9c86f-7198-409f-9ac1-107e21042c60	Duration: 90.84 ms	Billed Duration: 91 ms	Memory Size: 128 MB	Max Memory Used: 66 MB	Init Duration: 184.91 ms

8.8.8.8に変更後のdns.getServers()も変わらず169.254.78.1でした。名前解決の結果も同じなので本当に変更されているのか分かりませんね。

一時的にVPC LambdaのENIへのElastic IPアドレスの関連付けを解除して再実行します。

START RequestId: c98dfe39-f3ed-43a7-a873-ac43a8012a59 Version: $LATEST
2023-08-12T03:21:41.388Z	c98dfe39-f3ed-43a7-a873-ac43a8012a59	INFO	[ '169.254.78.1' ]
2023-08-12T03:21:41.419Z	c98dfe39-f3ed-43a7-a873-ac43a8012a59	INFO	[ '13.248.175.13', '76.223.57.58' ]
2023-08-12T03:21:41.428Z	c98dfe39-f3ed-43a7-a873-ac43a8012a59	INFO	[ '169.254.78.1' ]
2023-08-12T03:21:44.367Z c98dfe39-f3ed-43a7-a873-ac43a8012a59 Task timed out after 3.01 seconds

END RequestId: c98dfe39-f3ed-43a7-a873-ac43a8012a59
REPORT RequestId: c98dfe39-f3ed-43a7-a873-ac43a8012a59	Duration: 3007.92 ms	Billed Duration: 3000 ms	Memory Size: 128 MB	Max Memory Used: 66 MB	Init Duration: 174.21 ms

8.8.8.8を指定した後に名前解決をした場合はタイムアウトになりましたね。Elastic IPアドレスが関連付けられていないがためにインターネットに抜けることができず、8.8.8.8にアクセスできないためです。

つまりは、Lambda関数内で参照するDNSサーバーを変更することはできそうです。

挙動からして、リンクローカルアドレスのフォワーダーを介して、指定したDNSサーバーにフォワーディングして名前解決をしているのだと推測します。

DHCPオプションセットでDNSサーバーを指定してみる

それでは、本題のDHCPオプションセットでDNSサーバーを指定してみます。

指定するDNSサーバーは存在しないIPアドレス10.1.1.10を指定します。つまりは名前解決をしようとすると必ず失敗する状態です。

処理の中で参照先DNSサーバーを8.8.8.8に変更して、再度名前解決をしてみます。

実行するコードは以下です。

import * as dns from "dns";

async function resolveName() {
  try {
    const resolvePromise = dns.promises.resolve4("dev.classmethod.jp");
    const timeoutPromise = new Promise((_, reject) =>
      setTimeout(() => reject("Timeout"), 1000)
    );
    console.log(await Promise.race([resolvePromise, timeoutPromise]));
  } catch (e) {
    console.log(e);
  }
}

export const handler = async (event) => {
  console.log(dns.getServers());
  await resolveName();

  dns.setServers(["8.8.8.8"]);
  console.log(dns.getServers());
  await resolveName();
};

実行結果は以下のとおりです。

START RequestId: 233e77fa-f5d5-443f-96d6-5e5a5d1cb522 Version: $LATEST
2023-08-12T03:40:06.192Z	233e77fa-f5d5-443f-96d6-5e5a5d1cb522	INFO	[ '169.254.78.1' ]
2023-08-12T03:40:07.213Z	233e77fa-f5d5-443f-96d6-5e5a5d1cb522	INFO	Timeout
2023-08-12T03:40:07.214Z	233e77fa-f5d5-443f-96d6-5e5a5d1cb522	INFO	[ '169.254.78.1' ]
2023-08-12T03:40:07.229Z	233e77fa-f5d5-443f-96d6-5e5a5d1cb522	INFO	[ '76.223.57.58', '13.248.175.13' ]
END RequestId: 233e77fa-f5d5-443f-96d6-5e5a5d1cb522
REPORT RequestId: 233e77fa-f5d5-443f-96d6-5e5a5d1cb522	Duration: 1077.43 ms	Billed Duration: 1078 ms	Memory Size: 128 MB	Max Memory Used: 66 MB	Init Duration: 179.23 ms

最初に名前解決をした時 = DHCPオプションセットで指定したDNSサーバーを使った名前解決はタイムアウトしてしまいましたが、8.8.8.8に変更した場合の名前解決は成功しています。ちなみに、dns.setServers(['169.254.169.253'])に変更しても結果は同じでした。

つまりはDHCPオプションセットの影響を受けることが判明しました。

Hyperplane ENIからENIにNATされたのち、DHCPオプションセットに従い名前解決をするのですね。

v2n-architecture-1024x613

抜粋 : Announcing improved VPC networking for AWS Lambda functions | AWS Compute Blog

Hyperplaneの詳細は以下記事をご覧ください。

もう少し確認してみましょう。

Lambdaの/etc/resolv.confを表示します。

使用するコードは以下のとおりです。

import { execSync } from "child_process"

export const handler = async (event) => {
  console.log(`cat /etc/resolv.conf : ${execSync("cat /etc/resolv.conf").toString()}`)
};

実行結果は以下のとおりです。

START RequestId: 3e645921-a5ad-4368-9245-716a4d809ae3 Version: $LATEST
2023-08-13T05:09:43.680Z	3e645921-a5ad-4368-9245-716a4d809ae3	INFO	cat /etc/resolv.conf : options timeout:2 attempts:5
; generated by /sbin/dhclient-script
; configured nameserver 10.1.1.10
nameserver 169.254.78.1

END RequestId: 3e645921-a5ad-4368-9245-716a4d809ae3
REPORT RequestId: 3e645921-a5ad-4368-9245-716a4d809ae3	Duration: 120.05 ms	Billed Duration: 121 ms	Memory Size: 128 MB	Max Memory Used: 67 MB	Init Duration: 199.74 ms

; configured nameserver 10.1.1.10とDHCPオプションセットで指定したDNSサーバーのIPアドレスが記載されています。

DHCPオプションセットをデフォルトのものに変更して再実行します。実行結果は以下のとおりです。

START RequestId: 11b65181-2e97-470c-92b1-9730dbcae49c Version: $LATEST
2023-08-13T05:08:24.101Z	11b65181-2e97-470c-92b1-9730dbcae49c	INFO	cat /etc/resolv.conf : options timeout:2 attempts:5
; generated by /sbin/dhclient-script
search ec2.internal
; configured nameserver 10.0.0.2
nameserver 169.254.78.1

END RequestId: 11b65181-2e97-470c-92b1-9730dbcae49c
REPORT RequestId: 11b65181-2e97-470c-92b1-9730dbcae49c	Duration: 111.19 ms	Billed Duration: 112 ms	Memory Size: 128 MB	Max Memory Used: 67 MB	Init Duration: 196.46 ms

; configured nameserver 10.0.0.2とIPアドレスがVPC CIDR +2 のアドレスに変わりましたね。

ちなみに/etc/resolv.confを無理やり書き換えようとすると怒られます。また、Lambda関数にsudoはありません。

START RequestId: 4fc7ef64-84c8-42c7-b92c-1fb7550e3847 Version: $LATEST
2023-08-13T05:42:07.288Z	4fc7ef64-84c8-42c7-b92c-1fb7550e3847	INFO	cat /etc/resolv.conf : /etc/resolv.conf

2023-08-13T05:42:07.467Z	4fc7ef64-84c8-42c7-b92c-1fb7550e3847	INFO	ls -l  /etc/resolv.conf : -rw-r--r-- 1 root root 124 Aug 13 05:42 /etc/resolv.conf

sed: couldn't open temporary file /etc/sedeFyYCN: Read-only file system
2023-08-13T05:42:07.530Z	4fc7ef64-84c8-42c7-b92c-1fb7550e3847	ERROR	Invoke Error 	{"errorType":"Error","errorMessage":"Command failed: sed -i 's/nameserver 169.254.78.1/nameserver 8.8.8.8/g' /etc/resolv.conf\nsed: couldn't open temporary file /etc/sedeFyYCN: Read-only file system\n","status":4,"signal":null,"output":[null,{"type":"Buffer","data":[]},{"type":"Buffer","data":[115,101,100,58,32,99,111,117,108,100,110,39,116,32,111,112,101,110,32,116,101,109,112,111,114,97,114,121,32,102,105,108,101,32,47,101,116,99,47,115,101,100,101,70,121,89,67,78,58,32,82,101,97,100,45,111,110,108,121,32,102,105,108,101,32,115,121,115,116,101,109,10]}],"pid":25,"stdout":{"type":"Buffer","data":[]},"stderr":{"type":"Buffer","data":[115,101,100,58,32,99,111,117,108,100,110,39,116,32,111,112,101,110,32,116,101,109,112,111,114,97,114,121,32,102,105,108,101,32,47,101,116,99,47,115,101,100,101,70,121,89,67,78,58,32,82,101,97,100,45,111,110,108,121,32,102,105,108,101,32,115,121,115,116,101,109,10]},"stack":["Error: Command failed: sed -i 's/nameserver 169.254.78.1/nameserver 8.8.8.8/g' /etc/resolv.conf","sed: couldn't open temporary file /etc/sedeFyYCN: Read-only file system","","    at checkExecSyncError (node:child_process:885:11)","    at execSync (node:child_process:957:15)","    at Runtime.handler (file:///var/task/index.mjs:6:93)","    at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1083:29)"]}
END RequestId: 4fc7ef64-84c8-42c7-b92c-1fb7550e3847
REPORT RequestId: 4fc7ef64-84c8-42c7-b92c-1fb7550e3847	Duration: 362.74 ms	Billed Duration: 363 ms	Memory Size: 128 MB	Max Memory Used: 69 MB	Init Duration: 199.77 ms

名前解決はできなくともLambdaの実行ログはCloudWatch Logsに出力できている謎

ここでふと、「DHCPオプションセットで存在しないDNSサーバーを参照させているのに、どうしてLambdaの実行ログはCloudWatch Logsに出力できているのか」と疑問に思いました。

LambdaからCloudWatch Logsへの通信は特別扱いなのでしょうか。それとも、Lambdaの実行ログのみ名前解決できなくともCloudWatch Logsに出力できるのでしょうか。

動作確認をして謎に迫ってみます。

DCHPオプションセットをデフォルトのものにします。

この状態でCloudWatch Logsのサービスエンドポイントの名前解決と、CloudWatch Logsのログストリームを作成するLambda関数を実行します。

実際のコードは以下のとおりです。

import * as dns from "dns";
import {
  CloudWatchLogsClient,
  CreateLogStreamCommand,
} from "@aws-sdk/client-cloudwatch-logs";

const logsClient = new CloudWatchLogsClient({region : "us-east-1"});

async function resolveName() {
  try {
    const resolvePromise = dns.promises.resolve4("logs.us-east-1.amazonaws.com");
    const timeoutPromise = new Promise((_, reject) =>
      setTimeout(() => reject("Timeout"), 500)
    );
    console.log(await Promise.race([resolvePromise, timeoutPromise]));
  } catch (e) {
    console.log(e);
  }
}

async function createLogStream(logGroupName, logStreamName) {
  const params = {
    logGroupName,
    logStreamName,
  };
  const command = new CreateLogStreamCommand(params);
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject("PutLogEvents Timeout"), 500)
  );
  try {
    const response = await Promise.race([
      logsClient.send(command),
      timeoutPromise,
    ]);
    console.log(response)
    console.log(`Log stream ${logStreamName} created in group ${logGroupName}`);
  } catch (e) {
    console.log(e);
  }
}

export const handler = async (event) => {
  console.log(dns.getServers());
  await resolveName();
  await createLogStream(
    "/aws/lambda/vpc-lambda",
    `test-log-stream-${new Date().getTime().toString()}`
  );
};

実行結果は以下のとおりです。

START RequestId: 9a182f92-18e5-4eb1-bd73-2cada974654f Version: $LATEST
2023-08-13T02:34:55.398Z	9a182f92-18e5-4eb1-bd73-2cada974654f	INFO	[ '169.254.78.1' ]
2023-08-13T02:34:55.456Z	9a182f92-18e5-4eb1-bd73-2cada974654f	INFO	[
  '44.202.79.238',
  '44.202.79.255',
  '3.236.94.136',
  '3.236.94.178',
  '44.202.79.141',
  '44.202.79.161',
  '3.236.94.129',
  '3.236.94.166'
]
2023-08-13T02:34:55.958Z	9a182f92-18e5-4eb1-bd73-2cada974654f	INFO	{
  '$metadata': {
    httpStatusCode: 200,
    requestId: '77b8ced1-2de3-4ff3-8302-81738eec2c67',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  }
}
2023-08-13T02:34:55.958Z	9a182f92-18e5-4eb1-bd73-2cada974654f	INFO	Log stream test-log-stream-1691894095457 created in group /aws/lambda/vpc-lambda
END RequestId: 9a182f92-18e5-4eb1-bd73-2cada974654f
REPORT RequestId: 9a182f92-18e5-4eb1-bd73-2cada974654f	Duration: 615.97 ms	Billed Duration: 616 ms	Memory Size: 128 MB	Max Memory Used: 91 MB	Init Duration: 570.62 ms

名前解決ができており、ログストリームの作成もできていそうですね。

続いて、存在しないIPアドレス10.1.1.10をDNSサーバーに指定したDHCPオプションセットに変更して再実行します。

実行結果は以下のとおりです。

START RequestId: 898dd7de-cf17-4126-8238-4421fe5fd492 Version: $LATEST
2023-08-13T02:35:49.934Z	898dd7de-cf17-4126-8238-4421fe5fd492	INFO	[ '169.254.78.1' ]
2023-08-13T02:35:50.474Z	898dd7de-cf17-4126-8238-4421fe5fd492	INFO	Timeout
2023-08-13T02:35:50.976Z	898dd7de-cf17-4126-8238-4421fe5fd492	INFO	PutLogEvents Timeout
END RequestId: 898dd7de-cf17-4126-8238-4421fe5fd492
REPORT RequestId: 898dd7de-cf17-4126-8238-4421fe5fd492	Duration: 1054.27 ms	Billed Duration: 1055 ms	Memory Size: 128 MB	Max Memory Used: 90 MB	Init Duration: 549.33 ms

サービスエンドポイントの名前解決、ログストリームの作成のどちらも失敗しています。

しかし、ご覧の通りLambda関数の実行ログはCloudWatch Logsに出力されています。

そのため、Lambdaの実行ログの出力の場合のみ、LambdaのService VPC上のHyperplane ENIで名前解決と、実際の通信が行われているのだと考えます。

おまけ : Lambda上でnslookupを実行してみる

Lambda上でdignslookupを実行することはできません。実行しようとしても「そんなコマンドはない」と怒られます。

どうしても実行してみたかったので、コマンドやライブラリをzipで固めてLambda Layerに登録します。

Node.js 18のマネージドランタイムはAmazon Linux 2なので、Amazon linux 2のコンテナイメージから必要なコマンドやライブラリを引っ張ってきます。

使用するDockerfileは以下のとおりです。

FROM public.ecr.aws/amazonlinux/amazonlinux:2

RUN yum install bind-utils which -y

RUN mkdir /layer/ && \
  ldd $(which nslookup) > /layer/libraries.txt

RUN mkdir -p /layer/lib && \
  for lib in $(awk '{ print $3 }' /layer/libraries.txt | sed '/^$/d'); do \
    if [ -f "$lib" ]; then \
      cp -L "$lib" /layer/lib/; \
    fi \
  done

RUN mkdir -p /layer/bin
RUN cp /usr/bin/nslookup /layer/bin/

こちらのDockerfileを使ってビルドを行い、ビルド結果からコマンドやライブラリを抽出してzipで固めます。

> docker build -t lambda-layer .
[+] Building 16.0s (10/10) FINISHED
 => [internal] load build definition from Dockerfile                                                                           0.0s
 => => transferring dockerfile: 535B                                                                                           0.0s
 => [internal] load .dockerignore                                                                                              0.0s
 => => transferring context: 2B                                                                                                0.0s
 => [internal] load metadata for public.ecr.aws/amazonlinux/amazonlinux:2                                                      0.2s
 => CACHED [1/6] FROM public.ecr.aws/amazonlinux/amazonlinux:2@sha256:fc2f8916205f191471880b45ca3c34fc13fa895ac75abdcb31e3c97  0.0s
 => [2/6] RUN yum install bind-utils which -y                                                                                 13.7s
 => [3/6] RUN mkdir /layer/ &&   ldd $(which nslookup) > /layer/libraries.txt                                                  0.4s
 => [4/6] RUN mkdir -p /layer/lib &&   for lib in $(awk '{ print $3 }' /layer/libraries.txt | sed '/^$/d'); do   if [ -f "$li  0.4s
 => [5/6] RUN mkdir -p /layer/bin                                                                                              0.3s
 => [6/6] RUN cp /usr/bin/nslookup /layer/bin/                                                                                 0.4s
 => exporting to image                                                                                                         0.5s
 => => exporting layers                                                                                                        0.5s
 => => writing image sha256:0ffcace840ed8413fdbb96a90f5926197dd32cdb297841666430ab7cefeb2459                                   0.0s
 => => naming to docker.io/library/lambda-layer                                                                                0.0s

> docker create --name temp-container lambda-layer
72b207b6974f49ccc142b8603c2b8327206ed266d83c80cdaef578fdb51752d8

>  docker cp temp-container:/layer .
Preparing to copy...
Copying from container - 32.77kB
Copying from container - 65.54kB
Copying from container - 98.3kB
.
.
(中略)
.
.
Copying from container - 13.47MB
Copying from container - 13.5MB
Copying from container - 13.51MB
Successfully copied 13.51MB to /<ディレクトリパス>/.

> zip -r layer.zip ./layer
  adding: layer/ (stored 0%)
  adding: layer/.DS_Store (deflated 95%)
  adding: layer/libraries.txt (deflated 75%)
  adding: layer/bin/ (stored 0%)
.
.
(中略)
.
.
  adding: layer/lib/libcap.so.2 (deflated 74%)
  adding: layer/lib/libdl.so.2 (deflated 91%)
  adding: layer/lib/libbind9.so.160 (deflated 58%)
  adding: layer/lib/libisc.so.169 (deflated 58%)

作成したzipファイルを使ってLambda Layerを作成します。

dns-layer

作成したLambda LayerをLambda関数に割り当てます。

割り当て後、このままだとLambda Layer上のライブラリのパス/opt/layer/libを見てくれないので、環境変数LD_LIBRARY_PATH/opt/layer/lib:/lib64:/usr/lib64:$LAMBDA_RUNTIME_DIR:$LAMBDA_RUNTIME_DIR/lib:$LAMBDA_TASK_ROOT:$LAMBDA_TASK_ROOT/lib:/opt/libを指定します。

LD_LIBRARY_PATH

LD_LIBRARY_PATHは以下AWS公式ドキュメントを参考にしました。

この状態で以下コードを実行します。

import { execSync } from "child_process"

export const handler = async (event) => {
  console.log(`echo $LD_LIBRARY_PATH" : ${execSync("echo $LD_LIBRARY_PATH").toString()}`)
  console.log(`cat /etc/resolv.conf : ${execSync("cat /etc/resolv.conf").toString()}`)
  console.log(`/opt/layer/bin/nslookup logs.us-east-1.amazonaws.com -debug : ${execSync("/opt/layer/bin/nslookup logs.us-east-1.amazonaws.com -debug").toString()}`)
};

DHCPオプションセットはデフォルトのものに設定してLambdaを実行します。実行結果は以下のとおりです。

START RequestId: 570c56f5-58aa-4a38-aa53-b515d3ad9fde Version: $LATEST
2023-08-13T09:40:26.712Z	570c56f5-58aa-4a38-aa53-b515d3ad9fde	INFO	echo $LD_LIBRARY_PATH" : /opt/layer/lib:/lib64:/usr/lib64:$LAMBDA_RUNTIME_DIR:$LAMBDA_RUNTIME_DIR/lib:$LAMBDA_TASK_ROOT:$LAMBDA_TASK_ROOT/lib:/opt/lib

2023-08-13T09:40:26.790Z	570c56f5-58aa-4a38-aa53-b515d3ad9fde	INFO	cat /etc/resolv.conf : options timeout:2 attempts:5
; generated by /sbin/dhclient-script
search ec2.internal
; configured nameserver 10.0.0.2
nameserver 169.254.78.1

2023-08-13T09:40:27.990Z	570c56f5-58aa-4a38-aa53-b515d3ad9fde	INFO	/opt/layer/bin/nslookup logs.us-east-1.amazonaws.com -debug : Server:		169.254.78.1
Address:	169.254.78.1#53

------------
    QUESTIONS:
	logs.us-east-1.amazonaws.com, type = A, class = IN
    ANSWERS:
    ->  logs.us-east-1.amazonaws.com
	internet address = 3.236.94.231
	ttl = 18
    ->  logs.us-east-1.amazonaws.com
	internet address = 3.236.94.176
	ttl = 18
    ->  logs.us-east-1.amazonaws.com
	internet address = 3.236.94.198
	ttl = 18
    ->  logs.us-east-1.amazonaws.com
	internet address = 44.202.79.147
	ttl = 18
    ->  logs.us-east-1.amazonaws.com
	internet address = 3.236.94.242
	ttl = 18
    ->  logs.us-east-1.amazonaws.com
	internet address = 3.236.94.226
	ttl = 18
    ->  logs.us-east-1.amazonaws.com
	internet address = 3.236.94.233
	ttl = 18
    ->  logs.us-east-1.amazonaws.com
	internet address = 3.236.94.237
	ttl = 18
    AUTHORITY RECORDS:
    ADDITIONAL RECORDS:
------------
Non-authoritative answer:
Name:	logs.us-east-1.amazonaws.com
Address: 3.236.94.231
Name:	logs.us-east-1.amazonaws.com
Address: 3.236.94.176
Name:	logs.us-east-1.amazonaws.com
Address: 3.236.94.198
Name:	logs.us-east-1.amazonaws.com
Address: 44.202.79.147
Name:	logs.us-east-1.amazonaws.com
Address: 3.236.94.242
Name:	logs.us-east-1.amazonaws.com
Address: 3.236.94.226
Name:	logs.us-east-1.amazonaws.com
Address: 3.236.94.233
Name:	logs.us-east-1.amazonaws.com
Address: 3.236.94.237
------------
    QUESTIONS:
	logs.us-east-1.amazonaws.com, type = AAAA, class = IN
    ANSWERS:
    AUTHORITY RECORDS:
    ->  logs.us-east-1.amazonaws.com
	origin = ns-140.awsdns-17.com
	mail addr = awsdns-hostmaster.amazon.com
	serial = 1
	refresh = 7200
	retry = 900
	expire = 1209600
	minimum = 60
	ttl = 4
    ADDITIONAL RECORDS:
------------


END RequestId: 570c56f5-58aa-4a38-aa53-b515d3ad9fde
REPORT RequestId: 570c56f5-58aa-4a38-aa53-b515d3ad9fde	Duration: 1342.54 ms	Billed Duration: 1343 ms	Memory Size: 128 MB	Max Memory Used: 81 MB	Init Duration: 192.16 ms

正常にnslookupコマンドが叩けていますね。

続いて、存在しないIPアドレス10.1.1.10をDNSサーバーに指定したDHCPオプションセットに変更して再実行します。

実行結果は以下のとおりです。

START RequestId: 7b9fc2bb-5414-415b-abd9-11bfee6df925 Version: $LATEST
2023-08-13T09:45:20.083Z	7b9fc2bb-5414-415b-abd9-11bfee6df925	INFO	echo $LD_LIBRARY_PATH" : /opt/layer/lib:/lib64:/usr/lib64:$LAMBDA_RUNTIME_DIR:$LAMBDA_RUNTIME_DIR/lib:$LAMBDA_TASK_ROOT:$LAMBDA_TASK_ROOT/lib:/opt/lib

2023-08-13T09:45:20.163Z	7b9fc2bb-5414-415b-abd9-11bfee6df925	INFO	cat /etc/resolv.conf : options timeout:2 attempts:5
; generated by /sbin/dhclient-script
; configured nameserver 10.1.1.10
nameserver 169.254.78.1

2023-08-13T09:45:23.020Z 7b9fc2bb-5414-415b-abd9-11bfee6df925 Task timed out after 3.01 seconds

END RequestId: 7b9fc2bb-5414-415b-abd9-11bfee6df925
REPORT RequestId: 7b9fc2bb-5414-415b-abd9-11bfee6df925	Duration: 3006.09 ms	Billed Duration: 3000 ms	Memory Size: 128 MB	Max Memory Used: 80 MB	Init Duration: 200.86 ms

タイムアウトしました。存在しないDNSサーバーに問い合わせようとしているので当然です。

Lambdaの処理の中で参照先のDNSサーバーを変更する方法はない認識なので、DHCPオプションセットをカスタマイズするときは気をつけよう

VPC LambdaはDHCPオプションセットの影響を受けるかどうか検証してみました。

Lambdaの処理の中で参照先のDNSサーバーを変更する方法はない認識です。DHCPオプションセットをカスタマイズする場合は、影響範囲を気につけましょう。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!