パラメータシートから AWS CloudFormation テンプレートを自動で作成する

こんにちは。SCSKのひるたんぬです。
2024年になり、早二週間が経過しました。時間の経過が早く感じるようになった今日このごろです。
早速余談なのですが、「人生の体感時間は年齢を重ねるごとに短くなる」と言われており、これを数式を用いて表現したものを「ジャネーの法則」と言うそうです。この法則に従い計算をすると、私はすでに人生の約75%を終えた¹ことになっているみたいですね…一日一日を大切に生きたいと思います。

¹寿命については、厚生労働省が発表した「令和4年簡易生命表」より、男性の平均寿命である81.05歳で計算しています。計算ツールはこちらで提供されているものを使用しました。

ジャネーの法則について原著(と思われる文書)はこちらです。
フランス語ですが、興味のある方は是非読んでみてください。
本題に戻ります。今回は新人である私が、配属されてからおよそ3ヶ月ほどで取り組んだ「AWSを活用したサービスの構築」についてご紹介しようと思います。構築の際に用いた開発環境については、前回の私の記事にてご紹介しておりますので、よろしければご覧ください。

サービスの概要

今回は、「各種サービスで設定値を控えておくパラメータシートを読み込み、そこからCloudFormationで用いる事のできるYAMLファイルを自動的に生成する」という機能を構築しました。全体的なアーキテクチャを下図に示します。Cloud9上で開発を行い、構築したプログラムをCodeCommitへコミットしています。

 

利用者が設定値の記載されたパラメータシートをCodeCommitにコミットすると、最終的にCloudFormationテンプレートであるYAMLファイルがS3バケットに格納されるという流れです。

目的

成果物を作成するにあたり、指導員の方と相談していたところ、

現状では人手で作成したCloudFormationテンプレートで構築したリソースの値と、パラメータシートに記載された値を人間が一つずつ手作業で比較・確認しており、非効率かつ間違いが起きる可能性がある

というお話をいただき、それぞれの設定値を自動で比較するサービスを作ろう!ということになりました。

…ですが、その後の話し合いの中で、

そもそもCloudFormationのテンプレートをパラメータシートから自動で生成してくれれば、このような問題は起こらない

ということになり、先述したサービスを構築する運びとなりました。
これが実現することにより、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ファイルを生成します。

組み込み関数(!Sub xxx など)を認識させるためにはクォーテーションを外す必要があったのですが、ワイルドカードである「*」についてはクォーテーションを付けないとエラーが出てしまうという点に少々苦戦しました。
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を設定し、構築が終わるまで待機します。

今回は”create_stack”関数を直接呼び出していますが、これではスタック作成に失敗した際に例外エラーとなるので、例外をキャッチする仕組みや、事前にテンプレートの妥当性を検証する“validate_template”関数を挿入しても良いと思います。
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そのものが小文字で表記されています。
なぜ格納方法や表記が異なっているのかは私には分かりませんが、統一してくれると分かりやすくなるのではないかなぁと感じました。

今後は、最初の工程であるパラメータシートの作成をより分かりやすく改良していき、そして他のリソースについても対応できるように拡張させていきたいなと考えております。

最後までご覧いただき、ありがとうございました!

タイトルとURLをコピーしました