TL; DR
!Join
がここで使えるんだ!
はじめに
CloudFormationには、パラメーターの型に List<Number>
型と CommaDelimitedList
型が存在します。
これらは、パラメーター値として ,
(カンマ)区切りの数値または文字列を入力すると、カンマで分割したリストに変換してくれるというものです。
List<Number>
カンマで区切られた整数または浮動小数点値の配列。AWS CloudFormation は、このパラメータを数値として検証しますが、テンプレート内の他の場所で使用した場合には (Ref 組み込み関数を使用した場合など) 一連の文字列として扱います。
たとえば、"80,20" と指定し、Ref を使用した場合には ["80","20"] となります。
CommaDelimitedList
カンマで区切られたリテラル文字列の配列。文字列の合計数は、カンマの合計数よりも 1 つ多いはずです。また、各メンバー文字列の前後の空白は削除されます。
たとえば、"test,dev,prod" と指定し、Ref を使用した場合には ["test","dev","prod"] となります。
これを使うことで、設定したい項目数を動的に変更したい場合1にも、表現の幅が広がります。
クロスアカウントアクセス
1例として、S3バケットのクロスアカウントアクセスに必要なAWSアカウントIDが挙げられます。
クロスアカウントアクセスとは、とあるAWSアカウントのS3バケットを、他AWSアカウントから見るようにできる設定のことを意味します。
設定方法は省略しますが、下記が参考になります。
Resources:
Account1BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: account1-bucket
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS:
- 000000000000 # クロスアカウントアクセス可能なAWSアカウント
- 123456789012 # クロスアカウントアクセス可能なAWSアカウント
Action:
- s3:GetObject
- s3:ListBucket
Resource:
- arn:aws:s3:::account1-bucket
- arn:aws:s3:::account1-bucket/*
Principal
ハッシュにおける AWS
キーの値として、 複数のAWSアカウントを指定できます。
これにより、 Account1 の account1-bucket バケットを、 000000000000 アカウントと 123456789012 アカウントが GetObject および ListBucket できます。
AWS
キーの値として、パラメーター CommaDelimitedList
型の値を参照することで、下記のように1つ以上のAWSアカウントを設定できます。
Parameters:
AccountIdList:
Type: CommaDelimitedList
Resources:
Account1BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: account1-bucket
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS: !Ref AccountIdList # クロスアカウントアクセス可能なAWSアカウントのリスト
Action:
- s3:GetObject
- s3:ListBucket
Resource:
- arn:aws:s3:::account1-bucket
- arn:aws:s3:::account1-bucket/*
AWS::S3::BucketPolicy - AWS CloudFormation
これは嬉しいですよね。
問題点
しかし、1つだけ問題があります。
それは、0つ以上のAWSアカウントを設定できない点です。
もし上記のCFn.yamlファイルを実行する際、パラメーターに空文字を入力する場合(または、Defaultを空文字にした上でパラメーターを与えない場合)、正しく動作しません。
これは、 Principal
ハッシュにおける AWS
キーの値に空文字を入力できないためです。
これを防ぐためには、 Condition によってリソース自体を除去するしか方法はありません。
しかし、下記の場合はエラーとなります。
every Fn::Equals object requires a list of 2 string parameters.
と言われてしまうためです。
Parameters:
AccountIdList:
Type: CommaDelimitedList
Default: ''
Condition:
IsEmpty:
!Equals [ !Ref AccountIdList, [] ] # エラー!
Resources:
Account1BucketPolicy:
Type: AWS::S3::BucketPolicy
Condition: IsEmpty # false の場合に Account1BucketPolicy リソース自体を作成しないようにしたい
Properties:
Bucket: account1-bucket
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS: !Ref AccountIdList # クロスアカウントアクセス可能なAWSアカウントIDのリスト
Action:
- s3:GetObject
- s3:ListBucket
Resource:
- arn:aws:s3:::account1-bucket
- arn:aws:s3:::account1-bucket/*
解決策
!Equals
組込み関数は、 2つの値が両方とも String 型でなければならない リスト同士の比較はできないそうです。
ドキュメントにはそんなこと一言も書いてないどころか、任意の型で判定できるかのように書いてあるんですけどね。
value
比較する任意の型の値です。
これを解決する手法の1つに、 !Join
組込み関数によるリストの文字列への変換があります。
正直、 !Join
が役立つ時が来ると思ってませんでした。
次の例は、
"a:b:c"
を返します。JSON
"Fn::Join" : [ ":", [ "a", "b", "c" ] ]
YAML
!Join [ ":", [ a, b, c ] ]
これで解決!と思いきや、実はもう一つ、隠れた理由があります。
それは、 Principal
ハッシュにおける AWS
キーの値に空文字を設定したままである問題を解決できていないためです。
この問題は、ポリシードキュメントの文法規則の穴を突くことで、解決できます。
すなわち、自身のAWSアカウントIDを代入するのです。
そもそもの原因は、ポリシードキュメントでは、事前にAWSアカウントIDが存在していることを確認するような挙動をしています。
自身のAWSアカウントIDを代入することで、ポリシードキュメントを騙し、 Condition により、ダミーポリシー自体を除去できます。
まとめると、下記の通りです。
Parameters:
AccountIdList:
Type: CommaDelimitedList
Default: ''
Condition:
IsEmpty:
!Equals [ !Ref AccountIdList, '' ] # エラー!
+ !Equals [ !Join [ ',', !Ref AccountIdList ], '' ]
Resources:
Account1BucketPolicy:
Type: AWS::S3::BucketPolicy
Condition: IsEmpty # false の場合に Account1BucketPolicy リソース自体を作成しないようにしたい
Properties:
Bucket: account1-bucket
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
- AWS: !Ref AccountIdList # クロスアカウントアクセス可能なAWSアカウントIDのリスト
+ AWS:
+ - !If
+ - IsEmpty
+ - !Ref AWS::AccountId # 自身のAWSアカウントID
+ - !Ref AccountIdList # クロスアカウントアクセス可能なAWSアカウントIDのリスト
Action:
- s3:GetObject
- s3:ListBucket
Resource:
- arn:aws:s3:::account1-bucket
- arn:aws:s3:::account1-bucket/*
-
IsEmpty
の!Equals
組込み関数内で、!Join
を利用する -
Principal
ハッシュにおけるAWS
キーの値で!If
を利用し、リストが空の場合は自身のAWSアカウントIDを設定する
これにより、 CommaDelimitedList の空判定ができました。
また、ついでに不要なポリシーの除去もできました。
CloudFormation Condition on CommaDelimitedList : aws
ちなみに
別のステートメントがポリシードキュメント内に存在する場合は、 Condition によるリソース除去ではなく、 !If
組込み関数による空ステートメントの挿入だけで良いです。
空ステートメントには、 AWS::NoValue
疑似変数を利用します。
AWS::NoValue
は、ステートメントの定義ハッシュ自体を除去するため、自身のAWSアカウントIDを設定するという抜け道を利用しなくても、ポリシードキュメントの文法を問題なく通過できます。
Parameters:
AccountIdList:
Type: CommaDelimitedList
Default: ''
Condition:
IsEmpty:
!Equals [ !Join [ ',', !Ref AccountIdList ], '' ]
Resources:
Account1BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: account1-bucket
PolicyDocument:
Version: 2012-10-17
Statement:
# 静的に定義するステートメント
- Effect: Allow
Action: *
Resource: *
# 動的に定義したいステートメント
- !If
- IsEmpty
- !Ref AWS::NoValue # リストが空の場合
- Effect: Allow # リストが空じゃない場合
Principal:
AWS: !Ref AccountIdList # クロスアカウントアクセス可能なAWSアカウントIDのリスト
Action:
- s3:GetObject
- s3:ListBucket
Resource:
- arn:aws:s3:::account1-bucket
- arn:aws:s3:::account1-bucket/*
AWS::NoValue
Fn::If
組み込み関数の戻り値として指定すると、対応するリソースプロパティを削除します。
おわりに
CFnを触り続けてひと月が経過しましたが、宣言型文法で動的処理をするのは骨が折れますね。
ただ、CFnはAWSの全てのサービスを大雑把でも理解していないと触れないと思うので、AWSに強くなったひと月だったと思います。
皆さんも、CommaDelimitedListで抜け道面白い使い方を見つけてみてください。