WSL2でGUIアプリケーションを動かす

先に結論

Microsoft公式ブログで、やり方が紹介されていました。それを参考にしたら簡単にできたよ、という話です。

自分の環境

Windows

Windows 10 Home

バージョン2004(OSビルド19041.508)

WSL2

WSL2はインストール済み。 WSL2のインストールガイドは こちら

$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.1 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal

手順

今回は、ディスプレイサーバーをWindowsにインストールすることで対応する。

ちなみに、WSLにデスクトップ環境(ディスプレイサーバー含む)を構築し、Windowsからリモートデスクトップ接続する、といった方法もある。こちらの手順も上述のMS公式ブログに書いてあります。

X Windows Serverの用意

VcXsrv Windows X ServerをWindowsにインストールする。ブラウザからインストーラーをダウンロードしてインストールした。 Installerはデフォルト設定のままインストール。

インストール後、XLaunchを起動。 アクセスコントロールをオフ(Disable access controlにチェックを入れる)、 Save configuration を押して設定を保存する。

Windowsファイアウォールの警告が出るので許可する。

WSLの用意

DISPLAY環境変数を設定する。

export DISPLAY="`grep nameserver /etc/resolv.conf | sed 's/nameserver //'`:0"

動作確認

sudo apt install x11-apps
xeyes

日本語対応

sudo ln -s /mnt/c/Windows/Fonts /usr/share/fonts/windows
sudo fc-cache -fv

参考: https://qiita.com/momomo_rimoto/items/51d533ae9529872696ce#4-gui%E3%81%A7%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88%E3%82%92%E4%BD%BF%E3%81%88%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B

自動起動設定

動作確認が取れたら、設定を自動化する。

Windows

スタートアップフォルダに、 Save configuration を押したときに作成されたconfig.xlaunchファイルを置いておく。VcXsrvが自動起動するようになる。

参考: https://www.atmarkit.co.jp/ait/articles/1812/06/news040.html

WSL

以下をシェルの設定ファイル(例: .bashrc)に追記し、自動で設定されるようにする。

export DISPLAY="`grep nameserver /etc/resolv.conf | sed 's/nameserver //'`:0"

その他

自分の場合、使いたかったアプリを起動したら、以下のエラーが出た。

libGL error: No matching fbConfigs or visuals found
libGL error: failed to load driver: swrast

-nowgl を追記したら消えた。

参考: https://veresk.hatenablog.com/entry/2020/02/26/190000

-[no]wgl
    Enable the GLX extension to use the native Windows WGL interface for hardware-accelerated OpenGL

参考: https://gist.github.com/ctaggart/68ead4d0d942b240061086f4ba587f5f

GUIアプリのサポートはWSL公式でサポートする見通し

2020/05のMSの公式ブログで言及されている。 2020年末にさらなる報告があるかも、とのこと。

参考: https://devblogs.microsoft.com/commandline/the-windows-subsystem-for-linux-build-2020-summary/#wsl-gui

CloudFormationヘルパースクリプト(cfn-init等)でEC2の構成管理

経緯

最近CloudFormationを触っています。CloudFormationでは、cfn-initをはじめとする、「CloudFormationヘルパースクリプト」という構成管理ツールをサポートしています。 CloudFormationヘルパースクリプトの挙動が当初わかりづらかったので、少し前の自分のために書きます。いま思えば、デバッグの方法がわかりにくかったですね。

動画もあります

概要説明、およびデモをYouTubeにアップしました。 https://youtu.be/wdXYkzio6t0

動画から飛んできた方へ

動画内にて、ブログで補足します、と話した情報は、下記の場所にあります

CloudFormationヘルパースクリプトとは何か

公式ドキュメント はこちら。「スタックの一部として作成する Amazon EC2 インスタンスでソフトウェアをインストールしたりサービスを開始したりするために使用」とのこと。ざっくりいうとEC2インスタンスのプロビジョニング&構成管理ツール。

使い方はざっくり以下の通り。

CloudFormationテンプレートファイル(例: template.yml)のEC2のメタ情報に、設定情報を記述する。設定情報を記述してCloudFormationスタックをデプロイすると、EC2インスタンス初回起動時に、設定情報に応じて初期設定が自動で行われる。

更新も可能。EC2起動後も、設定情報はEC2インスタンスにインストールされたCloudFormationヘルパースクリプトが定期的に読み取っている。設定情報を更新してスタックを更新すると、変更が適用される。

4つのPythonスクリプトから成る。

  • cfn-init
    • 設定それ自体を実行する。
  • cfn-signal
    • 設定完了をAWSに通知する。オートスケーリングの準備完了判定、スタックのロールバック判定に使う。
  • cfn-get-metadata
    • 設定情報を読み取る。
    • 自分はほとんど使わない。
  • cfn-hup
    • 更新で活躍する。
    • デーモン。
    • 定期的に設定情報を読み取る。設定情報に変更があれば、指定の操作(例: cfn-initの呼び出し)を実行する。

UserDataとの違いはなにか

EC2では、UserDataパラメータにインスタンス起動時の処理を記述できる。(https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/user-data.html)CloudFormationにおいてももちろん活用可能。テンプレートファイルに記述できる。CloudFormationヘルパースクリプトの役割と重複する部分が大きい。わざわざCloudFormationヘルパースクリプトとのはなにか?

  • CloudFormationヘルパースクリプトのメリット
    • 宣言的に書ける(自分はわかりやすいと感じる)
    • 公式によるメリットの説明
    • EC2インスタンスのパブリックIP、パブリックDNSが変わらない。おそらく上記の「インスタンスを作り直さない」ことに関係するのだろう。
      • 前回案件では、諸々の事情から、DNSの向き先をEC2パブリックIPにしていた。その構成のだとEC2パブリックIPが変わると困ったことになる。
    • 想定された項目の設定が簡単
      • パッケージインストール、グループ作成、ユーザー作成、ファイル作成、コマンド実行、サービス(デーモン)設定、ファイルダウンロード。
    • 失敗の検知が簡単
      • cfn-signalで簡単に検知可能。
      • 失敗したらロールバック、がほぼコードをかかずに実行可能。
  • CloudFormationヘルパースクリプトのデメリット
    • スクリプトのコピペで済まない。
      • ミドルウェアの公式サイトに載ってる手順がコピペできない。
    • CloudFormationにより構成管理に慣れてないため、ハマることがある
      • amazon-linux-extrasがインストールできないのでハマった。下記TIPS参照。
      • いわゆる学習コストがかかる、ということだろうか?

CloudFormationヘルパースクリプトを始めるにはどうすればいいのか

  • Amazon Linux AMI イメージを使っている場合、ヘルパースクリプトは始めから入っている。
    • 最新バージョンでは /opt/aws/bin に存在
    • 2020/09現在、最新バージョンをtar, zip, exe形式で配布している
    • 1系はrpmも配布している
  • 本記事の主題から外れるが、CloudFormation自体を新規に始める場合。
    • CloudFormation Designer
    • CloudFormer
      • 「既存のAWS リソースからAWS CloudFormationテンプレートを作成できる」、とのこと。
      • 自分はちょっと試して、使いこなせないと感じた。CloudFormationの理解度が低い中で、同じく理解度の低いCloudFormerも使うと、意図しない挙動が起きた時どちらのツールによるものか推測できなくなる。(一般に、わからないレイヤーが複数あると、単一の場合よりもはるかに原因解明がやりづらくなると思う。この現象に名前をつけたい。)
    • チュートリアル などからリンクされている サンプルテンプレート から始める
      • 自分はここから始めた。
      • CloudFormationヘルパースクリプトを活用するためには、ヘルパースクリプト自体の設定、呼び出し処理をtemplate.ymlに書く必要が要る。上記テンプレートなら記述済み。

デバッグはどうやるのか

UserDataのログを確認する

方法1: AWS CLIから確認する

aws ec2 get-console-output --instance-id <value> --output text で確認可能。

方法2: ログをファイルに残す

以下をUserDataに追記する。自分は頭に書いている。

#!/bin/bash -xe
exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1

すると、EC2インスタンス内、 /var/log/user-data.log にて出力が記録される。

参考: https://aws.amazon.com/jp/premiumsupport/knowledge-center/ec2-linux-log-user-data/

ヘルパースクリプトのログを確認する

/var/log/cfn-*.log に記録されます。

テンプレートのAWS::CloudFormation::Initの変更をすばやく反映する

cfn-hupによる確認は、デフォルト15分間隔。おそらく最短5分間隔。EC2内で、直接cfn-initを呼ぶと即座に反映できる。

例:

/opt/aws/bin/cfn-init -v --stack cfn-test-stack --resource WebServerInstance --configsets InstallAndRun --region ap-northeast-1

立ち上げ失敗時、ロールバックしないようにする

設定によっては、AWS::CloudFormation::Initによる初期設定が失敗するとロールバックする。運用時は便利だが、スタックの初回立ち上げまでの試行錯誤中には、スタックが削除されるとログが消え、かつスタック削除自体に数分の時間がかかるのでデバッグしづらい。

CreationPolicyaws cloudformation create-stackコマンドのdisable-rollbackon-failureオプションに注意して、ロールバックしないようにしたほうが良い。 自分はSAMを使っていたのだが、簡単に上記のdisable-rollbackon-failureオプションを指定する方法がわからなかった。CreationPolicyの指定をコメントアウトしてデバッグしていた。

その他TIPS

amazon-linux-extras パッケージがインストールできない

amazon-linux-extras enable <PACKAGE>

してから、パッケージインストールする。

例:

01_prepare_install_docker:
  commands:
    01_enable_docker_in_amazon_linux_extras:
      command: 'amazon-linux-extras enable docker'
02_install_docker:
  packages:
    yum:
      docker: []

参考: https://cloudonaut.io/migrating-to-amazon-linux-2/#cfn-init-is-not-integrated-with-the-Extras-Library

UserDataを更新してスタック更新したら、反映されるのか?

反映される。

ただし、UserDataとの違いはなにか の「CloudFormationヘルパースクリプトのメリット」で挙げた恩恵は受けられないので注意。

UserDataやcfn-initで更新すると、サーバー内で手動で作成したファイルは消えちゃうの?

消えたことがありません。

参考

https://qiita.com/yasuhiroki/items/8463eed1c78123313a6f https://dev.classmethod.jp/articles/cfn-init/

長いおまけ: 挙動を実際に確認してみた

こちらのリポジトリより確認可能です。https://github.com/matzryo/cfn-test 以下手順内のコミットのハッシュ値は上記リポジトリのものになります。

# 最初のコミット
git checkout d916fd300f78777ce26a5d040d1f4ca14560c52d
make create-stack
aws cloudformation create-stack --template-body file://./template.yml --cli-input-json file://./cli-input.json
{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:<************>:stack/cfn-test-stack/76ce5280-ff24-11ea-be1e-0e7ed7880c96"
}
make wait-create-stack
aws cloudformation wait stack-create-complete --stack-name cfn-test-stack
# 返ってきたらOutputsでURLを確認、情報が多く、かつブログに出したくない情報もあるためqueryオプションで結果を絞る。
make get-url
aws cloudformation describe-stacks --stack-name cfn-test-stack --query "Stacks[0].Outputs[0]"
{
    "OutputKey": "WebsiteURL",
    "OutputValue": "http://ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com",
    "Description": "URL for newly created LAMP stack"
}
# たしかに上記URLにアクセス可能である。
> make ssh HOST=ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com
ssh -i ~/.ssh/cfn-test-key.pem ec2-user@ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com

# 指定パッケージがインストールされている。
[ec2-user@ip-172-31-42-181 ~]$ yum list installed | egrep 'mysql|php|httpd'
httpd.x86_64                         2.2.34-1.16.amzn1             @amzn-main   
httpd-tools.x86_64                   2.2.34-1.16.amzn1             @amzn-main   
mysql.noarch                         5.5-1.6.amzn1                 @amzn-main   
mysql-config.x86_64                  5.5.62-1.23.amzn1             @amzn-updates
mysql-libs.noarch                    5.5-1.6.amzn1                 @amzn-main   
mysql-server.noarch                  5.5-1.6.amzn1                 @amzn-main   
mysql55.x86_64                       5.5.62-1.23.amzn1             @amzn-updates
mysql55-libs.x86_64                  5.5.62-1.23.amzn1             @amzn-updates
mysql55-server.x86_64                5.5.62-1.23.amzn1             @amzn-updates
php.x86_64                           5.3.29-1.8.amzn1              @amzn-main   
php-cli.x86_64                       5.3.29-1.8.amzn1              @amzn-main   
php-common.x86_64                    5.3.29-1.8.amzn1              @amzn-main   
php-mysql.x86_64                     5.3.29-1.8.amzn1              @amzn-main   
php-pdo.x86_64                       5.3.29-1.8.amzn1              @amzn-main 

# ファイルが作成されている。
[ec2-user@ip-172-31-42-181 ~]$ sudo head /var/www/html/index.php
<html>
  <head>
    <title>AWS CloudFormation PHP Sample</title>
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
  </head>
  <body>
    <h1>Welcome to the AWS CloudFormation PHP Sample</h1>
    <p/>
    <?php
      // Print out the current data and time

# サービスが起動している。
[ec2-user@ip-172-31-42-181 ~]$ sudo service --status-all | egrep 'mysqld|httpd|cfn-hup'
cfn-hup (pid  3117) is running...
httpd (pid  3084) is running...
mysqld (pid  3437) is running...

# コマンドが実行されている。
[ec2-user@ip-172-31-42-181 ~]$ mysql -umatzryo --password=q40stBlSEjwi
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 7
Server version: 5.5.62 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

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 |
| test               |
| testdb             |
+--------------------+
3 rows in set (0.00 sec)

mysql> Bye

# cfn関連ログファイル。
[ec2-user@ip-172-31-42-181 ~]$ ls /var/log/cfn*
/var/log/cfn-hup.log  /var/log/cfn-init-cmd.log  /var/log/cfn-init.log  /var/log/cfn-wire.log

# 15分ごとにメタデータを確認している。
[ec2-user@ip-172-31-42-181 ~]$ ls /var/log/cfn*
/var/log/cfn-hup.log  /var/log/cfn-init-cmd.log  /var/log/cfn-init.log  /var/log/cfn-wire.log
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-hup.log 
2020-09-25 11:45:37,446 [DEBUG] CloudFormation client initialized with endpoint https://cloudformation.ap-northeast-1.amazonaws.com
2020-09-25 11:45:37,447 [DEBUG] Creating /var/lib/cfn-hup/data
2020-09-25 11:45:37,452 [INFO] No umask value specified in config file. Using the default one: 022
# 以下は別インスタンスの例。
[ec2-user@ip-172-31-30-239 log]$ head cfn-hup.log 
2020-09-25 01:04:09,409 [DEBUG] CloudFormation client initialized with endpoint https://cloudformation.ap-northeast-1.amazonaws.com
2020-09-25 01:04:09,410 [DEBUG] Creating /var/lib/cfn-hup/data
2020-09-25 01:04:09,416 [INFO] No umask value specified in config file. Using the default one: 022
2020-09-25 01:19:09,554 [INFO] cfn-hup processing is alive.
2020-09-25 01:34:09,670 [INFO] cfn-hup processing is alive.
2020-09-25 01:49:09,786 [INFO] cfn-hup processing is alive.
2020-09-25 02:04:09,901 [INFO] cfn-hup processing is alive.
2020-09-25 02:19:10,017 [INFO] cfn-hup processing is alive.
2020-09-25 02:34:10,133 [INFO] cfn-hup processing is alive.
2020-09-25 02:49:10,249 [INFO] cfn-hup processing is alive.

# 手動で作成したファイルが消されないかテストするためにファイルを作成。
[ec2-user@ip-172-31-42-181 ~]$ echo created in instance manually. > /home/ec2-user/manual.txt
[ec2-user@ip-172-31-42-181 ~]$ cat /home/ec2-user/manual.txt
created in instance manually.
# cowsayは存在しない。
[ec2-user@ip-172-31-42-181 ~]$ which cowsay
/usr/bin/which: no cowsay in (/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin)
exit
# EC2インスタンスが再作成されたかわかるよう、インスタンス情報を保存しておく
aws ec2 describe-instances > first-instance.txt

# template.ymlを更新
# CFNヘルパースクリプトによる追加の確認
# パッケージ1つ、ファイル2つ
git checkout a38b4850c43f766cf1eb5a17951f1bb3d6500b98
> git log -p -1 a38b4850c43f766cf1eb5a17951f1bb3d6500b98
diff --git a/template.yml b/template.yml
index a39f3fb..b47ab98 100644
--- a/template.yml
+++ b/template.yml
@@ -108,7 +108,20 @@ Resources:
               httpd: []
               php: []
               php-mysql: []
+              cowsay: []
           files:
+            /tmp/test-cfn-create.txt:
+              content: |
+                hello from cfn-hup!
+              mode: '000400'
+              owner: root
+              group: root
+            /tmp/test-cfn-update.txt:
+              content: |
+                hello from cfn-hup!
+              mode: '000400'
+              owner: root
+              group: root
             /var/www/html/index.php:
               content: !Join 
                 - ''

# パッケージ、ファイルの追加テスト
# 再度EC2インスタンスが初期化されないかテスト
> make deploy
aws cloudformation deploy --stack-name cfn-test-stack --template-file ./template.yml

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cfn-test-stack

> make ssh HOST=ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com
ssh -i ~/.ssh/cfn-test-key.pem ec2-user@ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com
# 作成したファイルは存在する
[ec2-user@ip-172-31-42-181 ~]$ cat /home/ec2-user/manual.txt 
created in instance manually.
# インストールされてない。
[ec2-user@ip-172-31-42-181 ~]$ which cowsay
/usr/bin/which: no cowsay in (/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin)
[ec2-user@ip-172-31-42-181 ~]$ ls /tmp/test-cfn-create.txt
ls: /tmp/test-cfn-create.txt にアクセスできません: そのようなファイルやディレクトリはありません
[ec2-user@ip-172-31-42-181 ~]$ ls /tmp/test-cfn-update.txt
ls: /tmp/test-cfn-update.txt にアクセスできません: そのようなファイルやディレクトリはありません
[ec2-user@ip-172-31-42-181 ~]$ cat /var/log/cfn-hup.log 
2020-09-25 11:45:37,446 [DEBUG] CloudFormation client initialized with endpoint https://cloudformation.ap-northeast-1.amazonaws.com
2020-09-25 11:45:37,447 [DEBUG] Creating /var/lib/cfn-hup/data
2020-09-25 11:45:37,452 [INFO] No umask value specified in config file. Using the default one: 022
2020-09-25 12:00:37,590 [INFO] cfn-hup processing is alive.

# ~15分後~
# create-databaseなど愚直に繰り返して失敗している。
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-init.log 
2020-09-25 12:15:40,808 [DEBUG] Test command output: 
2020-09-25 12:15:40,808 [INFO] Test failed with code 1
2020-09-25 12:15:40,808 [DEBUG] Running command 02_create_database
2020-09-25 12:15:40,808 [DEBUG] Running test for command 02_create_database
2020-09-25 12:15:40,824 [DEBUG] Test command output: 
2020-09-25 12:15:40,824 [INFO] Test failed with code 1
2020-09-25 12:15:40,824 [DEBUG] No services specified
2020-09-25 12:15:40,824 [INFO] ConfigSets completed
2020-09-25 12:15:40,824 [DEBUG] Not clearing reboot trigger as scheduling support is not available
2020-09-25 12:15:40,825 [INFO] -----------------------Build complete-----------------------
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-init-cmd.log 
2020-09-25 12:15:40,532 P24817 [INFO] ------------------------------------------------------------
2020-09-25 12:15:40,532 P24817 [INFO] Completed successfully.
2020-09-25 12:15:40,784 P24817 [INFO] ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2020-09-25 12:15:40,785 P24817 [INFO] Config Configure
2020-09-25 12:15:40,785 P24817 [INFO] ============================================================
2020-09-25 12:15:40,785 P24817 [INFO] Test for Command 01_set_mysql_root_password
2020-09-25 12:15:40,807 P24817 [ERROR] Exited with error code 1
2020-09-25 12:15:40,808 P24817 [INFO] ============================================================
2020-09-25 12:15:40,808 P24817 [INFO] Test for Command 02_create_database
2020-09-25 12:15:40,823 P24817 [ERROR] Exited with error code 1
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-hup.log 
2020-09-25 11:45:37,446 [DEBUG] CloudFormation client initialized with endpoint https://cloudformation.ap-northeast-1.amazonaws.com
2020-09-25 11:45:37,447 [DEBUG] Creating /var/lib/cfn-hup/data
2020-09-25 11:45:37,452 [INFO] No umask value specified in config file. Using the default one: 022
2020-09-25 12:00:37,590 [INFO] cfn-hup processing is alive.
2020-09-25 12:15:37,706 [INFO] cfn-hup processing is alive.
2020-09-25 12:15:37,821 [INFO] Data has changed from previous state; action for cfn-auto-reloader-hook will be run
2020-09-25 12:15:37,821 [INFO] Running action for cfn-auto-reloader-hook
# パッケージインストールされた
[ec2-user@ip-172-31-42-181 ~]$ which cowsay
/usr/bin/cowsay
[ec2-user@ip-172-31-42-181 ~]$ cowsay installed!
 ____________ 
< installed! >
 ------------ 
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
# ファイルも作成された
[ec2-user@ip-172-31-42-181 ~]$ sudo cat /tmp/test-cfn-create.txt 
hello from cfn-hup!
[ec2-user@ip-172-31-42-181 ~]$ sudo cat /tmp/test-cfn-update.txt 
hello from cfn-hup!

# CFNヘルパースクリプトによる更新、削除の確認
# パッケージ削除、ファイル削除、ファイル更新
git checkout 4a2db25db123668bf861a3b85165a7bcd191ce63
git log -p -1 4a2db25db123668bf861a3b85165a7bcd191ce63
diff --git a/template.yml b/template.yml
index b47ab98..3e296df 100644
--- a/template.yml
+++ b/template.yml
@@ -108,17 +108,10 @@ Resources:
               httpd: []
               php: []
               php-mysql: []
-              cowsay: []
           files:
-            /tmp/test-cfn-create.txt:
-              content: |
-                hello from cfn-hup!
-              mode: '000400'
-              owner: root
-              group: root
             /tmp/test-cfn-update.txt:
               content: |
-                hello from cfn-hup!
+                hello from cfn-hup! content has been updated!
               mode: '000400'
               owner: root
               group: root
> make deploy
aws cloudformation deploy --stack-name cfn-test-stack --template-file ./template.yml
make ssh HOST=ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com
ssh -i ~/.ssh/cfn-test-key.pem ec2-user@ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com
# cfn-hupによる更新前
[ec2-user@ip-172-31-42-181 ~]$ which cowsay
/usr/bin/cowsay
[ec2-user@ip-172-31-42-181 ~]$ sudo cat /tmp/test-cfn-create.txt 
hello from cfn-hup!
[ec2-user@ip-172-31-42-181 ~]$ sudo cat /tmp/test-cfn-update.txt 
hello from cfn-hup!
# cfn-hupによる更新実行
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-init.log 
2020-09-25 12:30:42,676 [DEBUG] Test command output: 
2020-09-25 12:30:42,676 [INFO] Test failed with code 1
2020-09-25 12:30:42,677 [DEBUG] Running command 02_create_database
2020-09-25 12:30:42,677 [DEBUG] Running test for command 02_create_database
2020-09-25 12:30:42,683 [DEBUG] Test command output: 
2020-09-25 12:30:42,683 [INFO] Test failed with code 1
2020-09-25 12:30:42,683 [DEBUG] No services specified
2020-09-25 12:30:42,684 [INFO] ConfigSets completed
2020-09-25 12:30:42,684 [DEBUG] Not clearing reboot trigger as scheduling support is not available
2020-09-25 12:30:42,684 [INFO] -----------------------Build complete-----------------------
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-init-cmd.log
2020-09-25 12:30:41,354 P25033 [INFO] ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2020-09-25 12:30:41,354 P25033 [INFO] Config Install
2020-09-25 12:30:42,669 P25033 [INFO] ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2020-09-25 12:30:42,669 P25033 [INFO] Config Configure
2020-09-25 12:30:42,669 P25033 [INFO] ============================================================
2020-09-25 12:30:42,670 P25033 [INFO] Test for Command 01_set_mysql_root_password
2020-09-25 12:30:42,676 P25033 [ERROR] Exited with error code 1
2020-09-25 12:30:42,677 P25033 [INFO] ============================================================
2020-09-25 12:30:42,677 P25033 [INFO] Test for Command 02_create_database
2020-09-25 12:30:42,683 P25033 [ERROR] Exited with error code 1
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-hup.log
2020-09-25 11:45:37,446 [DEBUG] CloudFormation client initialized with endpoint https://cloudformation.ap-northeast-1.amazonaws.com
2020-09-25 11:45:37,447 [DEBUG] Creating /var/lib/cfn-hup/data
2020-09-25 11:45:37,452 [INFO] No umask value specified in config file. Using the default one: 022
2020-09-25 12:00:37,590 [INFO] cfn-hup processing is alive.
2020-09-25 12:15:37,706 [INFO] cfn-hup processing is alive.
2020-09-25 12:15:37,821 [INFO] Data has changed from previous state; action for cfn-auto-reloader-hook will be run
2020-09-25 12:15:37,821 [INFO] Running action for cfn-auto-reloader-hook
2020-09-25 12:30:40,854 [INFO] cfn-hup processing is alive.
2020-09-25 12:30:40,969 [INFO] Data has changed from previous state; action for cfn-auto-reloader-hook will be run
2020-09-25 12:30:40,969 [INFO] Running action for cfn-auto-reloader-hook
# パッケージは削除されない。ファイルも消されない。更新は適用された。
[ec2-user@ip-172-31-42-181 ~]$ which cowsay
/usr/bin/cowsay
[ec2-user@ip-172-31-42-181 ~]$ sudo cat /tmp/test-cfn-create.txt 
hello from cfn-hup!
[ec2-user@ip-172-31-42-181 ~]$ sudo cat /tmp/test-cfn-update.txt 
hello from cfn-hup! content has been updated!
# 手動作成したファイルは当然残っている。
[ec2-user@ip-172-31-42-181 ~]$ cat /home/ec2-user/manual.txt 
created in instance manually.

# 手動更新のテスト
> git checkout 7ec05844d427de0156c28b27b44a2ad207237c5e
> git log -p -1 7ec05844d427de0156c28b27b44a2ad207237c5e
diff --git a/template.yml b/template.yml
index 3e296df..8da39bb 100644
--- a/template.yml
+++ b/template.yml
@@ -111,7 +111,7 @@ Resources:
           files:
             /tmp/test-cfn-update.txt:
               content: |
-                hello from cfn-hup! content has been updated!
+                hello from cfn-hup! content has been updated! updated again!
               mode: '000400'
               owner: root
               group: root

matzryo@tp-x1c-5 ~/D/cfn (master)> make deploy
aws cloudformation deploy --stack-name cfn-test-stack --template-file ./template.yml

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cfn-test-stack
matzryo@tp-x1c-5 ~/D/cfn (master)> make ssh HOST=ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com
ssh -i ~/.ssh/cfn-test-key.pem ec2-user@ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com
# 手動更新前。ちなみに現在、15,30,45,00分ころに自動チェックが入っている状態。 現在40分。
[ec2-user@ip-172-31-42-181 ~]$ sudo cat /tmp/test-cfn-update.txt
hello from cfn-hup! content has been updated!
[ec2-user@ip-172-31-42-181 ~]$ sudo /opt/aws/bin/cfn-init -v --stack cfn-test-stack --resource WebServerInstance --configsets InstallAndRun --region ap-northeast-1
# 即座に再確認。更新されている。
[ec2-user@ip-172-31-42-181 ~]$ sudo cat /tmp/test-cfn-update.txt
hello from cfn-hup! content has been updated! updated again!
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-init.log
2020-09-25 12:40:09,474 [DEBUG] Test command output: 
2020-09-25 12:40:09,475 [INFO] Test failed with code 1
2020-09-25 12:40:09,475 [DEBUG] Running command 02_create_database
2020-09-25 12:40:09,475 [DEBUG] Running test for command 02_create_database
2020-09-25 12:40:09,481 [DEBUG] Test command output: 
2020-09-25 12:40:09,481 [INFO] Test failed with code 1
2020-09-25 12:40:09,481 [DEBUG] No services specified
2020-09-25 12:40:09,482 [INFO] ConfigSets completed
2020-09-25 12:40:09,482 [DEBUG] Not clearing reboot trigger as scheduling support is not available
2020-09-25 12:40:09,482 [INFO] -----------------------Build complete-----------------------
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-init-cmd.log
2020-09-25 12:40:08,135 P25263 [INFO] ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2020-09-25 12:40:08,135 P25263 [INFO] Config Install
2020-09-25 12:40:09,467 P25263 [INFO] ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2020-09-25 12:40:09,467 P25263 [INFO] Config Configure
2020-09-25 12:40:09,468 P25263 [INFO] ============================================================
2020-09-25 12:40:09,468 P25263 [INFO] Test for Command 01_set_mysql_root_password
2020-09-25 12:40:09,474 P25263 [ERROR] Exited with error code 1
2020-09-25 12:40:09,475 P25263 [INFO] ============================================================
2020-09-25 12:40:09,475 P25263 [INFO] Test for Command 02_create_database
2020-09-25 12:40:09,481 P25263 [ERROR] Exited with error code 1
# cfn-hupはスタック更新後、実行されていない。手動更新により、ファイルが更新された。
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-hup.log
2020-09-25 11:45:37,446 [DEBUG] CloudFormation client initialized with endpoint https://cloudformation.ap-northeast-1.amazonaws.com
2020-09-25 11:45:37,447 [DEBUG] Creating /var/lib/cfn-hup/data
2020-09-25 11:45:37,452 [INFO] No umask value specified in config file. Using the default one: 022
2020-09-25 12:00:37,590 [INFO] cfn-hup processing is alive.
2020-09-25 12:15:37,706 [INFO] cfn-hup processing is alive.
2020-09-25 12:15:37,821 [INFO] Data has changed from previous state; action for cfn-auto-reloader-hook will be run
2020-09-25 12:15:37,821 [INFO] Running action for cfn-auto-reloader-hook
2020-09-25 12:30:40,854 [INFO] cfn-hup processing is alive.
2020-09-25 12:30:40,969 [INFO] Data has changed from previous state; action for cfn-auto-reloader-hook will be run
2020-09-25 12:30:40,969 [INFO] Running action for cfn-auto-reloader-hook

# 再度動かしてみる。意味はない。
[ec2-user@ip-172-31-42-181 ~]$ sudo /opt/aws/bin/cfn-init -v --stack cfn-test-stack --resource WebServerInstance --configsets InstallAndRun --region ap-northeast-1
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-init.log 
2020-09-25 12:43:49,390 [DEBUG] Test command output: 
2020-09-25 12:43:49,390 [INFO] Test failed with code 1
2020-09-25 12:43:49,390 [DEBUG] Running command 02_create_database
2020-09-25 12:43:49,390 [DEBUG] Running test for command 02_create_database
2020-09-25 12:43:49,396 [DEBUG] Test command output: 
2020-09-25 12:43:49,397 [INFO] Test failed with code 1
2020-09-25 12:43:49,397 [DEBUG] No services specified
2020-09-25 12:43:49,397 [INFO] ConfigSets completed
2020-09-25 12:43:49,397 [DEBUG] Not clearing reboot trigger as scheduling support is not available
2020-09-25 12:43:49,397 [INFO] -----------------------Build complete-----------------------
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-init-cmd.log 
2020-09-25 12:43:48,088 P25388 [INFO] ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2020-09-25 12:43:48,088 P25388 [INFO] Config Install
2020-09-25 12:43:49,382 P25388 [INFO] ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2020-09-25 12:43:49,382 P25388 [INFO] Config Configure
2020-09-25 12:43:49,383 P25388 [INFO] ============================================================
2020-09-25 12:43:49,383 P25388 [INFO] Test for Command 01_set_mysql_root_password
2020-09-25 12:43:49,390 P25388 [ERROR] Exited with error code 1
2020-09-25 12:43:49,390 P25388 [INFO] ============================================================
2020-09-25 12:43:49,390 P25388 [INFO] Test for Command 02_create_database
2020-09-25 12:43:49,396 P25388 [ERROR] Exited with error code 1
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-hup.log 
2020-09-25 11:45:37,446 [DEBUG] CloudFormation client initialized with endpoint https://cloudformation.ap-northeast-1.amazonaws.com
2020-09-25 11:45:37,447 [DEBUG] Creating /var/lib/cfn-hup/data
2020-09-25 11:45:37,452 [INFO] No umask value specified in config file. Using the default one: 022
2020-09-25 12:00:37,590 [INFO] cfn-hup processing is alive.
2020-09-25 12:15:37,706 [INFO] cfn-hup processing is alive.
2020-09-25 12:15:37,821 [INFO] Data has changed from previous state; action for cfn-auto-reloader-hook will be run
2020-09-25 12:15:37,821 [INFO] Running action for cfn-auto-reloader-hook
2020-09-25 12:30:40,854 [INFO] cfn-hup processing is alive.
2020-09-25 12:30:40,969 [INFO] Data has changed from previous state; action for cfn-auto-reloader-hook will be run
2020-09-25 12:30:40,969 [INFO] Running action for cfn-auto-reloader-hook
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-hup.log 
2020-09-25 11:45:37,446 [DEBUG] CloudFormation client initialized with endpoint https://cloudformation.ap-northeast-1.amazonaws.com
2020-09-25 11:45:37,447 [DEBUG] Creating /var/lib/cfn-hup/data
2020-09-25 11:45:37,452 [INFO] No umask value specified in config file. Using the default one: 022
2020-09-25 12:00:37,590 [INFO] cfn-hup processing is alive.
2020-09-25 12:15:37,706 [INFO] cfn-hup processing is alive.
2020-09-25 12:15:37,821 [INFO] Data has changed from previous state; action for cfn-auto-reloader-hook will be run
2020-09-25 12:15:37,821 [INFO] Running action for cfn-auto-reloader-hook
2020-09-25 12:30:40,854 [INFO] cfn-hup processing is alive.
2020-09-25 12:30:40,969 [INFO] Data has changed from previous state; action for cfn-auto-reloader-hook will be run
2020-09-25 12:30:40,969 [INFO] Running action for cfn-auto-reloader-hook

> aws ec2 describe-instances --filters Name=instance-state-name,Values=running > second-instance-before-update-user-data.txt
# UserData追記、コミット
> git checkout 3f5ae0169511dff276bb1e2a78455871a33b9c8f
> git log -p -1 3f5ae0169511dff276bb1e2a78455871a33b9c8f
diff --git a/template.yml b/template.yml
index 8da39bb..c6e4915 100644
--- a/template.yml
+++ b/template.yml
@@ -378,6 +378,11 @@ Resources:
             - !Ref 'AWS::Region'
             - |+
 
+            - |
+              # craete file from UserData
+            - 'echo test from UserData > created-by-user-data.txt '
+            - |+
+
     CreationPolicy:
       ResourceSignal:
         Timeout: PT5M
> date
2020年  9月 25日 金曜日 23:14:51 JST
> make deploy
aws cloudformation deploy --stack-name cfn-test-stack --template-file ./template.yml

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cfn-test-stack
# ホストのパブリックDNSアドレスが変わってしまった。
> make ssh HOST=ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com
ssh -i ~/.ssh/cfn-test-key.pem ec2-user@ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com
^Cmake: *** [Makefile:27: ssh] 割り込み
> make get-url
aws cloudformation describe-stacks --stack-name cfn-test-stack --query "Stacks[0].Outputs[0]"
{
    "OutputKey": "WebsiteURL",
    "OutputValue": "http://ec2-18-181-198-234.ap-northeast-1.compute.amazonaws.com",
    "Description": "URL for newly created LAMP stack"
}
> date
2020年  9月 25日 金曜日 23:17:44 JST
> make ssh HOST=ec2-18-181-198-234.ap-northeast-1.compute.amazonaws.com
ssh -i ~/.ssh/cfn-test-key.pem ec2-user@ec2-18-181-198-234.ap-northeast-1.compute.amazonaws.com
[ec2-user@ip-172-31-42-181 ~]$ sudo cat /tmp/test-cfn-update.txt
hello from cfn-hup! content has been updated! updated again!
[ec2-user@ip-172-31-42-181 ~]$ sudo cat /tmp/test-cfn-create.txt 
hello from cfn-hup!
# 気づいていなかったが、バックアップもあった。おそらく前回のcfn-hup更新時に作成されたのだろう。
[ec2-user@ip-172-31-42-181 ~]$ sudo cat /tmp/test-cfn-update.txt.bak 
hello from cfn-hup! content has been updated!
[ec2-user@ip-172-31-42-181 ~]$ cat /home/ec2-user/manual.txt 
created in instance manually.
[ec2-user@ip-172-31-42-181 ~]$ which cowsay
/usr/bin/cowsay
[ec2-user@ip-172-31-42-181 ~]$ ログアウト]
# パブリックDNS名、パブリックIDアドレスが変わった。
# ちなみにプライベートIPアドレス、およびインスタンスIDは変わっていない。
> diff second-instance-before-update-user-data.txt third-instance-after-update-user-data.txx 
12c12
<                     "LaunchTime": "2020-09-25T11:44:41+00:00",
---
>                     "LaunchTime": "2020-09-25T14:15:40+00:00",
24,25c24,25
<                     "PublicDnsName": "ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com",
<                     "PublicIpAddress": "52.192.162.51",
---
>                     "PublicDnsName": "ec2-18-181-198-234.ap-northeast-1.compute.amazonaws.com",
>                     "PublicIpAddress": "18.181.198.234",
53,54c53,54
<                                 "PublicDnsName": "ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com",
<                                 "PublicIp": "52.192.162.51"
---
>                                 "PublicDnsName": "ec2-18-181-198-234.ap-northeast-1.compute.amazonaws.com",
>                                 "PublicIp": "18.181.198.234"
80,81c80,81
<                                         "PublicDnsName": "ec2-52-192-162-51.ap-northeast-1.compute.amazonaws.com",
<                                         "PublicIp": "52.192.162.51"
---
>                                         "PublicDnsName": "ec2-18-181-198-234.ap-northeast-1.compute.amazonaws.com",
>                                         "PublicIp": "18.181.198.234"

# cfn-get-metadataも試してみる。
[ec2-user@ip-172-31-42-181 ~]$ /opt/aws/bin/cfn-get-metadata --stack=cfn-test-stack --resource=WebServerInstance --region=ap-northeast-1
{
    "AWS::CloudFormation::Init": {
        "Configure": {
            "commands": {
                "02_create_database": {
                    "test": "$(mysql testdb -u root --password='q40stBlSEjwi' >/dev/null 2>&1 </dev/null); (( $? != 0 ))",
                    "command": "mysql -u root --password='q40stBlSEjwi' < /tmp/setup.mysql"
                },
                "01_set_mysql_root_password": {
                    "test": "$(mysql testdb -u root --password='q40stBlSEjwi' >/dev/null 2>&1 </dev/null); (( $? != 0 ))",
                    "command": "mysqladmin -u root password 'q40stBlSEjwi'"
                }
            }
        },
        "Install": {
            "files": {
(略)

# intervalの最小値はいくらか
# intervalをデフォルトの15から、1を明示的に指定した。毎分確認されるか?
> git checkotut 270ca343aa48401630c85490bc4b41e58f20dc80
> git log -p -1
diff --git a/template.yml b/template.yml
index c6e4915..d92d2d0 100644
--- a/template.yml
+++ b/template.yml
@@ -270,6 +270,9 @@ Resources:
                   - !Ref 'AWS::Region'
                   - |+
 
+                  - interval=1
+                  - |+
+
               mode: '000400'
               owner: root
               group: root

> make deploy
aws cloudformation deploy --stack-name cfn-test-stack --template-file ./template.yml
[ec2-user@ip-172-31-42-181 ~]$ sudo cat /etc/cfn/cfn-hup.conf
[main]
stack=arn:aws:cloudformation:ap-northeast-1:491846999241:stack/cfn-test-stack/76ce5280-ff24-11ea-be1e-0e7ed7880c96
region=ap-northeast-1
interval=1
# 5分ごとに確認されている。これが最短なのかもしれない。
[ec2-user@ip-172-31-42-181 ~]$ tail /var/log/cfn-hup.log 
2020-09-26 07:11:56,103 [INFO] cfn-hup processing is alive.
2020-09-26 07:16:56,682 [INFO] cfn-hup processing is alive.
2020-09-26 07:21:57,261 [INFO] cfn-hup processing is alive.
2020-09-26 07:26:57,889 [INFO] cfn-hup processing is alive.
2020-09-26 07:31:58,468 [INFO] cfn-hup processing is alive.
2020-09-26 07:36:59,046 [INFO] cfn-hup processing is alive.
2020-09-26 07:41:59,625 [INFO] cfn-hup processing is alive.
2020-09-26 07:47:00,203 [INFO] cfn-hup processing is alive.
2020-09-26 07:52:00,782 [INFO] cfn-hup processing is alive.
2020-09-26 07:57:01,361 [INFO] cfn-hup processing is alive.
[ec2-user@ip-172-31-42-181 ~]$ 

AWS 認定ソリューションアーキテクト – アソシエイト に合格しました

概要

  1. 自分のAWS習熟度
    • クラウドプラクティショナーは取得済み、過去案件でEC2くらいはさわったことあり
  2. 使用した教材
    • Udemyコース
  3. 勉強期間
    • 3週間
  4. 勉強時間
    • おそらく40時間程度
  5. 得点
    • 778/1000 (正解率72%以上で合格)

経緯

会社より、「アソシエイト」レベルのAWS技術認定1取得を推奨されました。 おそらく、APN(aws partner network)の「ティア」ランクアップの知識要件を満たすためと推測します。

勉強開始時点の自分のAWS習熟度

AWS 認定クラウドプラクティショナー は2020/03/01に取得しています。スコアは874/1000でした。合格に必要な最低スコアは700です。 当時、どのような勉強をしたかは忘れてしまいました… 少なくとも、2020/02/23に模擬試験を受けていたようです。

実務では、ひとつの案件で、EC2, S3, CloudWatch, CloudFormationを使ってシステムを構築したことがあります。

受験対象認定を選択

私の現在の職務内容と関連しそうなため、「デベロッパー - アソシエイト」に興味を持ちました。いっぽう、巷の教材や合格体験記は「ソリューションアーキテクト - アソシエイト」が多く、合格しやすそうに見えました。また、私の場合、職場に「ソリューションアーキテクト - アソシエイト」取得者のかたがいて、教材を借りたり、質問ができる状況でした。

「デベロッパー - アソシエイト」は教材が少なかったです。デベロッパー認定専用の試験対策本は見つかりませんでしたし、AWS Web問題集(AWS WEB問題集で学習しよう)を軽く拝見した感じ、問題量がアーキテクトの1/5程度しかないようでした。(130問:27問 2020/06/29現在)

受験を真剣に検討し始めた時点で、1か月以内に取得してほしいという要望を会社より受けていました。 早急な取得が求められていることを重視し、今回は「ソリューションアーキテクト - アソシエイト」を選択しました。

教材選び

教材を選びます。ざっと調べ、以下の教材が見つかりました。

  1. Exam Readiness: AWS Certified Solutions Architect – Associate (Digital)
  2. CQ: The AWS Certification Quiz Show | Episode 01
  3. AWS Solutions Architect - Associate Practice
  4. AWS 認定ソリューションアーキテクト – アソシエイト AWS Certified Solutions Architect – Associate (SAA-C02) 試験問題サンプル
  5. AWS Black Belt Online Seminar
  6. WHIZLABS
  7. 徹底攻略 AWS認定 ソリューションアーキテクト – アソシエイト教科書
  8. AWS認定資格試験テキスト AWS認定 ソリューションアーキテクト-アソシエイト
  9. これだけでOK! AWS 認定ソリューションアーキテクト – アソシエイト試験突破講座(SAA-C02試験対応版)
  10. 【SAA-C02版】AWS 認定ソリューションアーキテクト アソシエイト模擬試験問題集(6回分390問)
  11. AWS WEB問題集で学習しよう

計画

以下、HはHourの略です。かかる時間の見積です。

6/1より勉強開始しました。6月中に合格するのが目標でした。

  1. AWS 認定ソリューションアーキテクト – アソシエイト AWS Certified Solutions Architect – Associate (SAA-C02) 試験問題サンプル
    • 0.5H
  2. CQ: The AWS Certification Quiz Show | Episode 01
    • 6H
  3. Exam Readiness: AWS Certified Solutions Architect – Associate (Digital)
    • 2H
  4. AWS Solutions Architect - Associate Practice
    • 6/6
  5. AWS WEB問題集で学習しよう
    • 時間の許す限り
  6. AWS 認定ソリューションアーキテクト – アソシエイト 本試験
    • 6/14
  7. 余裕があれば以下の補助教材を使う
    • Whizlabs
    • BlackBelt
    • 書籍
    • Udemy
      • 22.5H

いつものことながら、当初の予定通りには進みませんでした…

実際の勉強内容

  1. AWS 認定ソリューションアーキテクト – アソシエイト AWS Certified Solutions Architect – Associate (SAA-C02) 試験問題サンプル
    • 0.5H
  2. CQ: The AWS Certification Quiz Show | Episode 01
    • 1H
    • 英語だと頭に入ってきづらいと気づきました。日本語教材を使うことにしました。
  3. これだけでOK! AWS 認定ソリューションアーキテクト – アソシエイト試験突破講座(SAA-C02試験対応版)
    • たぶん30H程度
    • 動画での学習をやったことがなかったので、試す目的もあり選択
    • 講義動画を2倍速で視聴
    • ハンズオンは8割程度実施。最後のほうは疲れて飛ばしました。
    • 都度気になることをGoogle検索
  4. 【SAA-C02版】AWS 認定ソリューションアーキテクト アソシエイト模擬試験問題集(6回分390問)
    • 貴重な模擬問題
    • 試験形式になっているのもありがたいです
  5. AWS Solutions Architect - Associate Practice
    • 6/14 実施
    • Udemy講座に変更したため、受験を予定より一週間遅らせました
  6. AWS 認定ソリューションアーキテクト – アソシエイト 本試験
    • 6/22
  7. 補助教材
    • 徹底攻略 AWS認定 ソリューションアーキテクト – アソシエイト教科書
    • AWS認定資格試験テキスト AWS認定 ソリューションアーキテクト-アソシエイト
    • Udemy動画講座、模擬試験の解説を見てもピンと来ない個所を適宜調べる形で使いました。でも総計10分も読んでいないと思います。

試験結果

本章は、ほとんどの方は読まなくていいと思います。

受験予定者の進捗比較にお使いいただければと思い、詳細を記録します。

(正解率72%以上で合格)

  1. AWS 認定ソリューションアーキテクト – アソシエイト AWS Certified Solutions Architect – Associate (SAA-C02) 試験問題サンプル
    • 40%
  2. これだけでOK! AWS 認定ソリューションアーキテクト – アソシエイト試験突破講座(SAA-C02試験対応版)
    1. 演習テスト1: 模擬試験
      • 6/12
      • 69%
    2. 演習テスト2: 模擬試験
      • 6/12
      • 73%
  3. AWS Solutions Architect - Associate Practice
    • 6/14
    • 75%
  4. 【SAA-C02版】AWS 認定ソリューションアーキテクト アソシエイト模擬試験問題集(6回分390問)
    1. 演習テスト1: 基本問題の模擬試験①
      • 6/15
      • 73%
    2. 演習テスト2: 高難易度の模擬試験①
      • 6/16
      • 60%
    3. 演習テスト3: 高難易度の模擬試験①
      • 6/17
      • 76%
    4. 演習テスト4: 高難易度の模擬試験①
      • 6/18
      • 73%
    5. 演習テスト5: 高難易度の模擬試験①
      • 6/19
      • 58%
    6. 演習テスト6: 02版向け追加模擬試験⑥
      • 6/19
      • 53%
  5. AWS 認定ソリューションアーキテクト – アソシエイト 本試験
    • 6/22
    • 778/1000

感想

仕事では、疑問点をピンポイントで調べて、納得したら終わり、という形で勉強することが多いです。興味ないことも含めて勉強する、ということがちょっと辛かったです。

はじめて本格的に動画で学習しました。Udemy講義動画コース修了後、黒本を数ページ見たとき、情報のカラフルさが違う、と感じました。動画のほうが頭を使わなくても情報が入ってきます。疑似体験のレベルが違う、と感じました。コンソール操作とか。百聞は一見に如かず、ということでしょうか。

ただし問題で細部を問われるとわからないことが多々ありました。詳細の把握というより、全体像をつかむのによさそうと思いました。

10数時間動画を見たあたりから、見続けることに慣れてきました。それまでは数時間観ると限界でした。学習効率がどう変わったかは不明です。

自分が準備開始時点に戻って、もう一度勉強するならばどの教材を選択してどう勉強するか

  1. AWS Solutions Architect - Associate Practice
    • 0.5H
  2. これだけでOK! AWS 認定ソリューションアーキテクト – アソシエイト試験突破講座(SAA-C02試験対応版)
    • 11H
    • 講義動画を2倍速で視聴
    • 脳内にインデックスを作っておく
    • ハンズオンは飛ばす。別途自分でシステム構築することでカバーする。
  3. Udemy模擬試験
    • 14H
    • やる。
    • 試験対策は問題を解くに限ります。
    • 一時間で解き、一時間で復習する。7回ぶんあるので14H。
  4. 【SAA-C02版】AWS 認定ソリューションアーキテクト アソシエイト模擬試験問題集(6回分390問)
    • 一応やる。申し込み手順、試験方式に慣れておくことが目的。
    • Udemy模擬試験と比べると、問題のコストパフォーマンスが悪い。
  5. 自分でシステム構築してみる
    • なにか適当なWebアプリケーションを用意して、アーキテクチャ設計および構築をやってみる。
    • 目的意識(Webアプリケーションを運用する)を持って構築する
    • ハマるところもあるかもしれません。しかし、そういう勉強方法の方がはるかに頭に残ると思います。

  1. 認定一覧は、 AWS認定 の「Available AWS Certifications」 がわかりやすかったです。

git-flowでのフレキシブルなリリースについて

問題意識

git-flowでリポジトリを運用しているプロジェクトで、開発済み機能の、リリース時期、リリース対象機能をフレキシブルに選択できるようにしたい。

たとえば、機能Aと機能Bを開発中で、機能Aは来週リリース、機能Bは他社連携先機能のリリースに合わせたいとする。他社のスケジュールによって、機能Bが機能のリリースに前後したり、一緒になったりする、といったケースに対応したい。

いまの弊社プロジェクトのgit-flowの運用だと、すべての開発済みコミットをdevelopブランチに集約しているので、リリース時に対象機能のコミットを、人力でピックアップする羽目になっている。これはダサい。

注釈含め、以下と同じ気持ちだ。

追記 なぜdevelopブランチが複数あるのかという疑問が散見されたので,我々のケースについて書いておきます ざっくり言ってしまえばリリースサイクルの違いです.例えば大きめの新機能を実装しつつ既存コードの保守もするという時,その時点で稼働しているコード (master) から派生したdevelop/A (便宜上Aとします) の上で新機能の実装を入れてしまうと,保守のコードと新機能コードが混ざってしまい,保守のためのデプロイのタイミングで出て欲しくない新機能のコードまで露出してしまうことになるので,それを防ぐ (分離させる) という目的でdevelopを複数に分ける (例えばdevelop/Bで新機能の開発は行なう) というような形にしています.保守の為のdevelopブランチでも,新機能の為のdevelopでも,どちらも独立したステージング環境では見ておきたいので……

(中略)

*1:git-flowの解説ではdevelopブランチが複数存在している場合についてあまり言及されてない気がする……

moznion.hatenadiary.com

ググってみる

上の「問題意識」で引用した記事では、developを機能別に複数作ればいいという意見だ。以降、「複数developブランチパターン」と呼ぶことにする。

他にも、featureからfeatureを切ればいいという意見がある。「featureブランチネストパターン」と呼ぶことにする。

stackoverflow.com

比較検討

上の2つのやりかたに加え、弊社の現状の運用方法(「cherry-pickパターン」と呼ぶことにする)を加えた、3つの運用方法を比較検討する。

複数developブランチパターン

詳細

develop/x.x.x という命名規則でdevelopブランチを切る(x.x.xはバージョン番号)。リリース時は、対応するバージョンのdevelpブランチをベースに、featureブランチを切る。

感想

どのdevelopブランチになんの機能変更が入ったか、わからなくなりそう?

自社のプロジェクトの場合、リリースの順番が定まらないまま開発が始まったり、開発開始してから順番が変更になったりすることが多い。リリースごとに、バージョン番号が増加するとは限らない。わかりづらいので、バージョン番号を命名規約にすることは避けたい。追加内容別にブランチを切ればいいか。

複数のdevelopブランチをまとめてリリースしたいとき、とりまとめはどのブランチで行えばいいか?releaseブランチで行うのが適当か?

featureブランチネストパターン

詳細

大きな機能を開発開始するとき、featureブランチを切る。大きな機能のサブタスクを開発開始するとき、featureブランチからさらにfeatureブランチを切る。

感想

featureブランチはリリース直前までdevelopブランチにマージしない、ということになるのかな?大きな機能追加/変更時は、まずdevelopからfeatureブランチを切り、そのfeatureブランチからサブタスクごとにfeatureブランチをネストして切る。一段目のfeatureブランチが、従来のdevelopブランチになるイメージ。

一段目のfeatureブランチを切る粒度は、リリースの選択対象となるように。 二段目のfeatureブランチは、従来通りで良い。弊社ならRedmineのチケット単位。

リリース時、リリースに含まれる変更内容がわかりやすくなりそう。リリースノートが作りやすそう。 しかし、孫featureの親featureがどれか、わからなくなりそうか?命名規約で対処する?いや、切った本人は覚えているか。孫featureは基本的に開発担当者のローカルPCで完結するので、本人が覚えていれば十分。

hotfixするほどでないちょっとしたバグ修正はdevelopに入れてしまう?もしくは,feature/bugfixブランチを切る?後者のほうが、統一されてわかりやいかな。

cherry-pickパターン

現状。開発はRedmineのチケット単位(1日〜2,3日で終わるくらい)の粒度でdevelopからfeatureブランチを切り、都度developにマージする。 リリース時は、releaseブランチをmasterから切って、リリース対象機能と開発の順番がたまたま合っていれば、まだリリースしない開発の開始コミット直前までマージする。そうでなければ、リリース対象機能のコミットをdevelopブランチからcherry-pickする。

感想

masterとdevelopとの差分が把握しづらい。 マージした場合、リリース内容が把握しづらい。cherry-pickする場合、ピックするかどうかの判断が大変。

まとめ

機能を二段階の粒度で捉えることにする。 リリース時に取捨選択の対象になるような、大きな粒度の機能を「大きな機能」と呼ぶ。(例: ブログ機能の追加) 大きな機能のサブタスクを「小さな機能」と呼ぶ。(例: ブログへのいいね機能の追加)

ようするに、大きな機能単位でコミットを管理したい。 また、リリース時に含める大きな機能を、フレキシブルに選択したい。

cherry-pickパターンはどちらもできないのでボツ。 複数ブランチパターンは、リリース時の対象選択や、他developブランチへのバックポートの手順が煩雑そう。つまり、git-flowプラグインのコマンドひとつではできなさそう。 featureブランチネストパターンなら、コミット管理も、対象選択も簡単そうだ。採用。

結論

featureブランチネストパターンを採用する。

大きな機能を開発開始するときは、developブランチからfeatureブランチを切る。 今後、大きな機能をのfeatureブランチは、開発完了しても、リリース前までdevelopにはマージしないようにする。 リリース時に、大きな機能のfeatureブランチから含める機能をマージする。

git flow feature start <大きな機能>
git flow feature start <小さな機能> feature/<大きな機能>

でブランチを切れば、ネストしたfeatureブランチが切れる。

git flow feature finish <小さな機能>

で、うまく分岐元のfeature/<大きな機能>に取り込まれる。

ちょっとしたバグフィックスなども、直接developに入れるのではなく、feature/bugfix などのブランチを切ってい、そこにコミットしたほうが管理しやすいかと思う。

ネストしたfeatureブランチの動作確認

git-flowプラグインはnvie版とavh版がある。以下,avh版では期待通り動作することを確認した。

github.com

nvie版はプルリクエストはあるが、放置されている模様。

github.com

Welcome to fish, the friendly interactive shell
~ $ mkdir nested-features
~ $ cd nested-features/
~/nested-features $ git init
Initialized empty Git repository in /home/matsui/nested-features/.git/
~/nested-features (master|✔) $ git flow version
1.11.0 (AVH Edition)
~/nested-features (master|✔) $ git --version
git version 2.18.0
~/nested-features (master|✔) $ git flow init
No branches exist yet. Base branches must be created now.
Branch name for production releases: [master]
Branch name for "next release" development: [develop]

How to name your supporting branch prefixes?
Feature branches? [feature/]
Bugfix branches? [bugfix/]
Release branches? [release/]
Hotfix branches? [hotfix/]
Support branches? [support/]
Version tag prefix? []
Hooks and filters directory? [/home/matsui/nested-features/.git/hooks]
~/nested-features (develop|✔) $ echo base change > base.txt
~/nested-features (develop|…) $ git add .
~/nested-features (develop|●1) $ git commit -m "[add] base.txt"
[develop 16e1e67] [add] base.txt
 1 file changed, 1 insertion(+)
 create mode 100644 base.txt
~/nested-features (develop|✔) $ git flow feature start big-feature
Switched to a new branch 'feature/big-feature'

Summary of actions:
- A new branch 'feature/big-feature' was created, based on 'develop'
- You are now on branch 'feature/big-feature'

Now, start committing on your feature. When done, use:

     git flow feature finish big-feature

~/nested-features (feature/big-feature|✔) $
echo new small feature > small-feature.txt
~/nested-features (feature/big-feature|…) $ git add .
~/nested-features (feature/big-feature|●1) $
git commit -m "[add] small-feature.txt"
[feature/big-feature f1b0032] [add] small-feature.txt
 1 file changed, 1 insertion(+)
 create mode 100644 small-feature.txt
~/nested-features (feature/big-feature|✔) $
git flow feature start another-small-feature feature/big-feature
Switched to a new branch 'feature/another-small-feature'

Summary of actions:
- A new branch 'feature/another-small-feature' was created, based on 'feature/big-feature'
- You are now on branch 'feature/another-small-feature'

Now, start committing on your feature. When done, use:

     git flow feature finish another-small-feature

~/nested-features (feature/another-small-feature|✔) $
git log --graph --one-line
fatal: unrecognized argument: --one-line
~/nested-features (feature/another-small-feature|✔) $
git log --graph --all --oneline
* f1b0032 (HEAD -> feature/another-small-feature, feature/big-feature) [add] small-feature.txt
* 16e1e67 (develop) [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (feature/another-small-feature|✔) $ git log --graph --all --oneline
* f1b0032 (HEAD -> feature/another-small-feature, feature/big-feature) [add] small-feature.txt
* 16e1e67 (develop) [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (feature/another-small-feature|✔) $
echo another small feature > another-small-feature.txt
~/nested-features (feature/another-small-feature|…) $ git add .
~/nested-features (feature/another-small-feature|●1) $ git commit
Aborting commit due to empty commit message.
~/nested-features (feature/another-small-feature|●1) $ git commit -m "[add] another-small-feature.txt"
[feature/another-small-feature fe5c067] [add] another-small-feature.txt
 1 file changed, 1 insertion(+)
 create mode 100644 another-small-feature.txt
~/nested-features (feature/another-small-feature|✔) $ git log --graph --all --oneline
* fe5c067 (HEAD -> feature/another-small-feature) [add] another-small-feature.txt
* f1b0032 (feature/big-feature) [add] small-feature.txt
* 16e1e67 (develop) [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (feature/another-small-feature|✔) $ git flow feature finish
Switched to branch 'feature/big-feature'
Updating f1b0032..fe5c067
Fast-forward
 another-small-feature.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 another-small-feature.txt
Deleted branch feature/another-small-feature (was fe5c067).

Summary of actions:
- The feature branch 'feature/another-small-feature' was merged into 'feature/big-feature'
- Feature branch 'feature/another-small-feature' has been locally deleted
- You are now on branch 'feature/big-feature'

~/nested-features (feature/big-feature|✔) $ git log --graph --all --oneline
* fe5c067 (HEAD -> feature/big-feature) [add] another-small-feature.txt
* f1b0032 [add] small-feature.txt
* 16e1e67 (develop) [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (feature/big-feature|✔) $ # 期待通り
~/nested-features (feature/big-feature|✔) $ # developにはマージされていない。
~/nested-features (feature/big-feature|✔) $ git checkout develop
Switched to branch 'develop'
~/nested-features (develop|✔) $ git flow feature start anohter-big-feature
Switched to a new branch 'feature/anohter-big-feature'

Summary of actions:
- A new branch 'feature/anohter-big-feature' was created, based on 'develop'
- You are now on branch 'feature/anohter-big-feature'

Now, start committing on your feature. When done, use:

     git flow feature finish anohter-big-feature

~/nested-features (feature/anohter-big-feature|✔) $ echo another big feature > another-big-feature.txt
~/nested-features (feature/anohter-big-feature|…) $ git add .
~/nested-features (feature/anohter-big-feature|●1) $ git commit -m "[add] another-big-feature.txt"
[feature/anohter-big-feature b47cd48] [add] another-big-feature.txt
 1 file changed, 1 insertion(+)
 create mode 100644 another-big-feature.txt
~/nested-features (feature/anohter-big-feature|✔) $ git log --graph --all --oneline
* b47cd48 (HEAD -> feature/anohter-big-feature) [add] another-big-feature.txt
| * fe5c067 (feature/big-feature) [add] another-small-feature.txt
| * f1b0032 [add] small-feature.txt
|/
* 16e1e67 (develop) [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (feature/anohter-big-feature|✔) $ git checkout feature/big-feature
Switched to branch 'feature/big-feature'
~/nested-features (feature/big-feature|✔) $ git branch
  develop
  feature/anohter-big-feature
* feature/big-feature
  master
~/nested-features (feature/big-feature|✔) $ git flow feature finish
Switched to branch 'develop'
Merge made by the 'recursive' strategy.
 another-small-feature.txt | 1 +
 small-feature.txt         | 1 +
 2 files changed, 2 insertions(+)
 create mode 100644 another-small-feature.txt
 create mode 100644 small-feature.txt
Deleted branch feature/big-feature (was fe5c067).

Summary of actions:
- The feature branch 'feature/big-feature' was merged into 'develop'
- Feature branch 'feature/big-feature' has been locally deleted
- You are now on branch 'develop'

~/nested-features (develop|✔) $ git log --graph --all --oneline
*   ef90ebe (HEAD -> develop) Merge branch 'feature/big-feature' into develop
|\
| * fe5c067 [add] another-small-feature.txt
| * f1b0032 [add] small-feature.txt
|/
| * b47cd48 (feature/anohter-big-feature) [add] another-big-feature.txt
|/
* 16e1e67 [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (develop|✔) $ git checkout feature/anohter-big-feature
Switched to branch 'feature/anohter-big-feature'
~/nested-features (feature/anohter-big-feature|✔) $ git checkout develop
Switched to branch 'develop'
~/nested-features (develop|✔) $ ls
another-small-feature.txt  base.txt  small-feature.txt
~/nested-features (develop|✔) $ git checkout feature/anohter-big-feature
Switched to branch 'feature/anohter-big-feature'
~/nested-features (feature/anohter-big-feature|✔) $ git flow feature finish
Switched to branch 'develop'
Merge made by the 'recursive' strategy.
 another-big-feature.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 another-big-feature.txt
Deleted branch feature/anohter-big-feature (was b47cd48).

Summary of actions:
- The feature branch 'feature/anohter-big-feature' was merged into 'develop'
- Feature branch 'feature/anohter-big-feature' has been locally deleted
- You are now on branch 'develop'

~/nested-features (develop|✔) $ git log --graph --all --oneline
*   d581427 (HEAD -> develop) Merge branch 'feature/anohter-big-feature' into develop
|\
| * b47cd48 [add] another-big-feature.txt
* |   ef90ebe Merge branch 'feature/big-feature' into develop
|\ \
| |/
|/|
| * fe5c067 [add] another-small-feature.txt
| * f1b0032 [add] small-feature.txt
|/
* 16e1e67 [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (develop|✔) $

fish shell cdしたらlsしたい

確認環境

$ fish --version
fish, version 2.7.1

結論

設定ファイル(例えば~/.config/fish/config.fish)に、以下を追記する。

functions --copy cd standard_cd

function cd
  standard_cd $argv; and ls
end

説明

fish shellには2つのcdが存在する。ひとつはビルトインのcd。もうひとつは、ビルトインのcdをラップした、関数のcd。関数のcdは、おもに過去のディレクトリへの移動機能が加わっている。cd -など。

普通にcdすると、関数のほうが呼ばれる。

今回の変更では、関数のcdをさらにラップし、実行後にlsを行うようにした。

調査メモ

ググってみる。以下の2つの記事が、具体例付きで参考になりそう。

前者は、追記するコードが長い。長いということは、内容を把握するのに時間がかかる。そして自分の理解が間違っていないか不安になる。デフォルト設定に加え、lsを加えた関数を定義しているとのこと。元ネタのデフォルト設定はどこにあるんだろう?またこれだと、デフォルト設定のほうが改良されても、自動で反映されないのでは?(記事を書いてくれた方、ネガティブなことばかり書いてごめんなさい)

後者は短い。ぱっと見、自分のやりたいことにマッチしているように見える。これにしようかな? しかし、試してみると、cd - が機能しない。 というか、cd -のほかにもfish cdならではの便利機能があって、見落としてるとかはないか?その便利機能が、上記のデフォルト設定な気もする。

fishのいい感じの配慮を活かしつつ、lsを追加したい。 かつ、長い設定を書きたくない。だからfishを使ってるんです。

cd -が効かない件についてググってみる。

https://github.com/fish-shell/fish-shell/issues/4869 このisssueだ。

知りたいことは以下のコメントにすべて書いてあった。ありがとうkrader1961さん。

dirh, prevd, and nextd do not work as usual after `builtin cd` · Issue #4869 · fish-shell/fish-shell · GitHub

このコメントの方法に従うと、cd -も行ける。

コメントによると、fishでは、builtin cd をラップするfunction cdを用意しているらしい。その関数のおかけでcd -が機能するとのこと。 以下、man cdからの抜粋。

Fish also ships a wrapper function around the builtin cd that understands cd - as changing to the previous directory. See also prevd. This wrapper function maintains a history of the 25 most recently visited directories in the $dirprev and $dirnext global variables. If you make those universal variables your cd history is shared among all fish instances.

上のGithubのコメントによると、type cdで関数のファイルの場所と処理内容が確認可能とのこと。 関数での処理内容はざっと見た感じ、移動履歴の管理関係。 引数の個数による例外処理、サブシェル時はビルトインcdを起動する、 cd -で前のディレクトリへ戻る(しかも戻って戻るともとにもどる)、 戻るじゃないときは履歴に追加(cdhなどで移動できる)、など。

上で参考にした、[fish-shell] ご注文はcdしたら自動でlsですか?で言うデフォルト設定とは、function cdの処理内容の模様。

調査まとめ

追加調査メモ

builtin cdだとcdhに記憶されない。cd - も効かない。確認してみる。

Welcome to fish, the friendly interactive shell
~ $ cdh
No previous directories to select. You have to cd at least once.
~ $ builtin cd ~/Documents/work/
~/D/work $ cd -
Hit end of history…
~/D/work $ cdh
No previous directories to select. You have to cd at least once.

function cdだと履歴が記録される。cd -でひとつ前にもどれる。確認してみる。

Welcome to fish, the friendly interactive shell
~ $ cdh
No previous directories to select. You have to cd at least once.
~ $ cd Documents/
~/Documents $ cd ~/Downloads/
~/Downloads $ cdh
 b  2)  ~
 a  1)  ~/Documents
Select directory by letter or number:
~/Downloads $ cd -
~/Documents $

以上

Rails4, Rails5の時間の足し算の挙動の違い

経緯

Rails4で、日付の足し算の挙動が自分の期待と異なっていたので調査した。 1/31の翌月の翌月も、1/31の二ヶ月後も、どちらも2018/3/31になるのを期待していたが、実際の結果は異なった。 前者は「1/31の翌月の翌月」 => 「2/28の翌月」 => 「3/28」と処理されているもよう。

Loading development environment (Rails 4.2.10)
irb(main):001:0> 1.month + 1.month == 2.month
=> true
irb(main):002:0> Date.new(2018, 1, 31) + (1.month + 1.month)
=> Wed, 28 Mar 2018
irb(main):003:0> Date.new(2018, 1, 31) + (2.month)
=> Sat, 31 Mar 2018

後に、Rails5でも確認したところ、挙動が変更され、自分の期待と同様に動くことを確認した。

結論

Rails5なら、自分の期待通りに動く。 Rails4のときは、 Date.new + x.month + y.momnthDate.new + (x + y).month は計算結果が異なることがあるので、足し方に注意。

調査環境

Rails4, Rails5での挙動は、下記の環境で確認した。

# Rails4環境
~/t/rails4-sandbox (master|✔) $ docker-compose exec web rails --version
Rails 4.2.10
~/t/rails4-sandbox (master|✔) $ docker-compose exec web ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
~/t/rails4-sandbox (master|✔) $ docker-compose exec db  psql -U postgres -c "SELECT version()"
                                         version

--------------------------------------------------------------------------------
----------
 PostgreSQL 9.6.5 on x86_64-pc-linux-gnu, compiled by gcc (Debian 4.9.2-10) 4.9.
2, 64-bit
(1 row)
# Rails5環境
~/t/rails-sandbox (master|✔) $ docker-compose exec web rails --version
Rails 5.2.1
~/t/rails-sandbox (master|✔) $ docker-compose exec web ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
~/t/rails-sandbox (master|✔) $ docker-compose exec db  psql -U postgres -c "SELECT version()"
                                         version

------------------------------------------------------------------------------------------
 PostgreSQL 9.6.5 on x86_64-pc-linux-gnu, compiled by gcc (Debian 4.9.2-10) 4.9.2, 64-bit
(1 row)

調査メモ

問題の挙動は下記の通り。 どちらも「二ヶ月後」の3/31になると期待していた。

Loading development environment (Rails 4.2.10)
irb(main):001:0> 1.month + 1.month == 2.month
=> true
irb(main):002:0> Date.new(2018, 1, 31) + (1.month + 1.month)
=> Wed, 28 Mar 2018
irb(main):003:0> Date.new(2018, 1, 31) + (2.month)
=> Sat, 31 Mar 2018

Rails5では自分の期待通りに動くようになっている。

Loading development environment (Rails 5.2.1)
irb(main):001:0> 1.month + 1.month == 2.month
=> true
irb(main):002:0> Date.new(2018, 1, 31) + (1.month + 1.month)
=> Sat, 31 Mar 2018
irb(main):003:0> Date.new(2018, 1, 31) + (2.month)
=> Sat, 31 Mar 2018
irb(main):004:0>

Rails4での、ActiveRecord::Durationどうしの足し算の処理方法は、下記の通り。

# https://github.com/rails/rails/blob/4-2-stable/activesupport/lib/active_support/duration.rb#L16-L24
    # Adds another Duration or a Numeric to this Duration. Numeric values
    # are treated as seconds.
    def +(other)
      if Duration === other
        Duration.new(value + other.value, @parts + other.parts)
      else
        Duration.new(value + other, @parts + [[:seconds, other]])
      end
    end

Rails4では、Durationどうしの足し算は、@parts部分は、じつは足していない。配列で足すべき要素を覚えているだけ。試しに実行してみると以下のようになる。

Loading development environment (Rails 4.2.10)
irb(main):001:0> (1.month + 1.month).parts
=> [[:months, 1], [:months, 1]]
irb(main):002:0> 2.month.parts
=> [[:months, 2]]

Rails5のDurationどうしの足し算処理の内容は下記の通り。@parts部分はHash。自分の期待通りに足している。

# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/duration.rb#L234-L247
    # Adds another Duration or a Numeric to this Duration. Numeric values
    # are treated as seconds.
    def +(other)
      if Duration === other
        parts = @parts.dup
        other.parts.each do |(key, value)|
          parts[key] += value
        end
        Duration.new(value + other.value, parts)
      else
        seconds = @parts[:seconds] + other
        Duration.new(value + other, @parts.merge(seconds: seconds))
      end
    end

実行結果。

Loading development environment (Rails 5.2.1)
irb(main):001:0> (1.month + 1.month).parts
=> {:months=>2}
irb(main):002:0> 2.month.parts
=> {:months=>2}

Date + Durationの処理は、Duration#sumが呼ばれる。

parts.injectで、足す単位(seconds, minutesなど)に応じてDate#advance || Date#sinceしている。

minutes, hoursの場合,Rails4ではadvanceだったのが、Rails5では秒換算でsinceされる。なぜだろう?

advenceされるのは、後のadvanceのコードを見ればわかるとおり、years, months, weeks, daysの場合だ。

# Rails4
# https://github.com/rails/rails/blob/3804d017333da16d76d9fc6633faf5635c7b03d7/activesupport/lib/active_support/duration.rb#L133-L145
      def sum(sign, time = ::Time.current) #:nodoc:
        parts.inject(time) do |t,(type,number)|
          if t.acts_like?(:time) || t.acts_like?(:date)
            if type == :seconds
              t.since(sign * number)
            else
              t.advance(type => sign * number)
            end
          else
            raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
          end
        end
      end
# Rails5
# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/duration.rb#L402-L418
      def sum(sign, time = ::Time.current)
        parts.inject(time) do |t, (type, number)|
          if t.acts_like?(:time) || t.acts_like?(:date)
            if type == :seconds
              t.since(sign * number)
            elsif type == :minutes
              t.since(sign * number * 60)
            elsif type == :hours
              t.since(sign * number * 3600)
            else
              t.advance(type => sign * number)
            end
          else
            raise ::ArgumentError, "expected a time or date, got #{time.inspect}" 
          end
        end
      end

Date#sinceは以下の処理。Rails4もRails5も同じ。

# https://github.com/rails/rails/blob/3804d017333da16d76d9fc6633faf5635c7b03d7/activesupport/lib/active_support/core_ext/date/calculations.rb#L57-L62
# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/core_ext/date/calculations.rb#L59-L64
  # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
  # and then adds the specified number of seconds
  def since(seconds)
    in_time_zone.since(seconds)
  end
  alias :in :since

。。 Date#advanceは以下の処理。Rails4もRails5も同じ。

# https://github.com/rails/rails/blob/3804d017333da16d76d9fc6633faf5635c7b03d7/activesupport/lib/active_support/core_ext/date/calculations.rb#L108-L118
# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/core_ext/date/calculations.rb#L110-L120
def advance(options)
  options = options.dup
  d = self
  d = d >> options.delete(:years) * 12 if options[:years]
  d = d >> options.delete(:months)     if options[:months]
  d = d +  options.delete(:weeks) * 7  if options[:weeks]
  d = d +  options.delete(:days)       if options[:days]
  d
end

秒数換算(1ヶ月30日)の@valuesと、seconds, months, weeks, daysなどの人間に優しい値?の@partsを持っている。

Loading development environment (Rails 4.2.10)
irb(main):001:0> 1.month.instance_variables
=> [:@value, :@parts]
irb(main):002:0> 1.month.instance_variable_get(:@value)
=> 2592000
irb(main):003:0> 1.month.instance_variable_get(:@parts)
=> [[:months, 1]]
irb(main):004:0> (1.month + 2.days + 3.month).instance_variable_get(:@value)
=> 10540800
irb(main):005:0> (1.month + 2.days + 3.month).instance_variable_get(:@parts)
=> [[:months, 1], [:days, 2], [:months, 3]]
irb(main):006:0> d = 1.month + 1.month.value
=> 1 month and 2592000 seconds
irb(main):007:0> 1.month.instance_variables
=> [:@value, :@parts]
irb(main):008:0> d.instance_variable_get(:@value)
=> 5184000
irb(main):009:0> d.instance_variable_get(:@parts)
=> [[:months, 1], [:seconds, 2592000]]
irb(main):010:0> d.value
=> 5184000
Loading development environment (Rails 5.2.1)
rb(main):001:0> 1.month.instance_variables
=> [:@parts, :@value]
irb(main):002:0> 1.month.instance_variable_get(:@value)
=> 2629746
irb(main):003:0> 1.month.instance_variable_get(:@parts)
=> {:months=>1}
irb(main):004:0> (1.month + 2.days + 3.month).instance_variable_get(:@value)
=> 10691784
irb(main):005:0> (1.month + 2.days + 3.month).instance_variable_get(:@parts)
=> {:months=>4, :days=>2}
irb(main):006:0> d = 1.month + 1.month.value
=> 1 month and 2629746 seconds
irb(main):007:0> 1.month.instance_variables
=> [:@parts, :@value]
irb(main):008:0> d.instance_variable_get(:@value)
=> 5259492
irb(main):009:0> d.instance_variable_get(:@parts)
=> {:months=>1, :seconds=>2629746}
irb(main):010:0> d.value
=> 5259492

1.yearは360日でも365日でもない。365.25日。1.yearと1.monthのvalue換算の値の大きさが、Rails4とRails5で異なっている。

Loading development environment (Rails 4.2.10)
irb(main):001:0> 1.year.value
=> 31557600.0
irb(main):002:0> 1.month.value * 12
=> 31104000
irb(main):003:0> 1.day.value * 365
=> 31536000

Rails4での各単位の秒数については下記の通り。

# Rails4
# https://github.com/rails/rails/blob/3804d017333da16d76d9fc6633faf5635c7b03d7/activesupport/lib/active_support/duration.rb#L59-L81
   # Returns the number of seconds that this Duration represents.
    #
    #   1.minute.to_i   # => 60
    #   1.hour.to_i     # => 3600
    #   1.day.to_i      # => 86400
    #
    # Note that this conversion makes some assumptions about the
    # duration of some periods, e.g. months are always 30 days
    # and years are 365.25 days:
    #
    #   # equivalent to 30.days.to_i
    #   1.month.to_i    # => 2592000
    #
    #   # equivalent to 365.25.days.to_i
    #   1.year.to_i     # => 31557600
    #
    # In such cases, Ruby's core
    # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
    # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision
    # date and time arithmetic.
    def to_i
      @value.to_i
    end

Rails5での各単位の秒数については下記の通り。 1ヶ月は30日でなく、1年の12分の1に変更されている。

# Rails5.2
# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/duration.rb#L322-L344
    # Returns the number of seconds that this Duration represents.
    #
    #   1.minute.to_i   # => 60
    #   1.hour.to_i     # => 3600
    #   1.day.to_i      # => 86400
    #
    # Note that this conversion makes some assumptions about the
    # duration of some periods, e.g. months are always 1/12 of year
    # and years are 365.2425 days:
    #
    #   # equivalent to (1.year / 12).to_i
    #   1.month.to_i    # => 2629746
    #
    #   # equivalent to 365.2425.days.to_i
    #   1.year.to_i     # => 31556952
    #
    # In such cases, Ruby's core
    # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
    # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision
    # date and time arithmetic.
    def to_i
      @value.to_i
    end
Loading development environment (Rails 5.2.1)
irb(main):001:0> 1.year.value
=> 31556952
irb(main):002:0> 1.month.value * 12
=> 31556952
irb(main):003:0> 1.day.value * 365
=> 31536000
irb(main):004:0>

余談:SQL

SQLでも、「二ヶ月間 + 一ヶ月間」と「一ヶ月+二ヶ月間」では、結果が違うことがある。

psql (9.6.5)
Type "help" for help.

postgres=# select date('2017-01-31'), (date('2017-01-31') +  make_interval(months := 2) + interval '1 month') AS expire;
    date    |       expire
------------+---------------------
 2017-01-31 | 2017-04-30 00:00:00
(1 row)

postgres=# select date('2017-01-31'), (date('2017-01-31') +  make_interval(months := 1) + interval '2 month') AS expire;
    date    |       expire
------------+---------------------
 2017-01-31 | 2017-04-28 00:00:00
(1 row)

postgres=# select date('2017-01-31'), (date('2017-01-31') +  make_interval(months := 1) + make_interval(months := 2)) AS expire;
    date    |       expire
------------+---------------------
 2017-01-31 | 2017-04-28 00:00:00
(1 row)

postgres=# select date('2017-01-31'), (date('2017-01-31') +  interval '1 month' + interval '2 month') AS expire;
    date    |       expire
------------+---------------------
 2017-01-31 | 2017-04-28 00:00:00
(1 row)

以下の様に書くと、つねに(私の)期待どおり動作する。

psql (9.6.5)
Type "help" for help.

postgres=# select date('2017-01-31'), (date('2017-01-31') +  make_interval(months := 1 + 2));
    date    |      ?column?
------------+---------------------
 2017-01-31 | 2017-04-30 00:00:00
(1 row)

docker-composeを環境別に使い分けたい

経緯

もともと、docker-compose.ymlで開発環境を用意していた。 プレビュー環境構築にあたり、諸々の事情により、プレビュー環境では別途新規に書き起こされたdocker-compose.yml、Dockerfileを使うことになった。 既存の設定は開発環境で使いつつ、プレビュー環境では新規の設定を使いたい。

やりたいこと

以下のような配慮をしつつ、Docker環境を使い分けたい。

  • 開発用PCでは、開発用docker-composeとプレビュー用docker-composeを適宜使い分けられるようにしたい。
  • プレビュー用ホストではプレビュー用docker-composeのみを使いたい。
  • プレビュー用ホストでうっかり開発用docker-composeで立ち上げることは絶対にしたくない。フールプルーフにしたい。
  • 開発PCでうっかりプレビュー用docker-composeを立ち上げてしまうのも避けたい。逆よりはマシだが。

実現案

  1. docker-composeのファイル名を分ける(例: docker-compose.development.yml)
    1. メリット
      1. フールプルーフ
      2. ファイル名を見れば用途がわかりやすい
    2. デメリット
      1. docker-compose -fオプションでいちいち指定するのが面倒
        1. だからといってよくつかう環境用の設定ファイルの名前をdocker-compose.ymlにすると、他の環境で使用してしまうというオペミスが発生する
        2. 環境変数COMPOSE_FILEで使用するdocker-compose.ymlを指定すれば対処できる 参考: suin.io
          1. メリット
            1. 一度設定すれば-fオプションが不要
          2. デメリット
            1. 環境構築時、知らない/忘れたひとは-fオプション無しで起動してハマる
              1. README.mdに書いておけばいいのでは
            2. 特定ディレクトリ下で環境変数を設定するいいかんじの方法が、自分はまだわからない => dotenvがよさそう。他の開発者と共有しやすそうなので。
              1. dotenv
              2. bashrcに記述 https://stackoverflow.com/a/14463040
              3. tmuxinator
  2. ブランチを分ける。追加開発分は適宜プレビュー用ブランチにマージする
    1. メリット
      1. -fオプションが不要
      2. 知らないひとがとりあえずdocker-compose upしてイライラすることもない
    2. デメリット
      1. フールプルーフでもフェイルセーフでもない。知らないで適当にdevelopブランチをプルしてきてdocker-compose upするとまずい
        1. previewで動かすとき、developブランチで合ってるのかな?というのは普通に気にするよね?
      2. docker-compose.ymlを変更したとき、おそらく変更がマージされてしまう
  3. 差分の設定を上書く
    1. メリット
      1. 公式の安心感 Use Compose in production
      2. ブランチを分ける必要がない
    2. デメリット
      1. フールプルーフでもフェイルセーフでもない。うっかりするとdevelopmentモードで起動してしまう
      2. docker-compose設定の全体像を把握しづらそう
      3. docker-compose設定を変更しづらそう
      4. 今回のケースだと、docker-compose設定が違いすぎるので、向かなさそう
  4. 環境ごとにわけない
    1. メリット
      1. 環境が分かれていることによるミスが発生しない
      2. じつは公式にも似たようなことが書いてある (The easiest way to deploy an application is to run it on a single server, similar to how you would run your development environment. If you want to scale up your application, you can run Compose apps on a Swarm cluster.)
    2. デメリット
      1. 当然、環境別の変更設定ができない

とりあえずの結論

1つ目の案でやってみる。フールプルーフで、かつ環境ごとに変更できるため。環境変数はdotenvを使って設定する。