cfn-docgen - AWS CloudFormationテンプレートからドキュメントを自動生成するツールを公開しました

この記事は、[NTTテクノクロス Advent Calendar 2023](https://qiita.com/advent-calendar/2023/ntt-tx)の1日目の記事になります。

メリークリスマス！NTTテクノクロスの堀江です。普段はAWSやAzure上でのシステム設計、構築や実装、調査検証系の案件を幅広く担当しています。

## はじめに

本記事では、今年自分が開発・公開したOSSツールである、[cfn-docgen](https://github.com/horietakehiro/cfn-docgen)(及び、そのVSCode拡張機能版である、[cfn-docgen-vsc-extension](https://github.com/horietakehiro/cfn-docgen-vsc-extension))について紹介していきます。

## cfn-docgenとは

**cfn-docgenとは、YAML/JSON形式で記述されたAWS CloudFormation(以降、cfnと呼称)テンプレートファイルから、マークダウン形式のドキュメントファイルを自動生成することが出来るツールです。**

cfn-docgenが実現することは主に以下の2点です。
- リソースの構成図や設計思想といった情報をcfnテンプレートファイル内に完結して記述出来るため、cfnテンプレートファイルと詳細設計書(aka.パラメータシート)の二重管理の手間を省く
- YAML/JSONファイルから、より人間が読みやすい形式のドキュメントを生成することで、顧客や非AWS技術者に対する情報伝達を円滑にする

以下のように、AWSリソースとユーザ独自のDescription(詳細は後述)を定義した左のcfnテンプレートファイルから、右のようなマークダウン形式のドキュメントを自動生成する出来ます。

![cfn-docgen例](https://github.com/horietakehiro/cfn-docgen/raw/master/docs/images/source-template-and-dest-document.png)

基本的にはCLIツール(Python3.10以上が必要)ですが、[後述](#配布形式)の通り、様々な形式で使用が可能です。

```Bash:コマンド実行例
$ cfn-docgen docgen \
    --format markdown \
    --source docs/sample-template.yaml \
    --dest ./docs/
[INFO] successfully generate document [./docs/sample-template.md] from template [docs/sample-template.yaml]
```

## 使用方法

cfn-docgenにはdocgen機能とskeleton機能という2種類のサブ機能があります。

### docgen機能

cfnテンプレートファイルからドキュメントを自動生成するための機能

#### ドキュメント生成パターン

cfn-docgenでは、ローカルファイルだけでなく、S3バケット上に格納されたファイルもドキュメント生成対象やドキュメントのアップロード先に直接指定することが可能です。また、ディレクトリ(S3パス)配下の複数のcfnテンプレートファイルから、複数のドキュメントを一括で自動生成することも可能です。

```Bash:ローカルの単一cfnテンプレートファイルからドキュメント生成
$ cfn-docgen docgen \
    --format markdown \
    --source docs/sample-template.yaml \
    --dest ./docs/
[INFO] successfully generate document [./docs/sample-template.md] from template [docs/sample-template.yaml]
```

```Bash:S3バケット上の単一cfnテンプレートファイルからドキュメント生成
$ cfn-docgen docgen \
  --source s3://bucket/prefix/sample-template.yaml \
  --dest s3://bucket/prefix/sample-template.md
[INFO] successfully generate document [s3://bucket/prefix/sample-template.md] from template [s3://bucket/prefix/sample-template.yaml]
```

```Bash:複数cfnテンプレートファイルからドキュメント一括生成
$ tree ./templates/
./templates/
├── sample-template-1.yaml
└── subfolder
    └── sample-template-2.yaml

1 directory, 2 files
# 指定したディレクトリ配下のcfnテンプレートファイルからドキュメントを一括生成
$ cfn-docgen docgen \
    --source ./templates/ \
    --dest s3://bucket/documents/
[INFO] successfully generate document [s3://bucket/documents/sample-template-1.md] from template [./templates/sample-template-1.yaml]
[INFO] successfully generate document [s3://bucket/documents/subfolder/sample-template-2.md] from template [./templates/subfolder/sample-template-2.yaml

```
#### Descriptionの埋め込み

そのcfnテンプレートや各リソース、各リソースのプロパティに関して明記が必要な情報を、cfnテンプレートファイル内の`Metadata`セクションにDescriptionとして記述する(埋め込む)ことで、それらの情報も含めて綺麗にドキュメントが生成されます。

```Yaml:テンプレート全体のDescription例
AWSTemplateFormatVersion: 2010-09-09
Description: This template creates 1 VPC and 2 public subnets in it.
Metadata:
  CfnDocgen:
    Description: |
      このテンプレートファイル東京リージョン上で以下のリソースを作成します
      - VPC
      - パブリックサブネット(2AZに1つずつ)

      ![Archtecture](./images/sample-template.drawio.png)


      **注意点**
      - このテンプレートファイルは**東京リージョン**上でのみの使用に制限しています
      - このテンプレートファイルを使用する前に、[東京リージョン上に作成可能なVPCの最大数の設定](https://ap-northeast-1.console.aws.amazon.com/servicequotas/home/services/vpc/quotas/L-F678F1CE)を確認することを推奨します(デフォルトは5VPC)**
```

![テンプレート全体のDescription例](https://github.com/horietakehiro/cfn-docgen/raw/master/docs/images/top-level-description.png)

```Yaml:各セクションに対するDescription例
AWSTemplateFormatVersion: 2010-09-09
Metadata:
  CfnDocgen:
    Mappings:
      CidrBlockMap: CidrBlocks for each environment
    Conditions:
      EnvCondition: Check if the value of parameter `EnvType` is `prod`
    Rules:
      RegionRule: This template is available only in ap-northeast-1
```

![各セクションに対するDescription例](https://github.com/horietakehiro/cfn-docgen/raw/master/docs/images/each-section-description.png)

```Yaml:リソースとプロパティに対するDescription例
AWSTemplateFormatVersion: 2010-09-09
Resources: 
  VPC:
    Metadata:
      CfnDocgen:
        Description: アプリケーションサーバを稼働させるために使用するVPC
        Properties:
          EnableDnsHostnames: アプリケーションサーバのホスト名でパブリックIPを名前解決できるように有効化する
    Type: AWS::EC2::VPC
    Properties: 
      EnableDnsHostnames: true
      CidrBlock: ...
```

![リソースとプロパティに対するDescription例](https://github.com/horietakehiro/cfn-docgen/raw/master/docs/images/resource-level-description.png)

AWS CDKコードに対しても、Descriptionの埋め込み及びドキュメントの自動生成が可能です。

```Python:AWS CDKへのDescription埋め込み例
from aws_cdk import (
    Stack,
    aws_ec2 as ec2,
    
)
from constructs import Construct
from typing import Any
from cfn_docgen.domain.model.cfn_template import (
    CfnTemplateMetadataCfnDocgenDefinition as Metadata,
    CfnTemplateResourceMetadataDefinition as ResourceMetadata,
    CfnTemplateResourceMetadataCfnDocgenDefinition as CfnDocgen
)
class CfnDocgenSampleCdkStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs:Any) -> None:
        super().__init__(scope, construct_id, **kwargs)
        # テンプレート全体に対するDescriptionの埋め込み
        self.add_metadata(
            "CfnDocgen", Metadata(
                Description="テンプレート全体に対するDescriptionの埋め込み"
            ).model_dump(),
        )
        self.vpc_construct = ec2.Vpc(self, "VpcConstruct", max_azs=1)
        # リソースに対するDescriptionの埋め込み
        self.vpc_construct.node.default_child.cfn_options.metadata = ResourceMetadata(
            CfnDocgen=CfnDocgen(Description="リソースに対するDescriptionの埋め込み")
        ).model_dump()


```

![AWS CDKへのDescription埋め込み例](https://github.com/horietakehiro/cfn-docgen/raw/master/docs/images/table-of-contents-from-cdk-generated-template.png)

#### カスタムリソース仕様の使用

独自のリソース仕様を定義したファイルを読み込ませることで、デフォルトでは非対応のサードパーティーのリソースタイプや`Custom::SomeCustomResource`のようなカスタムリソースタイプに関してもドキュメント生成することが出来ます。

※カスタムリソース仕様ファイルの例は[こちら](https://github.com/horietakehiro/cfn-docgen/blob/master/docs/custom-specification.json)を参照

```Bash:カスタムリソース仕様の使用例
$ cfn-docgen docgen \
  -s docs/sample-template.yaml \
  -s docs/sample-template.md \
  -c docs/custom-specification.json
```

### skeleton機能

(こちらの機能は副産物のようなものですが、)以下のように、任意のリソースタイプのcfnテンプレート定義のスケルトンを出力することが出来ます。

後述するように[VSCode拡張機能](#VSCode拡張機能)として使用することで、cfnテンプレートファイル執筆を効率化させることを目的としています。

```Bash:skeleton機能使用例
$ cfn-docgen skeleton --type AWS::EC2::VPC --format yaml
Type: AWS::EC2::VPC
Metadata:
  Documentation: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html
  CfnDocgen:
    Description: ''
    Properties: {}
Properties:
  CidrBlock: String
  EnableDnsHostnames: Boolean
  EnableDnsSupport: Boolean
  InstanceTenancy: String
  Ipv4IpamPoolId: String
  Ipv4NetmaskLength: Integer
  Tags:
    - Key: String
      Value: String
```

## 配布形式

### CLI

使用にはPython3.10以上が必要になります。
※OSはLinux、Windows共に対応

```Bash:CLIのインストール
$ pip install cfn-docgen
```

### VSCode拡張機能

前述したdocgen機能とskeleton機能を[VSCode拡張機能](https://github.com/horietakehiro/cfn-docgen-vsc-extension)としても使用可能です。

※別途[CLI](#cli)のインストールは必須

- docgen機能の使用例
![docgen機能の使用例](https://github.com/horietakehiro/cfn-docgen-vsc-extension/raw/master/images/single-dest-single-source.gif)

- skeleton機能の使用例
![skeleton機能の使用例](https://github.com/horietakehiro/cfn-docgen-vsc-extension/raw/master/images/skeleton.gif)

### Dockerイメージ

```Bash:Dockerイメージとしての実行例
# pull image from DockerHub
$ docker pull horietakehiro/cfn-docgen:latest

# local directory(before)
$ tree /tmp/sample/
/tmp/sample/
└── sample-template.json

0 directories, 1 files

# run as command
$ docker run \
  -v /tmp/sample/:/tmp/ \
  horietakehiro/cfn-docgen:latest docgen \
    --source /tmp/sample-template.json \
    --dest /tmp/
[INFO] successfully generate document [/tmp/sample-template.md] from template [/tmp/sample-template.json]

# local directory(after)
$ tree /tmp/sample/
/tmp/sample/
├── sample-template.json
└── sample-template.md

0 directories, 2 files

```

### Pythonモジュール

```Python:Pythonモジュールとしての使用例
from cfn_docgen import (
    CfnDocgenService, CfnDocgenServiceCommandInput,
    CfnTemplateSource, CfnDocumentDestination
)
service = CfnDocgenService.with_default()
service.main(
    command_input=CfnDocgenServiceCommandInput(
        template_source=CfnTemplateSource("s3://bucket/template.yaml", service.context),
        document_dest=CfnDocumentDestination("s3://bucket/document.md", service.context),
        fmt="markdown",
    )
)
```

### サーバレスアプリケーション

AWS上のサーバレスアプリケーションとしても使用可能です。

[リポジトリ](https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/create/app?applicationId=arn:aws:serverlessrepo:ap-northeast-1:382098889955:applications/cfn-docgen-serverless)からリソースをデプロイすると`cfn-docgen-${AWS::AccountId}-${AWS::Region}`という名前のS3バケットが作成されます。そのバケットの`templates/`パス配下にcfnテンプレートファイルをアップロードすることで、`documents/`パス配下にドキュメントが自動生成されます。


## cfn-docgenの開発理由

最後に、このツールを開発した理由について書こうと思います。

AWSを扱うようになってはや4年、cfnテンプレートでAWS上のシステムを定義・構築するインフラ周りの仕事で、同時にシステムやリソースの詳細設計書(aka. パラメータシート)の執筆も求められる、という状況に度々遭遇しました。

システムの構成やリソース同士の相互関係を表現した図表や、リソース設定の具体値を決めた経緯や特記事項の文章を、綺麗な形でドキュメントに残すこと自体は重要です。システムの全体像や設計の意図を、顧客や経験の浅いメンバ(≒非AWS技術者)にも分かり易く伝えることが出来るためです。
しかしそのドキュメントをExcelやWordとして執筆していくとなると、途端に様々な手間や危険が増えます。
- 作業コストが単純増加
    - 詳細設計書やパラメータシートで記述する内容と、cfnテンプレートファイルで定義する内容は(表現は違っていても)情報としてはほぼ等価であることが殆どです(リソースの定義内容をYAML/JSON形式で書くか、表形式で書くかの違い)。同じ内容のドキュメントを2種類作成・保守することになります。
- 設計内容と実装内容の乖離リスク
    - 設計(詳細設計書)と実装(cfnテンプレートファイル)のドキュメントが分かれていると、片方の内容の変更をもう片方に反映し忘れる(或いは不適切な内容で反映する)という、変更管理のリスクが常に付き纏います。また、詳細設計書がExcelやWordの場合、バージョンごとの差分の確認が面倒です。
- 等々。

cfn-docgenは、そういった問題を解消することを目的に開発しました。
- システムの構成図や設計の意図といった情報も全てcfnテンプレートファイルに完結して記述することで、ドキュメント執筆・保守の手間を省く一方で、ドキュメントの長期的な品質を維持し易いようにする。
- cfnテンプレート(YAML/JSON形式)から、より人間が読みやすいマークダウン形式のドキュメントを生成することで、その内容を顧客や非AWS技術者でも容易に理解出来るようにする。

<br/>

このツールが、皆さんのAWSシステム開発・保守のお力になれば幸いです。

---

明日のNTTテクノクロス Advent Calendar 2023は、 @etctaro さんによる、Compose Multiplatform関する記事です。お楽しみに！