Tailscaleに接続できるAWS LambdaをCDKで作成する
はじめに
前回の記事で紹介した通り、P2P型のVPNであるTailscaleは設定が簡単であり、非常に便利だ。
そんなTailscale ネットワークにAWS Lambdaを接続できるらしい。が、公式ドキュメントを読んでも今一つピンとこなかった。
比較的新しい機能ということもあって事例も見つからないため、試しに作って動かしてみた。
前提知識
AWS CDK
Tailscale on AWS Lambdaはコンテナで動かすことを前提にしている。
AWS上で管理するコンテナイメージは通常ECRを利用するが、検証目的で作成・削除を繰り返す際に、中のコンテナイメージを削除するのが少々面倒である。
であるが、先日AWS CDK v2.70.0で追加となったRepository deletion
機能を使えば、自動で削除できて便利だ。
cdk-docker-image-deployment
CDKでECRを構築するには工夫が必要である。
2023/4/1時点で公式が提供しているcdk-docker-image-deployment
が無難なので、こちらを利用する。
※本題から逸れるため、選定理由は割愛した。詳細を知りたい場合はこのブログを読むと背景を含めてわかる。
環境構築
1. CDK環境の整備
適当なフォルダを作ってprojenでプロジェクトを作成する。
mkdir tailscaleFn
cd tailscaleFn/
npx projen new awscdk-app-ts
cdk-docker-image-deploymentを導入するため、deps
を更新する。
const { awscdk } = require('projen');
const project = new awscdk.AwsCdkTypeScriptApp({
cdkVersion: '2.1.0',
defaultReleaseBranch: 'main',
name: 'tailscaleFn',
deps: ["cdk-docker-image-deployment"], /* Runtime dependencies of this module. */
// description: undefined, /* The description is just a string that helps people understand the purpose of the package. */
// devDeps: [], /* Build dependencies for this module. */
// packageName: undefined, /* The "name" in package.json. */
});
project.synth();
設定値を反映
npx projen
2. Tailscale Imageの作成
プロジェクトの中にapp/
ディレクトリを作成し、Imageに必要な資材を作成する。
bootstrap
bootstrapファイルは、前述のTailscaleのサンプルに、AWSのチュートリアルのサンプルを繋げて作っている。
#!/bin/sh
mkdir -p /tmp/tailscale
/var/runtime/tailscaled --tun=userspace-networking --socks5-server=localhost:1055 &
/var/runtime/tailscale up --authkey=${TAILSCALE_AUTHKEY} --hostname=aws-lambda-app
echo Tailscale started
ALL_PROXY=socks5://localhost:1055/ /var/runtime/my-app
set -euo pipefail
# Initialization - load function handler
source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh"
# Processing
while true
do
HEADERS="$(mktemp)"
# Get an event. The HTTP request will block until one is received
EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")
# Extract request ID by scraping response headers received above
REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)
# Run the handler function from the script
RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA")
# Send the response
curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response" -d "$RESPONSE"
done
この組み合わせによって、Tailscaleの初期設定が済んでいるBashのカスタムランタイムが起動する、というわけだ。
ちなみに${TAILSCALE_AUTHKEY}
は、後述するLambdaの環境変数経由で読み取るので、そのままでよい。
handler
Lambdaで動かすアプリケーションをbashで作成する。
こちらも前述のチュートリアルからの引用だが、差分としてsleep文を追加している。これしないとすぐ終了し、動作確認ができないからだ。今回は90秒間、起動する設定としている。
function handler () {
EVENT_DATA=$1
RESPONSE="{\"statusCode\": 200, \"body\": \"Hello from Lambda!\"}"
echo $RESPONSE
sleep 90
}
Dockerfile
tailscale公式の手順を参考に、Dockerfileを作成する。
差分は最後のENTRYPOINTをCMDに変更して、handlerを動作させている。
FROM public.ecr.aws/lambda/provided:al2 as builder
WORKDIR /app
COPY . ./
# This is where one could build the application code as well.
FROM alpine:latest as tailscale
WORKDIR /app
ENV TSFILE=tailscale_1.38.3_amd64.tgz
RUN wget https://pkgs.tailscale.com/stable/${TSFILE} && \
tar xzf ${TSFILE} --strip-components=1
FROM public.ecr.aws/lambda/provided:al2
# Copy binary to production image
COPY /app/bootstrap /var/runtime/bootstrap
COPY /app/hello.sh /var/task/hello.sh
COPY /app/tailscaled /var/runtime/tailscaled
COPY /app/tailscale /var/runtime/tailscale
RUN mkdir -p /var/run && ln -s /tmp/tailscale /var/run/tailscale && \
mkdir -p /var/cache && ln -s /tmp/tailscale /var/cache/tailscale && \
mkdir -p /var/lib && ln -s /tmp/tailscale /var/lib/tailscale && \
mkdir -p /var/task && ln -s /tmp/tailscale /var/task/tailscale
# Run on container startup.
# ENTRYPOINT ["/var/runtime/bootstrap"]
CMD ["hello.handler"]
注意:ファイルの権限設定
初回で失敗したのだがLamdbaをコンテナで動かす場合、実行権限を付与しないと失敗する。
chmod +x app/*
3 CDKによるデプロイ
準備
Tailscaleのセットアップ用するために必要なAuth Keyは、以下を参考に取得しておく。
Code
Auth keyは、今回はLambdaの環境変数で簡単に設定している。<AUTH_KEY>を各自のキーで置き換えて欲しい。
import { App, Duration, Stack, StackProps, RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as imagedeploy from 'cdk-docker-image-deployment';
const imageTag = "tailscale-init"
const repoName = "example-tailscale"
export class RepoStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps = {}) {
super(scope, id, props);
// 使い捨て用のECR Repositoryを作成
const repo = new ecr.Repository(this, 'MyTempRepo', {
repositoryName: repoName,
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteImages: true,
});
// `app/`配下をDocker Buildし、ECR RepositoryへPush
new imagedeploy.DockerImageDeployment(this, "ExampleTailscale",
{
source: imagedeploy.Source.directory("./app"),
destination: imagedeploy.Destination.ecr(repo, {
tag: imageTag,
}),
}
);
}
}
export class FnStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps = {}) {
super(scope, id, props);
// `RepoStack`で作成済みのRepositoryを名前指定で参照
const repo = ecr.Repository.fromRepositoryName(this, 'ExistRepo', "example-tailscale");
// `RepositoryのイメージからLambdaを作成`
// 環境変数`TAILSCALE_AUTHKEY`にキーを指定
new lambda.DockerImageFunction(this, 'ECRFunction', {
code: lambda.DockerImageCode.fromEcr(repo, {
tagOrDigest: imageTag,
}),
timeout: Duration.minutes(15),
environment: {
TAILSCALE_AUTHKEY: "<AUTH_KEY>"
}
});
}
}
// for development, use account/region from cdk cli
const devEnv = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
};
const app = new App();
new RepoStack(app, 'repo', { env: devEnv });
new FnStack(app, 'fn', { env: devEnv });
app.synth();
Deploy
以下のコマンドで、自動的にコンテナイメージのビルドも含めた一連のデプロイが完了する。
cdk deploy repo
cdk deploy fn
ちなみにスタックを分割している理由は、ECRへのDeployを待たずにLambdaがデプロイされてしまうのを避けるため、明示的に分割したかったからだ。
※fn.addDependsOn(deployment)
のように書けば動く可能性はある。
4. 動作確認
Lambdaをテスト実行し、90秒ほど動作し続けている間に、tailscaleネットワーク上の他のマシンからpingを打つと疎通することがわかる。
ping aws-lambda-app
PING aws-lambda-app.tail5c971.ts.net (100.85.119.28) 56(84) bytes of data.
64 bytes from aws-lambda-app.tail5c971.ts.net (100.85.119.28): icmp_seq=1 ttl=64 time=17.0 ms
64 bytes from aws-lambda-app.tail5c971.ts.net (100.85.119.28): icmp_seq=2 ttl=64 time=3.93 m
...
当然だが、Lambdaの実行が終了すると返事がなくなる。想定通りの動作だ。
まとめ
細かい情報を集めるのに苦労したが、Tailscaleに接続できるAWS LambdaをCDKで作成することに成功した。
実際のワークロードにのせる場合は、複数台実行した際のライセンス周りが気になるところである。
Discussion