プログラミング初心者がアーキテクトっぽく語る

見苦しい記事も多数あるとは思いますが訂正しつつブログと共に成長していければと思います

Terraformの基礎

TerraformはInfrastrcuture As Codeを支えるオーケストレーションツールの1つである。

クラウド/非クラウドに関わらずデータセンタ全体のインフラを横断的にコード管理できることから最近、注目度が高まっているようである。抑えておいて損はない。

このような変化を受けて2020年に投稿した簡潔な記事に大幅な加筆を行った。内容は初学者向けに留めてある。


概要

  • Vagrantで有名なHashiCorpが開発したInfrastructure as code (IaC) ソフトウェア
  • クラウド環境の管理に強い
  • 最近、非クラウド(所謂、オンプレミス装置)への対応にも力を入れている

特徴

宣言型

  • Terraformはシステムの期待する状態を設定ファイルに定義する宣言型のツール
  • 宣言型の設定ファイルはGit管理、CI/CD、IaCとの相性もよい

Plan

  • 適用予定の設定を事前に確認できる
terraform plan

Terraform Provider

  • Terraformが管理対象のクラウドやオンプレミス装置とやりとりするためのドライバのようなもの
  • Go言語で記述され、Terraform Registryで多数、公開されている
  • 実行時に管理対象システムに応じたTerraform Providerが自動的にダウンロードされる

構成管理ツールとの連携

  • Orchestrationツールと構成管理ツールは排他的な存在ではない
  • 「Terraformで作ったVMをpuppet/Ansibleで設定する」といった形で連携させることができる

作業の流れ

  1. 運用者はHCLを使って設定ファイルにシステムの期待する状態を定義する
  2. terraform initでworking directoryを初期化する
    • providerはこのタイミングでダウンロードされる
  3. terraform fmtで設定ファイルのフォーマットをチェックする
    • IDEが実施していれば実施不要
  4. terraform validateで設定ファイルの文法をチェックする
  5. terraform planでPlanを確認する
  6. terraform applyで設定を適用する
  7. terraform show / terraform state listで状態を確認する
  8. terafform destoryで作成した全リソースを削除する

設定ファイル

  • 設定ファイルは独自言語 HashiCorp Configuration Language (HCL)で定義する
  • 拡張子は.tfである
  • 「ブロック」という形式で記述する
    • 以下にブロックの基本的な書式を示す
<block type> "<block label>" "<block label>" {
  <identifier> = <expression>
}
  • 設定ファイルには主にterraformブロック、providerブロック、resourceブロックを記述する
    • 詳細後述

terraformブロック

  • 主に利用するTerraform Providerの情報を記述する
  • Terraform CoreやTerraform Providerの版数を指定することもできる
  • 以下の例ではAWS用のTerraform Providerを指定している
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
    }
  }
}

providerブロック

  • 主に管理対象のクラウドやオンプレミス装置へのアクセス情報を定義する
  • 以下にAWSのproviderブロックの記述例を示す。
provider "aws" {
    access_key = "ACCESS_KEY_HERE"
    secret_key = "SECRET_KEY_HERE"
    region = REGION
}

resourceブロック

  • resourceブロックは管理対象システムの状態を定義する
    • わかりやすく言えばシステムに投入する設定
  • 書式は以下の通り。
resource "<リソースタイプ>" "<リソース名>" {
  設定項目 = 設定値
}
  • リソースタイプは予めTerraform Providerで定義されている。
    • AWSのEC2設定ならリソースタイプは「aws_instance」
    • VPC設定ならリソースタイプは「aws_vpc」等
  • リソース名は任意。
  • ブロックの中は「設定項目 = 設定値」という形でリソースのパラメータを定義する。
    • 設定項目もTerraform Providerで定義されている
  • 以下にresourceブロックの記述例を示す。
resource "aws_vpc" "example" {
  cidr_block = "10.0.0.0/16"
}

variableブロック

  • 変数を定義する
  • 書式は以下の通り。
variable "変数名" {
  type = 型
}
  • ${変数名}で参照する
  • 変数の値の指定は、以下の3つの方法がある
    • terraform apply時に引数で指定する
    • 環境変数で渡す
    • tfvarsファイルに記述する
  • Validationルールを定義することもできる

outputブロック

  • 処理結果を変数に格納する
  • 以降の処理で参照することができる
  • 汎用プログラミング言語の関数のreturn文に近いイメージ
  • 書式は以下の通り。
output "任意の変数名" {
 value = 処理結果
}

ファイル構成

  • 設定は1つのファイルにすべてを記述することも、複数に分割して記述することもできる
  • Best Practiceでは可読性の観点から以下のように分割することが推奨されている
example
├── main.tf
├── outputs.tf
├── variables.tf
├── providers.tf
└── versions.tf

main.tf

  • resourceブロックを記述

variables.tf

  • variableブロックを記述

outputs.tf

  • outputブロックを記述

providers.tf

  • providerブロックを記述

versions.tf

  • terraformブロックを記述

Module

  • resourceを組み合わせてmoduleを作成することができる
  • 汎用プログラミング言語の自作関数に近いイメージ
  • 可読性、再利用性の観点からmoduleを作成して利用することが推奨されている

実行順序

  • resourceブロックはmain.tfに記載された順に上から実行されるわけではない
  • resource間の依存関係をTerraformが計算して順序を決定する
    • terraform graphで計算結果を確認できる
  • 例えばresource Aのoutuputをresource Bが利用していた場合、resource Aは記載場所に関係なくresource Bより先に実行される

State

  • terraformは自分が投入した設定をStateファイルに記録している
  • StateファイルはTerraformが宣言的に振る舞う上で重要な役割を果たす
    • main.tfにある + Stateにない = 設定新規作成
    • main.tfにある + Stateにある = なにもしない
    • main.tfにない + Stateにある = 設定削除
    • main.tfとStateで設定が違う = 設定変更
  • Stateと実際の状態がズレると面倒なのでTerraform導入後はTerraform以外の手段で設定を変更しないことが推奨される
  • Stateの管理が必要な点が純粋な宣言型ではないAnsibleと大きく異なる

Backend

  • Stateファイルの場所場所
  • ローカル、S3、Terraform Cloudなど様々な選択肢があり、backendブロックで指定する
  • なにも指定しなければデフォルトのローカル(作業フォルダ)が選択される
  • Backendの種類によってはStateファイルのロック機能やバージョン管理機能が利用できる
  • Terraformをチームで利用する場合はローカル以外の、共有空間(S3やTerraform Cloud)を利用すること

企業向けソリューション

  • AnsibleのAnsible Automation Platform(旧Ansible Tower)に相当するのがTerraform Cloud
  • 有償版はTerraform Enterprise

Ansbileは宣言型か?手続き型か?

Ansibleは宣言型だろうか?手続き型だろうか?

ドキュメントによって見解が異なるので混乱しやすいポイントだ。

Ansible寄りのドキュメントでは宣言型と言われることが多い。

一方、Terraform、Puppet、Kuberenetesなど純粋な宣言型ツールの視点からはAnsibleは手続き型と言われることが多い。


Ansibleの本質は手続き型

AnsibleはPlaybookに手順を記述しているので個人的には手続き型だと考える。

しかし冪等性があるので条件によっては宣言的な使い方も可能だ。 以下に詳しく見ていこう。


宣言的に動作するケース

望む状態

例えばリソースA、BをAnsibleで作成した後、リソースCを追加して、「リソースA、B、C」にしたくなったとする。

宣言

望んだ状態が「リソースA、B、C」なので宣言的なツールでは定義ファイルに「リソースA、B、C」と記載することになる。

そこでリソースA、Bを作成したPlaybookにリソースCを作成する手順を追記してみよう。

結果

Playbookを実行しよう。

リソースA、Bを作成する手順は既に実行済みなので冪等性によりAnsbileはスキップする。 未実施のリソースCを作成する処理のみが実施され、結果は「リソースA、B、C」になる。

望んだ状態が「リソースA、B、C」で、Playbookの宣言も「リソースA、B、C」で、結果も「リソースA、B、C」なので宣言的に動作していると言える。

しかしこれを持ってAnsibleを宣言的と理解するのは危険だ。 次は宣言的に動作しないケースを見てみよう。


宣言的に動作しないケース1:削除

望む状態

例えばリソースA、BをAnsibleで作成した後、Bを削除して「リソースA」にしたくなったとする。

宣言

そこでリソースA、Bを作成したPlaybookを編集し、リソースBを作成する手順を削除する。

今、Playbookに宣言されているリソースはAのみだ。 このPlaybookを実行すると宣言通り「リソースA」になるだろうか?

結果

Playbookを実行しよう。

リソースAを作成する手順は実施済みなのでスキップする。 リソースBに対する処理はなにも記述していないのでAnsibleはリソースBに対してなにも実施しない。 結果は「リソースA、B」になる。

宣言通りのは結果にならない。

望む状態にするには?

「リソースA」にするにはリソースA、Bを作成したPlaybookを編集し、リソースBを作成する手順を削除して、リソースBを削除する手順を追記する。

手順を記述しないと望む状態にならないので手続き的と言えるだろう。


宣言的に動作しないケース2:変更

望む状態

例えばリソースA、BをAnsibleで作成した後、BをCに変更して「リソースA、C」にしたくなったとする。

宣言

リソースA、Bを作成したPlaybookを編集し、リソースBを作成する手順を削除して、リソースCを作成する手順を記述する。

Playbookに宣言されているリソースはAとCだ。 このPlaybookを実行すると宣言通り「リソースA、C」になるだろうか?

結果

このPlaybookを実行したときの結果はリソースB、CがMutable(変更可能)かImmutable(変更不可)かによって異なる。

Mutableな場合

Mutableなリソースは直接変更することができる。

Playbookを実行するとBがCに変更されて、結果は「リソースA、C」となる。

望んだ状態が「リソースA、C」で、Playbookの宣言も「リソースA、C」で、結果も「リソースA、C」なので宣言的だ。

Immutableな場合

Immutableなリソースは直接変更できない。 既存リソース(B)を削除して新規リソース(C)を作成する必要がある。

Playbookを実行するとBを削除する手順がないので既存リソースBは削除されない。 新規リソースCが作成されて、結果は「リソースA、B、C」となる。

宣言通りのは結果にならない。

望む状態にするには?

「リソースA、C」にするにはリソースA、Bを作成したPlaybookを編集し、リソースBを作成する手順を削除して、リソースBを削除する手順とリソースCを作成する手順を追記することだ。

手順を記述しないと望む状態にならないので手続き的と言えるだろう。


結論

以上のようにAnsibleを純粋な宣言型だと考えると痛い目にあう可能性がある。

冪等性は宣言型であることを保証しない。

Ansibleは本質的には手続き型だと考えた方が無難だろう。

Javaでprivateなフィールドやメソッドを使う

Javaで単体試験を書いているとPrivateなフィールドやメソッドにアクセスしたくなることがあります。

そんなときはコソッと修飾子をprivateからpackage privateやprotectedに変更したくなりますが絶対にやってはいけません。

危険なだけでなく、アクセス修飾子の意味に一貫性がなくなり可読性、保守性が低下します。


まずはそのprivateなフィールドやメソッドへのアクセスが本当に必要なのかもう一度、考えてみましょう。

privateなフィールドへ直接アクセスせずとも、publicなメソッドの戻り値を取得することで同じ確認が実施できないでしょうか?

publicなメソッドを試験することでprivateメソッドの試験も包含できないでしょうか?

適切に設計されたクラスならばprivateなフィールドやメソッドへのアクセスが不要なことも少なくありません。

そうでない場合はそのようにクラスやメソッドを再設計した方がよいと思います。


検討の結果、どうしてもアクセスしないといけない場合もあるでしょう。

例えば他人が書いたコードの単体試験を書いているときは再設計する余地がないかもしれません。

また、APIを作成している場合は大半のメソッドをprivateにするので、それらprivateメソッドを一切試験せず一部のHigh Levelなpublicメソッドだけでカバレッジを確保しようとすると無理がある場合もあるでしょう。

そんなときにおすすめなのが下記のようなスタティックメソッドを単体試験コードに用意しておくことです。

簡単にPrivateなフィールドやメソッドにアクセスできて試験しやすくなります。

public class TestUtil {
    public static <T> T getPrivateField(Object obj, String privateFieldName) throws NoSuchFieldException, IllegalAccessException {
        Field privateField = obj.getClass().getDeclaredField(privateFieldName);
        privateField.setAccessible(true);
        //noinspection unchecked
        return (T) privateField.get(obj);
    }

    public static Object callPrivateMethod(Object obj, String privateMethodName, Class[] paramTypes, Object[] params) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        Method method = obj.getClass().getDeclaredMethod(privateMethodName, paramTypes);
        method.setAccessible(true);
        return method.invoke(obj, params);
    }
}

privateなフィールドへアクセスする例です。

List<Book> books = TestUtil.getPrivateField(shoppingCart, "books");

privateなメソッドを呼び出す例です。

TestUtil.callPrivateMethod(
        book,
        "setTitleName",
        new Class[]{String.class},
        new Object[]{"Lord Of The Ring"});

pexpectの基本的な使い方

pexpectはTelnetSSHで実施するような対話形式の作業を自動化するPythonのライブラリです。

pexpectでは、expectで指定する正規表現をうまく設計しないと思わぬ結果を招き、デバッグ作業に時間を取られることがあります。REST APIなどを利用したAPIベースの自動化手法と比較して使いやすいとは言えません。

しかしちょっとした手作業をPythonから自動化したいときには役に立つので知っておくとよいでしょう。


インストール

pip install pexpect

Import

import pexpect

基本的な使い方

connection = pexpect.spawn("ssh user@1.1.1.1 -p 1000", timeout=10, encoding='utf-8')
connection.expect("password:")
connection.sendline("my_password")
connection.expect("user#")
connection.sendline("exit")

spawnで対象機器に接続します。 「-p」はポート番号です。

expectで機器から特定の文字列が送信されてくるのを待ちます。受信した文字列は後述する方法で取得することができます。

sendlineで文字列を送ります。

接続を終了するときは「exit」を送るのが一般的です。


受信した文字列の取得

expectでマッチするまでの文字列

print(connection.before)

内容はexpectしている文字列次第ですが一般的にはsendlineで投入したコマンドとその出力結果が含まれます。 プロンプトは含まれていないことが多いです。

expectでマッチした文字列

print(connection.after)

一般的にはプロンプトが含まれます。


うまく動かないとき

意図しない箇所でexpectがマッチしている可能性が高いです。 出力内容を精査し、正規表現を見直しましょう。

結果に含まれる改行コードが「\r\n」か「\n」かもデバッグ等で確認するとよいです。

OpenPyXLの基本的な使い方

最強のお絵かきソフトExcelは色々なところで色々な用途で使われています。 このためExcelファイルをプログラムで処理する機会は多いです。

PythonExcelファイルを処理する場合はOpenPyXLを使うことが多いでしょう。 今回はOpenPyXLの基本的な使い方を紹介します。

APIの詳細はこちらを参照してください

https://openpyxl.readthedocs.io/en/stable/index.html#api-documentation


インストール

pip install openpyxl

Import

import openpyxl

Excelファイル(Workbook)操作

開く

workbook = openpyxl.load_workbook(<ファイル名>)

保存する

workbook.save(<ファイル名>)

シート操作

読む

特定の名前のシートを取得する

worksheet = workbook[<シート名>]

全シートのListを取得する

for worksheet in workbook.worksheets:
    print(worksheet.title')

書く

シートを作成する

workbook.create_sheet(title=<シート名>)

セル操作

読む

特定の位置のセルを読む。 行と列は1から始まることに注意!

print(worksheet.cell(row=行番号, column=列番号).value)

シート内の全セルを読む

for row in worksheet.rows:
    for cell in row:
        print(cell.value)

セルの位置を取得する

「B1」のような形式で取得する。 内部処理で扱いづらいのであまり使わない。 ユーザへの出力用には向いている。

cell.coordinate

位置情報を番号で取得する。 1から始まることに注意!

cell.row
cell.column

書く

cell.value = <値>

ウォーターフォール開発の歴史

ウォーターフォール以前

1960年代まではシステム開発は各職人がそれぞれの勘と経験を頼りに行っていた。 しかし1970年代頃になると確立されたシステム開発手法の重要性が強く叫ばれるようになる。

初期のウォーターフォール

ウォーターフォールの原型は1970年頃にロイスという人物によって提唱された。 ロイスの論文では「ウォーターフォール」という言葉は使われていなかった。 またロイスは1度のIterationではうまくいかないことを指摘しており、最低でも2回のIterationを実施することを推奨していた。

「反復が不可欠」という考え方がアジャイル宣言の30年前にあったのだ。 それもアジャイルの正反対にいるはずの「ウォーターフォール」の原型で提唱されていたのだからなお興味深い。

ウォーターフォールの発展

ロイスの論文は多くの人々に受け入れられた。 Bell、Thayer、Boehamといった人物の論文や著書で取り上げられ、それらを通して「ウォーターフォール」という名前が固定化される。 またアメリ国防省の調達仕様の一部となったり、IEEEで文書化されるなど高い評価を受けた。

ウォーターフォールの変化

IEEEという権威ある団体が文書化したことがウォーターフォールの国際的地位を絶対的なものにしたといえる。 そしてIEEEが文書化した時点でロイスが推奨した「複数回のIterationを実施する」という考え方は既に失われていた。

失われるに至った詳しい経緯は不明だが、アメリ国防省の調達仕様が改変される中で何らかの事情で削除され、そのままIEEEの文書に反映されたと考えられる。

いずれにせよ、IEEEの文書化が「ウォーターフォール=反復なし」という考え方を一般的なものにしたと思われる。

ウォーターフォールへの批判と脱却

反復のないウォーターフォールは数々のソフトウェアプロジェクトの失敗の原因として批判されるようになる。

1990年頃、アメリ国防省のソフトウェア調達失敗案件に関する民間調査が行われ、ここでもウォーターフォールはその主要因として槍玉に挙げられた。

その反動でアジャイルという考え方が指示されるようになる。現在、多くの組織がアジャイルに沿った開発をしていることを謳っている。

ウォーターフォールからの脱却の難しさ

現在、アメリ国防省の調達はアジャイルが必須となっている。しかし表面上、アジャイルと謳っているだけで実態はウォーターフォールという「偽アジャイル」が相当数あるとの噂だ。

開発手法というのはただの業務フローではない。 ユーザとベンダの商習慣という部分も大きい。 一度、染み付いた商慣習がなかかなか消せないのは日米共通の課題のようだ。

Beans、DTO、Entityの違い

Beans、Data Transfer Object、Entity。

いずれもJavaにおいてデータを保持するためのクラスだ。

あまり深く考えず雰囲気で使い分けていた。

ビジネスを表現するデータは「Entity」、

その中でも小粒なものは「VO」。

それ以外でアプリケーションが処理しやすいようにまとめたデータは「DTO」。

Beans Validationするときとかは結びつきを連想しやすいように「Bean」。

そんな感じ。


偶然、よい説明に出会った。是非、読んでいただきたい。

https://yyyank.blogspot.com/2013/07/javabeansbeandtoentityvoformwhat-is.html

Bean

  • JavaBeans系のオブジェクトの総称
  • DTOもEntityもJavaBeansのパターンのひとつ

Entity

  • 永続化可能なBean
  • DBのエントリに相当

DTO

  • 大きな境界をまたぐときに受け渡すBean
  • 処理効率化を目的として必要な情報を色々なところからかき集めて作る
  • シリアライズ可能にする
    • 境界の先が同一マシンのこともあれば別マシンのこともある
    • 今は同一マシン上でもリファクタされて将来、別マシンになるかもしれない

VO

  • BeanというよりDDDの要素

なるほど。。。

DTOにSerializableを必ず付ける人がいるのはこういうポリシーだったのかと納得。

これからはもう少し考えて付けるようにします。