ただ YAML を読めるようになりたい

# はじめに

今までほんとうにただの雰囲気で読み書きしてきており、YAMLの構文についてもうちょっと体系的に理解したいと思ったので記事にしました。超初級編です。

# 参考

以下 `Ansible` のドキュメントが割といい感じにまとまっていたので、こいつを参考にします。

https://docs.ansible.com/ansible/2.9_ja/reference_appendices/YAMLSyntax.html

※ `Ansible` のドキュメントを参考にしていますが、YAMLの基本的な考え方としては他のDevOps系のツール（`GitHub Actions` / `Docker Compose` / `Kubernetes` など）も同様です。

# もくじ

- [そもそもYAMLとは](#そもそもyamlとは)
  - [YAMLの概要](#yamlの概要)
  - [YAMLの特徴](#yamlの特徴)
  - [YAMLのデータ単位](#yamlのデータ単位)
- [YAML読解ざっくり](#yaml読解ざっくり)
  - [YAMLの学び方](#yamlの学び方)
  - [YAMLで立ち止まったら](#yamlで立ち止まったら)
- [YAML構文の基礎](#yaml構文の基礎)
  - [ドキュメント分割](#ドキュメント分割)
  - [Scalar：文字列](#scalar文字列)
  - [Scalar：数値](#scalar数値)
  - [Scalar：日付](#scalar日付)
  - [Scalar：ブール（真偽値）](#scalarブール真偽値)
  - [Scalar：Null値](#scalarnull値)
  - [Sequence（配列 / リスト）](#sequence配列--リスト)
  - [mapping（ハッシュ / ディクショナリ）](#mappingハッシュ--ディクショナリ)
  - [Sequence × Mapping（リストとディクショナリの組み合わせ）](#sequence--mappingリストとディクショナリの組み合わせ)
  - [補足：インラインスタイル（フロースタイル）](#補足インラインスタイルフロースタイル)
  - [コメントアウト](#コメントアウト)
  - [データの再利用（Anchor / Alias）](#データの再利用anchor--alias)
  - [複数行の文字列（改行あり）](#複数行の文字列改行あり)
  - [複数行の文字列（改行なし）](#複数行の文字列改行なし)
- [おわりに](#おわりに)

# そもそもYAMLとは

## YAMLの概要

「YAMLはデータシリアライズ言語です！」とよく説明されることが多いが、もうちょいかみ砕いた説明をするなら、**「YAMLはデータをシリアライズ（データ構造を保存や転送に適した形に変換すること）するための形式言語の１種です！」** と説明できる。なので、YAMLはデータシリアライズ言語であり、同時に形式言語であるとも言えそう。

- **データシリアライズ言語：**
  - データ構造をテキスト形式に変換するための言語で、それによってデータの保存や転送が容易になる
  - YAMLはこの目的に使われ、データをテキストベースの形式で表現する
- **形式言語：**
  - 文字列の集合を生成するための厳格な規則（文法）を持った言語
  - YAMLは特定の構文規則に従って書かれることで、データの構造を表す

そういう意味で、YAMLとよく比較されるJSONやXMLも同様に、データシリアライズ言語であり形式言語であると言える。つまり、みんなプログラミング言語のように計算を実行するためのものではなく、むしろデータを表現、交換、保存するための言語。

https://zenn.dev/yutoo89/articles/0c8dad23dccec8


## YAMLの特徴

同じデータシリアライズ言語であり形式言語であるJSONやXMLと比較したときのYAMLの特徴は何といってもその可読性。

- **XMLやJSONなどの他の形式言語に比べてより簡潔で可読性が高いとされる**
  - インデントを利用してデータの階層構造を表現でき、視覚的にデータ構造を把握しやすい
  - 中括弧や波括弧（JSON）、開始タグや閉じタグ（XML）がなく必要最低限の記号しか使わない（ファイルの中身がきれいな状態になる）
  - 数値や真偽値、日付といった基本的なデータ型が自然にサポートされている（XMLではデータ型をマークアップによって明示的に指定する必要があったりする）
  - コメントがシャープ記号 (`#`)で簡単に書ける
  - DevOpsツールで広く採用されており、CI/CD、コンテナオーケストレーションなどで好まれている
  - などなど
- **データのシリアライズがそのプロセスを含めて容易になる**
  - 構文がシンプルである点
    - YAMLは少ない記号で構成されているため他のフォーマットよりも非常にシンプルかつ直感的
    - これがデータ記述の誤りを減らし、結果としてデータのシリアライズがスムーズに進む
  - データのシリアライズが容易（な場合が多い）
    - 多くの言語で提供される専用ライブラリを利用することで、複雑なコードを書かずにデータ構造を YAML 形式へ変換したりその逆を行ったりできる
    - 例えば、`Python` の` pyyaml` や `Java` の `SnakeYAML` など
    - これらの利用で、数行のコードを書けばデータの読み書きが可能になる

## YAMLのデータ単位

データの大きさという単位で考えるとイメージしやすいかも？

- ファイル：拡張子は「`.yaml`」でも「`.yml`」でも可
- データの塊（大きさの単位）
  - ドキュメント：ファイル内に記述したデータのまとまり
    - `scalar`（スカラー）：文字列や数値などの単一のデータ
      - 文字列
      - 数値（整数・浮動小数点数）
      - 日付
      - 真偽値
      - Null値
    - `mapping`（ハッシュ / ディクショナリ）：
      - キーと値のペア
      - `key: value` の形式で表現する
    - `sequences`（配列 / リスト）：
      - 順序付けられた要素のリスト
      - `- `（ハイフンとスペース）の記号で各要素が始まる
    - コメント：
      - `#` を使って単一行をコメントアウトできる


# YAML読解ざっくり

## YAMLの学び方

学ぶというか、YAMLを使ったDevOps系のツールを理解するには２つの観点を意識することが大切な気がしている。

- **観点１：DevOps系ツールやその仕様理解**
  - ツール類
    - `GitHub Actions` / `Docker Compose` / `Ansible` / `Kubernetes` など
  - ツールの基本的な仕組み
  - ツール固有の用語や概念
- **観点２：YAMLの理解**
  - 文法や構文
  - 実例との関連（「このファイルはどういう目的のファイル？」の視点）

実務とかでなにかのYAMLファイルに触れてみて、詰まったタイミングでYAMLの体系的な理解を挟むと理解が一歩進む。最初にYAMLを体系的に学んでしまうのもアリ。

で、同じYAMLといってもツールによって使えるキーワードやその使用方法が違ったりするので、ツール単位で使えるキーワードや書き方をざっくりつかんで理解するのもとてもよいと思う。例えば、以下のようにツールによってよく使われるキーワードが全然違う。

- **`GitHub Actions` におけるworkflowファイルで用いられるYAMLキーワード**
  - `name`：Workflowの名前を設定
  - `on`：Workflowが実行されるトリガーのイベントを定義
  - `jobs`：実行するジョブを定義
  - `runs-on`：ジョブが実行される環境（例：ubuntu-latestなど）を指定
  - `steps`：ジョブ内で実行するステップの一覧を設定
  - `uses`：公式アクションやコミュニティアクション（例えば、actions/checkout@v2）を使用するときに指定
  - `run`：シェルコマンドの実行
- **`Docker Compose` で使用するdocker-compose.ymlファイルにおけるYAMLキーワード**
  - `version`：Docker Composeファイルのバージョンを指定
  - `services`：コンテナ化されたアプリケーションのサービスを定義
  - `image`：コンテナのベースとなるイメージを指定
  - `build`：Dockerfileからイメージをビルドするための設定
  - `ports`：ホストとコンテナ間のポートマッピングを定義
  - `volumes`：データ永続化のためのボリュームマウントを指定
  - `environment`：環境変数を設定
  - `networks`：コンテナ間のネットワーキングを定義
  - `depends_on`：他のサービスへの依存関係を指定

ちなみに上記のキーワードはすべてディクショナリ（mapping）型のキーにあたるものなので、この辺のデータ型の理解をまずできると理解がぐっと進むはず。

## YAMLで立ち止まったら

1. YAMLファイル内のデータ構造の把握（全体的にどういったデータ構造なのか確認する） 
1. このYAMLファイルがどういった目的で利用されるのか？の視点で１つ１つ丁寧に確認する

見ていく中でデータ構造が分からなくなったら、１度JSONに変換すると分かりやすいと思う。（リストなのかディクショナリなのか、データ構造などが分かりやすくなる）

https://onlineyamltools.com/convert-yaml-to-json

データ構造さえ分かってしまえばなんてことない気がしてる。

# YAML構文の基礎

公式ドキュメントにも記載があるけど、基本的にはプリミティブなデータ型を把握できてしまえば基礎は出来上がると思う。

> There are many kinds of data structures, but they can all be adequately represented with three basic primitives: mappings (hashes/dictionaries), sequences (arrays/lists) and scalars (strings/numbers). 
> 
> （データ構造には多くの種類があるが、それらはすべて
> - マッピング（ハッシュ／ディクショナリ）
> - シーケンス（配列／リスト）
> - スカラー（文字列／数値）
> 
> という3つの基本的なプリミティブで適切に表現できる。）

https://yaml.org/spec/1.2.2/#chapter-1-introduction-to-yaml

個人的には、マッピング（ディクショナリ）とシーケンス（リスト）が分かればあとはだいたいOK！感がある。

書くときに「これ構文イケてる？」を確認するのにYAMLのLinter使うと捗るので使ってみてもいいかも。

https://www.yamllint.com/

## ドキュメント分割

YAMLでは、最も大きなデータの単位を「ドキュメント」と呼ぶ。

YAMLの構文では複数のドキュメントを単一のファイルで扱うことができます。その際にセパレータ（`---`）を使用して、ドキュメントの区切りを記述することができる。

ドキュメント分割の主な目的は、異なるデータ構造を独立して定義・管理しやすくすることであり、複数の異なる設定項目をグループ化させたい場合や、複数のリソースを1つのYAMLファイルに統合して管理したいい場合に利用される。

```yaml:ドキュメントの分割
# 開発環境
--- 
database:
  host: localhost
  username: dev_user
  password: dev_pass

# テスト環境
---
database:
  host: test-db.example.com
  username: test_user
  password: test_pass
```

とはいえ、一般的に「ファイル内にドキュメントを１つしか定義しない場合」はセパレータ（`---`）の既述は任意。

## Scalar：文字列

文字列を表現するとき、３つの方法がある

- **クォートなし：**
  - シンプルで特殊文字が含まれない文字列はクォートなしで記述可能
  - その場合は以下のような予約語や構文に気を付ける必要がある（YAMLパーサーに誤認される）
    - 例えば以下
      - 文字列内にスペースが含まれている
      - 特定のキーワード（`true` / `false` / `null` など）がある
      - 構文を誤解させる（`[]` や `{}` とか）
- **シングルクォート：**
  - 特殊文字をエスケープせずにそのままの形で扱われる
  - つまり `\n` や `\t` などのエスケープシーケンスはただの文字列として扱われ、改行やタブとして解釈されない
  - 文字列内でシングルクォートを使用したい場合は、2つのシングルクォート (`''`) を使用してエスケープする
    - 例：`example: 'Here''s a string in YAML'`
    - YAML Lint：`example: Here's a string in YAML`
- **ダブルクォート：**
  - ダブルクォートで囲まれた文字列はエスケープシーケンスを使用できる
  - これにより改行（`\n`）やタブ（`\t`）、ユニコード文字（`\u003e`）などが利用可能

```yaml: 文字列
Apple: red
Orange: orange
Mango: yellow

# シングルクォートを使う（より明示的）
Apple: 'red'
Orange: 'orange'
Mango: 'yellow'

# ダブルクォートを使う（より明示的）
Apple: "red"
Orange: "orange"
Mango: "yellow"

# string型を明示的に指定する
Apple: !!str red
Orange: !!str orange
Mango: !!str yellow

# YAML Lintは上記すべてこれになる
---
Apple: red
Orange: orange
Mango: yellow
```

## Scalar：数値

数値型は大きく２つ、整数（`int`）または浮動小数点数（`float`）がある

```yaml: 数値
# int型
Apple: 1
Orange: 2
Mango: 3

# int型を明示的に指定する
Apple: !!int 1
Orange: !!int 2
Mango: !!int 3

# float型
Apple: 1.11
Orange: 2.22
Mango: 3.33

# float型を明示的に指定する
Apple: !!float 1.11
Orange: !!float 2.22
Mango: !!float 3.33
```

## Scalar：日付

YAMLではISO8601標準に準拠し、日付や時刻を表現するいくつかの形式をサポートしている。

- 完全な日付時間は `YYYY-MM-DDTHH:MM:SSZ` の形式が基本
  - `T` は日付と時刻の区切りを示す
  - `Z` はUTC（協定世界時）であることを示す
    - ※ `Z` の代わりに正または負の時間オフセット（例：`+HH:MM` もしくは `-HH:MM`）を利用することで、異なるタイムゾーンを指定することが可能
- 秒（second）以下のより詳細な時間表現もできる（例： `YYYY-MM-DDTHH:MM:SS.SSSZ`）
- 時刻を省略して日付のみを `YYYY-MM-DD` で記述もできる
- YAML 1.2 以降では、`ISO8601` の拡張形式による表記もサポートされている

んーーー日付はいつもややこしい！！

```yaml: 日付
# 完全な日付時間の例（UTCを表す 'Z' の使用）
fullDateTime: 2024-06-23T13:45:30Z

# timestamps型を明示的に指定する
dateTimeWithTag: !!timestamps 2024-06-23T13:45:30Z

# 日付と時刻の区切りを示す 'T' の使用
dateAndTimeSeparator: 2024-06-23T13:45:30Z

# UTCの代わりに時間オフセットを利用した例（日本標準時を指定）
timeWithOffset: 2024-06-23T13:45:30+09:00

# タイムゾーンを持つ別の時間オフセット例（東部標準時を指定）
timeWithOffsetEST: 2024-06-23T13:45:30-05:00

# タイムゾーンなし（タイムゾーンを特定しないローカルタイム）
localDateTime: 2024-06-23T13:45:30

# 秒以下のより詳細な時間（ミリ秒を含む）表現の例
detailedTime: 2024-06-23T13:45:30.123Z

# 時刻を省略し、日付のみの表現の例
justDate: 2024-06-23

# ISO8601の拡張形式の使用例（週番号を使用した日付）
iso8601Extended: 2024-W25-7
```

## Scalar：ブール（真偽値）

TRUE/FALSEを表現する。実はtrue/falseだけでなくいろいろ表現方法がある。

- 真：`true`, `TRUE`, `y`, `Y`, `Yes`, `on`
- 偽：`false`, `FALSE`, `n`, `N`, `No`, `off`

```yaml: ブール
boolVal: True
boolVal: False

# ブール型を明示的に指定する
boolVal: !!bool True
boolVal: !!bool False
```


## Scalar：Null値

値が何もないことを表す。

`null` だけでなく、`NULL`, `~` でも表現できる。

```yaml: NULL
name: Null

# NULLを明示的に指定する
name: !!null Null 
```


## Sequence（配列 / リスト）
### リストの基礎

- リストを作成するために各要素の前に`-`（ダッシュ）を置き、その後にスペースを１つ入れる
- 重要なのが、リストの各要素は同じレベルでインデントされている必要があること

```yaml:リスト（シーケンス）
- Apple
- Orange
- Strawberry

# JSONにするとこうなっている
[
  "Apple",
  "Orange",
  "Strawberry"
]
```

NGなパターン

```yaml: リスト（シーケンス）のNGパターン１
- Apple
  Orange
  Strawberry

# JSONにすると、１つの文字列として解釈される
[
  "apple orange banana"
]
```

```yaml: リスト（シーケンス）のNGパターン２
- Apple
  - Orange
  - Strawberry

# JSONにすると、１つの文字列として解釈される
[
  "Apple - Orange - Strawberry"
]
```

ちなみに、途中に空の要素がある場合は、一般的に `Null` で判定される。（YAMLパーサー側で空をどう認識されるかはパーサー側の仕様による）

```yaml: 要素を指定しない場合
- Apple
- 
- Strawberry
- Mango

# JSONにするとこうなっている
[
  "Apple",
  null,
  "Strawberry",
  "Mango"
]
```

### リスト（シーケンス）のネスト（２重配列）

リストの中にリストを持たせることももちろんできる。

```yaml: リストの入れ子構造
- 
  - Apple

# JSONにするとこうなっている
[
  [
    "Apple"
  ]
]
```

```yaml: リストの入れ子構造２
- 
  - Apple
- 
  - Orange
- 
  - Strawberry

# 上記と同じ
- - Apple
- - Orange
- - Strawberry

# JSONにするとこうなっている
[
  [
    "Apple"
  ],
  [
    "Orange"
  ],
  [
    "Strawberry"
  ]
]
```

### 補足：大文字と小文字の区別

```yaml: 大文字小文字の区別
- Apple
- Orange
- Strawberry
- apple

# JSONにすると別のものとして認識されている
[
  "Apple",
  "Orange",
  "Strawberry",
  "apple"
]
```

## mapping（ハッシュ / ディクショナリ）

キーと値のペアを `key: value` の形式で表現する。（コロンの後ろにスペースが必要）

```yaml: ディクショナリ（マッピング）
Apple: 1
Orange: 2
Strawberry: 3

# 上記と同じ
Apple:
  1
Orange:
  2
Strawberry:
  3

# JSONにするとこうなっている
{
  "Apple": 1,
  "Orange": 2,
  "Strawberry": 3
}
```

```yaml: ディクショナリ（マッピング）
# １つのキーAppleに対して、値として複数のディクショナリを持たせる場合
Apple: 
  small: 1
  large: 2

# mapping型を明示的に指定する
Apple: !!map
  small: 1
  large: 2

# JSONにするとこうなっている
{
  "Apple": {
    "small": 1,
    "large": 2
  }
}
```

NGパターン

```yaml:ディクショナリのNGパターン（キーAppleに対して、値として複数のディクショナリを持たせたい）
Apple: 
small: 1 # インデントなしではキーAppleの値にならない
large: 2

# JSONにするとこうなっている
{
  "Apple": null,
  "small": 1,
  "large": 2
}
```

## Sequence × Mapping（リストとディクショナリの組み合わせ）

`fruits` というキーに対してリスト形式で複数の値（`Apple`, `Orange`, `Strawberry`）が割り当てられている構造

```yaml: キーfruitsの値としてリストを持つ構造
fruits:
  - Apple
  - Orange
  - Strawberry

# シーケンス型を明示的に指定する
fruits: !!seq
  - Apple
  - Orange
  - Strawberry

# JSONにするとこうなっている
{
  "fruits": [
    "Apple",
    "Orange",
    "Strawberry"
  ]
}
```

`fruits` というキーに対してリスト形式で複数の値（`Apple`, `Orange`, `Strawberry`）が割り当てられている構造、かつ、リストの各項目はキーと値のペア（ディクショナリ型）として表現されている構造。

```yaml: キーfruitsの値としてリストを持つ構造２
fruits:
  - Apple: 1
  - Orange: 2
  - Strawberry: 3

# シーケンス型を明示的に指定する
fruits: !!seq
  - Apple: 1
  - Orange: 2
  - Strawberry: 3

# JSONにするとこうなっている
{
  "fruits": [
    {
      "Apple": 1
    },
    {
      "Orange": 2
    },
    {
      "Strawberry": 3
    }
  ]
}
```

リストの中に複数のディクショナリを持つデータ構造。（よく見るやつ）

```yaml: リストの中に複数のディクショナリを持つ構造
- Apple: 1
  Orange: 2
  Strawberry: 3

# JSONにするとこうなっている
[
  {
    "Apple": 1,
    "Orange": 2,
    "Strawberry": 3
  }
]
```

## 補足：インラインスタイル（フロースタイル）

YAMLのデータ構造をJSONに似た構文で、1行で表現することもできる。この形式を「インライン形式」（または「フロースタイル」）と呼ばれる。

```yaml: キーfruitsの値としてリストを持つ構造
fruits:
  - Apple
  - Orange
  - Strawberry

# インラインスタイル
{fruits: [Apple, Orange, Strawberry]}
```

```yaml: キーfruitsの値としてリストを持つ構造
fruits:
  - Apple: 1
  - Orange: 2
  - Strawberry: 3

# インラインスタイル
{fruits: [{Apple: 1}, {Orange: 2}, {Strawberry: 3}]}
```

## コメントアウト

YAMLにおいてコメントアウトは `#` を使う。単一行のコメントアウトしかサポートされていないので、複数行の記述をコメントアウトしたい場合はそれぞれの文頭に `#` をつける必要がある。

```yaml: コメントアウトする
# これはコメント１
# これはコメント２
# これはコメント３
description: >
  YAML can often be read and written based on intuition.
  However, truly understanding the syntax is crucial.
```


## データの再利用（Anchor / Alias）

YAMLでのAnchorとAliasの概念は、繰り返しデータを使いまわすための非常に便利な機能。

- **Anchor (アンカー)：**
  - `&` を使うことで特定のデータに名前を付けることができる
  - これはアンカーと呼ばれ、この名前（アンカー）を付けることで、データを他の場所で再利用する準備が整う
- **Alias (エイリアス)：**
  - `*` と アンカーとして名付けたデータ を使って、アンカーとして設定したデータを参照する
  - これはエイリアスと呼ばれ、実質的に「ここにアンカーとして設定したデータを挿入してね」という指示になる

```yaml: Anchor と Alias
default_settings: &defaultSettingsAsAnchor  # ここでアンカーを設定
  setting1: 'STR VALUE'
  setting2: 2

profile1:
  <<: *defaultSettingsAsAnchor # Alias：アンカーを参照するときは（<<: ）と（* + アンカー）を使う
  setting1: 'CUSTOM VALUE' # Anchorで設定した値をここでは上書きする

profile2:
  <<: *defaultSettingsAsAnchor  # 同じアンカーを使いまわすこともできる

# JSONにするとこうなっている
{
  "default_settings": {
    "setting1": "STR VALUE",
    "setting2": 2
  },
  "profile1": {
    "setting1": "CUSTOM VALUE",
    "setting2": 2
  },
  "profile2": {
    "setting1": "STR VALUE",
    "setting2": 2
  }
}
```

https://rheb.hatenablog.com/entry/2019/12/08/YAML%E3%81%AEAnchor%E3%81%A8Alias%E3%82%92Ansible%E3%81%A7%E4%BD%BF%E3%81%86


## 複数行の文字列（改行あり）

キーに対してVALUEを書く前にパイプ記号を記述することで、YAMLパーサーに複数行であると認識させることができる。

```yaml: 改行を表現する
description: |
  YAML can often be read and written based on intuition.
  However, truly understanding the syntax is crucial.

# JSONにするとこうなっている
{
  "description": "YAML can often be read and written based on intuition.\nHowever, truly understanding the syntax is crucial.\n"
}
```

## 複数行の文字列（改行なし）

キーに対してVALUEを書く前に `>` 記号を記述することで、YAMLパーサーに「改行しないよ！」と認識させることができる。

```yaml: 改行しないを表現する
description: >
  YAML can often be read and written based on intuition.
  However, truly understanding the syntax is crucial.

# JSONにするとこうなっている
{
  "description": "YAML can often be read and written based on intuition. However, truly understanding the syntax is crucial.\n"
}
```


# おわりに

思ったより長い記事になっちゃった。

でも、だいたいこんくらい分かっておけばやっていけそうな気がするでしょ。