【小ネタ】Serverless Frameworkを利用してCloudFrontのIPレンジを自動でセキュリティグループに反映させる

2016.11.14

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

はじめに

2016年12月1日追記
CloudFrontのリージョナルエッジキャッシュ導入により、CloudFrontで利用しているIPレンジが大幅に増えたようです。そのため、こちらのエントリで紹介しているスクリプトをそのまま実行してしまうとセキュリティグループのインバウンドルールデフォルト上限である50に引っかかってしまうようです。お使いになる際はご注意ください。ワークアラウンドとしては上限緩和申請などが考えられます。

こんにちは、中山です。

最近Serverless Frameworkを触り始めています。以前はLambdaのフレームワークとして主にApexを利用していましたが、最近噂を耳にしたので浮気気味です。どちらも素晴らしいフレームワークなのですが、Apexがシンプルさを重視しているのに対して、Serverless Frameworkの方はプラグイン機構などがあり、まさに「フレームワーク」といった感じで触っていて非常に面白いプロダクトだと思いました。

今回はServerless Frameworkのお勉強がてら、以前Apexで作成したCloudFrontのIPアドレスレンジを自動でセキュリティグループに反映するLambda関数を作成してみました。と言っても、Lambda関数そのものはawslabs/aws-cloudfront-samplesからの流用なのですが。。。Lambda関数ではなく「Serverless Frameworkの学習」ということで。。。

以前のエントリは以下を参照してください。

コード

いつも通りGitHubに置いておきました。ご自由にお使いください。

このコードは、CloudFrontなどのAWSリソースで利用しているIPレンジが書かれたJSONファイルに更新があった場合、それを特定のセキュリティグループに反映させるLambda関数をデプロイするものとなっています。

使い方

Serverless Frameworkの基本的な使い方は弊社のエントリにまとまっています。そちらを参照してください。

今回は上記リポジトリを利用する際に特有の内容について解説します。

1. コードのclone

リポジトリからコードをcloneしてください。注意点として、Lambda関数はサブモジュールで取り込んであります。そのため、 --recurse-submodule を利用してcloneと同時にサブモジュールも取り込んでくる必要があります(もちろん後からでもできます)。

$ git clone git@github.com:knakayama/serverless-auto-update-cloudfront-ips.git --recurse-submodules
Cloning into 'serverless-auto-update-cloudfront-ips'...
remote: Counting objects: 10, done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 10 (delta 0), reused 10 (delta 0), pack-reused 0
Receiving objects: 100% (10/10), done.
Submodule 'function' (https://github.com/awslabs/aws-cloudfront-samples.git) registered for path 'function'
Cloning into '****************************************************************************'...
Submodule path 'function': checked out 'e49899a227c8b988db22cc01a658f2d761250080'

ディレクトリ構造は以下のようになっています。

$ tree serverless-auto-update-cloudfront-ips
serverless-auto-update-cloudfront-ips
├── README.md
├── env
│   └── dev
├── event.json
├── function
│   ├── LICENSE
│   ├── NOTICE.txt
│   ├── README.md
│   └── update_security_groups_lambda
│       ├── README.md
│       └── update_security_groups.py
└── serverless.yml

4 directories, 8 files

2. 変数用ファイルの作成

serverless.yml では custom プロパティを利用して変数が書かれているファイルを読み込んでいます。この仕組みを利用することでAWSアカウントIDなど、あまりリポジトリへ直接保存しておきたくない情報を設置することが可能です。 serverless.yml は以下のようになっています。

service: serverless-auto-update-cloudfront-ips

provider:
  name: aws
  runtime: python2.7
  stage: dev
  region: us-east-1
  memorySize: 128
  timeout: 10

  iamRoleStatements:
    - Effect: Allow
      Action:
        - ec2:AuthorizeSecurityGroupIngress
        - ec2:RevokeSecurityGroupIngress
      Resource:
        Fn::Join:
          - ""
          -
            - "arn:aws:ec2:${self:provider.region}:"
            - ${self:custom.accountId}
            - ":security-group/*"
    - Effect: Allow
      Action:
        - ec2:DescribeSecurityGroups
      Resource:
        - "*"

custom: ${file(env/${self:provider.stage}/secrets.yml)}

functions:
  func:
    handler: function/update_security_groups_lambda/update_security_groups.lambda_handler
    # TODO: not work properly
    #events:
    #  - sns: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged

変数用ファイルは以下のコマンドで作成可能です。

$ echo 'accountId: <_YOUR_AWS_ACCOUNT_ID_>' > env/dev/secrets.yml

Serverless Frameworkは柔軟な変数の利用が可能になっており、変数展開のネストなどもサポートされているます。詳細はドキュメントを参照してください。

3. Lambda関数のデプロイ

後はいつもどおり sls コマンドでデプロイすればOKです。簡単ですね。

$ sls deploy -v
Serverless: Packaging service…
Serverless: Uploading CloudFormation file to S3…
Serverless: Uploading service .zip file to S3…
Serverless: Updating Stack…
Serverless: Checking Stack update progress…
CloudFormation - UPDATE_IN_PROGRESS - AWS::CloudFormation::Stack - serverless-auto-update-cloudfront-ips-dev
CloudFormation - CREATE_IN_PROGRESS - AWS::IAM::Role - IamRoleLambdaExecution
CloudFormation - CREATE_IN_PROGRESS - AWS::IAM::Role - IamRoleLambdaExecution
CloudFormation - CREATE_COMPLETE - AWS::IAM::Role - IamRoleLambdaExecution
CloudFormation - CREATE_IN_PROGRESS - AWS::IAM::Policy - IamPolicyLambdaExecution
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Function - FuncLambdaFunction
CloudFormation - CREATE_IN_PROGRESS - AWS::IAM::Policy - IamPolicyLambdaExecution
CloudFormation - CREATE_COMPLETE - AWS::IAM::Policy - IamPolicyLambdaExecution
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Function - FuncLambdaFunction
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Function - FuncLambdaFunction
CloudFormation - UPDATE_COMPLETE_CLEANUP_IN_PROGRESS - AWS::CloudFormation::Stack - serverless-auto-update-cloudfront-ips-dev
CloudFormation - UPDATE_COMPLETE - AWS::CloudFormation::Stack - serverless-auto-update-cloudfront-ips-dev
Serverless: Stack update finished…

Service Information
service: serverless-auto-update-cloudfront-ips
stage: dev
region: us-east-1
api keys:
  None
  endpoints:
    None
    functions:
      serverless-auto-update-cloudfront-ips-dev-func: arn:aws:lambda:us-east-1:************:function:serverless-auto-update-cloudfront-ips-dev-func

      Stack Outputs
      FuncLambdaFunctionArn: arn:aws:lambda:us-east-1:************:function:serverless-auto-update-cloudfront-ips-dev-func
      ServerlessDeploymentBucketName: serverless-auto-update-c-serverlessdeploymentbuck-************

デプロイ後のLambda関数は以下のコマンドで確認できます。

$ sls info -v

Service Information
service: serverless-auto-update-cloudfront-ips
stage: dev
region: us-east-1
api keys:
  None
  endpoints:
    None
    functions:
      serverless-auto-update-cloudfront-ips-dev-func: arn:aws:lambda:us-east-1:************:function:serverless-auto-update-cloudfront-ips-dev-func

      Stack Outputs
      FuncLambdaFunctionArn: arn:aws:lambda:us-east-1:************:function:serverless-auto-update-cloudfront-ips-dev-func
      ServerlessDeploymentBucketName: serverless-auto-update-c-serverlessdeploymentbuck-************

4. Lambda関数のトリガーとなるSNSトピックの設定

Serverless FrameworkでデプロイしたLambda関数は、 event プロパティによってInvoke周りのさまざまな処理を簡単に記述可能です。これはApexにはないServerless Frameworkの大きな利点だと思います。しかし、今回このプロパティでは対応できないようです。Serverless Frameworkは内部的にCloudFormationを利用してAWSリソースを作成/アップデートしています。 event プロパティも最終的にCloudFormationのテンプレートに変換され、スタックが作成されます(生成されたスタックは .serverless ディレクトリ以下に設置されます)。

現時点(2016年11月14日)でCloudFormationは既存SNSトピックのサブスクリプションに未対応なため、すでに存在するSNSトピックからInvokeされる設定をこのプロパティでは定義できません。詳細はドキュメントを参照してください。しょうがないのでこの部分はマネジメントコンソールから設定します。もちろんawscliでもOKです。以下のようにLambdaのトリガーを設定してください。SNSトピックのARNは「arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged」です。

serverless-ip-1

2016年11月20日追記
CloudFormationを利用した既存SNSトピックのサブスクリプションは2016年11月17日のアップデートで対応しています。

動作確認

Lambdaの準備が整ったので動作確認をしてみます。まずは、テスト用のセキュリティグループを作成します。このLambda関数は以下のタグが付いたものを対象にしてインバウンドルールをアップデートします。

  • Name: cloudfront
  • AutoUpdate: true

以下のコマンドを実行してテスト用セキュリティグループを作成してください。

$ export AWS_DEFAULT_REGION=us-east-1
$ aws ec2 create-tags \
  --resources \
    "$(aws ec2 create-security-group \
      --group-name test \
      --description test \
      --vpc-id \
        "$(aws ec2 describe-vpcs \
          --query 'Vpcs[?IsDefault==`true`].VpcId' \
          --output text)" \
      --output text)" \
  --tags Key=Name,Value=cloudfront Key=AutoUpdate,Value=true
$ aws ec2 describe-security-groups \
  --query 'SecurityGroups[?Tags[?Value==`cloudfront`]]'
[
    {
        "IpPermissionsEgress": [
            {
                "IpProtocol": "-1",
                "IpRanges": [
                    {
                        "CidrIp": "0.0.0.0/0"
                    }
                ],
                "UserIdGroupPairs": [],
                "PrefixListIds": []
            }
        ],
        "Description": "test",
        "Tags": [
            {
                "Value": "cloudfront",
                "Key": "Name"
            },
            {
                "Value": "true",
                "Key": "AutoUpdate"
            }
        ],
        "IpPermissions": [],
        "GroupName": "test",
        "VpcId": "vpc-********",
        "OwnerId": "************",
        "GroupId": "sg-c5b61eb8"
    }
]

続いて、Lambda関数をInvokeします。本当は実際に先のSNSトピックからパブリッシュしたいところですが、権限的にそれはできないので(当たり前ですが)トップディレクトリにある event.json というサンプルデータを利用します。 invoke サブコマンドを実行後、以下のように出力されたら成功です。

$ sls invoke --function func --path event.json --log
[
    "Updated sg-c5b61eb8",
    "Updated 1 of 1 SecurityGroups"
]
--------------------------------------------------------------------
START RequestId: 8fc36dc2-a961-11e6-b5c0-4711afca47cf Version: $LATEST
Received event: {
  "Records": [
    {
      "EventVersion": "1.0",
      "EventSource": "aws:sns",
      "EventSubscriptionArn": "arn:aws:sns:EXAMPLE",
      "Sns": {
        "SignatureVersion": "1",
        "Timestamp": "1970-01-01T00:00:00.000Z",
        "Signature": "EXAMPLE",
        "SigningCertUrl": "EXAMPLE",
        "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
        "Message": "{\"create-time\": \"yyyy-mm-ddThh:mm:ss+00:00\", \"synctoken\": \"0123456789\", \"md5\": \"2d4d8d65073bf1ef395aafd91b38c2ca\", \"url\": \"https://ip-ranges.amazonaws.com/ip-ranges.json\"}",
        "Type": "Notification",
        "UnsubscribeUrl": "EXAMPLE",
        "TopicArn": "arn:aws:sns:EXAMPLE",
        "Subject": "TestInvoke"
      }
    }
  ]
}
Updating from https://ip-ranges.amazonaws.com/ip-ranges.json
Found CLOUDFRONT range: 13.32.0.0/15
Found CLOUDFRONT range: 52.46.0.0/18
Found CLOUDFRONT range: 52.84.0.0/15
Found CLOUDFRONT range: 52.222.128.0/17
Found CLOUDFRONT range: 54.182.0.0/16
Found CLOUDFRONT range: 54.192.0.0/16
Found CLOUDFRONT range: 54.230.0.0/16
Found CLOUDFRONT range: 54.239.128.0/18
Found CLOUDFRONT range: 54.239.192.0/19
Found CLOUDFRONT range: 54.240.128.0/18
Found CLOUDFRONT range: 204.246.164.0/22
Found CLOUDFRONT range: 204.246.168.0/22
Found CLOUDFRONT range: 204.246.174.0/23
Found CLOUDFRONT range: 204.246.176.0/20
Found CLOUDFRONT range: 205.251.192.0/19
Found CLOUDFRONT range: 205.251.249.0/24
Found CLOUDFRONT range: 205.251.250.0/23
Found CLOUDFRONT range: 205.251.252.0/23
Found CLOUDFRONT range: 205.251.254.0/24
Found CLOUDFRONT range: 216.137.32.0/19
Found 1 SecurityGroups to update
sg-c5b61eb8: Adding 13.32.0.0/15:80
sg-c5b61eb8: Adding 52.46.0.0/18:80
sg-c5b61eb8: Adding 52.84.0.0/15:80
sg-c5b61eb8: Adding 52.222.128.0/17:80
sg-c5b61eb8: Adding 54.182.0.0/16:80
sg-c5b61eb8: Adding 54.192.0.0/16:80
sg-c5b61eb8: Adding 54.230.0.0/16:80
sg-c5b61eb8: Adding 54.239.128.0/18:80
sg-c5b61eb8: Adding 54.239.192.0/19:80
sg-c5b61eb8: Adding 54.240.128.0/18:80
sg-c5b61eb8: Adding 204.246.164.0/22:80
sg-c5b61eb8: Adding 204.246.168.0/22:80
sg-c5b61eb8: Adding 204.246.174.0/23:80
sg-c5b61eb8: Adding 204.246.176.0/20:80
sg-c5b61eb8: Adding 205.251.192.0/19:80
sg-c5b61eb8: Adding 205.251.249.0/24:80
sg-c5b61eb8: Adding 205.251.250.0/23:80
sg-c5b61eb8: Adding 205.251.252.0/23:80
sg-c5b61eb8: Adding 205.251.254.0/24:80
sg-c5b61eb8: Adding 216.137.32.0/19:80
sg-c5b61eb8: Adding 13.32.0.0/15:443
sg-c5b61eb8: Adding 52.46.0.0/18:443
sg-c5b61eb8: Adding 52.84.0.0/15:443
sg-c5b61eb8: Adding 52.222.128.0/17:443
sg-c5b61eb8: Adding 54.182.0.0/16:443
sg-c5b61eb8: Adding 54.192.0.0/16:443
sg-c5b61eb8: Adding 54.230.0.0/16:443
sg-c5b61eb8: Adding 54.239.128.0/18:443
sg-c5b61eb8: Adding 54.239.192.0/19:443
sg-c5b61eb8: Adding 54.240.128.0/18:443
sg-c5b61eb8: Adding 204.246.164.0/22:443
sg-c5b61eb8: Adding 204.246.168.0/22:443
sg-c5b61eb8: Adding 204.246.174.0/23:443
sg-c5b61eb8: Adding 204.246.176.0/20:443
sg-c5b61eb8: Adding 205.251.192.0/19:443
sg-c5b61eb8: Adding 205.251.249.0/24:443
sg-c5b61eb8: Adding 205.251.250.0/23:443
sg-c5b61eb8: Adding 205.251.252.0/23:443
sg-c5b61eb8: Adding 205.251.254.0/24:443
sg-c5b61eb8: Adding 216.137.32.0/19:443
sg-c5b61eb8: Added 40, Revoked 0
END RequestId: 8fc36dc2-a961-11e6-b5c0-4711afca47cf
REPORT RequestId: 8fc36dc2-a961-11e6-b5c0-4711afca47cf  Duration: 2419.45 ms    Billed Duration: 2500 ms        Memory Size: 128 MB     Max Memory Used: 60 MB

先程作成したセキュリティグループを見てみると、インバウンドルールが更新されていることを確認できます。

$ aws ec2 describe-security-groups \
  --query 'SecurityGroups[?Tags[?Value==`cloudfront`]]'
[
    {
        "IpPermissionsEgress": [
            {
                "IpProtocol": "-1",
                "IpRanges": [
                    {
                        "CidrIp": "0.0.0.0/0"
                    }
                ],
                "UserIdGroupPairs": [],
                "PrefixListIds": []
            }
        ],
        "Description": "test",
        "Tags": [
            {
                "Value": "cloudfront",
                "Key": "Name"
            },
            {
                "Value": "true",
                "Key": "AutoUpdate"
            }
        ],
        "IpPermissions": [
            {
                "PrefixListIds": [],
                "FromPort": 80,
                "IpRanges": [
                    {
                        "CidrIp": "13.32.0.0/15"
                    },
                    {
                        "CidrIp": "204.246.164.0/22"
                    },
                    {
                        "CidrIp": "204.246.168.0/22"
                    },
                    {
                        "CidrIp": "204.246.174.0/23"
                    },
                    {
                        "CidrIp": "204.246.176.0/20"
                    },
                    {
                        "CidrIp": "205.251.192.0/19"
                    },
                    {
                        "CidrIp": "205.251.249.0/24"
                    },
                    {
                        "CidrIp": "205.251.250.0/23"
                    },
                    {
                        "CidrIp": "205.251.252.0/23"
                    },
                    {
                        "CidrIp": "205.251.254.0/24"
                    },
                    {
                        "CidrIp": "216.137.32.0/19"
                    },
                    {
                        "CidrIp": "52.222.128.0/17"
                    },
                    {
                        "CidrIp": "52.46.0.0/18"
                    },
                    {
                        "CidrIp": "52.84.0.0/15"
                    },
                    {
                        "CidrIp": "54.182.0.0/16"
                    },
                    {
                        "CidrIp": "54.192.0.0/16"
                    },
                    {
                        "CidrIp": "54.230.0.0/16"
                    },
                    {
                        "CidrIp": "54.239.128.0/18"
                    },
                    {
                        "CidrIp": "54.239.192.0/19"
                    },
                    {
                        "CidrIp": "54.240.128.0/18"
                    }
                ],
                "ToPort": 80,
                "IpProtocol": "tcp",
                "UserIdGroupPairs": []
            },
            {
                "PrefixListIds": [],
                "FromPort": 443,
                "IpRanges": [
                    {
                        "CidrIp": "13.32.0.0/15"
                    },
                    {
                        "CidrIp": "204.246.164.0/22"
                    },
                    {
                        "CidrIp": "204.246.168.0/22"
                    },
                    {
                        "CidrIp": "204.246.174.0/23"
                    },
                    {
                        "CidrIp": "204.246.176.0/20"
                    },
                    {
                        "CidrIp": "205.251.192.0/19"
                    },
                    {
                        "CidrIp": "205.251.249.0/24"
                    },
                    {
                        "CidrIp": "205.251.250.0/23"
                    },
                    {
                        "CidrIp": "205.251.252.0/23"
                    },
                    {
                        "CidrIp": "205.251.254.0/24"
                    },
                    {
                        "CidrIp": "216.137.32.0/19"
                    },
                    {
                        "CidrIp": "52.222.128.0/17"
                    },
                    {
                        "CidrIp": "52.46.0.0/18"
                    },
                    {
                        "CidrIp": "52.84.0.0/15"
                    },
                    {
                        "CidrIp": "54.182.0.0/16"
                    },
                    {
                        "CidrIp": "54.192.0.0/16"
                    },
                    {
                        "CidrIp": "54.230.0.0/16"
                    },
                    {
                        "CidrIp": "54.239.128.0/18"
                    },
                    {
                        "CidrIp": "54.239.192.0/19"
                    },
                    {
                        "CidrIp": "54.240.128.0/18"
                    }
                ],
                "ToPort": 443,
                "IpProtocol": "tcp",
                "UserIdGroupPairs": []
            }
        ],
        "GroupName": "test",
        "VpcId": "vpc-********",
        "OwnerId": "************",
        "GroupId": "sg-c5b61eb8"
    }
]

まとめ

いかがだったでしょうか。

まだ、Serverless Frameworkを触り始めてから日が経っていないですが、CloudFormationやLambda関数など使い慣れたサービスを内部的に利用しているため、すんなりと入門できました。今後はプラグイン機構などより高度なトピックをご紹介できればと思います。

本エントリがみなさんの参考になれば幸いです。