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

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



# 使うもの

- CloudFormation
- Lambda(Python3)

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


# 事の始まり

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

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

AWSのユーザガイド([AWS::EC2::SecurityGroup Ingres](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group-rule-1.html))を見る限りまとめてIPは指定できず、
CloudFormationにはループ構造が見当たりませんでした。

探した結果[AWS::CloudFormation::Macro](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-macro.html)にたどり着きましたが、
日本語の参考サイトがほとんどなく、初学者の自分に優しくなかったので同じ人の助けになればと思い書きました。
英字含めたらそれなりにありましたが検証した方が早いってなりました。

一部検証した結果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とかもろもろ書くのがめんどくさくて手動で作りました。
ソースは以下です。


```python: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のこちらの記事](https://aws.amazon.com/jp/blogs/news/cloudformation-macros/)を参考にしていただければと思います。

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

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

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

## `AWS::CloudFormation::Macro`を定義

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


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

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

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

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

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

```yml: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`](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-transform.html)はあまり理解せずになんかいい感じに展開されるんだなくらいで使いました。
検証した結果`Name`に指定したマクロ(Lambda関数)を`Parameter`の値をを引数にして呼び出して
返却をそのままここに展開しているようです。

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

```yml
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364764/7fee46bd-aa0e-171f-7e97-06f57cffaee4.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/