Closed12
Serverless Framework × Private API Gateway × VPC Lambda × ExpressでHTTP Proxy
動機
- VPC内からしかアクセスできないHTTPプロキシを作りたい
- プロキシ先がIP制限下のため、LambdaのIPを固定したい
- IP節約のためLambda関数はなるべく少なく→ルーティングはアプリ内で行う
まずは何はともあれ
$ npx serverless create --template aws-nodejs-typescript
package.json
"dependencies": {
"@vendia/serverless-express": "^4.10.1",
"express": "^4.18.2",
"http-proxy-middleware": "^2.0.6"
},
"devDependencies": {
...
"serverless-offline": "^11.1.3",
...
},
この4つを入れる
app.ts
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
const app = express();
app.use(
'/test',
createProxyMiddleware({
target: 'https://httpbin.org/ip',
pathRewrite: { '^/test': '' },
changeOrigin: true,
})
);
export default app;
handler.ts
import serverlessExpress from '@vendia/serverless-express';
import app from './app';
export const main = serverlessExpress({ app });
これだけでプロキシとしては動作する
レスポンスデータの頭に「1e3f\r\n」のような謎文字列がつくので、回避する場合以下を適用
app.ts
onProxyRes: (proxyRes, req, res) => {
// https://github.com/chimurai/http-proxy-middleware/discussions/574
// Capture the response from the backend and check if response is being chunked
const chunked = /chunked/.test(proxyRes.headers['transfer-encoding']);
if (chunked) {
// If chunked, then gather all chunks and pass it on unchunked. Remove transfer-encoding header.
delete proxyRes.headers['transfer-encoding'];
res.write = ((override) => {
return (chunk, encoding, callback) => {
override.call(res, chunk, 'binary', callback);
};
})(res.write) as Response['write'];
res.end = ((override) => {
return (chunk, encoding, callback) => {
override.call(res, chunk, 'binary', callback);
};
})(res.end) as Response['end'];
}
},
プロキシ先がTransfer-Encoding: chunked
でデータを返してくる場合に、チャンク化された生データをTransfer-Encoding: chunked
して返してる=二重にチャンク化してるのではないか、と推測した
なので、proxyRes
でヘッダーを消した上で、チャンクをまとめた解釈後のデータを返してもらうようにする…みたいな?
serverless.ts
import type { AWS } from '@serverless/typescript';
import proxy from '@functions/proxy';
const serverlessConfiguration: AWS = {
...
provider: {
...
endpointType: 'PRIVATE',
...
},
...
custom: {
...
'serverless-offline': {
reloadHandler: true,
},
},
};
module.exports = serverlessConfiguration;
必須変更点はこのぐらい
serverless.ts
import type { AWS } from '@serverless/typescript';
import proxy from '@functions/proxy';
const serverlessConfiguration: AWS = {
...
params: {
default: {
prefix: 'XXX-${self:provider.stage}-proxy',
subnetIds: '${ssm:${param:prefix}-lambda-instance-subnet-ids-param}',
securityGroupIds: '${ssm:${param:prefix}-lambda-instance-sg-id-param}',
},
},
...
};
module.exports = serverlessConfiguration;
事前にSSM Parameter StoreにVPC Lambdaを作成する上で必要な情報を仕込んでおく
params
でそれを取得
index.ts
import { handlerPath } from '@libs/handler-resolver';
export default {
handler: `${handlerPath(__dirname)}/handler.main`,
vpc: {
subnetIds: { 'Fn::Split': [',', '${param:subnetIds}'] },
securityGroupIds: ['${param:securityGroupId}'],
},
events: [
{
http: {
method: 'any',
path: '{proxy+}',
},
},
],
};
今回サブネットIDはコンマ区切り文字列で持たせたので、配列にしたいが、JavaScriptによるsplit()
は通用しないので注意が必要
CloudFormationの関数で対応できた
Error:
CREATE_FAILED: ApiGatewayDeployment1666675080734 (AWS::ApiGateway::Deployment)
Resource handler returned message: "Private REST API doesn't have a resource policy attached to it (Service: ApiGateway, Status Code: 400, Request ID: fa1a4947-f9a0-4fe4-af86-db680721fe34)" (RequestToken: 06717e61-2022-bc16-612b-2dd6ae878f3f, HandlerErrorCode: InvalidRequest)
この状態でデプロイしても、「API Gatewayにリソースポリシーがねーよ」って怒られた
VPCエンドポイントIDで絞るのが良きとされていた
serverless.ts
import type { AWS } from '@serverless/typescript';
import proxy from '@functions/proxy';
const serverlessConfiguration: AWS = {
...
provider: {
...
apiGateway: {
...
resourcePolicy: [
{
Effect: 'Allow',
Resource: ['execute-api:/*'],
Action: ['execute-api:Invoke'],
Principal: '*',
},
{
Effect: 'Deny',
Resource: ['execute-api:/*'],
Action: ['execute-api:Invoke'],
Principal: '*',
Condition: {
StringNotEquals: {
'aws:SourceVpce': '${param:ingressVpceId}',
},
},
},
],
},
...
},
...
params: {
default: {
...
ingressVPCEndpointId: '${ssm:${param:prefix}-api-gateway-ingress-vpce-id-param}',
},
},
};
module.exports = serverlessConfiguration;
公式ドキュメントを参考にしてリソースポリシーを設定したところ、デプロイ成功
execute-api:/*
はexecute-api:/* を使用して、現在の API のすべてのステージ、メソッド、パスを表すことができます。
ってことらしい。へー
Webコンソールからテストを行う
何度実行してもIPアドレスに変化がない・NAT Gatewayに割り付けられたElastic IPと同一であることを確認
このスクラップは2022/11/06にクローズされました