こんにちは。SCSKのひるたんぬです。
2024年になり、早二週間が経過しました。時間の経過が早く感じるようになった今日このごろです。
早速余談なのですが、「人生の体感時間は年齢を重ねるごとに短くなる」と言われており、これを数式を用いて表現したものを「ジャネーの法則」と言うそうです。この法則に従い計算をすると、私はすでに人生の約75%を終えた¹ことになっているみたいですね…一日一日を大切に生きたいと思います。
¹寿命については、厚生労働省が発表した「令和4年簡易生命表」より、男性の平均寿命である81.05歳で計算しています。計算ツールはこちらで提供されているものを使用しました。
サービスの概要
今回は、「各種サービスで設定値を控えておくパラメータシートを読み込み、そこからCloudFormationで用いる事のできるYAMLファイルを自動的に生成する」という機能を構築しました。全体的なアーキテクチャを下図に示します。Cloud9上で開発を行い、構築したプログラムをCodeCommitへコミットしています。
利用者が設定値の記載されたパラメータシートをCodeCommitにコミットすると、最終的にCloudFormationテンプレートであるYAMLファイルがS3バケットに格納されるという流れです。
目的
成果物を作成するにあたり、指導員の方と相談していたところ、
というお話をいただき、それぞれの設定値を自動で比較するサービスを作ろう!ということになりました。
…ですが、その後の話し合いの中で、
ということになり、先述したサービスを構築する運びとなりました。
これが実現することにより、AWSに精通している人はもちろんのこと、そこまで詳しくない人でも簡単にパラメータシートを作成できるようになることが期待されます。
パラメータシート自動生成プログラム
このプログラムは下図に示すような流れで動作をします。それぞれの処理について、コードの一部²を交えながらご紹介します。
²コードについては読みにくい箇所や非効率な処理を行っている箇所も多々あると思いますが、ご容赦ください。
パラメータシートの作成
まずは、パラメータシートを作成します。パラメータシートはリソースの種類ごとに作成する必要があります。
以下にLambda用 IAM – Role パラメータシートの一例を示します。
パラメータシートの読み込み
次にCodeCommitにコミットされたパラメータシートを確認します。パラメータシートのファイル名を基に、プログラム内でどのパラメータシートがどのリソースの内容を記述しているのかを判別します。判別を行ったら、パラメータシートから設定値を取り込みます。以下は、Excelで作成されたパラメータシートから設定値を取得し、リストに格納するプログラムです。
def convert(source_dir: str) -> list: book = openpyxl.load_workbook(source_dir) ws = book.worksheets[0] # Read Key and Value data_from_ps = [] # Read from row 1 for row in ws.rows: # list型として各行の値を格納 key = [] for col_num in range(len(row)): # 条件:値が存在するセルのみ取り込み(最終列を除く) if col_num != (len(row) - 1) and (row[col_num].value == None or row[col_num].value == ""): pass else: key.append(row[col_num].value) data_from_ps.append(key) book.close() return data_from_ps
このリストをリソースに応じた辞書型に変換します。API Gateway – Accountの例を以下に示します。
class AWSApiGatewayAccount: def __init__(self): self.logical_id = "" self.type = "AWS::ApiGateway::Account" self.properties = { "CloudWatchRoleArn" : "" } # Setter def set_resource(self, data: list) -> None: for i in range(len(data)): # 各項目から設定項目・値を取得 setting_type = data[i][len(data[i])-2] setting_value = data[i][len(data[i])-1] # 項目に応じて値を格納 if not setting_value: continue if setting_type == "Logical ID": self.logical_id = setting_value elif setting_type == "CloudWatchRoleArn": self.properties["CloudWatchRoleArn"] = setting_value
CloudFormationテンプレートの作成
設定値を取り込んだらCloudFormationテンプレートの形に変換し、YAMLファイルを生成します。
def output(source: dict, file_name: str) -> None: with open(file_name, "w") as f: yaml.dump(source, f, sort_keys=False) with open(file_name, "r") as f: contents = f.read() contents = contents.replace("'", "") # *はクォーテーションで括っていないとエラー contents = contents.replace(" *", " '*'") with open(file_name, "w") as f: f.write(contents)
リソースの試験構築
YAMLファイルが生成されたら、このファイルを用いてCloudFormationでリソースの構築を実行し、正しく構築ができるか確認を行います。後段の作業のためにwaiterを設定し、構築が終わるまで待機します。
def convert(yaml_path: str, stack_name: str) -> None: f = open(yaml_path, "r") template_body = f.read() f.close() cfn = boto3.client("cloudformation") response = cfn.create_stack( StackName=stack_name, TemplateBody=template_body, Capabilities=[ "CAPABILITY_NAMED_IAM", ], ) waiter = cfn.get_waiter("stack_create_complete") waiter.wait(StackName=stack_name)
上記コードでスタックを作成する関数「create_stack」の引数に「Capabilities」を追加しています。これは、IAMに関連するリソースを作成するために行っているものです。
正しく構築できたら、構築されたリソースにアクセスし、リソースの設定値を取得します。
# Get properties from AWS def get_resource(self, logical_resource_id: str, physical_resource_id: str, stack_name: str) -> None: # 構成情報の取得 client = boto3.client("apigateway") response = client.get_account() self.logical_id = logical_resource_id self.properties["CloudWatchRoleArn"] = response["cloudwatchRoleArn"]
取得したリソースの設定値と、パラメータシートの値を一つのクラスに集約します。
# 2つのリソースファイルの結果を一つにまとめる def summarize_properties(self, cfn_template: dict) -> dict: logical_ids = [self.logical_id, cfn_template.logical_id] summary = template.summary.Summary(logical_ids, self.type) if self.properties["CloudWatchRoleArn"]: key = summary.key_default.copy() key.append("CloudWatchRoleArn") value = [self.properties["CloudWatchRoleArn"], cfn_template.properties["CloudWatchRoleArn"]] summary.properties[tuple(key)] = value return summary
設定値の比較
取得した設定値と、パラメータシートの値が等しいかどうかを確認します。
def compare(all_resources: list) -> list: compared_resources = all_resources.copy() # 1つずつのリソースの値を比較 for resource in compared_resources: property = resource.properties # 1つずつの設定値を比較 for key, values in property.items(): # 設定値の型により処理を分岐 if type(values[0]) == list: for val in values: result = val[0] == val[1] val.append(result) else: # 設定値の確認 result = values[0] == values[1] values.append(result) property[key] = values return compared_resources
ファイルの出力・最終処理
最終的に、生成したYAMLファイルと値の比較結果をまとめたExcelファイルをS3に出力した後に、試験構築したリソースを削除し、処理は完了です。
取り組んだ感想
今回は新人としての取り組みということで、LambdaやAPI Gatewayの一部のリソースのみを対象にしました。構築する際にそれぞれのCloudFormationのドキュメントを読み込んだので、CloudFormationの仕組みについて理解を深めることができたほか、リソースがどのように構築されるのか、それぞれがどのようなオプションを持っているのかを詳しく知ることができました。特にAPI Gatewayでは、異なるリソースで同じような設定項目がいくつもあったことが興味深かったです。
一方でリソースの値を取得するboto3の関数(get_xxxxx)の出力について、同じような項目でも表記方法が異なるものがあり、戸惑うことがありました。
例えばタグについて見てみると、IAMロールの情報を入手する”get_role”では、
'Tags': [ { 'Key': 'string', 'Value': 'string' }, ]
となっているのに対して、Lambda関数の情報を入手する”get_function”では、
'Tags': { 'string': 'string' }
となっており、KeyとValueの格納方法が異なっていることが分かります。また、API Gatewayのステージの情報を取得する”get_stage”では、
'tags': { 'string': 'string' }
と、タグのKeyそのものが小文字で表記されています。
なぜ格納方法や表記が異なっているのかは私には分かりませんが、統一してくれると分かりやすくなるのではないかなぁと感じました。
今後は、最初の工程であるパラメータシートの作成をより分かりやすく改良していき、そして他のリソースについても対応できるように拡張させていきたいなと考えております。
最後までご覧いただき、ありがとうございました!