1. ホーム
  2. 記事一覧
  3. 【Terraformハンズオン】LambdaとGatewayエンドポイントを使ってS3にファイルを配置しよう

2024.09.30

【Terraformハンズオン】LambdaとGatewayエンドポイントを使ってS3にファイルを配置しよう

AWS LambdaとAmazon S3は、AWSでよく使われるサービスです。特に、AWSを扱うインフラエンジニアは絶対に触れることになるサービスです。

Lambdaはサーバーレスでコードを実行できるサービスで、S3はオブジェクトストレージとして様々なファイルの保存に適しています。

この記事では、Gatewayエンドポイントについて解説し、Terraformを使用してプライベートサブネットにあるLambda関数から、S3バケットにファイルをアップロードするハンズオンを行います。

Terraformでのリソース構築もAWSを扱うインフラエンジアには必要なスキルですので、ぜひ手を動かして学んでみてください。

Gatewayエンドポイントとは

参考: ゲートウェイエンドポイント概要 https://docs.aws.amazon.com/ja_jp/vpc/latest/privatelink/gateway-endpoints.html#gateway-endpoint-overview

VPCでは、プライベートサブネットにあるリソースからインターネットゲートウェイやNATゲートウェイを使わずに、S3やDynamoDBなどのAWSサービスにアクセスするための仕組みを提供しています。

この仕組みの一つが「VPCエンドポイント」で、その中でもS3にアクセスする際に使われるのが「GatewayタイプのVPCエンドポイント」です。

https://docs.aws.amazon.com/ja_jp/vpc/latest/privatelink/gateway-endpoints.html#gateway-endpoint-overview

Gatewayエンドポイントの役割

通常、プライベートサブネット(外部のインターネットからアクセスできないネットワークセグメント)内にあるリソースがS3にアクセスするには、NATゲートウェイやインターネットゲートウェイを通してインターネットに出る必要があります。

Gatewayエンドポイントは、この問題を解決するための仕組みになります。

GatewayエンドポイントをVPC内に作成すると、インターネットを経由せずにプライベートサブネットから直接S3へアクセスできるようになり、データ通信をAWSネットワーク内で完結することができます。

Gatewayエンドポイントのメリット

ここからは、Gatewayエンドポイントにはどんなメリットがあるのかを解説します。特にS3へのアクセスに焦点を当てて、セキュリティやコスト面での利点を見ていきましょう。

セキュリティの向上

Gatewayエンドポイントのメリットの一つは、セキュリティの向上です。

通常、プライベートサブネット内のリソースがS3にアクセスするには、NATゲートウェイやインターネットゲートウェイを通してインターネットに出る必要があります。しかしこの方法では、インターネットを経由するため、外部からの不正アクセスや攻撃のリスクが増します。

Gatewayエンドポイントを使うと、VPC内のリソースがインターネットを経由せずに直接S3にアクセスできます。

データ通信がAWSの内部ネットワークで完結するため、外部からの攻撃のリスクを低減できます。機密情報などをS3に保存する場合、Gatewayエンドポイントを利用することでインターネットに出ることなく安全にデータをやり取りできます。

https://docs.aws.amazon.com/ja_jp/vpc/latest/privatelink/vpc-endpoints-s3.html

コスト削減

NATゲートウェイを利用してプライベートサブネット内のリソースがS3にアクセスする場合、NATゲートウェイを設置している間の利用料金とデータ転送料金がかかります。仮に大量のデータを頻繁にS3とやり取りする場合、コストは積み重なっていきます。

Gatewayエンドポイントを利用することで、NATゲートウェイを作成する必要がなくなるため、NATゲートウェイの料金を丸ごと削減できます。

Gatewayエンドポイント自体には追加の利用料金は発生しませんが、S3へのデータ転送料金は通常通りかかる点は理解しておく必要があります。

設定方法がシンプル

Gatewayエンドポイントの設定は比較的シンプルです。VPC内のルートテーブルにGatewayエンドポイントを追加するだけで利用でき、エンドポイントの作成後はVPC内のすべてのリソースがそのエンドポイントを通じてS3にアクセスできるようになります。

Gatewayエンドポイントの注意点

Gatewayエンドポイントは便利な機能ですが、利用する際にはいくつか注意点があります。ここでは、導入前に押さえておくべき重要なポイントを紹介します。

利用できるサービスは限られている

Gatewayエンドポイントは、すべてのAWSサービスで利用できるわけではありません。2024年9月時点では、S3とDynamoDBの2つのサービスにのみ対応しています。

プライベートサブネットから他のS3とDynamoDB以外のAWSサービスにアクセスする場合は、Interfaceエンドポイントを利用する必要があります。

https://docs.aws.amazon.com/ja_jp/vpc/latest/privatelink/create-interface-endpoint.html

Interfaceエンドポイントを利用する場合、料金が発生することを認識しておく必要があります。

https://aws.amazon.com/jp/privatelink/pricing/

リージョンごとのエンドポイント

Gatewayエンドポイントは、作成したVPCの特定のリージョンでのみ有効です。複数のリージョンでS3やDynamoDBにアクセスする際には、各リージョンで個別にGatewayエンドポイントを設定する必要があります。

セキュリティグループの設定

Gatewayエンドポイント自体にはセキュリティグループを設定することはできません。

代わりに、エンドポイントを通してS3にアクセスするリソース(EC2やLambda)のセキュリティグループルールで通信を制御することができます。

セキュリティグループの設定には、AWSが提供するAWS-managed prefix listsを利用すると便利です。プレフィックスリストを使用すると、S3のIPアドレス範囲を簡単に指定でき、セキュリティグループのルール管理が楽になります。

プレフィックスリストに関しては、公式ドキュメントを参照ください。

https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/working-with-aws-managed-prefix-lists.html#available-aws-managed-prefix-lists

コストに関する考慮

前述のとおり、Gatewayエンドポイント自体の利用に追加の料金はかかりませんが、S3へのデータ転送料金は発生します。プライベートサブネットから大量のデータをS3に転送する場合、この転送料金がかかる点は理解しておく必要があります。

その他の注意点に関しては、以下公式ドキュメントを参照ください。

https://docs.aws.amazon.com/ja_jp/vpc/latest/privatelink/vpc-endpoints-s3.html#gateway-endpoint-considerations-s3

TerraformでGatewayエンドポイントを作成してみよう

ここからは、TerraformでVPC内にLambdaを構築し、Gatewayエンドポイントを通してS3バケットへオブジェクトを配置します。

今回のハンズオンでは、プライベートサブネットを持つVPCを作成し、そのVPC内でLambdaを作成します。そして、Gatewayエンドポイントを通してS3にアクセスするようリソースを作成していきます。

ディレクトリ構成

ディレクトリ構成は以下のように作成しました。

.
├── lambda-functions
│   └── index.mjs
├── lambda-layer
│   └── nodejs
│       ├── node_modules
│       ├── package-lock.json
│       └── package.json
├── main.tf
├── terraform.tfstate
├── terraform.tfstate.backup
└── versions.tf

レイヤー用ディレクトリの作成

今回はAWS SDKをLambdaで使用するため、AWS SDK用のレイヤーを作成します。

以下コマンドでディレクトリを作成し、AWS SDKをインストールします。

mkdir -p lambda-layer/nodejs

cd lambda-layer/nodejs

npm init -y

npm install aws-sdk

index.mjsの作成

実際のLambda関数となる、index.mjsファイルを作成します。

// index.mjs

import AWS from 'aws-sdk';

const s3 = new AWS.S3();

export const handler = async (event) => {
    const params = {
        Bucket: 'gateway-s3-bucket-xxxx',
        Key: 'test-file.txt',
        Body: 'Create Gateway Endpoint!',
    };

    try {
        await s3.putObject(params).promise();
        return {
            statusCode: 200,
            body: JSON.stringify({ message: 'File uploaded successfully!' }),
        };
    } catch (error) {
        return {
            statusCode: 500,
            body: JSON.stringify({ error: error.message }),
        };
    }
};

BucketKeyでそれぞれバケット名とファイル名を指定し、Bodyでファイルの内容を作成する簡単な関数を作成します。

main.tf

main.tfを以下のように作成し、GatewayエンドポイントやLambda関数など、必要なリソースを作成します。

# main.tf

# VPCの作成
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

# プライベートサブネットの作成
resource "aws_subnet" "private_subnet" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

# ルートテーブルの作成
resource "aws_route_table" "private_route_table" {
  vpc_id = aws_vpc.main.id
}

# ルートテーブルにサブネットを関連付け
resource "aws_route_table_association" "subnet_association" {
  subnet_id      = aws_subnet.private_subnet.id
  route_table_id = aws_route_table.private_route_table.id
}

# IAMロールの作成
resource "aws_iam_role" "lambda_upload_role" {
  name = "lambda_upload_role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

# IAMポリシーの作成
resource "aws_iam_policy" "lambda_s3_put_policy" {
  name = "lambda_s3_put_policy"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::gateway-s3-bucket-xxxx/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateNetworkInterface",     # ネットワークインターフェイスの作成
        "ec2:DescribeNetworkInterfaces",  # ネットワークインターフェイスの参照
        "ec2:DeleteNetworkInterface"      # ネットワークインターフェイスの削除
      ],
      "Resource": "*"
    }
  ]
}
EOF
}

# IAMポリシーのアタッチ
resource "aws_iam_role_policy_attachment" "lambda_policy" {
  role       = aws_iam_role.lambda_upload_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy_attachment" "s3_policy_attachment" {
  role       = aws_iam_role.lambda_upload_role.name
  policy_arn = aws_iam_policy.lambda_s3_put_policy.arn
}

# Lambda Layer用ZIPファイルの作成
data "archive_file" "layer_zip" {
  type        = "zip"
  source_dir  = "${path.module}/lambda-layer"
  output_path = "${path.module}/aws_sdk_layer.zip"
}

# Lambda Layerの作成
resource "aws_lambda_layer_version" "aws_sdk_layer" {
  layer_name          = "aws_sdk_layer"
  compatible_runtimes = ["nodejs20.x"]
  filename            = data.archive_file.layer_zip.output_path
  source_code_hash    = data.archive_file.layer_zip.output_base64sha256
}

# Lambda関数用ZIPファイルの作成
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/lambda-functions"
  output_path = "${path.module}/lambda_function.zip"
}

# セキュリティグループの作成
resource "aws_security_group" "lambda_sg" {
  name   = "lambda_sg"
  vpc_id = aws_vpc.main.id
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Lambda関数の作成
resource "aws_lambda_function" "s3_upload_lambda" {
  function_name    = "s3-upload-function"
  role             = aws_iam_role.lambda_upload_role.arn
  handler          = "index.handler"
  runtime          = "nodejs20.x"
  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  # VPCの設定
  vpc_config {
    subnet_ids         = [aws_subnet.private_subnet.id]
    security_group_ids = [aws_security_group.lambda_sg.id]
  }

  # LayerをLambda関数に追加
  layers = [aws_lambda_layer_version.aws_sdk_layer.arn]
}

# S3 Gatewayエンドポイントの作成
resource "aws_vpc_endpoint" "s3_endpoint" {
  vpc_id          = aws_vpc.main.id
  service_name    = "com.amazonaws.ap-northeast-1.s3"
  route_table_ids = [aws_route_table.private_route_table.id]
}

# S3バケットの作成
resource "aws_s3_bucket" "gateway_s3_bucket" {
  bucket = "gateway-s3-bucket-xxxx"
}

# S3バケットポリシーの作成
resource "aws_s3_bucket_policy" "gateway_s3_policy" {
  bucket = aws_s3_bucket.gateway_s3_bucket.bucket

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::gateway-s3-bucket-xxxx/*"
    }
  ]
}
EOF
}

ネットワークインターフェイスの権限を付与

LambdaにアタッチするIAM Policyを以下のように作成します。

# IAMポリシーの作成
resource "aws_iam_policy" "lambda_s3_put_policy" {
  name = "lambda_s3_put_policy"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::gateway-s3-bucket-xxxx/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateNetworkInterface",     # ネットワークインターフェイスの作成
        "ec2:DescribeNetworkInterfaces",  # ネットワークインターフェイスの参照
        "ec2:DeleteNetworkInterface"      # ネットワークインターフェイスの削除
      ],
      "Resource": "*"
    }
  ]
}
EOF
}

Actionec2:Create~の部分で、VPC内でLambda関数を実行するために必要な3つのEC2アクションを許可しています。

このアクションを許可することで、LambdaがVPC内で動作する際に自動的にネットワークインターフェイスを作成、参照、削除することが可能になります。

バケットポリシーの作成

以下リソースで、作成したS3バケットへLambdaがオブジェクトを配置するための権限を許可します。

# S3バケットポリシーの作成
resource "aws_s3_bucket_policy" "gateway_s3_policy" {
  bucket = aws_s3_bucket.gateway_s3_bucket.bucket

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::gateway-s3-bucket-xxxx/*"
    }
  ]
}
EOF
}

Lambda関数、レイヤーの作成

Lambda関数の定義は次のように作成します。

vpc_configでどのサブネット、セキュリティグループに関数を関連付けるかを定義します。

layersでは、どのレイヤーを関数で使用するかを定義しています。

# Lambda関数の作成
resource "aws_lambda_function" "s3_upload_lambda" {
  function_name    = "s3-upload-function"
  role             = aws_iam_role.lambda_upload_role.arn
  handler          = "index.handler"
  runtime          = "nodejs20.x"
  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  # VPCの設定
  vpc_config {
    subnet_ids         = [aws_subnet.private_subnet.id]
    security_group_ids = [aws_security_group.lambda_sg.id]
  }

  # LayerをLambda関数に追加
  layers = [aws_lambda_layer_version.aws_sdk_layer.arn]
}

Lambda関数の定義、レイヤーの追加に関しては、過去ハンズオン記事を参照ください。

https://envader.plus/article/466

https://envader.plus/article/467

https://envader.plus/article/474

AWS CLIでLambda関数のテストを実行

ソースコードを作成後、plan、applyを実行しリソースを作成します。

terraform plan

terraform apply

リソース作成の完了後、AWS CLIでテストを実行します。

aws lambda invoke --function-name <関数名> --payload '{}' output.json

最後に、S3バケットにtest-file.txtが作成されているかを確認します。

s3://の後には、作成したバケット名を指定します。

aws s3 ls s3://gateway-s3-bucket-xxxx
2024-xx-xx xx:xx:xx         24 test-file.txt

test-file.txtが作成されていることが確認できれば完了です。

まとめ

この記事では、AWSのGatewayエンドポイントについて解説しました。

ハンズオンの部分では、IaCツールのTerraformを使用してプライベートサブネット内のLambda関数からS3バケットにファイルをアップロードするまでのリソース作成を行いました。

このハンズオンを通じて、VPC内でLambda関数を動かし、インターネットを経由せずにS3にアクセスする方法を体験いただけたと思います。

AWSを利用するインフラエンジニアにとって、こうした知識は日々の業務に役立つスキルとなりますので、ぜひ何回も手を動かしながら、理解を深めていきましょう。

参考記事

ゲートウェイエンドポイント概要

Amazon S3 のゲートウェイエンドポイント

インターフェイスVPCエンドポイント AWS のサービス を使用して にアクセスする

AWS PrivateLink の料金

使用可能な AWS マネージドプレフィックスリスト

【Terraformハンズオン】同期呼び出しのLambda関数をデプロイしてみよう

【Terraformハンズオン】非同期呼び出しのLambda関数をデプロイしてみよう

【Terraformハンズオン】Lambda関数にレイヤーを追加してみよう

【番外編】USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

IT未経験者必見 USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

プログラミング塾に半年通えば、一人前になれると思っているあなた。それ、勘違いですよ。「なぜ間違いなの?」「正しい勉強法とは何なの?」ITを学び始める全ての人に知って欲しい。そう思って書きました。是非読んでみてください。

「フリーランスエンジニア」

近年やっと世間に浸透した言葉だ。ひと昔まえ、終身雇用は当たり前で、大企業に就職することは一種のステータスだった。しかし、そんな時代も終わり「優秀な人材は転職する」ことが当たり前の時代となる。フリーランスエンジニアに高価値が付く現在、ネットを見ると「未経験でも年収400万以上」などと書いてある。これに釣られて、多くの人がフリーランスになろうとITの世界に入ってきている。私もその中の1人だ。数年前、USBも知らない状態からITの世界に没入し、そこから約2年間、毎日勉学を行なった。他人の何十倍も努力した。そして、企業研修やIT塾で数多くの受講生の指導経験も得た。そこで私は、伸びるエンジニアとそうでないエンジニアをたくさん見てきた。そして、稼げるエンジニア、稼げないエンジニアを見てきた。

「成功する人とそうでない人の違いは何か?」

私が出した答えは、「量産型エンジニアか否か」である。今のエンジニア市場には、量産型エンジニアが溢れている!!ここでの量産型エンジニアの定義は以下の通りである。

比較的簡単に学習可能なWebフレームワーク(WordPress, Rails)やPython等の知識はあるが、ITの基本概念を理解していないため、単調な作業しかこなすことができないエンジニアのこと。

多くの人がフリーランスエンジニアを目指す時代に中途半端な知識や技術力でこの世界に飛び込むと返って過酷な労働条件で働くことになる。そこで、エンジニアを目指すあなたがどう学習していくべきかを私の経験を交えて書こうと思った。続きはこちらから、、、、

note記事3000いいね超えの殿堂記事 今すぐ読む

エンベーダー編集部

エンベーダーは、ITスクールRareTECHのインフラ学習教材として誕生しました。 「遊びながらインフラエンジニアへ」をコンセプトに、インフラへの学習ハードルを下げるツールとして運営されています。

RareTECH 無料体験授業開催中! オンラインにて実施中! Top10%のエンジニアになる秘訣を伝授します! RareTECH講師への質疑応答可

関連記事