こんにちは!
サーバーレス開発が主流になってきた最近では、Lambda で固定 IP 制限環境へインターネットアクセスしたいなんてニーズが多くなってきました。
Serverless Framework を使用して Cloud Formation も書いたので、解説と共にシェアします!
Lamnda は Edge ロケーションで起動するサーバーなので、大前提としてLambda に固定 IP を付与することはできません。
基本的な固定 IP の付与の方法は Fargate と一緒で、NAT ゲートウェイ経由でインターネットへアクセスさせるようにします。
VPC などに慣れていないフロントエンドエンジニアの方の場合は、Edge ロケーション以外のサービスが入ってくるとい辛いところかもしれませんが、頑張ってついてきてください!( 不明点はお気軽に問い合わせください )
serverless.yaml で諸々インフラ情報を定義していきますが、最終的には Lambda to Lambda でリクエストし検証できるようにします。( 固定IPを持つ Lambda から Lambda へリクエストし API Gateway の SourceIP を確認することで、本当に固定 IP を持っているのか確認することが可能 )
まずはじめに、プラグイン含め npm インストールが必要です。
{
"name": "ragate-serverless",
"version": "1.0.0",
"description": "",
"scripts": {
"deploy": "sls deploy -v",
"remove": "sls remove"
},
"author": "Ragate",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"aws-sdk": "^2.680.0",
"serverless": "^1.71.1",
"serverless-webpack": "^5.3.2",
"webpack": "^4.43.0"
},
"dependencies": {
"axios": "^0.19.2",
"lodash": "^4.17.15"
}
}
次に、serverless.yaml を作成します。設定情報を外部ファイル読み込みしているので、後述します。
serverless.yaml
service: Example-Serverless
provider:
name: aws
runtime: nodejs12.x
stage: ${opt:stage,"dev"}
region: ap-northeast-1
profile: ragate # ここはよしなに変更してください
resources:
- ${file(./network.yml)} # 後述
functions: ${file(./api.yml)} # 後述
custom:
commonTag:
- Key: "Service"
Value: ${self:service}-${self:provider.stage}
webpack:
includeModules: true
packager: 'npm'
apiEndpoint:
Fn::Join:
- ""
- - "https://"
- Ref: "ApiGatewayRestApi"
- ".execute-api."
- ${self:provider.region}.
- Ref: "AWS::URLSuffix"
- "/"
- ${self:provider.stage}
package:
individually: true
plugins:
- serverless-webpack
network.yaml
Resources:
ExampleAppVpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: '10.0.0.0/16'
EnableDnsHostnames: false
EnableDnsSupport: true # ここがfalseになっていると外へ出て行った後に名前解決しません、Lambdaからインターネットへ通信ができません
InstanceTenancy: 'default'
Tags: ${self:custom.commonTag}
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId:
Ref: InternetGateway
VpcId:
Ref: ExampleAppVpc
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags: ${self:custom.commonTag}
VpcPrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: 'ap-northeast-1a'
CidrBlock: '10.0.1.0/24'
Tags: ${self:custom.commonTag}
VpcId:
Ref: ExampleAppVpc
VpcPublicSubnet:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: 'ap-northeast-1a'
CidrBlock: '10.0.2.0/24'
Tags: ${self:custom.commonTag}
VpcId:
Ref: ExampleAppVpc
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
Tags: ${self:custom.commonTag}
VpcId:
Ref: ExampleAppVpc
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
Tags: ${self:custom.commonTag}
VpcId:
Ref: ExampleAppVpc
PublicRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId:
Ref: VpcPublicSubnet
RouteTableId:
Ref: PublicRouteTable
PrivateRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId:
Ref: VpcPrivateSubnet
RouteTableId:
Ref: PrivateRouteTable
PublicRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId:
Ref: PublicRouteTable
DestinationCidrBlock: '0.0.0.0/0'
GatewayId:
Ref: InternetGateway
PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId:
Ref: PrivateRouteTable
DestinationCidrBlock: '0.0.0.0/0'
NatGatewayId:
Ref: NatGateway
PublicSecrityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "It is for public subnet instance"
GroupName: "PublicSubnetInstanceSecurityGroup"
SecurityGroupEgress:
- CidrIp: "0.0.0.0/0"
FromPort: 0
ToPort: "-1"
IpProtocol: "-1"
SecurityGroupIngress:
- CidrIp: '0.0.0.0/0'
FromPort: 80
ToPort: 443
IpProtocol: "tcp"
Tags: ${self:custom.commonTag}
VpcId:
Ref: ExampleAppVpc
PrivateSecrityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "It is for private subnet instance"
GroupName: "PrivateSubnetInstanceSecurityGroup"
SecurityGroupEgress:
- CidrIp: "0.0.0.0/0"
FromPort: 0
ToPort: "-1"
IpProtocol: "-1"
SecurityGroupIngress:
- CidrIp: "0.0.0.0/0"
FromPort: 80
ToPort: 443
IpProtocol: "tcp"
Tags: ${self:custom.commonTag}
VpcId:
Ref: ExampleAppVpc
NatElasticIP:
Type: AWS::EC2::EIP
Properties:
Domain: "vpc"
Tags: ${self:custom.commonTag}
NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId:
'Fn::GetAtt': [NatElasticIP,AllocationId]
SubnetId:
Ref: VpcPublicSubnet
Tags: ${self:custom.commonTag}
api.yaml
getExample:
handler: src/functions/example/get.handler
events:
- http:
path: /example/
method: get
vpc:
securityGroupIds:
- Ref: PrivateSecrityGroup
subnetIds:
- Ref: VpcPrivateSubnet
postExample:
handler: src/functions/example/post.handler
environment:
END_POINT: ${self:custom.apiEndpoint}
events:
- http:
path: /example/
method: post
vpc:
securityGroupIds:
- Ref: PrivateSecrityGroup
subnetIds:
- Ref: VpcPrivateSubnet
Lambda 関数を作成します。今回、固定IPでのリクエストを行う関数と、その取得・リクエストのソースIPを確認する Lambda 関数両方を構築します。
src/functions/example/get.js
export async function handler(event, context, callback) {
callback(null, {
statusCode: 200,
body: JSON.stringify({
IPAddress: event['requestContext']['identity']['sourceIp']
}),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true'
},
})
}
src/functions/example/post.js
const axios = require('axios')
export async function handler(event, context, callback) {
await axios.get(process.env.END_POINT + "/example")
.then(res => callback(null, {
statusCode: 200,
body: JSON.stringify(
res['data']
),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true'
},
}))
.catch(e => callback(null, {
statusCode: 200,
body: JSON.stringify(
// res['data']
{result: "Error"}
),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true'
},
}))
}
最後に、サーバーレスフレームワークでデプロイする Lambda 関数のソースコードを、webpack でビルドしているので、下記のファイルも追加します。
webpack.config.js
const path = require('path');
const slsw = require('serverless-webpack');
module.exports = {
entry: slsw.lib.entries,
target: 'node',
mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
stats: 'minimal',
devtool: 'nosources-source-map',
performance: {
hints: false,
},
resolve: {
extensions: ['.js', '.jsx', '.json'],
},
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, 'dist'),
filename: '[name].js',
sourceMapFilename: '[file].map',
},
};
これで諸々構築準備が完了しました。
こちらのコマンドでデプロイしてみましょう。
yarn deploy
実際に構築された API ゲートウェイのエンドポイントへ POST リクエストしてみましょう。すると、NAT に設定した固定IP( 厳格には NAT とつながっているENI の IPアドレス )がレスポンスに表示されるはずです。
この構築の難点として、Lambda を VPC 内に入れた場合の弊害としてコールドスタートのレイテンシーが高まるということです。
例えば、定期的に Lambda を Cloud Watch で起こして所定の場所へリクエストを行いたい場合は、指定時刻にしっかり実行されない危険性があります。注意して運用しましょう。
サーバーレス開発については、お気軽にお問い合わせください。
スモールスタート開発支援、サーバーレス・NoSQLのことなら
ラーゲイトまでご相談ください
低コスト、サーバーレスの
モダナイズ開発をご検討なら
下請け対応可能
Sler企業様からの依頼も歓迎