PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

Lambdaから外部サービスにリクエストする際のIPアドレスを固定した話

みなさまはじめまして。ソリューション技術部の岡田です。

年始に今年やりたいことを100個書き出したのですが、2ヶ月ほど過ぎた現在まだ6つしか達成できていない状況に若干の焦りを感じながら過ごしている今日この頃です。

さて今回はある開発にてAWS LambdaでAPIサーバーを構築したのですが、APIサーバーから外部サービスにリクエストする際に、IPアドレスの制限がかかっているためIPアドレスを固定化する必要があったので、その対応方法についてとそれをCloudFormationで対応する方法について紹介します。

CloudFormationではなくAWSのコンソール画面から対応したいという方にもどのような構成で対応すれば良いのか説明しておりますので、ぜひ読んでいただければと思います。

結論

先に結論からお伝えすると、

LambdaをVPC内のプライベートサブネットに配置し、外部通信する際はNAT Gateway経由でリクエストする

です。

最終的な構成図は以下のようなイメージになります。


それでは順番に説明していきます。

そもそもLambdaのIPアドレスって?

Lambdaはイベント駆動型のコンピューティングサービスです。

そのため、呼び出しがあるたびにLambdaに書かれたソースコードが実行されます。

常に起動しているEC2などのサーバーとは異なり、必要な時にのみ起動されるため毎回IPアドレスは変わります。

そのため、Lambda単体ではIPアドレスを固定化させることはできません。

※ 今回のような外部サービスなどに特定のIPアドレスでリクエストしたいなどの要件がない限りは、毎回IPアドレスが変わることでの影響はないため特に気にする必要はないと思います。

Lambdaからリクエストする際のIPアドレスを固定させるには?

では実際にIPアドレスを固定化させた方法について解説していきます。 以下の手順でLambdaのIPアドレスを固定化させました。

  1. VPCを用意して、パブリックサブネットとプライベートサブネットを作成
  2. プライベートサブネットにLambdaを配置し、セキュリティグループを設定
  3. パブリックサブネットにNAT Gatewayを配置
  4. プライベートサブネットからNAT Gatewayにアクセスできるようにルートテーブルを設定

参考: repost.aws

順番に解説していきます。

まずは、これから登場するAWSサービスについて簡単に説明します。

※ 既にご存知であれば飛ばしていただいても問題ありません。


ここからは以下のシステム構成をイメージしながら説明していきます。

  • 管理画面をEC2で建て、RDSでデータを管理
  • デプロイサーバをプライベートに建てる(モジュールをインストールするため外部への通信が発生する)
  • 冗長化のためマルチAZ構成にする


VPC:

VPC(Virtual Private Cloud)は、AWS上にプライベートネットワーク空間を構築できるサービスです。 AWSアカウント内に専用のネットワークを作成することができ、EC2やRDSなどのAWSリソースを配置することができます。

VPCはリージョンごとに作成することができますが、リージョンを跨ぐことはできません。

また、通信経路やIPアドレスなどの設定が可能なため、配置したリソースを外部からの不正アクセスや攻撃から守るようにすることができます。

今回のように普段はVPC外にあるリソースを配置することでそのリソースへのアクセス制限などを行うこともできます。


IGW(インターネットゲートウェイ):

VPCとインターネット間の通信を可能にするためのものになります。

イメージとしてはVPCの玄関のようなもので、VPCとインターネットを行き来する場合は必ずIGWを通る必要があります。

窓がない家に入るには玄関がないと入れないのと同じですね。(そんな家あるのか?)

※ ちなみに今回の構成でいえば、API Gatewayからプライベートサブネットに置かれているLambdaにIGWを経由しなくても直接アクセスすることができています。不思議ですね。

おそらくAWSのリソースからのアクセスはIGWを経由しなくても通信できる方法があるということだと思いますが、それがどのようにして実現されているかに関する資料は見つからなかったです。。残念


サブネット:

VPCのIPアドレスの範囲になります。 作成したサブネットにAWS リソースを配置していきます。

また、サブネットにはパブリックサブネットとプライベートサブネットが存在します。 これらの違いとしてはインターネットと通信できるかどうかになります。

具体的に言うと、ルートテーブルでIGWにルーティングされているものはインターネットに通信できるためパブリックサブネットとなり、そうではないものはプライベートサブネットという分け方になります。

それぞれの用途ですが、

  • パブリックサブネットは外部からアクセスしても問題ないWebサーバーなどを設置
  • プライベートサブネットには外部からアクセスされたくないサーバーやDBなどを設置(今回でいうとRDSやデプロイサーバー)

という用途が一般的です。


ルートテーブル:

VPC内の通信を管理するもので、通信経路を設定することができます。

例えば、プライベートサブネットからインターネットに接続するためにNAT Gatewayに接続したい場合は、ルートテーブルでインターネット宛の通信の向き先をNAT Gatewayに設定しておくとプライベートサブネットから外部へ通信ができる状態に設定することができます。


セキュリティグループ:

VPC内でリソースに適用することができる仮想ファイアウォール機能です。

リソースへのインバウンドとアウトバウンドをIPアドレスと通信プロトコルの組み合わせで制御することができます。

今回は使用しませんでしたが、セキュリティグループを設定することでリソースへのアクセスを特定のIPアドレスに制限することができます。

例えば、RDSへのアクセスはパブリックサブネットに置いたEC2からのみのような設定を行いたい場合は、EC2のIPアドレスをセキュリティグループのインバウンドルールとして設定することで実現できます。


NAT Gateway:

ネットワークアドレス変換サービスで、プライベートサブネットからインターネットにアクセスさせたい場合に使用します。

ルートテーブルでプライベートサブネットからNAT Gatewayに向かうようルーティングを設定すればインターネットに接続できるようになります。

また、NAT Gatewayはパブリックサブネットに作成する必要があり、作成すると必ずElastic IPを紐づける必要があるため、IPアドレスが固定化されます。

※ 逆に外からNAT Gatewayを経由してプライベートサブネットへの直接の通信はできません。 プライベートサブネットに通信したい場合はパブリックサブネットを経由して通信する必要があります。


では、LambdaのIPアドレスを固定化させる流れを説明していきます。

1. VPCを用意して、パブリックサブネットとプライベートサブネットを作成

まずはVPCを作成します。 その後パブリックサブネットとプライベートサブネットが各AZごとに必要なため、サブネットを6つ作成します。(今回は東京リージョンのため3AZ分作っています)

作成したうちの3つをパブリックサブネットにするため、ルートテーブルにてターゲットをIGWに設定したものを用意し、サブネットにアタッチします。

2. プライベートサブネットにLambdaを配置

次にLambdaをVPC内に置くために、プライベートサブネットに配置します。(VPC Lambdaとも言われる)

この時点ではまだ外部への通信はできない状態となります。

この時、セキュリティグループを設定するとLambdaへのアクセスを制御することができるため、より厳しいセキュリティをかけることができます。

また、今回の実装ではLambdaへのアクセスはAPI Gatewayにしているため、API Gatewayとの接続も設定しておきます。

API Gatewayに関しては、LambdaがVPCにあるかどうかを特に気にする必要はなく、API Gatewayから該当のLambda関数を指定するだけでAPI Gateway - Lambdaの接続を設定することができます。

3. パブリックサブネットにNAT Gatewayを配置

VPC Lambdaから外部への通信を可能にするためには、NAT Gatewayを用意する必要があります。

NAT Gateway作成時にはElastic IPをアタッチする必要があり、作成後はパブリックサブネットに配置します。

これでNAT Gatewayを通ったリクエストのIPアドレスは固定化されるようになります。

4. プライベートサブネットからNAT Gatewayにアクセスできるようにルートテーブルを設定

最後に、2と3で準備したプライベートサブネットとNAT Gatewayの通信経路を設定します。

ルートテーブルでプライベートサブネットのインターネット宛の通信の向き先をNAT Gatewayに設定すれば完了です。

これでLambdaから外部への通信もできるようになりました。

CloudFormationでの構築

ではここから上記の手順で行った作業を一気にCloudFormationで作成していきたいと思います。

登場人物としては以下になります。

  • VPC
  • サブネット
  • ルートテーブル
  • NAT Gateway
  • セキュリティグループ
  • Lambda
  • API Gateway

また、前提条件は以下になります。

  • 東京リージョン

※ 今回は冗長化のためマルチAZにしていますが、冗長化する必要がなければコストも抑えられるため1AZ構成で問題ないと思います。

実際のCloudFormationのテンプレートの内容は以下になります。

VPCネットワーク (クリックで展開)

vpc_network.yml

AWSTemplateFormatVersion: '2010-09-09'

Resources:
  # VPC定義
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: '10.32.0.0/16'
      EnableDnsHostnames: true
      EnableDnsSupport: true

  # パブリック用とプライベート用のSubnet作成
  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: '10.32.0.0/22'
      AvailabilityZone: 'ap-northeast-1a'
      MapPublicIpOnLaunch: 'false'
      VpcId: !Ref VPC
  PrivateSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: '10.32.4.0/22'
      AvailabilityZone: 'ap-northeast-1c'
      MapPublicIpOnLaunch: 'false'
      VpcId: !Ref VPC
  PrivateSubnetD:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: '10.32.8.0/22'
      AvailabilityZone: 'ap-northeast-1d'
      MapPublicIpOnLaunch: 'false'
      VpcId: !Ref VPC
  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: '10.32.12.0/22'
      AvailabilityZone: 'ap-northeast-1a'
      MapPublicIpOnLaunch: 'false'
      VpcId: !Ref VPC
  PublicSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: '10.32.16.0/22'
      AvailabilityZone: 'ap-northeast-1c'
      MapPublicIpOnLaunch: 'false'
      VpcId: !Ref VPC
  PublicSubnetD:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: '10.32.20.0/22'
      AvailabilityZone: 'ap-northeast-1d'
      MapPublicIpOnLaunch: 'false'
      VpcId: !Ref VPC

  # ルートテーブル定義
  PrivateRouteTableA:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
  PrivateRouteTableC:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
  PrivateRouteTableD:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  # インターネットゲートウェイ定義
  InternetGateway:
    Type: AWS::EC2::InternetGateway

  # インターネットゲートウェイをVPCにアタッチ
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    DependsOn: InternetGateway
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # NATGatewayA用のElasticIP定義
  NatEIpA:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
  NatEIpC:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
  NatEIpD:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  # NatGateway定義
  NATGatewayA:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatEIpA.AllocationId
      SubnetId: !Ref PublicSubnetA
  NATGatewayC:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatEIpC.AllocationId
      SubnetId: !Ref PublicSubnetC
  NATGatewayD:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatEIpD.AllocationId
      SubnetId: !Ref PublicSubnetD

  # ルートの指定
  AttachPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  AttachPrivateRouteA:
    Type: AWS::EC2::Route
    DependsOn: NATGatewayA
    Properties:
      RouteTableId: !Ref NatRouteTableA
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGatewayA
  AttachPrivateRouteC:
    Type: AWS::EC2::Route
    DependsOn: NATGatewayC
    Properties:
      RouteTableId: !Ref NatRouteTableC
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGatewayC
  AttachPrivateRouteD:
    Type: AWS::EC2::Route
    DependsOn: NATGatewayD
    Properties:
      RouteTableId: !Ref NatRouteTableD
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGatewayD

  # サブネットとルートテーブルの紐付け
  AttachPrivateSubnetAToRouteTable:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref NatRouteTableA
      SubnetId: !Ref PrivateSubnetA
  AttachPrivateSubnetCToRouteTable:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref NatRouteTableC
      SubnetId: !Ref PrivateSubnetC
  AttachPrivateSubnetDToRouteTable:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref NatRouteTableD
      SubnetId: !Ref PrivateSubnetD
  AttachPublicSubnetAToRouteTable:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetA
  AttachPublicSubnetCToRouteTable:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetC
  AttachPublicSubnetDToRouteTable:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetD

Outputs:
  VpcId:
    Value: !Ref VPC
    Export:
      Name: VpcId
  PrivateSubnetAId:
    Value: !Ref PrivateSubnetA
    Export:
      Name: private-subnet-a
  PrivateSubnetCId:
    Value: !Ref PrivateSubnetC
    Export:
      Name: private-subnet-c
  PrivateSubnetDId:
    Value: !Ref PrivateSubnetD
    Export:
      Name: private-subnet-d

Lambda & API Gateway (クリックで展開)

lambda.yml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  # API Gateway定義
  TestApiGateway:
    Type: AWS::Serverless::Api
    Properties:
      Auth:
        ResourcePolicy:
          CustomStatements:
            [{ 'Effect': 'Allow', 'Principal': '*', 'Action': 'execute-api:Invoke', 'Resource': 'execute-api:/*' }]
  # API Gatewayのパスマッピング定義
  TestApiPathMapping:
    Type: AWS::ApiGateway::BasePathMapping
    Properties:
      DomainName: !Ref TestDomain
      RestApiId: !Ref TestApiGateway
      BasePath: ''
      Stage: !Ref TestApiGateway.Stage
  # API Gatewayのdomain定義
  TestDomain:
    Type: AWS::ApiGateway::DomainName
    Properties:
      DomainName: !Ref Domain
      CertificateArn: !Ref CertificateArn

  # LambdaにアタッチするRole定義
  TestApiExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: LambdaVPCAccessExecutionRole
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeNetworkInterfaces
                  - ec2:CreateNetworkInterface
                  - ec2:DeleteNetworkInterface
                Resource: '*'
  # Lambdaの定義
  TestApi:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ../api    # API処理が書かれているディレクトリを指定
      Handler: index.handler
      Timeout: 180
      MemorySize: 512
      Runtime: nodejs20.x
      Environment:
        Variables:
          NODE_ENV: !Ref EnvType
          webhookApiDomain: !Ref TestDomain
      Role: !GetAtt TestApiExecutionRole.Arn
      Events:
        RequestCode:
          Type: Api
          Properties:
            Path: /test/request
            Method: get
            RestApiId: !Ref TestApiGateway
      # VpcConfigでVPC Lambdaの設定を行う(3AZ配置)
      VpcConfig:
        SubnetIds:
          - Fn::ImportValue:
              !Sub 'private-subnet-a'
          - Fn::ImportValue:
              !Sub 'private-subnet-c'
          - Fn::ImportValue:
              !Sub 'private-subnet-d'

ファイルはネットワーク周りとLambdaで分けました。 既にネットワーク周りが構築されている場合もあると思いますので、状況によって必要な部分を参考にしていただけたらと思います。

IAM Roleのことを説明していなかったのですが、今回LambdaにAWSLambdaVPCAccessExecutionRoleを付与しています。

これについて少し説明させてください。

そもそもLambda自体はAWSが管理しているLambda専用のVPC内に配置されています。

※ 余談: ちなみに他にもAPI GatewayもLambda同様、AWSが管理しているVPC内に配置されているようです。

aws.amazon.com

これを今回のように自分たちのAWSアカウントのVPCに配置すると、Lambda自体はそのままAWSが管理しているVPC内に配置されたままですが、自分たちのVPC内にENI(Elastic Network Interfaces)を作成し、それを経由して通信するようになります。

※ ENI: VPC内で利用する仮想ネットワークインターフェースのことで、物理的な環境におけるNIC(Network Interface Card)のこと。

AWSLambdaVPCAccessExecutionRoleはENIの作成や削除などを行う権限を付与するIAM Roleになります。

この権限がないと、ENIが作成できないためLambdaをVPCに接続させることができませんので、忘れずに付与するようにしてください。

まとめ

この記事では、Lambdaからリクエストする際のIPアドレスを固定化する方法とそれをCloudFormationで対応した方法について紹介しました。

VPC内の理解を深めることで、なぜLambdaのIPアドレスを固定できるのかイメージがついたかなと思います。

自分と同じようにLambdaから外部へリクエストする際のIPアドレスを固定化させたいという方の参考に少しでもなれば幸いです。

最後まで読んでいただきありがとうございました。