#13 僕とお喋りしませんか?

とても基本的な話かもしれないですが、RSpecを記述する際に意識すべきことを備忘録として残します。

rspec-coreのREADMEの一文

先日チームのメンバーとrspec-coreのリポジトリを読んでいたのですが、READMEに興味深い一文がりました。

github.com

その文章がこちら。

Basic Structure

RSpec uses the words "describe" and "it" so we can express concepts like a conversation:

訳すとこんな感じ?

基本構造

RSpecはDescribeとItという単語を使用することで会話のように概念を表現することができるようになります。

この一文の「会話のように概念を表現」の部分が特に印象に残りました。

どんな会話?

会話の具体例が書いてありました。

"Describe an order."

"It sums the prices of its line items."

訳すとこんな感じ?

orderがどう振る舞うか説明して!

いいよ!これはアイテムの金額を合計するんだ!

ちょっと訳が変かもしれませんが、そこにはプログラムがどう振る舞うかという説明が会話形式で記されています。

上記は describeit だけのパターンですが、これに context を加えると状況(条件)がわかるようになります。

ちなみに contextdescribe のaliasなので中身は一緒で用途が違うだけです。 同じ振る舞いをするaliasを用意している点からもRSpecがこの形式を重視しているように思います。

誰と誰の会話?

会話とはいったものの、これは誰と誰の会話なのでしょうか。

私は 現在の実装者と未来の実装者(同一人物の場合もある)がRSpecを介して会話して、現在の実装者がプログラムの振る舞いを説明している と解釈すると平和なのかなと考えてます。

もちろんRSpecはあくまでプログラムが期待通りに動作するとこを担保するためのものなので、説明形式でテストケースを作れば良いというわけではありません。

ただこの説明の形式を意識することでより大きな効果を得ることができると考えています。

RSpecはただ書くだけだと勿体ない

時々引数で文字列を渡していないエグザンプルや適当な文字列を渡しているコンテクストを見かけることがあります。 RSpecは、適切な構造・適切な説明を意識して記述することでプログラムがどのように振る舞うかを教えてくれるので、これらの点を手を抜いてしまうのは勿体ないと思います。

更にRSpec実行時に --format documentation のオプションを与えるとドキュメント形式で簡単な仕様がわかるような出力がされます。

私は最近忘れっぽく、先週書いたコードが全く見に覚えのないものだったりすることがあり、、、 このような細かい部分を意識することでRSpecの効果を最大限発揮してバグを減らし質の高いコーディングを心がけようと思いました。

具体例

具体例として自分でコードを書いてみました。

~/repos $ mkdir rspec-sample
~/repos $ cd rspec-sample/
~/repos/rspec-sample $ bundle init
Writing new Gemfile to /home/ezquerro/repos/rspec-sample/Gemfile
~/repos/rspec-sample $ vi Gemfile 
~/repos/rspec-sample $ cat Gemfile 
# frozen_string_literal: true

source "https://rubygems.org"

# gem "rails"
gem 'rspec-core'

~/repos/rspec-sample $ bundle install --path vendor/bundle
~/repos/rspec-sample $ ls -al
total 51
drwxrwxr-x 4 ezquerro ezquerro   7 Sep  9 14:17 .
drwxrwxr-x 9 ezquerro ezquerro   9 Sep  7 18:41 ..
drwxrwxr-x 2 ezquerro ezquerro   3 Sep  7 18:43 .bundle
-rw-rw-r-- 1 ezquerro ezquerro  94 Sep  7 19:01 Gemfile
-rw-rw-r-- 1 ezquerro ezquerro 205 Sep  7 19:01 Gemfile.lock
drwxrwxr-x 3 ezquerro ezquerro   3 Sep  7 18:43 vendor


~/repos/rspec-sample $ vi tax_calculator.rb 
~/repos/rspec-sample $ cat -n tax_calculator.rb
     1  class TaxCalculator
     2    class PriceMustBeIntegerError < StandardError; end
     3    class TaxRateMustBeFloatError < StandardError; end
     4  
     5    def initialize(original_price, tax_rate)
     6      raise PriceMustBeIntegerError unless original_price.kind_of?(Integer)
     7      raise TaxRateMustBeFloatError unless tax_rate.kind_of?(Float)
     8  
     9      @original_price = Integer(original_price)
    10      @tax_rate       = Float(tax_rate)
    11    end
    12  
    13    def calculate
    14      Integer @original_price * (1 + @tax_rate)
    15    end
    16  end
~/repos/rspec-sample $ irb
irb(main):001:0> require '~/repos/rspec-sample/tax_calculator'
=> true
irb(main):002:0> tc=TaxCalculator.new(1000, 0.13)
=> #<TaxCalculator:0x00007faea664fcd8 @original_price=1000, @tax_rate=0.13>
irb(main):003:0> tc.calculate
=> 1130

上記は税抜価格と税率から、税金適用後の価格を算出するクラスのコードです。

そして以下はそのRSpecのコードです。

~/repos/rspec-sample $ cat -n tax_calculator_spec.rb
     1  require 'rspec/core'
     2  require '~/repos/rspec-sample/tax_calculator'
     3                   
     4  RSpec.describe TaxCalculator do
     5    let(:price) { rand 1000..9999 } 
     6    let(:tax_rate) { rand 0.01..0.99 }
     7  
     8    describe '#initialize' do
     9      subject { TaxCalculator.new(price, tax_rate) }
    10  
    11      context 'with an Integer object as price and a Float object as tax rate' do
    12        it 'does not raise exception' do
    13          expect{ subject }.not_to raise_error
    14        end
    15  
    16        it 'returns an object of TaxCalculator class' do
    17          is_expected.to be_a_kind_of TaxCalculator
    18        end
    19      end
    20  
    21  
    22      context 'with nil as price' do
    23        let(:price) { nil }
    24  
    25        it 'raises TaxCalculator::PriceMustBeIntegerError' do
    26          expect{ subject }.to raise_error TaxCalculator::PriceMustBeIntegerError
    27        end
    28      end
    29  
    30      context 'with a String object as price' do
    31        let(:price) { '1000' }
    32  
    33        it 'raises TaxCalculator::PriceMustBeIntegerError' do
    34          expect{ subject }.to raise_error TaxCalculator::PriceMustBeIntegerError
    35        end
    36      end
    37  
    38      context 'with a Float object as price' do
    39        let(:price) { 1000.0 }
    40  
    41        it 'raises TaxCalculator::PriceMustBeIntegerError' do
    42          expect{ subject }.to raise_error TaxCalculator::PriceMustBeIntegerError
    43        end
    44      end
    45      
    46      context 'with nil as tax rate' do
    47        let(:tax_rate) { nil }
    48  
    49        it 'raises TaxCalculator::TaxRateMustBeFloatError' do
    50          expect{ subject }.to raise_error TaxCalculator::TaxRateMustBeFloatError
    51        end
    52      end
    53  
    54      context 'with a String object as tax rate' do
    55        let(:tax_rate) { '0.13' }
    56  
    57        it 'raises TaxCalculator::TaxRateMustBeFloatError' do 
    58          expect{ subject }.to raise_error TaxCalculator::TaxRateMustBeFloatError
    59        end
    60      end
    61  
    62      context 'with an Integer object as tax rate' do
    63        let(:tax_rate) { 1 }
    64  
    65        it 'raises TaxCalculator::TaxRateMustBeFloatError' do 
    66          expect{ subject }.to raise_error TaxCalculator::TaxRateMustBeFloatError
    67        end
    68      end
    69    end
    70  
    71    describe '#calculate' do
    72      let(:tax_calculator) { TaxCalculator.new(price, tax_rate) }
    73      subject { tax_calculator.calculate }
    74  
    75      it 'returns the amount after taking taxes into account' do
    76        is_expected.to eq Integer price * (1 + tax_rate)
    77      end
    78    end
    79  end
    80

これを会話形式で表現すると以下のようになります。(超訳

太郎「 TaxCalculator#initialize の振る舞いを説明して!」

花子「いいわよ!」

花子「価格として Integer クラスのオブジェクトを、税率として Float クラスのオブジェクトを引数に渡すと例外を発生させず TaxCalculator クラスのオブジェクトを返すよ」

花子「それ以外のクラスのオブジェクトを渡すとそれぞれ TaxCalculator::PriceMustBeIntegerErrorTaxCalculator::TaxRateMustBeFloatError が発生するよ」

太郎「ありがとう! TaxCalculator#calculate も教えて!」

花子「このメソッドを呼び出すと税率が適用された金額を Integer クラスのオブジェクトで返すよ」

太郎「ありがとう!」

こんな感じでしょうか?

また、オプションを渡して実行すると仕様が理解できるドキュメント形式で出力されます。

~/repos/rspec-sample $ bundle exec rspec tax_calculator_spec.rb --format documentation

TaxCalculator
  #initialize
    with an Integer object as price and a Float object as tax rate
      does not raise exception
      returns an object of TaxCalculator class
    with nil as price
      raises TaxCalculator::PriceMustBeIntegerError
    with a String object as price
      raises TaxCalculator::PriceMustBeIntegerError
    with a Float object as price
      raises TaxCalculator::PriceMustBeIntegerError
    with nil as tax rate
      raises TaxCalculator::TaxRateMustBeFloatError
    with a String object as tax rate
      raises TaxCalculator::TaxRateMustBeFloatError
    with an Integer object as tax rate
      raises TaxCalculator::TaxRateMustBeFloatError
  #calculate
    returns the amount after taking taxes into account

Finished in 0.00506 seconds (files took 0.0943 seconds to load)
9 examples, 0 failures

#12 拝啓、1年前の自分へ

拝啓、1年前の自分へ

君は来月から社内のCRM移行プロジェクトの開発リードを任されるようになる。 しかし開発リードとはいえ、実際は外部の協力会社との連携、社内の他部署のタスク管理、全体のスケジュール調整など幅広い役割を担うようになる。今現在ようやくプロジェクトは落ち着きつつあるけど、これから君は1年近く慣れない業務に試行錯誤を重ねることになる。この手紙ではこれからプロジェクトをリードしていく君へ、1年後の君自身からアドバイスを書き記した。 これを実践するかは君次第だが、このアドバイスで君に良い未来が来ることを願っている。

1. 体制図を作成し関係各位の役割と責任を明確にしよう

君がこれから携わるプロジェクトは登場人物とその役割が曖昧だ。 誰かは、他の誰かに何かを期待しているかもしれない。 とあるタスクに関してAさんはBさんが担当者だと思っている。けどBさんにはその自覚はない。 そうすると重要なタスクに漏れが発生したり、意思決定が出来なかったりする。 君はプロジェクトの登場人物をリストアップして各人の役割と責任を明確にしないといけない。

2. プロジェクトのゴールを明確にしよう

プロジェクトのゴールを明確にすることは一番大事かもしれない。 そのプロジェクトを完了させるには何をしなければいけないか、 やらなくても良いことや後回しにしても良いことはあるか、 それらが明確にならないとプロジェクトの完了が徒に先延ばしされてしまう。 ゴールを明確にすれば、集中しないといけないタスクは自ずと見えてくる。

3. ドキュメントは無闇に作りすぎず、メンテナンスして大切に育てよう

議事録や進捗、残タスクを管理するのにドキュメントを作成することは避けることができない。 しかし、無闇矢鱈に作成すると管理することが難しくなる。 メンテナンスされていない使われていないドキュメントは、正しい情報を供給しないので効率的な業務の妨げになる。 ドキュメントは役割ごとに限定してメンテナンスするようにしよう。

4. 関係者の認識を統一するために全体ミーティングを定期的に開催しよう

情報の流通が不十分だと各人で物事の認識が合わずタスクの漏れや質が低下する。 伝言ゲームをしていると内容が歪んでいくのは避けられない。 何一つ独立したタスクは存在しないのに、誰が何をしているか、何に困っているのか分からないと作業が無駄になったり、余計に時間がかかったりする。 定期的に全体ミーティングを開催し進捗や残タスクを共有することで、皆が全体を見通し、より効率よくプロジェクトを進めることができる。

5. 現実的なリリースの日程目標を立てよう

人はそれぞれ一定期間内に出来る作業量のキャパシティーがある。無理なものは無理。 仮にリリース期限があれば、それは本当に守る必要があるものなのか(WANTではなくMUSTなのか)確認しよう。 MUSTでキャパシティーを超えているものであれば助けを求めよう。応援が呼べないのであれば君のプロジェクトは単純に優先度が低いだけのことだ。 WANTなものであればタスクの見積もりをして実現可能な日程目標へ修正しよう。 君のチームはこの先、深夜残業や休日出勤をすることになる。 期限に合わせるためにチームのメンバーは一生懸命働いてくれる。スプリントのポイントが非現実的なものになってもメンバーは最善を尽くしてくれる。 しかし、後々見返すとその8割以上は無駄なものだったと言わざるを得ない。なぜならハードワークして働いた成果(コード)は数カ月後の今も本番稼働していないからだ。 残念だが今の君はリーダーとしては無能だ。だけど1年後は少し改善されているはずだ。

6. リリースそのものではなく、リリースした後にユーザーへ価値をもたらすことを最優先の目的としよう

プロジェクトは普通、うまく行かないものだ。次第に遅延を繰り返すことに慣れてしまうかもしれない。 それが遅延なのか、適切なスケジュールに最適化さるプロセスなのかは不明だ。 ただスケジュールの変更が繰り返されると一種の引け目を感じることになり、なんとかリリースしようと躍起になる。 しかし忘れてないけない。プロジェクトの目的はリリースそのものではなく、リリースすることでユーザーに何らかの価値をもたらすことである。 これが出来ないのであればプロジェクトを進める意味はない。中止する権限には君にはないし、社内で決まったことであればそれに全力を注ぐ選択肢以外は存在ない。ただ何らかの価値がもたらされるように働きかけることはいくらでもできる。 君や君のチームメンバーの苦労が無駄にならないように常に価値をもたらすことを意識しよう。

以上が君へのアドバイスだ。 忘れないでほしいのは、このアドバイスを書いている1年後の君もまだ発展途上だということだ。 これから君がプロジェクトを進めていく上でここには書かれていないことが君を助けることもあると思う。 君はこのアドバイスだけを頼りにせず、常に情報をインプットして試行錯誤を繰り返してほしい。 そしてここに書かれていない新しいTIPがあれば再び過去の自分へ手紙を書いてくれ。

幸運を祈る

敬具

#11 where a != 'sample' におけるNULLの挙動

先日アプリの不具合調査の際にコードを読んでいたが、よく理解できていなかったのでまとめてみました。 結論から言うとSQLのWHERE句で比較条件で = を使用する場合、NULLは評価されないです。

こちらの記事にもある通りですが、

NULLはデータの欠落を表すため、任意の値や別のNULLとの関係で等号や不等号は成り立ちません。

以下でMySQLPostgreSQLで実際にレコードを挿入して試してみました。

$ sudo docker run -e MYSQL_ROOT_PASSWORD=password mysql:latest
$ sudo docker exec -it d10510842887 bash
bash-4.4# mysql -u root -p password
Enter password: 
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
bash-4.4# 
bash-4.4# 
bash-4.4# 
bash-4.4# mysql -u root -ppassword
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 8.0.32 MySQL Community Server - GPL

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.03 sec)

mysql> create database sampledb;
Query OK, 1 row affected (0.05 sec)

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sampledb           |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

mysql> use sampledb;
Database changed
mysql> create table sample_table (
    -> id int,
    -> body varchar(50)
    -> );
Query OK, 0 rows affected (0.03 sec)

mysql> show tables;
+--------------------+
| Tables_in_sampledb |
+--------------------+
| sample_table       |
+--------------------+
1 row in set (0.01 sec)

mysql> insert into sample_table (id,body) values (1,'test1');
Query OK, 1 row affected (0.06 sec)

mysql> insert into sample_table (id,body) values (2,'test2');
Query OK, 1 row affected (0.04 sec)

mysql> insert into sample_table (id,body) values (3,'test3');
Query OK, 1 row affected (0.04 sec)

mysql> insert into sample_table (id,body) values (4,NULL);
Query OK, 1 row affected (0.04 sec)

mysql> insert into sample_table (id,body) values (5,NULL);
Query OK, 1 row affected (0.01 sec)

mysql> select * from sample_table;
+------+-------+
| id   | body  |
+------+-------+
|    1 | test1 |
|    2 | test2 |
|    3 | test3 |
|    4 | NULL  |
|    5 | NULL  |
+------+-------+
5 rows in set (0.00 sec)

mysql> select * from sample_tables where body is null;
ERROR 1146 (42S02): Table 'sampledb.sample_tables' doesn't exist
mysql> select * from sample_table where body is null;
+------+------+
| id   | body |
+------+------+
|    4 | NULL |
|    5 | NULL |
+------+------+
2 rows in set (0.01 sec)

mysql> select * from sample_table where body is not null;
+------+-------+
| id   | body  |
+------+-------+
|    1 | test1 |
|    2 | test2 |
|    3 | test3 |
+------+-------+
3 rows in set (0.00 sec)

mysql> select * from sample_table where body = null;
Empty set (0.01 sec)

mysql> select * from sample_table where body != null;
Empty set (0.00 sec)

mysql> select * from sample_table where body = 'test1';
+------+-------+
| id   | body  |
+------+-------+
|    1 | test1 |
+------+-------+
1 row in set (0.00 sec)

mysql> select * from sample_table where body != 'test1';
+------+-------+
| id   | body  |
+------+-------+
|    2 | test2 |
|    3 | test3 |
+------+-------+
2 rows in set (0.00 sec)
$ sudo docker run -e POSTGRES_PASSWORD=password postgres:latest
$ sudo docker exec -it c8dc79e0f625 bash
root@c8dc79e0f625:/# psql -U postgres
psql (15.2 (Debian 15.2-1.pgdg110+1))
Type "help" for help.

postgres=# \l
                                                List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    | ICU Locale | Locale Provider |   Access privileges   
-----------+----------+----------+------------+------------+------------+-----------------+-----------------------
 postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | 
 template0 | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | =c/postgres          +
           |          |          |            |            |            |                 | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | =c/postgres          +
           |          |          |            |            |            |                 | postgres=CTc/postgres
(3 rows)
sampledb=# \dt
Did not find any relations.
sampledb=# create table sample_table (
sampledb(#   id serial,
sampledb(#   body varchar (50)
sampledb(# );
CREATE TABLE
sampledb=# \dt
            List of relations
 Schema |     Name     | Type  |  Owner   
--------+--------------+-------+----------
 public | sample_table | table | postgres
(1 row)

sampledb=# insert into sample_table (id,sample) values (1,'test1');
ERROR:  column "sample" of relation "sample_table" does not exist
LINE 1: insert into sample_table (id,sample) values (1,'test1');
                                     ^
sampledb=# insert into sample_table (id,body) values (1,'test1');
INSERT 0 1
sampledb=# insert into sample_table (id,body) values (2,'test2');
INSERT 0 1
sampledb=# insert into sample_table (id,body) values (3,'test3');
INSERT 0 1
sampledb=# insert into sample_table (id,body) values (4,NULL);
INSERT 0 1
sampledb=# insert into sample_table (id,body) values (5,NULL);
INSERT 0 1
sampledb=# select * from sample_table;
 id | body  
----+-------
  1 | test1
  2 | test2
  3 | test3
  4 | 
  5 | 
(5 rows)

sampledb=# select * from sample_table where body is null;
 id | body 
----+------
  4 | 
  5 | 
(2 rows)

sampledb=# select * from sample_table where body is not null;
 id | body  
----+-------
  1 | test1
  2 | test2
  3 | test3
(3 rows)

sampledb=# select * from sample_table where body = null;
 id | body 
----+------
(0 rows)

sampledb=# select * from sample_table where body != null;
 id | body 
----+------
(0 rows)

sampledb=# select * from sample_table where body = 'test1';
 id | body  
----+-------
  1 | test1
(1 row)

sampledb=# select * from sample_table where body != 'test1';
 id | body  
----+-------
  2 | test2
  3 | test3
(2 rows)

#10 terraformの基本操作をおさらいしつつAWS VPCを構築する

今後仕事でAWSを触る機会が増えそうなので、もっと理解を深めたい!
ということで、たくさん検証環境を作って壊したいのでterraformを始めます。
先ずは公式ドキュメントに沿ってVPCとサブネットの構築をしたいと思います。
環境はUbuntu 22.04、terraformのバージョンは1.3.9です。
インストールは以下のドキュメントを参考に行いました。

developer.hashicorp.com

www.hashicorp.com

先ずは作業ディレクトリを作成し main.tf ファイルを作成します。

$ mkdir terraform-aws
$ cd terraform-aws/
$ touch main.tf

AWSプロバイダーを使用することを宣言

$ vi main.tf 
$ cat main.tf 
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

リージョンの指定と認証情報の設定を行う 認証情報はマスキングしてます。後ほど変数化して別ファイルで管理するか別の認証方式を採用してセキュリティを高めたいです。

# Configure the AWS Provider
provider "aws" {
  # Use Tokyo region
  region = "ap-northeast-1"
  access_key = "XXXXXXXXXX"
  secret_key = "XXXXXXXXXX" 
}

今回はCIDRが 10.0.0.0/16 で名前が terraform-awsVPCを作成します。

# Create a VPC
resource "aws_vpc" "example" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "terraform-aws"
  }
}

terraform init

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 4.0"...
- Installing hashicorp/aws v4.55.0...
- Installed hashicorp/aws v4.55.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

terraform plan

$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  + create

Terraform will perform the following actions:

  # aws_vpc.example will be created
  + resource "aws_vpc" "example" {
      + arn                                  = (known after apply)
      + cidr_block                           = "10.0.0.0/16"
      + default_network_acl_id               = (known after apply)
      + default_route_table_id               = (known after apply)
      + default_security_group_id            = (known after apply)
      + dhcp_options_id                      = (known after apply)
      + enable_classiclink                   = (known after apply)
      + enable_classiclink_dns_support       = (known after apply)
      + enable_dns_hostnames                 = (known after apply)
      + enable_dns_support                   = true
      + enable_network_address_usage_metrics = (known after apply)
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = "terraform-aws"
        }
      + tags_all                             = {
          + "Name" = "terraform-aws"
        }
    }

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

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run
"terraform apply" now.

terraform apply

$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  + create

Terraform will perform the following actions:

  # aws_vpc.example will be created
  + resource "aws_vpc" "example" {
      + arn                                  = (known after apply)
      + cidr_block                           = "10.0.0.0/16"
      + default_network_acl_id               = (known after apply)
      + default_route_table_id               = (known after apply)
      + default_security_group_id            = (known after apply)
      + dhcp_options_id                      = (known after apply)
      + enable_classiclink                   = (known after apply)
      + enable_classiclink_dns_support       = (known after apply)
      + enable_dns_hostnames                 = (known after apply)
      + enable_dns_support                   = true
      + enable_network_address_usage_metrics = (known after apply)
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = "terraform-aws"
        }
      + tags_all                             = {
          + "Name" = "terraform-aws"
        }
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_vpc.example: Creating...
aws_vpc.example: Creation complete after 2s [id=vpc-xxxxx]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

ここでVPCが作成されていることを確認

20230219-tfm-aws-vpc-created

terraform destroy

環境を削除する場合は terraform destroy

$ terraform destroy
aws_vpc.example: Refreshing state... [id=vpc-xxxxx]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  - destroy

Terraform will perform the following actions:

  # aws_vpc.example will be destroyed
  - resource "aws_vpc" "example" {

  ~~ 中略 ~~

    }

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

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

aws_vpc.example: Destroying... [id=vpc-xxxxx]
aws_vpc.example: Destruction complete after 1s

Destroy complete! Resources: 1 destroyed.

VPCが削除されていることを確認

これらは基本的なコマンドのドキュメントのリンクです

developer.hashicorp.com developer.hashicorp.com developer.hashicorp.com developer.hashicorp.com developer.hashicorp.com

#9 ナマケモノの為のPull Request作成ガイドライン

このドキュメントを作成した経緯

 ナマケモノである私にはPull Requestのレビューは骨が折れるものです。 コードだけでなく実装の経緯や既存コードへの影響、動作確認などチェックしないといけないことが多いので。

 そんな中、最近チームのメンバ-のPull Request(以下PR)のレビューでフラストレーションが溜まることがありました。 では自分は他のメンバーが快適にレビューできるようなPRが作成できているかと言われると必ずしもそうでないです。 前職の上司に「PRこそエンジニアの成果であり、優れたPRこそ正義!」と言われていたが、今更になってその言葉が心にしみています。

 ということで、先ず自分がPRの改善を実践し周りに良い影響を与えたいと考え、今回ガイドラインを作ってみました。 これは取り敢えずは自分用のガイドラインである事を理解していただきたいです。 勿論、良いと思うものは実践していただいて、可能であればフィードバックをいただければと思います。

基本的な考え方

 PRを作るとき、コードのDiff( Files Changed )が重視されがちだと思います。 確かにコーディングこそエンジニアの腕の見せ所だし、リポジトリに変更をもたらしてくれるものなので重要なのは間違いないです。 ただここに書かれている内容はコーディングの改善ではなく、それ以外で部分の改善についてです。 コーディング以外の部分の改善でレビュアーの負担を減らし業務効率化、そしてバグを減らすことができればと考えています。

Pull Requestを改善する目的

 Pull Requestを作成すること自体の目的は、リポジトリに加える変更を他者に確認してもらいメインブランチに反映させる許可をもらうことだと考えています。 また、過去にどのような修正が行われたか確認するドキュメントの役割も果たしていると考えています。

では何故改善するのか。多くの人に同意していただけると思うのですが、PRのレビューはとても大変な作業だと思っています。

First, let’s admit it: reviewing pull requests is really hard.

www.atlassian.com

 そのためPRの改善によって レビュアーの負担軽減適切なフィードバックを貰えること 、そして 数か月後の自分を含むすべての人が見返してもどのような修正が行われたか理解できるようにすること が重要であると考えています。

やること

 では、実際には何をやるかを以下に挙げていきます。 これは全てが必須というわけではなく、状況に応じて必要な項目をを選別して実践し、簡潔で必要な情報が揃っているPRを作成できればと考えています。

1. 最初に空のコミットをプッシュしてPRを作成し説明を記入する

 作業ブランチを切った後に空コミットを積んでプッシュすることで作業ブランチのPRを作成することができるようになります。 PRを先に作成し適切な説明を記述することで、実装者が何をしようとしているのかが他の人にもわかるようになります。また、小まめにプッシュすることで実装者が途中で別の作業をして作業ができなくなった場合でも進捗状況がわかるようになります。

$ git checkout -b target_branch
$ git commit --allow-empty --m "Initial commit"
$ git push -u origin target_branch

2. merge masterや rebase masterでブランチを最新の状態に保つ

 作業ブランチを作成してコミットを積んでいる間に、マージ先のブランチで新しい修正が入っている可能性は大いにあります。この時マージ先の変更を小まめに取り込んでいないと、レビューでLGTMをもらった後にコンフリクト解消で修正が入り再度レビューが必要になってしまうことがあります。勿論上記パターンは避けられない場合もありますがレビュアーの時間を余計に取らないように気を付ける必要はあると思います。

 マージ先ブランチの変更を作業ブランチに取り込む方法には主に merge と rebase があります。 merge はGitHubの場合はPR画面上から行うことができるのでハードルは低いですが、余計なマージコミットが積まれ履歴が複雑になってしまいます。 一方 rebase は奇麗な履歴を保てますがコミットの再書き込みが行われてしまいます。 作業ブランチの場合大抵は rebase で変更を取り込むほうがコストは低いと思います(私は rebase 派です)が、双方のメリットとデメリットを理解し適切に使い分けることが大切かと思います。

*例)rebaseの手順

# How to rebase
$ git checkout main
$ git fetch
$ git reset --hard origin/main
$ git checkout target_branch
$ git rebase main

www.atlassian.com

3. 作業ブランチのコミット履歴を整える

 こちらの項目の内容に関して、自分は今まで全く実践していませんでした。ただ、今回の記事を作成するための調べもをしている最中に発見したMoneyforwardの開発ブログが素晴らしいと思い追加しました。 コミットの履歴を整える目的は、どの修正がどのコミット混ざっているか分かるようにすることです。それによりレビュアーの負担を減らすだけでなく、過去の修正を遡る際に的確に探しているコミットを見つけられるようになると考えています。

記事にあるように、以下を実践しコミット履歴を整えるといいと思いました。

  1. 同じ機能のコミットは一つのコミットに統合して冗長性を無くす
  2. 分けられる機能が混ざっているコミットは分割して、それぞれの機能ごとの個別コミットを作る
  3. コミットを議論性の低い順番に並び替える

moneyforward-dev.jp

4. 適切なコミットメッセージを残す

 せっかく上記のコミット履歴を整えるを実践しても、コミットメッセージが適切でなければ効果が薄れてしまいます。常に適切なコミットメッセージを残していればレビュアーも履歴を追いやすいし、履歴を整える際も役に立つと思うので効果は高いと思います。 コミットメッセージの残し方に関しては以下の記事が簡潔で良いと思いました。

qiita.com

Have on-point commit messages Good commit messages can also provide a nice bullet-point-like summary of the code changes as well, and it helps reviewers who read the commits in addition to the diff.

www.atlassian.com

5. 一つのPRの修正範囲が大きくならないようにする

 必要最低限を備えた小さなPRを心がけることも重要です。 一度に200~400の行数のコードを60~90分にわたってレビューすることで70~90%の欠陥を発見できる、という研究結果もあるようです。

smartbear.com

 ただ、レビュアーの負担やバグの見落としを減らす事を考えると、PRのLOC(変更コードの行数)は少なければ少ないほど良いと思います。 勿論大きなロジックを実装する場合、どうしてもPRが大きくなってしまう場合もあると思います。 その際はレイヤー毎に切り分けてPRに依存関係を記載するのが良いかもしれません。

例えばRailsで新規にCRUDの実装を行う場合、以下のようにより小さなタスクへ切り分けることができます。

タスク - 新規CRUD実装

切り分けた後のタスク - データベースの修正 - モデルの追加 - ルーティングの設定 - コントローラーとビューの実装(それぞれ分離させる、若しくはアクション毎に実装する) - テストの実装 - etc...

6. コーディングを行い、動作確認が完了した段階でPRのWIPを外す

 こちらは運用上のルールになりますが、実装と動作確認が完了してからWIPを外しましょう。 例えば、動作確認の完了を待たずにWIPを外すとレビュアーは全ての作業が完了したと勘違いしコードレビューを始めてしまうかもしれません。そのコードは動かないのに。。。 もし動作確認により先行してレビューをして欲しい場合は、WIPを付けたまま事情説明を添えてレビュー依頼をするなど工夫が必要です。

7. PRの説明をしっかり書く

PRの説明に気を配ることは、PRを他者に快適にレビューしてもらう上で重要な要素だと思います。 また、過去の修正を遡る際にどのような修正が行われたか、コードそのものを確認せずとも理解できるようになります。

具体的に説明欄に何を書けばよいかは以下の記事がとても参考になりました。

applis.io

順番 見出し 備考
1 変更の概要 概要、関連するIssueやプルリクエス
2 なぜこの変更をするのか
3 やったこと チェックボックスで進捗を表す
4 変更内容 UIのスクリーンショットAPIのリクエスト/レスポンスなど
5 やらないこと プルリクエストのスコープ外とすること
6 影響範囲 ユーザーやメンバー、システムに影響すること
7 どうやるのか 変更したものの使い方や再現手順
8 課題 悩んでいるところ、とくにレビューしてほしいところ
9 備考

 記事にもある通り、本文は長ければよいというものではないと思います。 シンプルであることを心掛け必要だと思う項目を記載することが重要です。

 また、上記テーブルはブログから抜粋しておりますが、4のスクリーンショットに関してはGithub プルリクの添付画像はprivateでも認証なしで誰でも参照可能なので注意が必要です。

8. コードの説明をPRのコメントで残す

 PRの説明をしっかり書けば、そのPRの概要は概ね理解できるようになります。 その上で実装者がコードにコメントを残すことでレビュアーがどの部分を注意して確認すればよいか分かるようになり負担を減らすことができます。 変更の肝となる部分や止むを得ず可読性が低くなっている部分に対してコメントで説明があるとレビューのハードルはかなり低くなるかと思います。

最後に

 最後まで読んでくださった方、ありがとうございます! 色々詰め込んだらかなりの量になってしまいました。 大変そうですが実践あるのみ!チームに良い影響を与えられるといいな。。。 (ちなみに私は最近あまり業務でコードを書いていませんwww) 記事の内容に関して何かご意見やご感想、若しくは実践してみてフィードバックなどあればコメントしていただければと思います!

#8 「技術的に可能」は「運用可能」とは違うということ

備忘録として残したいので記事にしてます。
こんなこと考え直すと当たり前だと思うがタイトルが全て。

技術的に実現可能であるけどそれが運用可能なものなのかは全く別の話なのである。

自分は今社内プロジェクトのリードをしているのだが、とある機能の要件が実現可能か開発会社に確認したら可能だと。 ただ、いざ開発会社の指示通りに実装してみるとアプリケーションからのAPIリクエストが激増しサーバーの負荷が増えたり、GTMの追加で速度低下したりと運用していくうえでは致命的な問題が発生してしまった。

こういった問題は最初から捕捉できていれば大丈夫なのだが、必ずしもそうはいかず、実装してみないとわからない部分もあると思う。 経験や知識があれば持続可能な運用ができるようにあらかじめリスクを洗い出せるかもしれないが、少なくとも自分にはそれができなかった。

もしろん経験や知識でこのような問題は回避できるようになりたいしそこを目指したい。 ただ、先ずは常に「実装可能かどうか」と両軸で「持続的な運用が可能かどうか」を意識することが重要だと思う。

#7 Ruby 3.2.0 のReDoSに対する正規表現の改善を試してみる

Ruby 3.2.0 リリース

昨年末12月25日にRubyの3.2.0がリリースされました。 今回の主な改善はWebAssemblyサポートと正規表現の改善でしたが本記事では正規表現に関してどのような改善が行われたかまとめてみました。 リリースノートによると改善内容は

の二つになりますが、どちらもReDoSに対する対策になります。

ReDoSとは

そもそもReDoSとは、何だぁ~~?ということですが、 Regular expression Denial of Serviceの省略で、こちらのページによると

The Regular expression Denial of Service (ReDoS) is a Denial of Service attack, that exploits the fact that most Regular Expression implementations may reach extreme situations that cause them to work very slowly

とあり、極端な文字列のマッチングで計算リーソースが過剰に消費されることを悪用した攻撃のことを指します。

Regexpのマッチングアルゴリズムの改善

起票されたチケットはこちら。 内容を深堀するとオートマトンなどを説明しないといけないのですがここでは割愛(というか説明できない)。 具体的にどのように動きが変わったか実際のコードで見ていきます。 実行コードはこちらのQiitaの記事を参考にしています。 ReDoSの説明もあるのでもし興味があれば読んでみてください。

以下、実行時の引数のマッチングをテストするスクリプトで実行時間を見ていきます。

> cat regex01.rb 
re = /^(([a-zA-Z0-9])+)+$/

str = ARGV[0]
puts "INPUT: #{str}"

start_time = Time.now

str.match(re) { |match| puts "MATCHED" if match }
puts "RUNNING TIME: #{Time.now - start_time}[s]"

Rubyのバージョンが3.1.3の場合

Rubyのバージョンを確認。

> ruby -v
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [arm64-darwin21]

マッチする文字列をテストする。 実行時間は約4秒。

> ruby regex01.rb abcdef                           
INPUT: abcdef
MATCHED
RUNNING TIME: 4.0e-06[s]

次にマッチングしない文字列をテストする。 実行時間は約93秒。 このような文字列が極端に長くなるとリソースを消耗してしまう。

> ruby regex01.rb AbCdEfGh1JkLmN0pQR5TuAbCdEfGh1Jk@
INPUT: AbCdEfGh1JkLmN0pQR5TuAbCdEfGh1Jk@
RUNNING TIME: 93.541195[s]

Rubyのバージョンが3.2.0の場合

次に3.2.0で同じパターンを試す。

> ruby -v
ruby 3.2.0 (2022-12-25 revision a528908271) [arm64-darwin21]
> ruby regex01.rb abcdef  
INPUT: abcdef
MATCHED
RUNNING TIME: 4.0e-06[s]
> ruby regex01.rb AbCdEfGh1JkLmN0pQR5TuAbCdEfGh1Jk@
INPUT: AbCdEfGh1JkLmN0pQR5TuAbCdEfGh1Jk@
RUNNING TIME: 5.0e-06[s]

同じ文字列でもパターンの解析時間が大幅に短縮している。 このように実行時間の短縮=リソースの消耗を防ぐことでReDoS攻撃の対策を行っている。

Regexpタイムアウトの導入

次に正規表現タイムアウトに関して確認する。 起票されたチケットはこちら

先ずはタイムアウト無しで検証

上記スクリプトを改良してマッチする文字列をテストする。

> cat regex02.rb       
TIMES=50000000.freeze # Integer

re = /^(([a-zA-Z0-9])+)+$/

str = ARGV[0]  # Command line argument
puts "INPUT: #{str} * #{TIMES}"

start_time = Time.now

target = str * TIMES

begin
  target.match(re) { |match| puts "MATCHED" if match }
ensure
  puts "RUNNING TIME: #{Time.now - start_time}[s]"
end

実行時間は約4秒。

projects/ruby-test  > ruby regex02.rb abcdef
INPUT: abcdef * 50000000
MATCHED
RUNNING TIME: 4.065616[s]

Regexp.timeout を設定して検証。

> cat regex02.rb        
TIMES=50000000.freeze # Integer
TIMEOUT=1.0 # Float

Regexp.timeout = TIMEOUT

re = /^(([a-zA-Z0-9])+)+$/

str = ARGV[0]  # Command line argument
puts "INPUT: #{str} * #{TIMES}"

start_time = Time.now
target = str * TIMES

begin
  target.match(re) { |match| puts "MATCHED" if match }
ensure
  puts "RUNNING TIME: #{Time.now - start_time}[s]"
end

処理時間約1秒で Regexp::TimeoutError が発生する。

projects/ruby-test  > ruby regex02.rb abcdef
INPUT: abcdef * 50000000
RUNNING TIME: 1.023424[s]
regex02.rb:17:in `match': regexp match timeout (Regexp::TimeoutError)
    from regex02.rb:17:in `match'
    from regex02.rb:17:in `<main>'

正規表現オブジェクトに個別でタイムアウトを設定して検証。

> cat regex02.rb 
TIMES=50000000.freeze # Integer
TIMEOUT=1.0 # Float

Regexp.timeout = TIMEOUT

re = Regexp.new('^(([a-zA-Z0-9])+)+$', timeout: 2.0)

str = ARGV[0]  # Command line argument
puts "INPUT: #{str} * #{TIMES}"

start_time = Time.now

target = str * TIMES

begin
  target.match(re) { |match| puts "MATCHED" if match }
ensure
  puts "RUNNING TIME: #{Time.now - start_time}[s]"
end

全体のタイムアウトは1秒で設定されているが、 処理時間約2秒で Regexp::TimeoutError が発生する。

projects/ruby-test  > ruby regex02.rb abcdef
INPUT: abcdef * 50000000
RUNNING TIME: 2.023538[s]
regex02.rb:17:in `match': regexp match timeout (Regexp::TimeoutError)
    from regex02.rb:17:in `match'
    from regex02.rb:17:in `<main>'

#6 DebianにDockerをインストールするAnsible Playbookを作成する

昨年新たにRaspberry Piを購入したのですが、 Dockerを都度インストールするのがめんどくさいのでAnsibleでセットアップできるようにしました。 Raspberry Piの標準OSであるRaspberry Pi OSはDebianがベースとなっているので、 Debianのシステムにインストールするドキュメントを参考にしました。 Playbookにするのは以下の手順になります。

$ sudo apt-get remove docker docker-engine docker.io containerd runc
$ sudo apt-get update
$ sudo apt-get install \
    ca-certificates \
    curl \
    gnupg \
    lsb-release
$ sudo mkdir -p /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
$ echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
    $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

作成したplaybookはこんな感じ。 apt_key の箇所は公式ドキュメントと少し違うところがありますが、 ドキュメントとエラーメッセージをみながらよしなにへんこうしております。

$ cat playbooks/debian/_docker.yml 
- name: Uninstall old versions
  become: yes
  apt:
    name: ['docker', 'docker-engine', 'docker.io', 'containerd', 'runc']
    state: absent

- name: Set up the repository
  become: yes
  apt:
    name: ['ca-certificates', 'curl', 'gnupg', 'lsb-release', 'software-properties-common']
    update_cache: yes

- name: Add Dockers official GPG key
  become: yes
  apt_key:
    url: https://download.docker.com/linux/debian/gpg
    keyring: /etc/apt/trusted.gpg.d/docker.gpg

- name: Set debian architecture
  command: dpkg --print-architecture
  register: architecture

- name: Set ubuntu codename
  command: lsb_release -cs
  register: codename

- name: Set up the stable repository
  become: yes
  apt_repository:
    repo: deb [arch="{{ architecture.stdout }}" signed-by=/etc/apt/trusted.gpg.d/docker.gpg] https://download.docker.com/linux/debian "{{ codename.stdout }}" stable

- name: Install Docker Engine
  become: yes
  apt:
    name: ['docker-ce', 'docker-ce-cli', 'containerd.io', 'docker-compose-plugin']
    update_cache: yes

上記Playbookをモジュール化しました。

$ cat playbooks/setup_debian.yml 
- hosts: dev03_debian
  tasks:
  - include_tasks: ./debian/_docker.yml

inventoryファイルを設置します。

$ cat inventory
---

devs:
  hosts:
    dev03_debian:
      ansible_host: public_ip
      ansible_user: username
      ansible_ssh_private_key_file: "~/path/to/key.pem"

実行してみます。(初回実行ではないですが証跡)

$ ansible-playbook -i inventory playbooks/setup_debian.yml 

PLAY [dev03_debian] *****************************************************************************************************

TASK [Gathering Facts] **************************************************************************************************
ok: [dev03_debian]

TASK [include_tasks] ****************************************************************************************************
included: /home/ezquerro/projects/setup/playbooks/debian/_docker.yml for dev03_debian

TASK [Uninstall old versions] *******************************************************************************************
ok: [dev03_debian]

TASK [Set up the repository] ********************************************************************************************
ok: [dev03_debian]

TASK [Add Dockers official GPG key] *************************************************************************************
changed: [dev03_debian]

TASK [Set debian architecture] ******************************************************************************************
changed: [dev03_debian]

TASK [Set ubuntu codename] **********************************************************************************************
changed: [dev03_debian]

TASK [Set up the stable repository] *************************************************************************************
changed: [dev03_debian]

TASK [Install Docker Engine] ********************************************************************************************
changed: [dev03_debian]

PLAY RECAP **************************************************************************************************************
dev03_debian               : ok=9    changed=5    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

EC2で立てたDebian 11にログインしてDockerがインストールされていることを確認。

admin@ip-10-0-1-34:~$ sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
7050e35b49f5: Pull complete 
Digest: sha256:94ebc7edf3401f299cd3376a1669bc0a49aef92d6d2669005f9bc5ef028dc333
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (arm64v8)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

#5 Fedora Serverに公開鍵認証でSSHログインする

先日VMWareのFusion Proを衝動買いしてしまったので、使い倒していきたい。 今後Ansibleで開発サーバーをセットアップしていくに当たり Fusion上のFedora Serverに対して公開鍵認証でSSHログインできるようにする。 Fedora Serverのセットアップ方法は割愛する。

ホストマシーンで公開鍵と秘密鍵を生成する

$ ssh-keygen -t ed25519 -C "test@example.com"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/Users/lazy_ez/.ssh/id_ed25519): 
/Users/lazy_ez/.ssh/id_ed25519 already exists.
Overwrite (y/n)? y
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /Users/lazy_ez/.ssh/id_ed25519
Your public key has been saved in /Users/lazy_ez/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:Fjp1yPEJU+UJ3iiuJWQRXdFGLH40sDntgAKrydonhWI test@example.com
The key's randomart image is:
+--[ED25519 256]--+
|    . oo+o*Bo    |
|     o o.O.X*.   |
|    . + B.@+=.   |
| . + o = +.+.    |
|.E= . + S  ..    |
|.+ .   *         |
|. o . .          |
|   o             |
|                 |
+----[SHA256]-----+

SCPコマンドでFedora Serverに公開鍵を送る

この時パスワード認証でのSSH接続が可能である必要がある(公開鍵を移していないので)

$ scp ~/.ssh/id_ed25519.pub user@172.16.150.129:~/.ssh/
@172.16.150.129's password: 
id_ed25519.pub                                                                                                        100%  109    22.7KB/s   00:00 

Fedora Server上で公開鍵の設定を行う

[lazy_ez@fedora-server ~]$ touch ~/.ssh/authorized_keys
[lazy_ez@fedora-server ~]$ cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys

Fedora Server上のSSHの設定でパスワード認証を不可にする

[lazy_ez@fedora-server ~]$ sudo cat /etc/ssh/sshd_config
# PasswordAuthentication no # <- パスワード認証不可の部分がコメントアウトされているか yes になっている
[lazy_ez@fedora-server ~]$ sudo vi /etc/ssh/sshd_config
[lazy_ez@fedora-server ~]$ sudo cat /etc/ssh/sshd_config
PasswordAuthentication no

SSHを再起動する

[lazy_ez@fedora-server ~]$ sudo systemctl restart sshd
[lazy_ez@fedora-server ~]$ sudo systemctl status sshd
● sshd.service - OpenSSH server daemon
     Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; vendor pres>
     Active: active (running) since Sun 2022-10-30 11:13:12 EDT; 6s ago
       Docs: man:sshd(8)
             man:sshd_config(5)
   Main PID: 12423 (sshd)
      Tasks: 1 (limit: 2288)
     Memory: 1.3M
        CPU: 14ms
     CGroup: /system.slice/sshd.service
             └─ 12423 "sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups"

Oct 30 11:13:12 fedora systemd[1]: Starting sshd.service - OpenSSH server daemo>
Oct 30 11:13:12 fedora sshd[12423]: Server listening on 0.0.0.0 port 22.
Oct 30 11:13:12 fedora sshd[12423]: Server listening on :: port 22.
Oct 30 11:13:12 fedora systemd[1]: Started sshd.service - OpenSSH server daemon.
[eyoshida@fedora ~]$ 

試しにホストマシーンからSSHを試みる

鍵ファイル作成時に入力したパスフレーズの入力を求められる

$ ssh -i ~/.ssh/id_ed25519 user@172.16.150.129
Web console: https://fedora:9090/ or https://172.16.150.129:9090/

Last login: Sun Oct 30 16:56:53 2022 from 172.16.150.1
[lazy_ez@fedora-server ~]$ 

おまけ)Ansibleのinventoryファイルで秘密鍵のパスを指定してpingを実行する

こちらの記事を参考に、inventoryファイルの ansible_ssh_private_key_file の項目に秘密鍵のパスを指定する

$ cat inventory
---

devs:
  hosts:
    fedora_server:
      ansible_host: ipaddress 
      ansible_user: lazy_ez
      ansible_ssh_private_key_file: "~/path/to/private_key"

$ ansible-playbook -i inventory playbooks/ping.yml 

PLAY [fedora-server] ************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************
ok: [fedora-server]

TASK [Example from an Ansible Playbook] ****************************************************************************************************************
ok: [fedora-server]

PLAY RECAP *********************************************************************************************************************************************
fedora-server               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

これで完了! 今後はFedora Serverのセットアップ用のPlaybookで環境を整えていきます。

#3 Ansibleのpingモジュールで対象リモートホストに疎通確認する

事前準備

ここでの説明は割愛させて頂きますが、予め実行サーバーにAnsibleのインストールを行い、対象のリモートサーバーも用意しておく。

実行サーバはローカルの macOS Monterey 12.4 、Ansibleのバージョンは 2.13.5 で行う。

リモートサーバーは AWSAmazon Linux 2 を使用します。

Inventoryを準備

実行対象のリモートサーバーを管理するために以下の inventory ファイルを作成しリポジトリのルートディレクトリに配置します。

$ pwd 
/Users/lazy-ez/projects/playbook
$ cat inventory
---
  hosts:
    target-host:
      ansible_host: ip_address
      ansible_user: ec2-user
      ansible_ssh_private_key_file: "~/.ssh/path/to/key.pem"

Playbookを準備

こちらを参考にそのまま ping モジュールを記入する

$ pwd 
/Users/lazy-ez/projects/playbook
$ cat ping.yml 
- hosts: target-host
  tasks:
  - name: Example from an Ansible Playbook
    ansible.builtin.ping:

Pingモジュールを実行し疎通を確認

リポジトリのルートディレクトリからinventoryファイルとPlaybookファイルを指定して実行する

$ pwd 
/Users/lazy-ez/projects/playbook
$ ansible-playbook -i inventory ping.yml 

PLAY [target-host] *****************************************************************************************************************************************

TASK [Gathering Facts] **************************************************************************************************************************************
ok: [target-host]

TASK [Example from an Ansible Playbook] *********************************************************************************************************************
ok: [target-host]

PLAY RECAP **************************************************************************************************************************************************
target-host               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0