zaki work log

作業ログやら生活ログやらなんやら

[Ansible] 省略したいパラメタに変数指定せざるを得ない場合に使う変数omitとdefaultフィルタ

指定が任意なオプション扱いのパラメタで、指定するパラメタが無い場合は未指定にしたいけど、ループ処理などで決まった型の辞書型変数のリストでキーが有ったり無かったりする場合の処理について。

って書いてもうまく伝わらない気もするのでサンプルコードから。

  - name: create files by vars
    vars:
      files:
        - path: /var/tmp/ansible/var_test
          type: directory
          owner: zaki
          group: zaki
          mode: '0755'
        - path: /var/tmp/ansible/var_test/file1
          type: touch
    ansible.builtin.file:
      state: "{{ item.type }}"
      path: "{{ item.path }}"
      mode: "{{ item.mode | default(omit) }}"
      owner: "{{ item.owner | default(omit) }}"
      group: "{{ item.group | default(omit) }}"
    loop: "{{ files }}"

files変数に、path/type/pwner/group/modeを持った辞書型変数のリストを定義してるけど、1個目のディレクトリ定義は全て指定してるのに対して、2個目のファイル定義はowner/group/modeの指定無しの状態。

これをループ使って1発でansible.builtin.file使ってファイル/ディレクトリ作成しようとしても、mode,owner,groupの定義は2要素目の項目に無いということで

      mode: "{{ item.mode }}"
      owner: "{{ item.owner }}"
      group: "{{ item.group }}"

とplaybookに書いても、2ループ目は以下のように、変数にmode/owner/groupの定義が無いのでエラーになってしまう。

  msg: |-
    The task includes an option with an undefined variable. The error was: 'dict object' has no attribute 'mode'

マジック変数omit

こういうときに使うのが、Ansibleのマジック変数omitで、omitが指定されたパラメタは無視され「パラメタ指定なし(省略)」として動作する。

docs.ansible.com

docs.ansible.com

omit単体の動作は以下のコードで確認できる。

  vars:
    data:
      j2_none: "{{ none }}"
      var_omit: "{{ omit }}"
      undef: undefined
      blank: ""
      tilde: ~
      nullval: null

  tasks:
  - name: print
    debug:
      msg: "{{ data }}"

この内容のplaybookで実行すると、出力は以下の通り。

ok: [localhost] => 
  msg:
    blank: ''
    j2_none: ''
    nullval: null
    tilde: null
    undef: undefined

omitをセットしていたvar_omit変数はきれいさっぱり無くなっているのを確認できる。

ついでに、Jinja2テンプレート内(というよりPythonかな?)でnoneを使ってもAnsibleで受け取るとnullになったりせず空文字になり、YAMLnull,~はAnsibleでもnullとなる。

動作はAnsible 2.9.16と2.10.7で確認。

サンプル

github.com

外部ファイル入力などで未定義を表現できないとき

上の例は「辞書で未定義の場合(keyが無い場合)はdefaultフィルタでomitをセット」だったが、例えばAPI実行の結果を入力にしたいけどデータが無い場合は空文字になってるとか、csvファイルを入力にしたいけどカラムは固定なのでデータ無しは空文字扱い、といったパターンもある。

defaultフィルタの通常動作は「フィルタの入力変数が未定義の場合は引数の変数をセット」という動作のため、変数は存在するけどセットされている変数が0(int)false(bool)や空文字やnullの場合は反応しない。
これらの「偽」の場合にdefaultフィルタを使って値をセットするには、引数の第2引数にtrueをセットする。

jinja.palletsprojects.com

If you want to use default with variables that evaluate to false you have to set the second parameter to true:

{{ ''|default('the string was empty', true) }}

docs.ansible.com

If you want to use the default value when variables evaluate to false or an empty string you have to set the second parameter to true:

{{ lookup('env', 'MY_USER') | default('admin', true) }}

これを使えば、入力csvファイルでファイルとプロパティ一覧を以下のように作成し、

path,type,owner,group,mode
/var/tmp/ansible/csv_test,directory,zaki,zaki,0755
/var/tmp/ansible/csv_test2,directory,,,
/var/tmp/ansible/csv_test/file1,touch,,,

playbookではansible.builtin.read_csvを使って

  tasks:
  - name: read csv file
    ansible.builtin.read_csv:
      path: filelist.csv
    register: res_input

  - name: create files by csv
    ansible.builtin.file:
      state: "{{ item.type }}"
      path: "{{ item.path }}"
      mode: "{{ item.mode | default(omit, true) }}"
      owner: "{{ item.owner | default(omit, true) }}"
      group: "{{ item.group | default(omit, true) }}"
    loop: "{{ res_input.list }}"

みたいな実装をすることで、modeやowner指定が無い場合は省略してファイル作成ができる。

サンプル

github.com


ちなみに、fileモジュールのownergroupnull指定してもエラーになるが、modeについてはnull指定するとパラメタは無視されてデフォルト動作になるっぽい。

docs.ansible.com

この辺りの動作はモジュールに依存するので動作はよく確認すること。

静的サイトジェネレータMkDocsのGetting Startedおためし

KubernetesドキュメントサイトではHugoというサイトジェネレータを使ったのですが、似たようなツールにMkDocsというものがあり、試してみた。
(そのうちGitHub Pagesに置きたい)

www.mkdocs.org

大まかな流れとしては、Markdown形式でドキュメントを指定のパスに置いて、mkdocs serveするとローカルでサーバー起動して動作確認でき、mkdocs buildするとhtmlファイルやcssなどが生成される、というもの。

テーマとかまだ何も設定してないけど、まずは公式のGetting Startedにそって試してみた。

インストール

pipでインストールする。

(mkdocs) [zaki@cloud-dev mkdocs]$ pip list
Package    Version
---------- -------
pip        21.1.2
setuptools 39.2.0
(mkdocs) [zaki@cloud-dev mkdocs]$ ls
venv
(mkdocs) [zaki@cloud-dev mkdocs]$ cat requirements.txt 
mkdocs
(mkdocs) [zaki@cloud-dev mkdocs]$ 
(mkdocs) [zaki@cloud-dev mkdocs]$ 
(mkdocs) [zaki@cloud-dev mkdocs]$ pip install -r requirements.txt 
Collecting mkdocs
  Downloading mkdocs-1.1.2-py3-none-any.whl (6.4 MB)
     |████████████████████████████████| 6.4 MB 4.6 MB/s 
Collecting lunr[languages]==0.5.8
  Downloading lunr-0.5.8-py2.py3-none-any.whl (2.3 MB)
     |████████████████████████████████| 2.3 MB 11.3 MB/s 
Collecting click>=3.3
  Downloading click-8.0.1-py3-none-any.whl (97 kB)
     |████████████████████████████████| 97 kB 9.4 MB/s 
Collecting Markdown>=3.2.1
  Downloading Markdown-3.3.4-py3-none-any.whl (97 kB)
     |████████████████████████████████| 97 kB 9.8 MB/s 
Collecting Jinja2>=2.10.1
  Using cached Jinja2-3.0.1-py3-none-any.whl (133 kB)
Collecting livereload>=2.5.1
  Downloading livereload-2.6.3.tar.gz (25 kB)
Collecting PyYAML>=3.10
  Using cached PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl (640 kB)
Collecting tornado>=5.0
  Downloading tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl (427 kB)
     |████████████████████████████████| 427 kB 10.9 MB/s 
Collecting six>=1.11.0
  Using cached six-1.16.0-py2.py3-none-any.whl (11 kB)
Collecting future>=0.16.0
  Downloading future-0.18.2.tar.gz (829 kB)
     |████████████████████████████████| 829 kB 11.2 MB/s 
Collecting nltk>=3.2.5
  Downloading nltk-3.6.2-py3-none-any.whl (1.5 MB)
     |████████████████████████████████| 1.5 MB 11.4 MB/s 
Collecting importlib-metadata
  Downloading importlib_metadata-4.2.0-py3-none-any.whl (16 kB)
Collecting MarkupSafe>=2.0
  Using cached MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl (30 kB)
Collecting joblib
  Downloading joblib-1.0.1-py3-none-any.whl (303 kB)
     |████████████████████████████████| 303 kB 10.4 MB/s 
Collecting regex
  Downloading regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl (722 kB)
     |████████████████████████████████| 722 kB 10.5 MB/s 
Collecting tqdm
  Downloading tqdm-4.61.0-py2.py3-none-any.whl (75 kB)
     |████████████████████████████████| 75 kB 8.5 MB/s 
Collecting zipp>=0.5
  Using cached zipp-3.4.1-py3-none-any.whl (5.2 kB)
Collecting typing-extensions>=3.6.4
  Using cached typing_extensions-3.10.0.0-py3-none-any.whl (26 kB)
Using legacy 'setup.py install' for future, since package 'wheel' is not installed.
Using legacy 'setup.py install' for livereload, since package 'wheel' is not installed.
Installing collected packages: zipp, typing-extensions, importlib-metadata, tqdm, six, regex, joblib, future, click, tornado, nltk, MarkupSafe, lunr, PyYAML, Markdown, livereload, Jinja2, mkdocs
    Running setup.py install for future ... done
    Running setup.py install for livereload ... done
Successfully installed Jinja2-3.0.1 Markdown-3.3.4 MarkupSafe-2.0.1 PyYAML-5.4.1 click-8.0.1 future-0.18.2 importlib-metadata-4.2.0 joblib-1.0.1 livereload-2.6.3 lunr-0.5.8 mkdocs-1.1.2 nltk-3.6.2 regex-2021.4.4 six-1.16.0 tornado-6.1 tqdm-4.61.0 typing-extensions-3.10.0.0 zipp-3.4.1
(mkdocs) [zaki@cloud-dev mkdocs]$ 

お試し

プロジェクト作成

my-projectプロジェクトを作成

(mkdocs) [zaki@cloud-dev mkdocs]$ mkdocs new my-project
INFO    -  Creating project directory: my-project 
INFO    -  Writing config file: my-project/mkdocs.yml 
INFO    -  Writing initial docs: my-project/docs/index.md 
(mkdocs) [zaki@cloud-dev mkdocs]$ ls
my-project  requirements.txt  venv
(mkdocs) [zaki@cloud-dev mkdocs]$ cd my-project/
(mkdocs) [zaki@cloud-dev my-project]$ ls
docs  mkdocs.yml

動作確認用サーバー起動

(mkdocs) [zaki@cloud-dev my-project]$ mkdocs serve
INFO    -  Building documentation... 
INFO    -  Cleaning site directory 
INFO    -  Documentation built in 0.05 seconds 
[I 210527 09:11:55 server:335] Serving on http://127.0.0.1:8000
INFO    -  Serving on http://127.0.0.1:8000

[Errno 98] Address already in use

8000/TCPとか他で使ってるわ。。

(mkdocs) [zaki@cloud-dev my-project]$ mkdocs serve --help
Usage: mkdocs serve [OPTIONS]

  Run the builtin development server

Options:
  -a, --dev-addr <IP:PORT>        IP address and port to serve documentation
                                  locally (default: localhost:8000)
  --livereload                    Enable the live reloading in the development
                                  server (this is the default)
  --no-livereload                 Disable the live reloading in the
                                  development server.
  --dirtyreload                   Enable the live reloading in the development
                                  server, but only re-build files that have
                                  changed
  -f, --config-file FILENAME      Provide a specific MkDocs config
  -s, --strict                    Enable strict mode. This will cause MkDocs
                                  to abort the build on any warnings.
  -t, --theme [mkdocs|readthedocs]
                                  The theme to use when building your
                                  documentation.
  --use-directory-urls / --no-directory-urls
                                  Use directory URLs when building pages (the
                                  default).
  -q, --quiet                     Silence warnings
  -v, --verbose                   Enable verbose output
  -h, --help                      Show this message and exit.

helpを見るとポート単体はなさそうだけど、-aでlistenするアドレスとポート指定できるみたい。
ついでなのでリモートからも接続できるように0.0.0.0で起動。
(firewalld空けないといけないので結局これ使わずにsshのポートフォワードしたけど…)

警告も出てるけど、0.0.0.0はアクセス範囲広くていろいろ注意必要なので、ポートフォワードするなら127.0.0.1:8081で問題ない。

(mkdocs) [zaki@cloud-dev my-project]$ mkdocs serve -a 0.0.0.0:8081
INFO    -  Building documentation... 
WARNING -  Config value: 'dev_addr'. Warning: The use of the IP address '0.0.0.0' suggests a production environment or the use of a proxy to connect to the MkDocs server. However, the MkDocs' server is intended for local development purposes only. Please use a third party production-ready server instead. 
INFO    -  Cleaning site directory 
INFO    -  Documentation built in 0.05 seconds 
[I 210527 09:13:51 server:335] Serving on http://0.0.0.0:8081
INFO    -  Serving on http://0.0.0.0:8081
[I 210527 09:13:51 handlers:62] Start watching changes
INFO    -  Start watching changes
[I 210527 09:13:51 handlers:64] Start detecting changes
INFO    -  Start detecting changes

実行するとこの通り、フォアグラウンドで動作し、docs以下のMarkdownファイルを元にwebアクセス経由でページ表示できるようになる。

プロジェクト作成時に自動で生成されるインデックスページは以下の通り。

# Welcome to MkDocs

For full documentation visit [mkdocs.org](https://www.mkdocs.org).

## Commands

* `mkdocs new [dir-name]` - Create a new project.
* `mkdocs serve` - Start the live-reloading docs server.
* `mkdocs build` - Build the documentation site.
* `mkdocs -h` - Print help message and exit.

## Project layout

    mkdocs.yml    # The configuration file.
    docs/
        index.md  # The documentation homepage.
        ...       # Other markdown pages, images and other files.

このままサーバー起動しブラウザでアクセスすると以下のように表示される。

f:id:zaki-hmkc:20210527095414p:plain

ページ追加

docs/about.mdを追加してみる。
内容は以下の通り。

# about

@zaki_hmkc

サーバーは起動したままにしておくと、ファイルの変更を自動で検知し動的にページ更新してくれる。

[I 210527 09:18:27 watcher:111] Running task: builder (delay: None)
INFO    -  Running task: builder (delay: None)
INFO    -  Building documentation... 
WARNING -  Config value: 'dev_addr'. Warning: The use of the IP address '0.0.0.0' suggests a production environment or the use of a proxy to connect to the MkDocs server. However, the MkDocs' server is intended for local development purposes only. Please use a third party production-ready server instead. 
[I 210527 09:18:27 handlers:95] Reload 1 waiters: /home/zaki/local/mkdocs/my-project/docs/about.md
INFO    -  Reload 1 waiters: /home/zaki/local/mkdocs/my-project/docs/about.md
[I 210527 09:18:28 handlers:135] Browser Connected: http://localhost:8081/
INFO    -  Browser Connected: http://localhost:8081/
[I 210527 09:18:59 watcher:111] Running task: builder (delay: None)
INFO    -  Running task: builder (delay: None)
INFO    -  Building documentation... 
WARNING -  Config value: 'dev_addr'. Warning: The use of the IP address '0.0.0.0' suggests a production environment or the use of a proxy to connect to the MkDocs server. However, the MkDocs' server is intended for local development purposes only. Please use a third party production-ready server instead. 
[I 210527 09:18:59 handlers:95] Reload 1 waiters: /home/zaki/local/mkdocs/my-project/docs/about.md
INFO    -  Reload 1 waiters: /home/zaki/local/mkdocs/my-project/docs/about.md
[I 210527 09:18:59 handlers:135] Browser Connected: http://localhost:8081/
INFO    -  Browser Connected: http://localhost:8081/
[I 210527 09:19:06 handlers:135] Browser Connected: http://localhost:8081/
INFO    -  Browser Connected: http://localhost:8081/

生成されたページはこの通り。

f:id:zaki-hmkc:20210527095355p:plain

f:id:zaki-hmkc:20210527095337p:plain

サイト名の変更

ページ名はテンプレート生成したときの初期値はMy Docsになっているが、ルートのmkdocs.ymlで指定可能。

site_name: mkdocsお試し

この内容に変更すると、表示は以下の通り。

f:id:zaki-hmkc:20210527095309p:plain

サイトのビルド

(mkdocs) [zaki@cloud-dev my-project]$ ls
docs  mkdocs.yml
(mkdocs) [zaki@cloud-dev my-project]$ mkdocs build
INFO    -  Cleaning site directory 
INFO    -  Building documentation to directory: /home/zaki/local/mkdocs/my-project/site 
INFO    -  Documentation built in 0.05 seconds 
(mkdocs) [zaki@cloud-dev my-project]$ ls
docs  mkdocs.yml  site

mkdocs buildを実行すると、(デフォルトでは)siteディレクトリ以下にMarkdownを元にwebページ用のhtml他のファイルが作成される。

(mkdocs) [zaki@cloud-dev my-project]$ ls -F site/
404.html  about/  css/  fonts/  img/  index.html  js/  search/  sitemap.xml  sitemap.xml.gz

このファイルをwebサーバーに置けば、動作確認じに表示されたコンテンツにアクセスできる。
暫定なので(firewalldの設定変更なので)Dockerのhttpdで…

(mkdocs) [zaki@cloud-dev my-project]$ docker run --rm -p 8089:80 -v $PWD/site:/usr/local/apache2/htdocs -d httpd
e03d10f5a697f8e830a6d3c1403ce45f4e3e231539852f0c20a5f82dca878aaf

リモートからwebサーバーにアクセスするとこの通り。

f:id:zaki-hmkc:20210527095541p:plain

環境


本編と関係ないけど「このディレクトリのファイルをwebサーバー経由でアクセスしたい」はDocker使うと本当に楽だなぁ

[Ansible] ターゲットノードのvenvのPythonを指定して実行する (interpreter_python / ansible_python_interpreter / PYTHONPATH)

コントロールノードでなくターゲットノードで(pipで追加パッケージが必要などの理由で)venvを使いたい場合の方法について。
といっても、venvの中にあるPythonインタプリタのパスをinterpreter_pythonなどで指定すればOK。

Python2/Python3の切り替えでインタプリタを指定するという趣旨の記事が多いというか自分も以前書いてたりするけど、Pythonの仮想環境の指定にも使えたので動作確認した結果をまとめた。

ちなみに「『ターゲットノードでvenvを使う』というドンピシャな情報」を見つけられなかっただけなので、もしこの目的のための設定項目が別にあるなら教えてください…

お題はここでも使ったcommunity.kubernetesコレクションに含まれるk8s_cluster_infoを使ってクラスタ情報を得るというもの。

docs.ansible.com

環境

ansible-playbookを実行するコントロールノードはCentOS 7で、処理する接続先のターゲットノードはFedora 34。
ターゲットのPythonは、デフォルトでは/usr/bin/pythonが参照されるけど、k8s_infoの実行に必要なopenshiftパッケージはシステムワイドにはインストールせずに/home/zaki/tmp/venvに作った仮想環境に入れている、という状態。

ベースとなるplaybookは以下の通り。

---
- hosts: linux
  gather_facts: false

  tasks:
    - name: get cluster info
      community.kubernetes.k8s_cluster_info:
      register: result

    - name: print result
      debug:
        msg: "{{ result.version }}"

以下、いくつかの方法を書いてるけど、どれか一つを採用すればOK

ansible.cfg

defaultsセクションのinterpreter_pythonpythonのパスを指定する

[defaults]
interpreter_python = /home/zaki/tmp/venv/bin/python

全てのターゲットノードでパスが同じであればこれが一番楽。

ターゲットノード毎に設定

ホスト変数を使えばOK
ansible.cfgと異なり、指定する変数名はansible_python_interpreterにパスをセットする。

[linux]
fedora-node ansible_python_interpreter=/home/zaki/tmp/venv/bin/python

ターゲットノードによってvenvのパスが異なる場合で、全ての処理で常に同じPythonインタプリタを使うのであればこれで。

playやtask単位

ホスト変数と同じ要領で、playやtaskの定義でvarsなどを使って変数にパス指定すればOK

    - name: get cluster info
      community.kubernetes.k8s_cluster_info:
      vars:
        ansible_python_interpreter: /home/zaki/tmp/venv/bin/python
      register: result

処理(taskやrole)によって個別にインタプリタを切り替えたい場合はこれで指定する。

パッケージパスの指定

Pythonインタプリタでなく、環境変数PYTHONPATHにパッケージのパスを指定する方法でもいける。

    - name: get cluster info
      community.kubernetes.k8s_cluster_info:
      environment:
        PYTHONPATH: /home/zaki/venv/lib/python3.9/site-packages/
      register: result

ただしパスを見ての通りPythonのサブバージョンまでディレクトリ名に含まれてるので環境情報を把握しておく必要がある。


ちなみにansible-playbookの実行結果はこんな感じ。
戻り値の詳細はドキュメント参照。

ok: [fedora-node] => 
  msg:
    client: 0.12.0
    server:
      kubernetes:
        buildDate: '2021-01-21T01:11:42Z'
        compiler: gc
        gitCommit: faecb196815e248d3ecfb03c680a4507229c2a56
        gitTreeState: clean
        gitVersion: v1.20.2
        goVersion: go1.15.5
        major: '1'
        minor: '20'
        platform: linux/amd64

サンプルコード

github.com

[Terraform] EC2のセキュリティグループ指定はsecurity_groupsでなくvpc_security_group_idsを使えば強制再作成されない

結論

VPCEC2インスタンス作成する定義の場合は、security_groupsでなくvpc_security_group_idsでセキュリティグループを指定しましょう。

該当ドキュメントはこちら

実装例は以下。

github.com

事象

お題は以下のコード

zaki-hmkc.hatenablog.com

コード

resource "aws_instance" "bastion" {
  count = var.host_count

  ami                         = data.aws_ssm_parameter.amzn2_ami.value
  instance_type               = "t3.nano"
  key_name                    = aws_key_pair.my_key.id
  subnet_id                   = aws_subnet.prac_public.id
  security_groups             = [aws_security_group.allow_ssh_icmp.id]
  associate_public_ip_address = true

  tags = {
    Name = "bastion-${count.index}"
  }
}

以前Terraformで作ってたこの内容のEC作成の定義ファイル部分、作成は問題ないけど、EC2の再作成が必要ないはずの変更が生じても強制再作成(作成済みEC2は削除され、同じ定義で別のEC2が新しく作成)されてしまう。

なんなら、定義ファイルを一切変更しなくても、terraform planすると、"no changes"とならずに以下のように「replaced」となってしまう。

動作

前述の定義内容でterraform planすると、EC2部分は以下の通り。

$ terraform plan

[...]

Terraform will perform the following actions:

  # aws_instance.bastion[0] must be replaced
-/+ resource "aws_instance" "bastion" {
      ~ arn                          = "arn:aws:ec2:***:****:instance/i-****" -> (known after apply)
      ~ availability_zone            = "****" -> (known after apply)
      ~ cpu_core_count               = 1 -> (known after apply)
      ~ cpu_threads_per_core         = 2 -> (known after apply)
      - disable_api_termination      = false -> null
      - ebs_optimized                = false -> null
      - hibernation                  = false -> null
      + host_id                      = (known after apply)
      - iam_instance_profile         = "" -> null
      ~ id                           = "i-********" -> (known after apply)
      ~ instance_state               = "running" -> (known after apply)
      ~ ipv6_address_count           = 0 -> (known after apply)
      ~ ipv6_addresses               = [] -> (known after apply)
      - monitoring                   = false -> null
      ~ outpost_arn                  = "" -> (known after apply)
      ~ password_data                = "" -> (known after apply)
      ~ placement_group              = "" -> (known after apply)
      ~ primary_network_interface_id = "eni-****" -> (known after apply)
      ~ private_dns                  = "ip-172-25-10-169.***.compute.internal" -> (known after apply)
      ~ private_ip                   = "172.25.10.169" -> (known after apply)
      ~ public_dns                   = "ec2-********.compute.amazonaws.com" -> (known after apply)
      ~ public_ip                    = "********" -> (known after apply)
      ~ secondary_private_ips        = [] -> (known after apply)
      ~ security_groups              = [ # forces replacement
          + "sg-01281f2eb5db9a2b9",
        ]
        tags                         = {
            "Name" = "bastion-0"
        }
      ~ tenancy                      = "default" -> (known after apply)
      ~ vpc_security_group_ids       = [
          - "sg-01281f2eb5db9a2b9",
        ] -> (known after apply)
        # (7 unchanged attributes hidden)

      - credit_specification {
          - cpu_credits = "unlimited" -> null
        }

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + tags                  = (known after apply)
          + throughput            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      ~ enclave_options {
          ~ enabled = false -> (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      ~ metadata_options {
          ~ http_endpoint               = "enabled" -> (known after apply)
          ~ http_put_response_hop_limit = 1 -> (known after apply)
          ~ http_tokens                 = "optional" -> (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      ~ root_block_device {
          ~ delete_on_termination = true -> (known after apply)
          ~ device_name           = "/dev/xvda" -> (known after apply)
          ~ encrypted             = false -> (known after apply)
          ~ iops                  = 100 -> (known after apply)
          + kms_key_id            = (known after apply)
          ~ tags                  = {} -> (known after apply)
          ~ throughput            = 0 -> (known after apply)
          ~ volume_id             = "vol-0ca9e88e004237231" -> (known after apply)
          ~ volume_size           = 8 -> (known after apply)
          ~ volume_type           = "gp2" -> (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 1 to destroy.

最初は明示的に定義してないストレージ周りの定義でも影響してるんかなと思ったりしたけど、ターミナルで見ると赤字でわざわざ「# forces replacement」とコメント出力されている以下のセキュリティグループの設定が原因。

f:id:zaki-hmkc:20210522065033p:plain

対応

aws_instanceリソースのsecurity_groupsオプションの項のドキュメントを確認すると以下の通り、vpc_security_group_idsを使うように書いてある。

NOTE:

If you are creating Instances in a VPC, use vpc_security_group_ids instead.

ということでコード変更。

--- a/aws/practice/ec2.tf
+++ b/aws/practice/ec2.tf
@@ -14,7 +14,7 @@ resource "aws_instance" "bastion" {
   instance_type               = "t3.nano"
   key_name                    = aws_key_pair.my_key.id
   subnet_id                   = aws_subnet.prac_public.id
-  security_groups             = [aws_security_group.allow_ssh_icmp.id]
+  vpc_security_group_ids      = [aws_security_group.allow_ssh_icmp.id]
   associate_public_ip_address = true
 
   tags = {

この内容でterraform planを実行すると、期待通り"no changes"になった。

$ terraform plan
aws_key_pair.my_key: Refreshing state... [id=***]
aws_vpc.practice: Refreshing state... [id=***]
aws_internet_gateway.gw: Refreshing state... [id=***]
aws_security_group.allow_ssh_icmp: Refreshing state... [id=***]
aws_subnet.prac_priv2: Refreshing state... [id=***]
aws_subnet.prac_public: Refreshing state... [id=***]
aws_subnet.prac_priv1: Refreshing state... [id=***]
aws_route_table.public_route: Refreshing state... [id=***]
aws_security_group_rule.egress: Refreshing state... [id=***]
aws_security_group_rule.ssh: Refreshing state... [id=***]
aws_security_group_rule.icmp: Refreshing state... [id=***]
aws_instance.bastion[0]: Refreshing state... [id=***]
aws_route_table_association.public_subnet: Refreshing state... [id=***]

No changes. Infrastructure is up-to-date.

これで作成済みEC2に影響しないコード変更を行ったときに、余計なEC2再作成が発生しなくなった。

参考

qiita.com

教訓

勘と雰囲気と勢いも良いけど、ドキュメントちゃんと読もう。

[Terraform] countまたはfor_eachを使った複数リソース作成

countを使うことで簡単に同じリソースを複数作成できる。

learn.hashicorp.com

元ネタは以下で作成した定義ファイル。

zaki-hmkc.hatenablog.com

countを使った個数指定

www.terraform.io

例えばn人分の環境を作りたい場合で、とにかく数を指定してその個数のリソースを作るというパターン。

countの指定

resource "aws_instance" "bastion" {
  count  = 3

  ami                         = data.aws_ssm_parameter.amzn2_ami.value
  instance_type               = "t3.nano"
  key_name                    = aws_key_pair.my_key.id
  subnet_id                   = aws_subnet.prac_public.id
  security_groups             = [aws_security_group.allow_ssh_icmp.id]
  associate_public_ip_address = true

  tags = {
    Name = "bastion"
  }
}

count = 3 を追加している。

[zaki@cloud-dev practice (main)]$ ssh ec2-user@**.**.**.** -i ~/.ssh/id_aws_terraform 
The authenticity of host '**.**.**.** (**.**.**.**)' can't be established.
ECDSA key fingerprint is SHA256:lWRWcx/vBbr3QHTyJf10zxImxSfxY0l1V41k/u/wrqM.
ECDSA key fingerprint is MD5:7f:bd:48:2f:44:3a:8c:c3:cf:e3:6b:88:63:da:0e:c6.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '**.**.**.**' (ECDSA) to the list of known hosts.

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
[ec2-user@ip-172-25-10-145 ~]$ 

ログインもちゃんとできる。

ただ、一覧で見てもわかるとおり、全て同じtag名になっている。

f:id:zaki-hmkc:20210505123301p:plain

インデックス値参照

count.indexでいわゆるループ内のインデックス値を参照できる。
これをtag名に付与してやればOK。

  tags = {
    Name = "bastion-${count.index}"
  }

f:id:zaki-hmkc:20210505123412p:plain

これで連番をsuffixに付けることができた。

for_eachを使ったリスト指定

www.terraform.io

作成するリソースは同じだけど、用途が異なるためそれぞれ別の名前を付けたい、などのパターン。
例として「app」と「db」というEC2を作る場合。

リスト定義

ホスト一覧として、appdbという文字列型のリスト(配列)を定義。

variable "host_list" {
    description = "host count"
    type        = list(string)
    default     = [
        "app",
        "db",
    ]
}

for_eachの指定

resource "aws_instance" "server" {
  for_each = toset(var.host_list)

  ami                         = data.aws_ssm_parameter.amzn2_ami.value
  instance_type               = "t3.nano"
  key_name                    = aws_key_pair.my_key.id
  subnet_id                   = aws_subnet.prac_priv2.id
  security_groups             = [aws_security_group.allow_ssh_icmp.id]
  associate_public_ip_address = false

  tags = {
    Name = each.value
  }
}

countと同じような要領で、for_eachを使ってホスト一覧を指定する。
for_eachを使うことで、リストの要素ごとにリソースが作成される。

また、それぞれホスト毎の名前を指定したいtag名については、each.valueを指定することで要素の内容に置き換わる。

f:id:zaki-hmkc:20210505123445p:plain

残課題

「count数の環境ごとにfor_eachの要素を作成 (bastion/app/dbのセットをcount数分作る)」をやりたくて、

resource "aws_instance" "server" {
  for_each = toset(var.host_list)
  count = var.host_count

  ami                         = data.aws_ssm_parameter.amzn2_ami.value
  instance_type               = "t3.nano"
  key_name                    = aws_key_pair.my_key.id
  subnet_id                   = aws_subnet.prac_priv2.id
  security_groups             = [aws_security_group.allow_ssh_icmp.id]
  associate_public_ip_address = false

  tags = {
    Name = "${each.value}-${count.index}"
  }
}

みたいに書いてみたけど、Terraformはcountfor_eachを併用できなかった。

[zaki@cloud-dev practice (main)]$ terraform plan
╷
│ Error: Invalid combination of "count" and "for_each"
│ 
│   on ec2.tf line 26, in resource "aws_instance" "server":
│   26:   for_each = toset(var.host_list)
│ 
│ The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be
│ created.
╵

これはまた別のやり方で作る必要がある(未調査)

つかいわけ

同じサイズのコンピュートインスタンスを指定数分だけ並べたい程度であればcountでよさそうだが、たとえばサブネットの定義みたいに、CIDRとかのパラメタが異なるものを複数作る必要がある場合は、ListやMapで定義してfor_eachを使う方がよさそう。

[Terraform] 変数を定義・参照する

ハードコーディングしていた変数を別ファイルに定義してみる。

記事中のCIDRが172.26.だったり172.25.だったりしてるのはあまり意味なくて例示用。

元の定義ファイル

zaki-hmkc.hatenablog.com

上記では、こんな内容のVPCを定義したTerraformのファイルを作成していた。

resource "aws_vpc" "practice" {
  cidr_block = "172.26.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "vpc-aws-study-example"
  }
}

なのですが、VPCに設定している、"172.26.0.0/16"というネットワークのCIDRは定義内に直接指定するより、変数ファイルに記述した方が再利用性高いよね、ということで変数を定義してみる。

variableで変数定義

learn.hashicorp.com

拡張子が.tfであれば(変数定義とか関係なくTerraformの定義ファイルとして)実行時に読み込まれるので特にファイル名は何でもよいが、ここではvariables.tfで以下の内容のファイルを作成。
定義ファイル内でvariableを使用することで変数定義ができる。

書式はvariable "変数名" { ... }

variable "vpc_cidr_block" {
    description = "CIDR Block of VPC"
    type        = string
    default     = "172.26.0.0/16"
}

変数を参照するVPCの定義部分は以下の通り。

resource "aws_vpc" "practice" {
  cidr_block = var.vpc_cidr_block
  enable_dns_hostnames = true

  tags = {
    Name = "vpc-aws-study-example"
  }
}

これでplan実行すると、

[zaki@cloud-dev practice (main)]$ terraform plan
:
:
  # aws_vpc.practice will be created
  + resource "aws_vpc" "practice" {
      + arn                              = (known after apply)
      + assign_generated_ipv6_cidr_block = false
      + cidr_block                       = "172.26.0.0/16"
      + default_network_acl_id           = (known after apply)
      + default_route_table_id           = (known after apply)
      + default_security_group_id        = (known after apply)
:
:

今回はvariables.tfという変数定義用のファイルを別途作成したが、定義場所は別にどこでもよいので、変数を参照する同じファイルの先頭とかでも問題ない。

変数のセット

terraform.tfvarsで変数セット

variableに「デフォルト値」を定義できるけど、このデフォルト値を上書きする変数定義ファイルを別途使うこともできる。
ファイル名はterraform.tfvars固定。
(※ オプションでファイル名指定可能)

以下参照。
Assign values with a terraform.tfvars file

あくまでvariableを使って変数定義を行った変数に対して、指定の値をセットするというものなので、variableで定義していない変数は扱えないので注意。

追加で以下のvariableを定義。

variable "prac_public_cidr_block" {
    description = "CIDR Block of Public Subnet"
    type        = string
    default     = "172.26.10.0/24"
}
variable "prac_private1_cidr_block" {
    description = "CIDR Block of Private1 Subnet"
    type        = string
    default     = "172.26.20.0/24"
}
variable "prac_private2_cidr_block" {
    description = "CIDR Block of Private2 Subnet"
    type        = string
    default     = "172.26.30.0/24"
}

サブネットの変数の参照箇所は以下の通り。

resource "aws_subnet" "prac_public" {
  vpc_id            = aws_vpc.practice.id
  cidr_block        = var.prac_public_cidr_block
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "public-subnet-aws-study"
  }
}

で、terraform.tfvarsには以下のように変数セット。

vpc_cidr_block = "172.25.0.0/16"
prac_public_cidr_block = "172.25.10.0/24"

実行すると以下の通り。

[zaki@cloud-dev practice (main)]$ terraform plan

:
:

  # aws_subnet.prac_public will be created
  + resource "aws_subnet" "prac_public" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = "ap-northeast-1a"
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "172.25.10.0/24"

:
:

  # aws_vpc.practice will be created
  + resource "aws_vpc" "practice" {
      + arn                              = (known after apply)
      + assign_generated_ipv6_cidr_block = false
      + cidr_block                       = "172.25.0.0/16"
      + default_network_acl_id           = (known after apply)
      + default_route_table_id           = (known after apply)

コマンドライン引数でセット

以下参照。
Variables on the Command Line

terraform.tfvarsファイル以外に、-varオプションで変数セットもできる。
Ansibleでいうextra varsみたいな感じかな?

$ terraform plan -var='prac_public_cidr_block=172.26.60.0/24'

:
:

  # aws_subnet.prac_public will be created
  + resource "aws_subnet" "prac_public" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = "ap-northeast-1a"
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "172.26.60.0/24"
      + id                              = (known after apply)

:
:

こんな感じ。

環境変数でセット

以下参照。
Set values with environment variables

環境変数も使用可能。
その場合、環境変数名はTF_VAR_をprefixに付与しておけば、そのあとの変数名をTerraform定義ファイル内で参照できる。

[zaki@cloud-dev practice (main)]$ export TF_VAR_prac_private2_cidr_block="172.26.120.0/24"
[zaki@cloud-dev practice (main)]$ terraform plan

:
:

  # aws_subnet.prac_priv2 will be created
  + resource "aws_subnet" "prac_priv2" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = "ap-northeast-1c"
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "172.26.120.0/24"
      + id                              = (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)

変数参照の優先順位

以下参照。
Variable Definition Precedence

  1. 環境変数
  2. terraform.tfvarsファイル
  3. terraform.tfvars.jsonファイル
  4. *.auto.tfvarsファイルか*.auto.tfvars.jsonファイルを辞書順
  5. -var-var-fileによる指定

環境変数が一番弱く、設定されていてもterraform.tfvarsファイルがあれば上書きされる。

-varはやっぱAnsibleのextra varsと同じって認識でよさげね。

環境

[zaki@cloud-dev practice (main)]$ terraform -version
Terraform v0.15.1
on linux_amd64
+ provider registry.terraform.io/hashicorp/aws v3.37.0

サンプルコード

github.com

フライパンでご飯を炊く (炊飯器持ってないけど米を買った)

うちって炊飯器が無いのでご飯つぶを食べたいときは近所の弁当屋さんでライス単品を買ってきてたんだけど、フライパンで米炊けると聞いて試してみました。

環境

  • ガスコンロ (1ノード)
  • フライパン (20cm)
  • フライパン用の蓋 (蒸気穴あったほうが良さそうなイメージだけど無いタイプ)
  • ボウルかザル (米を研ぐときに使用)

蓋はガラスで中が見えるヤツじゃないと難易度上がるかも。

米と水の量

米1合に対して水どれだけ?ってやると、まず米1合の量を計る必要があった。(うちにあるカップは200cc用しかない)
2回試した結果、米の量1に対して水の量1.2にすればだいたい丁度よかったので、米が何合かどうかはあまり気にしないでいいかも。

手順

以下は米200ccの場合。
蓋は最後までしたまま。

  1. 米を研いで水気を切る
  2. 米と水をフライパンへ投入
  3. 30min浸水させる
  4. 蓋をして沸騰するまで強火で加熱
  5. 沸騰したら弱火で5min
  6. 30secくらい一度強火にするとチリチリ音がしだすので火を止める (これの効果はわかってない)
  7. 火を止めたら60secほどsleep

成果物

昼 (1回戦)

夜 (2回戦)

※ 夕食は昼に炊いたご飯を食べたので、2回戦は明日食べる分です笑

参考文献

www.kurashiru.com

oceans-nadia.com