Help us understand the problem. What is going on with this article?

【AWS】CloudFormation::Macroを使って多数のIPを接続許可する

初めての記事投稿です。
よろしくお願いします。

使うもの

  • CloudFormation
  • Lambda(Python3)

めんどくさいからソースと作り方だけくれって人向けにgithubに置いておきます。
コードは記事に書いてあるのと同じです。
https://github.com/nekotouma0114/CloudFormationMacroDemo

事の始まり

僕「テスト環境の接続許可するIPって何があります?
お客さん「xxxとyyyとzzz....(数十個羅列※)お願いね!」
僕「あ、はい」

※IPはばらばらでCIDRでまとめれないような状態

AWSのユーザガイド(AWS::EC2::SecurityGroup Ingres)を見る限りまとめてIPは指定できず、
CloudFormationにはループ構造が見当たりませんでした。

探した結果AWS::CloudFormation::Macroにたどり着きましたが、
日本語の参考サイトがほとんどなく、初学者の自分に優しくなかったので同じ人の助けになればと思い書きました。
英字含めたらそれなりにありましたが検証した方が早いってなりました。

一部検証した結果xxxだった!(パラメータの意味は完全に理解してないけど)
という部分もありますがお付き合いいただけばと思います。

作る必要があったもの

  • SecurityGroupを作るためのAWS::CloudFormation::Macro
  • AWS::CloudFormation::Macroで利用するためのLambda Funcion
  • テスト用のVPC(検証用)
  • 複数個の接続元からのHTTP/HTTPSへのアクセスを許可したSecurity Group

実際に作ってみる

手順

  1. AWS::CloudFormation::Macroで実行するLambda Functionを作る
  2. AWS::CloudFormation::Macroの本体を作成する
  3. AWS::CloudFormation::Macroを呼び出すCloudFormationテンプレートを作成する

Lambda Functionを作成する

以下のコードで適当なLambda関数を作ります。
本当はCloudFormationでLambda関数も作れれば良かったんですがRoleとかもろもろ書くのがめんどくさくて手動で作りました。
ソースは以下です。

lambda_funciton.py
import json

def lambda_handler(event, context):
    #今回の許可ポート対象
    allow_ports = (80,443)
    """
    NOTE: paramsの想定値
        event['params'] = {
            'description': 'sg description(全て共通)'
            'cidr_list': [
                'x.x.x.x/x',
                'y.y.y.y/y'
            ]
        }
    """
    print(event)
    if 'cidr_list' not in event['params'] or 'description' not in event['params']:
        #本当はしっかりしたパラメータを返した方がいい気がするけどエラーになることは変わりないので適当に返す
        return

    #responseの外形を生成
    response = {
        'requestId': event['requestId'],
        'status': 'success',
        'fragment': []
    }

    #内容物を生成
    for cidr in event['params']['cidr_list']:
        for allow_port in allow_ports:
            response['fragment'].append(generate_sg(cidr,event['params']['description'],allow_port))
    return response

#ログ出力用のデコレーターを作る予定
def generate_sg(cidr: str,description: str,port: int) -> dict:
    """
        Security GroupのSecurityGroupIngressに設定する単一の設定を返却する
        Protcolはべた書きでTPCのみ対応。
        Portも範囲指定は未対応
    """
    return {
        'Description': description,
        'IpProtocol': 'tcp',
        'FromPort': port,
        'ToPort': port,
        'CidrIp': cidr
    }

#検証用。ほんとはテストコードをしっかり書いてネ 
if __name__ == '__main__':
    event = dict()
    #コードで必要なもののみ抜粋本当はもっといろんなパラメータがある
    event['requestId'] = '1111'
    event['params'] = {
        'description': 'descrition',
        'cidr_list': ['192.168.2.1/32','192.168.2.2/32']
    }
    print(lambda_handler(event,""))

詳しくはAWSのこちらの記事を参考にしていただければと思います。

CloudFormationからLambdaに渡す値についてですがデバッグした結果、event['params']に設定されていました。
色々なサイトを見てるとevent['fragment']で受け取ってる雰囲気はあったんですが。なんで。

返却値は以下の形式でfragmentにデータ本体を入れれば良さそうなので、
今回はresponse['fragment']にパラメータを突っ込んで返却してます。

{
    "requestId": "$REQUEST_ID", 
    "status": "success", 
    "fragment": { ... }
}

AWS::CloudFormation::Macroを定義

本当は後述のSecurity Groupのテンプレートとのと一つにまとめられないかなと考えてたんですが
マクロを先に定義しないとマクロの定義ないってエラーになるのでだめでした。
これでまずスタックを一つ作ってください。
Lambda関数を手動で作ったので前手順で作ったLambda関数のarnの入力が必要です

macro.yml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  LambdaArn:
    Type: String
Resources:
  SgMacro:
    Type: "AWS::CloudFormation::Macro"
    Properties:
      FunctionName: !Ref LambdaArn
      Name: DemoMacro

上記のテンプレートですがデザイナーで確認して作成しようとしたら仕様なのかバグなのかリソースが一つもない扱いになって失敗しました。
ファイルアップロードだと生成に成功しました。

 2019/11/23 10:30:35 - テンプレートにエラーがあります。: Template format error: At least one Resources member must be defined.

マクロを呼び出してSecurity Groupを動的に作成する

上記とは別のスタックを以下で作ってください。
CustomerCidrList192.168.2.1/32,192.168.2.2/32みたいな感じCIDRを並べれば行けます

vpc.yml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  PjPrefix:
    Type: String
    Default: Demo
  CustomerCidrList:
    Type: List<String>

Resources:
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      #本当はここのパラメータもParametersで入力させてね
      CidrBlock: 192.168.1.0/24
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub ${PjPrefix}Vpc

  Sg:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupName: DemoSg
      GroupDescription: Demo
      VpcId: !Ref Vpc
      SecurityGroupIngress:
        Fn::Transform:
          Name: DemoMacro
          Parameters:
            description: "Demo description"
            cidr_list: !Ref CustomerCidrList

Fn::Transformはあまり理解せずになんかいい感じに展開されるんだなくらいで使いました。
検証した結果Nameに指定したマクロ(Lambda関数)をParameterの値をを引数にして呼び出して
返却をそのままここに展開しているようです。

実際に処理されたSgの部分のテンプレートを確認した結果
以下のように展開されたみたいです(jsonが見づらかったのでyamlに変換して載せてます)。

Sg:
  Properties:
    GroupName: DemoSg
    GroupDescription: Demo
    VpcId: !Ref Vpc
    SecurityGroupIngress:
      - Description: Demo description
        IpProtocol: tcp
        FromPort: 80
        ToPort: 80
        CidrIp: 192.168.2.1/32
      - Description: Demo description
        IpProtocol: tcp
        FromPort: 443
        ToPort: 443
        CidrIp: 192.168.2.1/32
      - Description: Demo description
        IpProtocol: tcp
        FromPort: 80
        ToPort: 80
        CidrIp: 192.168.2.2/32
      - Description: Demo description
        IpProtocol: tcp
        FromPort: 443
        ToPort: 443
        CidrIp: 192.168.2.2/32
  Type: 'AWS::EC2::SecurityGroup'

無事に作られていることを確認しました

927492fb6a508e2b270583bf6afb055f.png

どうして...

Parametersで指定してるし、IPが増えてもテンプレートテンプレート書き直す必要がない!これで安心と思ったんですが、
CustomerCidrListを更新してもNo updates are to be performed.になってスタックの更新処理に入れなかったです。
(おそらくマクロで作成される部分は実行後に動的に生成されるが、実行前のチェックはマクロ実行前の部分の静的チェックっていう感じかと)
このあたりはIPの追加に致命的な影響があるのでどうにかしたいと思います。

2019/11/26 追記

正しくはない方法だとは思うけど、適当にParametersに指定した値をSecurity Groupのタグに設定するようにして、
更新の度に入力値を変更させたら更新のトリガーに走ることに気づいたのでそれで対応可能(コードは書き直してないです)。

終わりに

もしかしたらこんなめんどくさいことしなくてもIPアドレスまとめて指定できるよ!
っていうのがあったら教えてください。
その場合こんな記事いらんやんけ!って恥ずかしい気持ちになりますが
マクロはこんな感じで使えるよって例にはなるので良しとしましょう。

補足

一つのSeuciryGroupに定義できる量には上限があるみたいなので(記事投稿時点でデフォルト60)注意した方が良いです

Amazon VPC のセキュリティグループの制限を増加させる方法を教えてください。
https://aws.amazon.com/jp/premiumsupport/knowledge-center/increase-security-group-rule-limit/

参考

AWS CloudFormation を AWS Lambda によるマクロで拡張する
https://aws.amazon.com/jp/blogs/news/cloudformation-macros/

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away