みなさまはじめまして。ソリューション技術部の岡田です。
年始に今年やりたいことを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アドレスを固定化させました。
- VPCを用意して、パブリックサブネットとプライベートサブネットを作成
- プライベートサブネットにLambdaを配置し、セキュリティグループを設定
- パブリックサブネットにNAT Gatewayを配置
- プライベートサブネットから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アカウントの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アドレスを固定化させたいという方の参考に少しでもなれば幸いです。
最後まで読んでいただきありがとうございました。