こんにちは河野です。
これはTECHSCORE Advent Calendar 2018 の3日目の記事です。
最近いろいろなツール、例えばCIとか、PaaSとかを触っているのですが、その設定ファイルにはYAMLが使われていることが多い気がします。
使い始めの頃はシンプルだった設定ファイルも、気づけば記述量が増えて見通しが悪くなります。また、Development, Staging, Productionのそれぞれで似たような内容だけどちょっとだけ違う、という設定ファイルをこしらえないといけないことがあります。
そうすると、簡単にYAMLファイルを記述したいとか、あわよくば共通化された部分とちょっとした差分を上手く管理したい、という欲求に駆られるものです。
今回はそういった気持ちで調べてみた、いくつかツールや方法などを紹介します。
YAMLのアンカー/エイリアスの機能
YAMLにはもともとアンカーとエイリアスの機能があります。これを使用すると、同じ記述を避けることができるようになります。例えば以下のようにできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
user1: &user1 id: "001" name: taro live_in: osaka user2: &user2 id: "002" name: hanako live_in: tokyo user3: &user3 id: "003" name: jiro live_in: hokkaido group1: - *user1 - *user2 group2: - *user2 - *user3 |
これをパースした結果 group1, group2 は以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
group1: - id: "001" live_in: osaka name: taro - id: "002" live_in: tokyo name: hanako group2: - id: "002" live_in: tokyo name: hanako - id: "003" live_in: hokkaido name: jiro |
サンプルが実用的ではないですが、雰囲気はわかっていただけるかと思います。
Pros: YAMLの仕様に準拠しているので、多くのツールでサポートされている(はず)
Cons: 複数のファイルをまたがった共通化はできない
標準の仕様なのでおそらくたいていのツールでは使えると思うのですが、AWSのCloudFormationではサポートされてないみたいです。また複数のファイルをまたがって共通化できないというのもちょっと物足りなさがありますね。
参考
- プログラマーのための YAML 入門 (初級編) アンカーとエイリアス
- YAMLのエイリアスでAnsibleファイルの重複を減らす | DevelopersIO
- YAMLのAnchorとAliasを使ってconfigをDRYに書く - valid,invalid
使用するツールでの拡張機能
ツールによっては、共通化のためのIncludeやTemplateの機能、実行時に値を渡すためのPropertiesやVariablesといった機能があります。
GitLab CI
GitLab CIでは異なるYAMLのファイルを引っ張ってくることができ、それを設定内容として利用することができます。
またVariablesの機能の組み合わせでは、includeした値を上書きすることもできます。
ベースとなるYAMLがこちらです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Content of https://company.com/autodevops-template.yml variables: POSTGRES_USER: user POSTGRES_PASSWORD: testing_password POSTGRES_DB: $CI_ENVIRONMENT_SLUG production: stage: production script: - install_dependencies - deploy environment: name: production url: https://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN only: - master |
CIが使用する.gitlab-ci.yml
では以下のように設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# Content of .gitlab-ci.yml include: 'https://company.com/autodevops-template.yml' image: alpine:latest variables: POSTGRES_USER: root POSTGRES_PASSWORD: secure_password stages: - build - test - production production: environment: url: https://domain.com |
POSTGRES_USER
, POSTGRES_PASSWORD
は上書きされますし、production
のタスクはurl
が上書きされます。タスクの内容自体はautodevops-template.yml
の内容が利用されます。
AWS CloudFormation
CloudFormationではParametersやMapping、また独自の関数があります。
1 2 3 4 5 6 7 8 |
Parameters: InstanceType: Type: 'AWS::SSM::Parameter::Value<String>' Resources: Instance: Type: 'AWS::EC2::Instance' Properties: InstanceType: !Ref InstanceType |
EC2のインスタンスタイプはPrameterのInstanceType
で設定された値が利用されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
AWSTemplateFormatVersion: "2010-09-09" Mappings: RegionMap: us-east-1: "32": "ami-6411e20d" "64": "ami-7a11e213" us-west-1: "32": "ami-c9c7978c" "64": "ami-cfc7978a" eu-west-1: "32": "ami-37c2f643" "64": "ami-31c2f645" ap-southeast-1: "32": "ami-66f28c34" "64": "ami-60f28c32" ap-northeast-1: "32": "ami-9c03a89d" "64": "ami-a003a8a1" Resources: myEC2Instance: Type: "AWS::EC2::Instance" Properties: ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", 32] InstanceType: m1.small |
Mappingでは指定したキーに対応する値が利用できます。例えばap-northeast-1
で実行された場合にはap-northeast-1.32
に対応する値であるami-9c03a89d
が使用されます。
これらの方法では以下のようなPros/Consがあります。
Pros: ツールに最適化された方法で設定の共通化が行え、複数のファイルをまたぐ共通化も可能になる
Cons: ツールが変わると再利用できなくなる。YAMLの拡張部分について学習する必要がある。
特にCloudFormationはすごいですね。でもなんか敷居の高さを感じてしまうし、書くのが大変だなぁという気持ちになります。
テンプレートエンジンを使う
YAML内部の項目や、与えるべきパラメータも動的に生成したいので、プログラムでやってしまおうというケースです。
Pythonでやる
CloudFormationのテンプレートを記述したいのですが、それに加えて
- 初期パスワードの生成が必要
- ユーザーが動的なためCloudFormationのParametersも動的に記述が必要
という理由で、実行時に使用するパラメータも生成してみた例です。
以下の例ではJinja2というテンプレートエンジンを使用しました。
処理するためのコードはこのようになります。ユーザーの情報はレンダリング処理の前に準備されている状態です。
1 2 3 4 5 6 |
from jinja2 import Template users = prepare_users() # load_jinja_yaml でテンプレートを読み込みます template = Template(load_jinja_yaml()) print(template.render(user_resources=users)) |
YAMLになるテンプレート部分は以下のようになります。for
を使用した繰り返しの記述ができるのが嬉しいですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
AWSTemplateFormatVersion: "2010-09-09" Description: Create IAM Users Parameters: {% for res in user_resources %} {{ res.param_name }}: Description: password for {{ res.user_name }} Type: String NoEcho: true {% endfor %} Resources: {% for res in user_resources %} {{res.resource_name}}: Type: AWS::IAM::User Properties: UserName: "{{ res.user_name }}" LoginProfile: Password: !Ref {{res.param_name}} PasswordResetRequired: true Groups: - Administrators ManagedPolicyArns: - "arn:aws:iam::aws:policy/IAMUserChangePassword" {% endfor %} |
これでCloudFormationのテンプレートを生成しています。
Pros: プログラムとテンプレートエンジンによる生成のため柔軟性が高い。好きなように生成できる。
Cons: 何度も使用する場合には、例外ケースなどを考慮する必要がある。ボリュームが大きくなるとめんどくさくなる
シェルスクリプト(コマンド)でなんとか
もっと楽にできないもんでしょうか。雑に以下のようにやってみました。
template.yaml
を用意します。
1 2 3 4 5 6 7 8 9 10 |
applications: - name: ${PREFIX}super-api buildpacks: - nodejs_buildpack memory: 128MB instances: 2 env: LOG_LEVEL: ${LOG_LEVEL} routes: - route: ${PREFIX}super-api.example.com |
そしてシェルスクリプトを用意します。create_yaml.sh
としました。
1 2 3 4 5 6 7 |
#!/bin/bash PREFIX=dev- LOG_LEVEL=debug MANIFEST_FILE=manifest.yaml eval "echo \"$(cat template.yaml)\"" | tee $MANIFEST_FILE |
これを実行すると、こんな感じになります!
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ bash ./create_yaml.sh applications: - name: dev-super-api buildpacks: - nodejs_buildpack memory: 128MB instances: 2 env: LOG_LEVEL: debug routes: - route: dev-super-api.example.com $ ls create_yaml.sh* manifest.yaml template.yaml |
template.yaml
の中に記述されている変数をevalを使って評価することで値を埋め込むことができました。なんか手っ取り早さは一番のような気がします。
Pros: シンプルにできる
Cons: 共通化や値の上書きはややこしくなりそう
おわりに
ツールによる拡張機能が一番使いやすいと思いますが、Includeがサポートされてないツールだとなかなかつらいものがあります。代替の方法をいろいろと模索してみましたが、保守性を下げずに簡単に記述できる銀の弾丸的なものはないですね。
サボろうとせず地道に書く、というのが一番かもしれません。