Amazon API Gateway だけでユーザのソース IP アドレスを返してくれる API をつくる [AWS CloudFormation テンプレート付き]

こんにちは、広野です。

SPA (Single Page Application) では、技術的な制約上、フロントエンドのアプリ内でユーザのソース IP アドレスを取得することができません。そのため、一旦インターネットに通信を送って取得する必要があります。

ソース IP アドレスを返してくれるサービスは世の中にあるのですが、安心して使用するために自分で Amazon API Gateway だけでつくってみました。

セキュリティ要件から、アプリ側でソース IP アドレスをもとに何かしらの制御が必要になった際に活用できると思います。

やりたいこと

イメージは以下の図の通りです。

実現方法

  • Amazon API Gateway Rest API を使用する。HTTP API ではダメ。
  • Amazon API Gateway には主にログ取得を目的とした変数をデフォルトで持っており、その中にソース IP アドレスの変数がある。これをユーザへのレスポンスとして返してあげる。
API Gateway マッピングテンプレートとアクセスのログ記録の変数リファレンス - Amazon API Gateway
マッピングテンプレート、アクセスログ記録などで使用される変数と関数のリファレンス
  • Amazon API Gateway で持つ変数を返すだけなので、他のサービスとの統合は不要。統合タイプは MOCK にする。
  • Amazon API Gateway はまだ IPv6 に対応していないようで、(公式なドキュメントは見つけられませんでした)取得できる IP アドレスは IPv4 のみとなる。

気を付けるところは赤枠の2箇所。

統合リクエストの追記はこんな感じ。↓

統合レスポンスの追記はこんな感じ。↓

これ以外に CORS の設定がありますが、手作業だとハマる箇所なので、実装は最後に紹介する AWS CloudFormation テンプレートで行った方が確実です。CORS 対応のため、上記で紹介した GET メソッド以外に OPTIONS メソッドの定義も必要になります。テンプレートには含まれています。

SPA との結合例

ここでは React アプリからこの API を呼び出すときの実装例を紹介します。

アプリ側で、以下のコードを書きます。(関係ないコードは省略しています)

import React, { useEffect, useState } from 'react';
import axios from 'axios';

//state定義
const [sourceIp, setSourceIp] = useState();

//ソースIP取得関数
const getSourceIp = async () => {
  const res = await axios.get("https://xxxxxxxxxxxxxxxxx.amazonaws.com/prod/GetSourceIp"); //API Gateway のエンドポイント
  setSourceIp(res.data.sourceIp);
  console.log(res.data); //データの確認用
};

//画面表示時にソースIP取得
useEffect(() => {
  getSourceIp();
}, []);

//画面表示
return (
  <p>sourceIp: {sourceIp}</p>
);

実際の画面では、以下のように表示されました。開発者コンソールでも、console.log で出力した情報が表示されています。

今回は画面表示させましたが、取得したソース IP アドレスをもとに条件分岐させて、画面内の処理を変えることができるようになると思います。

AWS CloudFormation テンプレート

とりあえず動く Amazon API Gateway のテンプレートを貼り付けておきます。

CORS 対応のためドメイン名をパラメータで入力しますが、ワイルドカード (*) にしたい方は適宜修正してご利用ください。

若干冗長な設定があるのは認識しているのですが、そこまで検証しきれず、リリースいたします。

AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation template that creates a API Gateway and a relevant IAM roles for getting the source IP address.

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  SystemName:
    Type: String
    Description: System name.
    Default: xxxxx
    MaxLength: 10
    MinLength: 1

  DomainName:
    Type: String
    Description: Domain name for URL. xxxxx.xxx
    Default: xxxxx.xxx
    MaxLength: 40
    MinLength: 5
    AllowedPattern: "[^\\s@]+\\.[^\\s@]+"

  SubDomainName:
    Type: String
    Description: Sub domain name for URL.
    Default: example-xxxxx
    MaxLength: 20
    MinLength: 1

Resources:
# ------------------------------------------------------------#
# API Gateway Role (IAM)
# ------------------------------------------------------------#
  ApiGatewayRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub example-ApiGatewayRole-${SystemName}
      Description: This role allows API Gateways to execute.
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - apigateway.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
        - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess

# ------------------------------------------------------------#
# API Gateway
# ------------------------------------------------------------#
  RestApiGetSourceIp:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: !Sub example-RestApi-GetSourceIp-${SystemName}
      Description: !Sub example REST API Gateway to get the user's source IP address for example-${SystemName}
      EndpointConfiguration:
        Types:
          - REGIONAL
      Tags:
        - Key: Cost
          Value: !Sub example-${SystemName}
  RestApiDeploymentGetSourceIp:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId: !Ref RestApiGetSourceIp
    DependsOn:
      - RestApiMethodGetSourceIpGet
      - RestApiMethodGetSourceIpOptions
  RestApiStageGetSourceIp:
    Type: AWS::ApiGateway::Stage
    Properties:
      StageName: prod
      Description: production stage
      RestApiId: !Ref RestApiGetSourceIp
      DeploymentId: !Ref RestApiDeploymentGetSourceIp
      TracingEnabled: false
      Tags:
        - Key: Cost
          Value: !Sub example-${SystemName}
    DependsOn: RestApiDeploymentGetSourceIp
  RestApiResourceGetSourceIp:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref RestApiGetSourceIp
      ParentId: !GetAtt RestApiGetSourceIp.RootResourceId
      PathPart: GetSourceIp
    DependsOn:
      - RestApiGetSourceIp
  RestApiMethodGetSourceIpGet:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestApiGetSourceIp
      ResourceId: !Ref RestApiResourceGetSourceIp
      HttpMethod: GET
      AuthorizationType: NONE
      Integration:
        Type: MOCK
        Credentials: !GetAtt ApiGatewayRole.Arn
        IntegrationResponses:
          - StatusCode: 200
            ResponseTemplates:
              application/json: '{"sourceIp": "$context.identity.sourceIp"}'
            ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
              method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'"
        PassthroughBehavior: WHEN_NO_MATCH
        RequestTemplates:
          application/json: '{"statusCode": 200}'
      MethodResponses:
        - StatusCode: 200
          ResponseModels:
            application/json: Empty
          ResponseParameters:
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true
            method.response.header.Access-Control-Allow-Origin: true
    DependsOn:
      - RestApiResourceGetSourceIp
      - ApiGatewayRole
  RestApiMethodGetSourceIpOptions:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestApiGetSourceIp
      ResourceId: !Ref RestApiResourceGetSourceIp
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      Integration:
        Type: MOCK
        Credentials: !GetAtt ApiGatewayRole.Arn
        IntegrationResponses:
          - ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
              method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'"
            ResponseTemplates:
              application/json: ''
            StatusCode: 200
        PassthroughBehavior: WHEN_NO_MATCH
        RequestTemplates:
          application/json: '{"statusCode": 200}'
      MethodResponses:
        - ResponseModels:
            application/json: Empty
          ResponseParameters:
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true
            method.response.header.Access-Control-Allow-Origin: true
          StatusCode: 200
    DependsOn:
      - RestApiResourceGetSourceIp
      - ApiGatewayRole

まとめ

いかがでしたでしょうか?

なにげに世の中にこのような情報が少なかったので、ブログ記事にしてみました。

本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

AWSサーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリや教育コンテンツをつくっています。ReactでSPAを書き始めたら、快適すぎて他の開発言語には戻れなくなりました。AWSサーバーレスやReactの仲間を増やしたいです。
取得資格:AWS認定は7つ、ITサービスマネージャ、ITIL v3 Expert、等
2020, 2021 APN AWS Top Engineers 受賞
2022 AWS Partner Ambassador 受賞
好きなAWSサービス:AWS Amplify / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする
クラウドに強いによるエンジニアブログです。
SCSKは専門性と豊富な実績を活かしたクラウドサービス USiZE(ユーサイズ)を提供しています。
USiZEサービスサイトでは、お客様のDX推進をワンストップで支援するサービスの詳細や導入事例を紹介しています。
AWSサーバレスアーキテクチャ技術ナレッジ
TechHarmony
タイトルとURLをコピーしました