【GitHub】ssh.github.comからcloneできない

他の人が書いたスクリプトの中で、 git clone git@ssh.github.com:user/repo.git のように ssh.github.com からcloneする処理があったが、なぜかこれに失敗して困った。

error: couldn't make loader for git@ssh.github.com:user/repo.git: trouble cloning git@ssh.github.com:user/repo.git: exit status 128

接続確認してみるが、github.com には接続できるものの ssh.github.com は失敗する。

ssh -T git@github.com 
Hi Udomomo! You've successfully authenticated, but GitHub does not provide shell access.
ssh -T git@ssh.github.com
git@ssh.github.com: Permission denied (publickey).

鍵が正しく作られていなければ両方失敗するはずなので、ローカルの id_rsa を調べてみたところ、その置き場所に原因があった。
公開鍵認証をする際は、デフォルトでは .ssh/id_rsa秘密鍵を置く。しかし自分の場合、普段GitHubだけでなくawsなど他のサービス用の秘密鍵を管理しやすくするため、 .ssh/github/id_rsa のようにディレクトリを分けてその中に秘密鍵を置いていた。秘密鍵のパスを替えた場合、 .ssh/config 内の IdentityFile でパスを指定できるのだが、今回の場合は以下のようになっていた。

  • github.com -> .ssh/config 内で IdentityFile を指定した状態で設定済み。そのため正しい秘密鍵を使って認証に成功。
  • ssh.github.com -> .ssh/config 内に設定がない。そのためデフォルトである .ssh/id_rsaを探すものの秘密鍵がなく認証に失敗。

対応としては、 ssh.github.com.ssh/config に追記すればよい。なお、github.com への接続を HostName を指定して ssh.github.com に飛ばすだけでは解決できない。コマンドラインで与えられたホスト名は .ssh/config 内の Host の項目のみを探索して判定されるため、 HostName に書いてもマッチせず、 IdentityFile の指定を渡すことができない。

Host github.com
    HostName ssh.github.com
    Port 443
    IdentityFile ~/.ssh/github/id_rsa
    User git

Host ssh.github.com
    Port 443
    IdentityFile ~/.ssh/github/id_rsa
    User git
ssh -T git@github.com 
Hi Udomomo! You've successfully authenticated, but GitHub does not provide shell access.
ssh -T git@ssh.github.com
Hi Udomomo! You've successfully authenticated, but GitHub does not provide shell access.

【Terraform】既存resourceの名前だけを変えたい

社内勉強会で教えてもらった内容。
Terraformで既存のresourceの名前を変えたいとき、普通にtfファイル内でresourceの名前を変更するだけだと、resource全体が作り直し(destory->create)されてしまう。
例えば以前の記事で使った aws_internet_gateway resourceの名前を変更しようとすると、resource全体に差分が生じてしまう。

$ cat aws_internet_gateway.tf 
resource "aws_internet_gateway" "container-era-webapp-internet-gateway" {
  vpc_id = aws_vpc.container-era-webapp-vpc.id
}
$ cat aws_route.tf
resource "aws_route" "container-era-webapp-public-route" {
  route_table_id         = aws_route_table.container-era-webapp-public-route-table.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.container-era-webapp-internet-gateway.id
}

# 名前をcontainer-era-webapp-gatewayに変更
$ cat aws_internet_gateway.tf 
resource "aws_internet_gateway" "container-era-webapp-gateway" {
  vpc_id = aws_vpc.container-era-webapp-vpc.id
}
$ cat aws_route.tf
resource "aws_route" "container-era-webapp-public-route" {
  route_table_id         = aws_route_table.container-era-webapp-public-route-table.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.container-era-webapp-gateway.id
}

$ terraform plan
Terraform will perform the following actions:

  # aws_internet_gateway.container-era-webapp-gateway will be created
  + resource "aws_internet_gateway" "container-era-webapp-gateway" {
      + arn      = (known after apply)
      + id       = (known after apply)
      + owner_id = (known after apply)
      + vpc_id   = "vpc-01ff3b511d075fb99"
    }

  # aws_internet_gateway.container-era-webapp-internet-gateway will be destroyed
  - resource "aws_internet_gateway" "container-era-webapp-internet-gateway" {
      - arn      = "arn:aws:ec2:ap-northeast-1:xxxxxxxxxxxx:internet-gateway/igw-xxxxxxxxxxxx" -> null
      - id       = "igw-xxxxxxxxxxxxxxx" -> null
      - owner_id = "xxxxxxxxxxxx" -> null
      - tags     = {} -> null
      - vpc_id   = "vpc-xxxxxxxxxxxx" -> null
    }

これが仮に本番環境で使われているリソースである場合、一時的にリソースが消えてしまい本番環境に影響が生じる可能性もある。

このような場合、 terraform state mv コマンドを使い、tfstateファイルを変更する必要がある。
今回はlocalでtfstateファイルの変化を見たいので、まずbackendをS3からlocalに変える。

$ cat config.tf
provider "aws" {
  region = "ap-northeast-1"
}

# terraform {
#   backend "s3" {
#     bucket = "container-era-terraform-udomomo"
#     key    = "sample-app/vpc/terraform.tfstate"
#     region = "ap-northeast-1"
#   }
# }

$ terraform init
...
Do you want to copy this state to the new "local" backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes

S3に置かれていたtfstateファイルがlocalにコピーされた。 terraform state mv コマンドを試してみる。

$ terraform state mv aws_internet_gateway.container-era-webapp-internet-gateway aws_internet_gateway.container-era-webapp-gateway
Move "aws_internet_gateway.container-era-webapp-internet-gateway" to "aws_internet_gateway.container-era-webapp-gateway"
Successfully moved 1 object(s).

terraform state mv コマンドを実行すると、自動でバックアップファイルが作られる。差分を見てみると以下のようになっている。

diff -u terraform.tfstate.1595057556.backup terraform.tfstate
--- terraform.tfstate.1595057556.backup    2020-07-18 16:32:36.000000000 +0900
+++ terraform.tfstate   2020-07-18 16:32:36.000000000 +0900
@@ -1,7 +1,7 @@
 {
   "version": 4,
   "terraform_version": "0.12.28",
-  "serial": 0,
+  "serial": 1,
   "lineage": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
   "outputs": {
     "cidr_block": {
@@ -25,7 +25,7 @@
     {
       "mode": "managed",
       "type": "aws_internet_gateway",
-      "name": "container-era-webapp-internet-gateway",
+      "name": "container-era-webapp-gateway",
       "provider": "provider.aws",
       "instances": [
         {
@@ -70,12 +70,7 @@
             "transit_gateway_id": "",
             "vpc_peering_connection_id": ""
           },
-          "private": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
-          "dependencies": [
-            "aws_internet_gateway.container-era-webapp-internet-gateway",
-            "aws_route_table.container-era-webapp-public-route-table",
-            "aws_vpc.container-era-webapp-vpc"
-          ]
+          "private": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
         }
       ]
     },

container-era-webapp-internet-gateway に依存していた aws_route resourceのdependenciesにもdiffがある。これはaws_internet_gatewayのみに変更を加えたためと思われる。公式レポジトリのissueによれば、tfstateファイルはどのような意図でリファクタリングが行われたかは一切関知しておらず、またdependenciesはresourceの削除時にしか使われないため、リファクタリング後に terraform apply すれば影響がなくなるとのことだった。

github.com

terraform plan で確認する。

$ terraform plan

No changes. Infrastructure is up-to-date.

This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.

resourceに影響がないと確認できたので、backendをS3に戻し、mvされた後のtfstateファイルをS3にコピーする。

$ cat config.tf
provider "aws" {
  region = "ap-northeast-1"
}

terraform {
  backend "s3" {
    bucket = "container-era-terraform-udomomo"
    key    = "sample-app/vpc/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

$ terraform init
...
Do you want to overwrite the state in the new backend with the previous state?
  Enter "yes" to copy and "no" to start with the existing state in the newly
  configured "s3" backend.

  Enter a value: yes
...
Terraform has been successfully initialized!

最後にtfファイルをapplyすればよい。

$ terraform apply
...
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

ちなみに、apply後にS3バケットに生成されていたtfstateファイルを確認したところ、先程diffが出ていた部分のdependenciesも正しく復活していた。

...
"private": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "dependencies": [
        "aws_internet_gateway.container-era-webapp-gateway",
        "aws_route_table.container-era-webapp-public-route-table",
        "aws_vpc.container-era-webapp-vpc"
    ]
...

【MySQL】あるはずの権限が使えない場合はログインアカウントを確認する

先日MySQLにログインしたとき、付与されているはずの権限がなくクエリを投げられなかったことがある。ユーザ名は確かに合っていたのだが、問題はログイン元のホストにあった。

MySQLのユーザに権限を付与するとき、以下のようにユーザ名だけではなくホストも指定する。

GRANT SELECT, INSERT, UPDATE, DELETE ON db.test TO 'udomomo'@'10.254.0.0/255.255.240.0';

すなわち、ユーザ名は同じでも異なるホストからのログインに対して、異なる権限を設定することもできる。(権限付与の際にホストを指定しない場合、任意のホストとみなされる)

GRANT SELECT ON db.test TO 'udomomo';

このようなことができる理由は、mysqlではユーザ名とホスト名を含めて1つの固有なアカウントとみなしているため。 mysql.user テーブルを確認すると、 'udomomo'@'10.254.0.0/255.255.240.0''udomomo'@'%' は異なるアカウントとして記録されている。

SELECT user, host FROM mysql.user where user="udomomo";
+----------+---------------------------+
| user      | host               |
+----------+---------------------------+
| udomomo | %                   |
| udomomo | 10.254.0.0/255.255.240.0  |
+----------+---------------------------+
2 rows in set (0.00 sec)

ではユーザ名 udomomo でログインすると、どちらのアカウントに該当するのだろうか。MySQLでは、ユーザ名のみで複数のアカウントに合致するときは、ホスト名の具体性が高い順に行をソートし、その順番にホストがマッチするか確認する。具体性とは % のようなワイルドカード指定が使われていないものほど先に来るという意味である。すなわち、例えば10.254.0.1からユーザ名 udomomo でログインすれば、必ず 'udomomo'@'10.254.0.0/255.255.240.0' の方にマッチする。

dev.mysql.com

今回の場合、間違って 10.254.0.0/255.255.240.0 に含まれないホストから udomomo でログインしたことで、 INSERT を行うことができなかった(当然パスワードは全く違う文字列なのだが、パスワードマネージャーを使っているため気づかなかった)。 このような時は、今ログインしているアカウントを確認するのが速い。 SELECT CURRENT_USER(); とすれば、今ログインしているアカウントがユーザ名・ホストともに表示される。

SELECT CURRENT_USER();
+--------------------------------+
| CURRENT_USER()                 |
+--------------------------------+
| udomomo@10.254.0.0/255.255.240.0 |
+--------------------------------+
1 row in set (0.01 sec)

【terraform】importコマンドでtfstateファイルを複数モジュールに分割する

「コンテナ時代のWebサービスの作り方」を読みながらTerraformを触ってみている。

booth.pm

その中で、Terraformを使う例としてVPCとEC2を作成する項目があった。本ではまず1つのtfファイルに両方のリソースをまとめて記述した後、ec2とvpcでモジュールを分けた書き方を紹介している。

  • パターンA: 1つのtfファイルに両方のリソースを記述する例 (tfstateファイルはS3バケットtest/ 以下に格納)
test
|- sample.tf #VPC, Subnet, EC2リソースを定義
# test
terraform apply sample.tf
...
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
  • パターンB: リソースの種類ごとにモジュールを分ける例(tfstateファイルはS3バケットtest/vpc/ test/ec2/ 以下にそれぞれ格納)
test
|- vpc
|   |- vpc.tf #VPC, Subnetリソースを定義
|- ec2
    |- ec2.tf #EC2リソースを定義

ここで疑問に思ったのが、パターンA -> パターンBのように、もし最初に1つのtfファイルをapplyしその後でモジュールを分けたくなった場合どうするのかということ。というのも、パターンAとBでは tfstate の格納場所が異なっているため、Aのtfファイルをapplyした後にBで分けた2つのtfファイルをapplyしようとしても、既にAで作られたtfstateファイルを検知できず、リソースが二重にapplyされかねない。

# test/vpc
terraform plan
...
Plan: 2 to add, 0 to change, 0 to destroy.

このような場合、 terraform import を使って既存リソースの情報を取り込んでおく必要がある。
import コマンドは ADDRESSID の2つの引数を必須としている。 ADDRESS はtfファイル内で定義した resource のアドレスを指す。例えば test/vpc/vpc.tf内で my_vpc という名前でVPCリソースを定義していた場合、 aws_vpc.my_vpc と指定すればよい。(すなわち、 import コマンドで既存のリソースを取り込むには、そのリソースに対応するtfファイルをあらかじめ作成しておかなければならない)
ID の方は、作成済みのリソースを一意に識別できるようにプロバイダ上で割り振られた値を指す。値の確認方法は各プロバイダによるが、AWSVPCであれば VPC ID を、EC2であれば Instance ID を使う。これらはAWSのコンソールやaws cliを使って確認できる。
これらを使って terraform import を実行する。デフォルトではコマンドを実行したディレクトリにあるtfファイルが参照される。

# test/vpc
terraform import aws_vpc.my_vpc vpc-xxxxxxxxxxxxxxxxx
aws_vpc.my_vpc: Importing from ID "vpc-xxxxxxxxxxxxxxxxx"...
aws_vpc.my_vpc: Import prepared!
  Prepared aws_vpc for import
aws_vpc.my_vpc: Refreshing state... [id=vpc-xxxxxxxxxxxxxxxxx]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

terraform import aws_subnet.my_subnet subnet-xxxxxxxxxxxxxxxxx
aws_subnet.my_subnet: Importing from ID "subnet-xxxxxxxxxxxxxxxxx"...
aws_subnet.my_subnet: Import prepared!
  Prepared aws_subnet for import
aws_subnet.my_subnet: Refreshing state... [id=subnet-xxxxxxxxxxxxxxxxx]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

import コマンドを実行するたびに、tfファイルの backend 等で指定された保存場所にtfstateファイルが作成される。直後に terraform plan を行うと、差分がなくなっている。

# test/vpc
terraform plan
...
No changes. Infrastructure is up-to-date.

後はEC2の方も同様に行えば、余計な差分を生まずにtfstateファイルを移行できる。

import コマンドは、現時点(terraform v0.12.28 + provider.aws v2.68.0)ではあくまでtfstateファイルを変更するのみで、tfファイルの自動生成はしてくれない。これを補ってくれるOSSツールもあるようだが、 公式ドキュメントには A future version of Terraform will also generate configuration. と書いてあるので少しだけ期待している。

【bash】コマンドの戻り値をワンライナーで確認するときの注意

bashスクリプトを書くとき、直前のコマンドの戻り値を見て条件分岐させたいことがよくある。戻り値は $? で取得できるのだが、このコマンドはいかなる場合でも直前のコマンドしか考慮してくれない。 例えば、以下は $? の誤った使い方である。

which aws | echo $?

この場合、 which aws | echo $? で一つのコマンドとみなされるため、 $? が表すのはその前のコマンドの戻り値となる。すなわち、パイプでつないだ前半部分のコマンドの戻り値を正しく判定できていない。例えば開発環境にsedコマンドが存在し、fooコマンドが存在しない場合、以下のようになる。

which foo # foo not found
which sed | echo $? # 1 
which sed # /usr/bin/sed
which foo | echo $? # 0

正しくはパイプではなく以下のように使う必要がある。

which sed; echo $?

Kafka Dockerで$KAFKA_HOMEと$ZKの値を設定する

Kafka Dockerを使ってKafkaの実行環境を作っていたが、コンテナに接続してtopic一覧を表示しようとした際に、チュートリアルにある環境変数 $KAFKA_HOME$ZK がどこで設定できるのかわからず詰まってしまった。

$KAFKA_HOME/bin/kafka-topics.sh --describe --topic topic --zookeeper $ZK

wurstmeister.github.io

まず $KAFKA_HOME だが、これは設定する必要がない。Kafka DockerのDockerfileを見てみると、ここで KAFKA_HOME=/opt/kafka という記述がある。

hub.docker.com

次に $ZK だが、これはGitHubレポジトリの start-kafka-shell.sh 経由でコンテナに接続した場合を想定した記法である。このシェルスクリプトの第2引数が、そのまま $ZK の値として設定される。

github.com

ここには docker-compose.yml で設定した KAFKA_ZOOKEEPER_CONNECT の値を入れれば良い。 wurstmeister/zookeeper を併用している場合、ホスト名は当該コンテナの hostname を参照、ポートはデフォルトでは2181になる。

zookeeper:
    image: wurstmeister/zookeeper
    hostname: zookeeper
kafka:
    ....
    environment:
        KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181

なお、 start-kafka-shell.sh はあくまでラッパースクリプトにすぎないので、 docker exec で直接コンテナに接続しても問題ない。その場合、 $KAFKA_HOME は使えるが $ZK は使えないので直接指定する必要がある。

【Java】Nested Classの使いどころ

Javaで特定のクラスに強く紐づく小さなクラスがあるとき、それをネストさせている書き方と、ネストさせずに1ファイル内に並列して置く書き方の両方をよくみる。どちらを使うと良いのか迷うことが多かったので、Effective Javaの項目24にあたってみた。

www.amazon.co.jp

まず、Nested Classには2種類ある。

  • non-static nested class: 自身のインスタンスの中から、外部クラスの全フィールドにアクセス可能
  • static nested class: 外部クラスのフィールドにはアクセスできない

このうちnon-static nested classの場合、自身をネストしているクラスのフィールドがたとえprivateであっても、そのフィールドにアクセス可能になる。

これをふまえると、Nested Classを宣言するときは、そのクラスがネストされているエンクロージングクラスからのみ使われ、かつ1メソッドとするには少し長すぎるという場合が良い。それが小さなクラスであれば、ネストさせた方が関係性が見えやすくなり、可読性も上がる。さらに、

では、non-static nested classとstatic nested classはどう使い分ければ良いだろうか。Nested classから見て、もしエンクロージングクラスのインスタンスやフィールドにアクセスすることがある場合、non-static nested classを使っても良い。non-static nested classを使う場合、Nested Classから使われているフィールドをprivateにでき、クラスのカプセル化を促進することができる。
しかし、エンクロージングクラスにアクセスすることが全く無い場合は、static nested classを使うことが推奨されている。non-static nested classの場合、エンクロージングクラスのオブジェクトと参照関係を持つので、それが全く使われずGCされると、エンクロージングクラスのオブジェクトが残ってしまいメモリリークを起こしうる。