ローカルコンピューターのパブリックIPを確認する checkip.amazonaws.com を自分で作ってみる

http://checkip.amazonaws.com/をご存知ですか? AWSの地味なサービスで、ローカルコンピューターのパブリックIPを表示するだけのサービスです。 このサービスをALB+Lambdaで作ったのでご紹介いたします。
2019.03.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

みなさん、http://checkip.amazonaws.com/をご存知ですか?

AWSの地味〜なサービスで、ローカルコンピューターのパブリックIPを表示するだけのサービスです。 AWS Batchのドキュメントに記載されています。 https://docs.aws.amazon.com/ja_jp/batch/latest/userguide/get-set-up-for-aws-batch.html

このサービス、シェルでcurlと組み合わせて、AWS CLIでSecurityGroupにあけるGlobalIPを指定する時に使ったり、とても地味に使えるやつなんです。

このサービス、AWSのサービスを組み合わせれば簡単に作れるのでは? と、思って作ってみたら、30分くらいでできたのでご紹介いたします。

構成図

ALBで受けて、Lambdaに流すだけです。

構築手順

9割方、CloudFormationで作ります。

---
AWSTemplateFormatVersion: '2010-09-09'
Description: checkip

Parameters:
  CidrPrefix:
    Type: String
    Default: 10.101

Resources:
  # VPC
  Igw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-igw"
  IgwAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref Igw
      VpcId: !Ref Vpc
  RouteDefault:
    Type: AWS::EC2::Route
    DependsOn: IgwAttachment
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref Igw
  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-route-table"
  SubnetApp0:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: !Sub "${CidrPrefix}.0.0/24"
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-app-subnet-0"
      VpcId: !Ref Vpc
  SubnetApp1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      CidrBlock: !Sub "${CidrPrefix}.1.0/24"
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-app-subnet-1"
      VpcId: !Ref Vpc
  SubnetRouteTableAttachment0:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref SubnetApp0
  SubnetRouteTableAttachment1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref SubnetApp1
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Sub "${CidrPrefix}.0.0/16"
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-vpc"

  # SecurityGroup
  SecurityGroupAlb:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${AWS::StackName}-alb-sg"
      GroupDescription: !Sub "${AWS::StackName}-alb-sg" 
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
      VpcId: !Ref Vpc

  # ALB
  # CloudFormationではTargetGroupにLambdaを設定することができないので、ALBルールとあわせてマネコンで作る。
  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub "${AWS::StackName}-alb"
      Type: application
      Scheme: internet-facing
      IpAddressType: ipv4
      SecurityGroups:
        - !Ref SecurityGroupAlb
      Subnets:
        - !Ref SubnetApp0
        - !Ref SubnetApp1
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-alb"

  # Lambda
  CheckIpLambda:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          # -- coding: utf-8 --

          def lambda_handler(event, context):
              print('Event: {}'.format(event))

              return {
                 'statusCode': 200,
                 'statusDescription': '200 OK',
                 'isBase64Encoded': False,
                 'headers': {
                     'Content-Type': 'text/html; charset=utf-8'
                  },
                  'body': '{}\n'.format(event['headers']['x-forwarded-for'])
              }
      Description: !Sub "${AWS::StackName}-lambda"
      FunctionName: !Sub "${AWS::StackName}-lambda"
      Handler: index.lambda_handler
      MemorySize: 128
      Role: !GetAtt RoleLambda.Arn
      Runtime: python3.7
      Timeout: 10
  RoleLambda:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Action": "sts:AssumeRole",
              "Principal": {
                "Service": "lambda.amazonaws.com"
              },
              "Effect": "Allow",
              "Sid": ""
            }
          ]
        }
      RoleName: !Sub "${AWS::StackName}-role"
  LogsPutLambdaPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:DescribeLogStreams"
              ],
              "Effect": "Allow",
              "Resource": ["*"]
            }
          ]
        }
      ManagedPolicyName: !Sub "${AWS::StackName}-policy"
      Roles:
        - !Ref RoleLambda

CloudFormationではまだ、ALBのTargetGroupにLambdaを設定することができないので、ここだけマネジメントコンソールから作ります。


TargetGroupを作成します。

ターゲットの種類にLambdaを選択して、リストからCFnで作成したLambdaを選択します。

CFnで作成したALBを選んでリスナーを追加します。

リスナーのプロトコルはHTTPのままにして、アクションの追加から転送先を選択します。

転送先に、先程作ったTargetGroupを選んで保存します。

これだけで出来上がりです。

動作確認

ALBのDNS名でブラウザからアクセスしてみます。

パブリックIPが表示されることが確認できました。

Lambdaの解説

Lambdaのpythonプログラムはこれだけです。

# -- coding: utf-8 --

def lambda_handler(event, context):
    print('Event: {}'.format(event))

    return {
        'statusCode': 200,
        'statusDescription': '200 OK',
        'isBase64Encoded': False,
        'headers': {
            'Content-Type': 'text/html; charset=utf-8'
        },
        'body': '{}\n'.format(event['headers']['x-forwarded-for'])
    }

ALBから受け取るイベントの中に、クライアントの送信元IPアドレスを指す x-forwarded-for があります。 これを利用すれば、LambdaからクライアントのIPアドレスが参照できそうです。 実際にALBアクセスしてLambdaを動かしてイベントの中身をCloudWatchLogsで見てみると、確かに入っています。

なので、イベントの中に入っているx-forwarded-forの値を参照してALBへreturnしてやります。

ALBからLambdaが受け取るイベントとALBへの応答の詳細が知りたい方は、ドキュメントを参照ください。

ロードバランサーからのイベントの受け取り | ターゲットとしての Lambda 関数 - Elastic Load Balancing

ロードバランサーへの応答 | ターゲットとしての Lambda 関数 - Elastic Load Balancing

おわりに

実際このWebサービスをLinuxで構築しようとすると、割とめんどくさそうだなーという気がします。 が、AWSを利用することでサクッとWebサービスが構築できます。

さらにRoute53使ってドメイン名つけるとか、 ALBとAWS Certificate Managerを組み合わせてHTTPS化するとかも簡単にできます。 AWSを利用してインフラ構築で楽しましょう。