初めての記事投稿です。
よろしくお願いします。
使うもの
- 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
実際に作ってみる
手順
-
AWS::CloudFormation::Macro
で実行するLambda Functionを作る -
AWS::CloudFormation::Macro
の本体を作成する -
AWS::CloudFormation::Macro
を呼び出すCloudFormation
テンプレートを作成する
Lambda Functionを作成する
以下のコードで適当なLambda関数を作ります。
本当はCloudFormationでLambda関数も作れれば良かったんですがRoleとかもろもろ書くのがめんどくさくて手動で作りました。
ソースは以下です。
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の入力が必要です
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を動的に作成する
上記とは別のスタックを以下で作ってください。
CustomerCidrList
は192.168.2.1/32,192.168.2.2/32
みたいな感じCIDRを並べれば行けます
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'
無事に作られていることを確認しました
どうして...
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/