CloudFormation のリソース仕様からコード生成してみる

はじめに

この間 AWS-CDK というプロジェクトが公開されていました。

aws.amazon.com github.com https://awslabs.github.io/aws-cdk/awslabs.github.io

まだ開発者プレビューという段階で正式な公開ではないようです。 中身はあまり詳しく見ていませんが、かなり便利に使えそうな印象がありました。 TypeScript, JavaScript, Java, .NET, Python などの言語で CloudFormation のリソースを記述して、これを元に色々なリソースを AWS 上に構築するのに使えるようです。

CloudFormation のテンプレートは YAMLJSON で書くことができますがなかなか大変です。 少し複雑なものを作ろうとするとリファレンスを参照しながら、トライアルアンドエラーでスタックを何度も立てたりして書いていた記憶があります。 色々な言語で IDE の補間やシンタックスチェックなどが効いた状態で書ければ便利になりそうです。 またプログラミング言語で書けるので、他のリソース情報を参照したりということもやりやすくなるのではないかと思います。

すこし調べていたところ CloudFormation のリソース仕様が公開されていると知りました。

docs.aws.amazon.com

cfn-lint のツールなどでもこの仕様が利用されているようです。 AWS-CDK の内部でもおそらくこの仕様からコード生成したりして利用されているのではないかと思います。

実際に使う場合には CDK などの公式のツールを使うのがいいと思いますが この仕様からコード生成するというのはそんなに難しくなくできそうなのと、ちょっと面白そうという気がしてコード生成するのを試してみました。

リソース仕様の構造

リソース仕様の構造についての詳しい説明はどこにあるかわかりませんでしたが、中身を見てみると意味はなんとなく理解できました。

一番上の階層に PropertyTypes, ResourceTypes, ResourceSpecificationVersion があります。

{
    "PropertyTypes": {...},
    "ResourceTypes": {...},
    "ResourceSpecificationVersion": "2.8.0"
}

ResourceTypes は色々なリソースの定義です。 PropertyTypes はリソースの中で使われているプロパティの定義のようです。 ResourceSpecificationVersion はこの仕様のバージョンだと思います。

基本の型など

基本の型 (PrimitiveType) として Boolean, Double, Integer, Long, String, Timestamp, Json があるようです。 このほかに MapTagPrimitiveType ではないですが定義なしで使われていて少し特殊な扱いのようでした。

Amazon API Gateway Deployment DeploymentCanarySettings - AWS CloudFormation

Map は例えば上のような部分で使われていました。キーと値が両方文字列の連想配列のようです。

AWS CloudFormation Resource Tags タイプ - AWS CloudFormation

Tag はキーと値の文字列のペアです。

それから List タイプがあります。List は配列です。List の場合は ItemType または PrimitiveItemType で要素の型が書かれていました。

プロパティタイプ定義

プロパティタイプ のドキュメントはここにありました。

リソースプロパティタイプのリファレンス - AWS CloudFormation

プロパティタイプ定義は例えば下のような構造になっています。

    ...
    "AWS::IAM::User.Policy": {
      "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-policy.html",
      "Properties": {
        "PolicyDocument": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-policy.html#cfn-iam-policies-policydocument",
          "PrimitiveType": "Json",
          "Required": true,
          "UpdateType": "Mutable"
        },
        "PolicyName": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-policy.html#cfn-iam-policies-policyname",
          "PrimitiveType": "String",
          "Required": true,
          "UpdateType": "Mutable"
        }
      }
    },
    ...

キーが "リソース名.プロパティ名" になっています。 内容はドキュメントのリンクとプロパティ一覧で、おそらくプロパティタイプのプロパティは基本の型か Map か Tag に限られています。

Required はプロパティが必須なものかどうかです。 UpdateType はリソース更新で変更できるかどうかという意味だと思います。Immutable, Mutable, Conditional のどれかが使われていました。

リソースタイプ定義

リソースタイプ のドキュメントはこれです。

AWS リソースプロパティタイプのリファレンス - AWS CloudFormation

リソースタイプ定義の例です。

    ...
    "AWS::IAM::User": {
      "Attributes": {
        "Arn": {
          "PrimitiveType": "String"
        }
      },
      "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html",
      "Properties": {
        "Groups": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html#cfn-iam-user-groups",
          "DuplicatesAllowed": true,
          "PrimitiveItemType": "String",
          "Required": false,
          "Type": "List",
          "UpdateType": "Mutable"
        },
        "LoginProfile": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html#cfn-iam-user-loginprofile",
          "Required": false,
          "Type": "LoginProfile",
          "UpdateType": "Mutable"
        },
        "ManagedPolicyArns": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html#cfn-iam-user-managepolicyarns",
          "DuplicatesAllowed": false,
          "PrimitiveItemType": "String",
          "Required": false,
          "Type": "List",
          "UpdateType": "Mutable"
        },
        "Path": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html#cfn-iam-user-path",
          "PrimitiveType": "String",
          "Required": false,
          "UpdateType": "Mutable"
        },
        "Policies": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html#cfn-iam-user-policies",
          "DuplicatesAllowed": true,
          "ItemType": "Policy",
          "Required": false,
          "Type": "List",
          "UpdateType": "Mutable"
        },
        "UserName": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html#cfn-iam-user-username",
          "PrimitiveType": "String",
          "Required": false,
          "UpdateType": "Immutable"
        }
      }
    }
    ...

プロパティタイプの定義とほぼ似た感じですがドキュメントリンクとプロパティ一覧以外に Attributes があります。 Attributes はリソース作成した時に付与される情報だと思います。 Attributes に使われるのは基本の型のみで RequiredUpdateType の指定はありません。 リソースタイプのプロパティは基本の型かプロパティタイプ定義で定義した型が使われています。

コード生成してみる

実験に使ったコードはこれです。

github.com

リソース定義を http-client で取得して JSON をパースし、テンプレートエンジンでコード生成という感じです。

仕様からコード生成するという流れはよくあるもののようで例えば Swagger や Protocol Buffers などでコード生成するというのを聞いたことがあります。 どちらもまだ使ったことがないのでこのような場合に使えるかわからないですが、また機会があればこの辺のツールも触ってみたいです。

生成したコードは下のような感じになりました。

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module AWS.IAM.User
    ( Policy(..)
    , LoginProfile(..)
    , User(..)
    , resourceJSON
    ) where

import AWS (Map, Tag)
import Data.Text (Text)
import Data.Aeson ((.=))
import qualified Data.Aeson as DA (FromJSON(..), ToJSON(..), Options(..), Value, defaultOptions, object)
import qualified Data.Aeson.TH as DA (deriveJSON)

data Policy = Policy
    { _PolicyPolicyDocument :: DA.Value
    , _PolicyPolicyName :: Text
    } deriving (Show, Eq)

data LoginProfile = LoginProfile
    { _LoginProfilePassword :: Text
    , _LoginProfilePasswordResetRequired :: Maybe Bool
    } deriving (Show, Eq)

data User = User
    { _UserGroups :: Maybe [Text]
    , _UserPath :: Maybe Text
    , _UserLoginProfile :: Maybe LoginProfile
    , _UserUserName :: Maybe Text
    , _UserManagedPolicyArns :: Maybe [Text]
    , _UserPolicies :: Maybe [Policy]
    } deriving (Show, Eq)

$(DA.deriveJSON DA.defaultOptions { DA.fieldLabelModifier = drop 7 } ''Policy)
$(DA.deriveJSON DA.defaultOptions { DA.fieldLabelModifier = drop 13 } ''LoginProfile)
$(DA.deriveJSON DA.defaultOptions { DA.fieldLabelModifier = drop 5 } ''User)

resourceJSON :: User -> DA.Value
resourceJSON a = DA.object [ "Type" .= ("AWS::IAM::User" :: Text), "Properties" .= a ]

生成したコードを使って一応は Haskell のデータでリソースを定義して JSON を出力するということはできるんじゃないかと思います。 (実際に使うつもりはあまりなく実験してみたという感じです。)

まとめ

CloudFormation のリソース定義をつかってのコード生成をやってみました。 やろうと思えば色々なツールを作ったりもできるんじゃないかと思います。 仕様がプログラムから利用しやすい形で公開されているのはいいなと思いました。

この他にも IAM のアクションの仕様なども公開されるとうれしいと思います。 こちらのページで IAM アクションが検索できるサービスが公開されていました。

Complete AWS IAM Reference

これは非公式なページで AWS ドキュメントから情報を抜き出したりして更新しているようです。 IAM アクション仕様が提供されていればこうしたツールも作りやすくなると思います。