技術書典で売られていたPragmatic Terraform on AWSがv0.12対応していて、そういえば昨年夏終わりくらいに出るって言ってたし、マイルストーンが99%に到達してるし、そろそろかなーと思っていろいろ試してみました。
とは言いつつも、何となく楽しそうだから試してみようかなというモチベーションです。
0.12.0-beta1を使って試しています。(これをまとめた後にv0.12.0-beta2が出て涙目でした)
ここ(Upgrading to Terraform v0.12)と
ここ(HashiCorp Terraform 0.12 Preview)と
ここ(Terraform v0.12で変わるHCLの記述について)
を読んで試してみます。
- 書くこと
- どう移行するか(暫定)
- 新文法を試してみる
- upgradeツールを試してみる
どう移行するか(暫定)
本番用のコードに対して適用していないので、ドキュメント読んだ限りでこうやって行こうかなということを書いています。
移行をスムーズに進めるために事前にどういうことに注意しておけばいいのか確認しておこうというくらいな感覚です。
公式ドキュメントに書いてあって、たぶん自分のところでは気にしなくていいかなというものは含めていなかったりします。
そもそも正式リリースされたものを使っていないので、この確認事項も将来的に変わるはずです。
- 1. まずはv0.11に上げる
- 2. providerのプラグインが対応しているか確認
- 3. 移行ツールでv0.12用のhclファイルに変更
- 4. コードの修正
- 4-1. terraform_remote_stateの値が変わるので注意
- 4-2. 移行ツールで勝手に書き換わった部分を確認する
- 4-3. モジュールのバージョンを確認
- 4-4. terraform planでエラーが出ないか確認
- 4-5. v0.12の新文法できれいに書けるところをきれいにする
- 5. その他確認事項
まずはv0.11にあげる
移行時の前提はv0.11からv0.12へのアップグレードが前提です。
公式的にv0.10以前使ってるなら、v0.11にアップグレードしてくれー、とのことなので、それ以前は考えません。
(そもそもv0.10以前を使ったことがないから気にしなくていいか)
providerのプラグインが対応しているか確認
まず簡単な例でterraform init
とterraform plan
してみようと思います。
providerがv0.12に対応しているかいないかが分かるはずです。
alpha1の時にも一回試してようかと思ったのですが、プラグインが対応してなさ過ぎて、どう実験していいかすらわかりませんでした。
対応してなかったら対応されるまで待つしかありません。
リリース時には主要プロバイダーはだいたい対応されていると思っていますが、試そうと思ってもプラグインが対応していなかった、ということが2回あったので、注意するに越したことはないかなと思いました。
移行ツールでv0.12用のhclファイルに変更
v0.12移行用にupgradeのコマンドが用意されています。
基本的にはこのコマンドを利用して、
$ terraform 0.12upgrade
でアップグレードしたほうが良さそうです。
このコマンドはtfファイルを直接編集して元に戻せないので、バージョン管理ツールで管理していることが必須です。
互換性のない部分は互換性が無いから手で修正して、とコメントが追加されます。
git diffでどこがどう変わったか確認し、必要に応じて自分で変更する必要があります。
試していてまじか・・・と思ったところはインデントをつけてくれないところですね。
将来的に改善されるのでしょうか?
このサブコマンドを使うとversions.tfが作られて、0.12以上を要求するようになります。
$ cat versions.tf
terraform {
required_version = ">= 0.12"
}
terraform_remote_stateの値が変わるので注意
terraform_remote_stateの書き方が変わってるので注意が必要。
- terraformのバイナリのバージョン
- tfstateがどのバージョンで作られているか
- terraformのコードがどのバージョンの方式で書かれているのか
この三つの要素で挙動が変わってくるかなと思い、確認してみました。
別のファイルで output "userdata" { ... }
したものを参照しようとしています。
こんな感じです。
data "terraform_remote_state" "userdata" {
backend = "local"
config = {
#path = "../v012/terraform.tfstate"
path = "../v011/terraform.tfstate"
}
}
output "userdata" {
#value = "${data.terraform_remote_state.userdata.outputs.userdata}" # v0.12
value = "${data.terraform_remote_state.userdata.userdata}" # v0.11
}
- v0.12 で v0.12のtfstate を v0.12の方式で参照するのは OK
- v0.12 で v0.12のtfstate を v0.11の方式で参照するのは NG
- v0.12 で v0.11のtfstate を v0.12の方式で参照するのは OK
- v0.12 で v0.11のtfstate を v0.11の方式で参照するのは NG
- v0.11 で v0.12のtfstate を v0.12の方式で参照するのは NG(そもそもバージョン違うと怒られる)
- v0.11 で v0.12のtfstate を v0.11の方式で参照するのは NG(そもそもバージョン違うと怒られる)
- v0.11 で v0.11のtfstate を v0.12の方式で参照するのは NG(参照する変数がない)
- v0.11 で v0.11のtfstate を v0.11の方式で参照するのは OK
ということで、依存している個所があるなら、基本的にはタイミング合わせてv0.12にする必要があります。
remote_stateを提供する側を先にv0.12にアップグレードして、提供される側はしばらくv0.11のまま、ということはできなさそうでした。
ただ、太字にしてある部分からするとremote_stateを提供される側を先にアップグレードして、提供する側をv0.11のままにするということはできそうです。
完全に後方互換があればあんまり気にしなくていいところなので、リリース後にもう一回確認しようと思いますが、この状態のままであれば、混在させつつアップグレードという流れでもいいかもしれません。1回の変更単位があまりにも大きくならないので。
移行ツールで勝手に書き換わった部分を確認する
例えばv0.11の場合
ebs_block_device = "${concat(var.extra_block_devices)}"
これを移行ツールでv0.12の変えた場合
dynamic "ebs_block_device" {
for_each = concat(var.extra_block_devices)
content {
# TF-UPGRADE-TODO: The automatic upgrade tool can't predict
# which keys might be set in maps assigned here, so it has
# produced a comprehensive set here. Consider simplifying
# this after confirming which keys can be set in practice.
delete_on_termination = lookup(ebs_block_device.value, "delete_on_termination", null)
device_name = ebs_block_device.value.device_name
encrypted = lookup(ebs_block_device.value, "encrypted", null)
iops = lookup(ebs_block_device.value, "iops", null)
snapshot_id = lookup(ebs_block_device.value, "snapshot_id", null)
volume_size = lookup(ebs_block_device.value, "volume_size", null)
volume_type = lookup(ebs_block_device.value, "volume_type", null)
}
}
こういう感じで変換されちゃういます。
逆にわかりにくくなってないか?という印象を受けました。
terraformは明示的にコードに意図が残されていることを良しとする傾向があるようです。
どう書いたらいいかは状況次第なので、自動ツールを信じすぎず、自分の管理しやすいように記述を変えたほうが良いと思います。
とりあえず自動出力されたコメントは消します。
モジュールのバージョンを確認
利用しているモジュールがv0.12に対応しているか確認します。
特に自分たち以外が作った外部製のモジュールを利用している場合は特に確認が必要だと思います。
暗黙的に互換性がある場合は利用できるみたいですが、やめたほうが良いはずです。
要はタイミングを調整するという話ですね。
とはいえ、外部のモジュール使ってないので詳しく調べてませんので、自分がやるときはモジュールも一気にコードを修正しようという感覚でいます。
terraform planでエラーが出ないか確認
後述しますが、自動アップグレードは完全じゃないようです(少なくとも現時点は)
変数の型予測が思いっきり間違っていて、そもそもplanが通りませんでした。
terraform.tfstateはrmeote_state以外、互換性を保っているような印象を受けましたが、planすれば本当に保てているかどうかもわかるので、これは必須の作業ですね。
v0.12の新文法できれいに書けるところをきれいにする
後述する新文法できれいに書けるところはきれいにしたいなと思います。
明示的にできて意図を残せるところも同様です。
まずはアップグレードしてからきれいにしていく、という方法が良いとは思うので、無理のない範囲でやります。
その他確認事項
- countという変数は定義済みとして扱えなくなったので、使ってる場合は確認が必要
- resourceの名前の先頭に数字を許可してたけど今度からは許可しない
- countを含むリソースの扱いが変わった
- リソースにカウントを含んでいる場合v0.11の時は
aws_instance.web-server.*.id[0]
とaws_instance.web-server.id
はaliasあつかい - v0.11の時はaws_instance.web-serverの属性としてidを持っているように参照できていた
- ただ
aws_instance.web-server
のようにしても参照はできなかった。 - v0.12になると厳密になって以下のようになる。
- countをもっていないリソースの場合、aws_instance.web-serverのオブジェクトが返ってきて、aws_instance.web-server.idというように利用できる
- countを持っているリソースの場合、aws_instance.web-serverはリストのオブジェクトが返ってきて、aws_instance.web-server.idというケースはエラーになる
- aws_instance.web-server[0].idというようなアクセスの仕方になる。
- length(aws_instance.example)としてもcountに値がセットされる。
- aws_instance.example.countとして参照できていたものは利用できなくなるので、この値を使ってるところがあったら注意
- countは数値型を要求するようになっている、true/falseで指定していたものが暗黙的に1,0に変換されなくなった
- リソースにカウントを含んでいる場合v0.11の時は
- connectionブロックで暗黙的に設定されていた部分、typeとhostを明示的に指定しなければいけなくなった
- コマンドライン引数のvarで渡したときにmap型の変数の場合、ファイルで定義していた値とマージできていたがv0.12からこれはやめる
$ terraform apply -var 'test= { type = "map" msg2 = "fuga" }' #v0.12からエラー
$ cat test.tf
variable "test" {
type = "map"
default = {
msg1 = "hoge"
}
}
output "test" {
value = var.test
}
新文法を試してみる
- 1. terraform v0.12のインストール
- 2. v0.11の記述をv0.12で書いたときにどうなるかを見る
- 3. 載せられなかったこと
terraform v0.12のインストール
以前はコード落としてきてコンパイルして試してましたが、tfenvがめちゃくちゃ楽なので利用します。
$ git clone https://github.com/tfutils/tfenv.git ~/.tfenv
$ echo 'export PATH=$HOME/.tfenv/bin:$PATH' >> .zshrc.local
$ source ~/.zshrc.local
$ tfenv list-remote ※なんのバージョンがあるか確認
$ tfenv install 0.12.0-beta1
$ terraform version
Terraform v0.12.0-beta1
beta1用のプロバイダーのプラグインが自動で落ちてこないので、公式のアナウンス通りインストールします。
$ mkdir -p ~/.terraform.d/plugins
$ cd ~/.terraform.d/plugins
$ wget http://terraform-0.12.0-dev-snapshots.s3-website-us-west-2.amazonaws.com/terraform-provider-aws/1.60.0-dev20190216H00-dev/terraform-provider-aws_1.60.0-dev20190216H00-dev_linux_amd64.zip
$ unzip terraform-provider-aws_1.60.0-dev20190216H00-dev_linux_amd64.zip
$ rm terraform-provider-aws_1.60.0-dev20190216H00-dev_linux_amd64.zip
v0.11の記述をv0.12で書いたときにどうなるかを見る
網羅出来てるか自信ないけどたぶんだいたい試せていると思います。
どれが新しい文法なのかはv0.12のバージョンでコメントしています。
全部1つのファイルに収めようとしているので、定義してる場所と、それを使ってる場所が散らばっています。
なので、どれとどれが同種かコメントに番号を振りました。
インスタンスを2台を作ろうとしているファイルです。
v0.11の場合
variable "region" { default = "ap-northeast-1" }
variable "extra_block_devices" {
type = "list"
default = [
{ device_name = "/dev/sdf", volume_type = "gp2", volume_size = "8" },
{ device_name = "/dev/sdg", volume_type = "gp2", volume_size = "8" },
]
}
locals {
userdata = <<EOL
#cloud-config
repo_update: true
repo_upgrade: all
packages:
- httpd
- nginx
runcmd:
- service httpd start
- service nginx start
- chkconfig httpd on
- chkconfig nginx on
EOL
}
provider "aws" {
region = "${var.region}"
}
resource "aws_instance" "web" {
count = 2
ami = "ami-0f9ae750e8274075b"
instance_type = "t2.micro"
tags = "${merge(map("Name", "HelloWorld"), map("Label", "web") )}"
user_data = "${local.userdata}"
ebs_block_device = "${concat(var.extra_block_devices)}"
}
output "ips" {
value = "${aws_instance.web.*.private_ip}"
}
output "devices" {
value = "${var.extra_block_devices[0]}"
}
output "ids" {
value = "${aws_instance.web.*.id}"
}
output "conditional1" {
value = "${length(var.extra_block_devices) > 0 ? "val" : ""}" # エラーになると思ったらならなかった
}
#output "conditional2" {
# value = "${var.region == "ap-northeast-1" ? [ "val1", "val2"] : [ "val1" ]}" # planの時点でエラー
#}
output "userdata" {
value = "${local.userdata}"
}
v0.12の場合
variable "region" { default = "ap-northeast-1" }
variable "extra_block_devices" {
type = "list"
default = [
{ device_name = "/dev/sdf", volume_type = "gp2", volume_size = "8" },
{ device_name = "/dev/sdg", volume_type = "gp2", volume_size = "8" },
]
}
# 5. Rich Value Types(modulesで変数を定義しておいてmodules { complex_values = { Label = "web" AZ = "a" } }みたいに渡せる)
variable "complex_values" {
type = object({
Label = string
AZ = string
})
default = {
Label = "web"
AZ = "a"
}
}
# 6. Template Syntax(ヒアドキュメントの中にforとか変数を入れられるっぽい)
locals {
packages = [
"httpd",
"nginx",
]
userdata = <<EOL
#cloud-config
repo_update: true
repo_upgrade: all
packages:
%{ for package in local.packages ~}
- ${package}
%{ endfor }
runcmd:
%{ for package in local.packages ~}
- service ${package} start
- chkconfig ${package} on
%{ endfor }
EOL
}
provider "aws" {
region = var.region # 1. First-ClassExpressions(変数を""でくくらなくてよくなった)
}
resource "aws_instance" "web" {
count = 2
ami = "ami-0f9ae750e8274075b"
instance_type = "t2.micro"
#tags = merge({ Name = "HelloWorld" }, { Label = "web" }) # 1. First-ClassExpressions(v0.11版を見れば分かるが以前はmap関数を使う必要があった)
tags = merge({ Name = "HelloWorld"}, var.complex_values) # 5. Rich Value Types(定義を参照してほしいがつまりは構造体が作れるようになった)
user_data = local.userdata # 6. TemplateSyntax(定義参照、ここは使ってるだけ)
dynamic "ebs_block_device" {
for_each = var.extra_block_devices #2. For(dynamicの識別子(ここではebs_block_device)に1ループずつオブジェクトが入る)
content {
device_name = ebs_block_device.value.device_name
volume_type = ebs_block_device.value.volume_type
volume_size = ebs_block_device.value.volume_size
}
}
}
output "ips" {
value = [
for instance in aws_instance.web: #2. For(listやmap型であればforeachでループ回せる)
instance.private_ip #resource複数定義やaws_instance全部に対して、みたいなことはできなさそう
]
}
output "devices" {
value = var.extra_block_devices[*].device_name # 3. Generalized Splat Operator(List型でcountは使えなかったが使えるようになった)
}
output "ids" {
value = aws_instance.web[*].id # 3. Generalized Splat Operator(v0.11はaws_instance.web.idみたいに定義出来てた(らしい)これがリスト型だと直感的にわからないのでv0.12からかけないようになった)
}
output "conditional1" {
value = length(var.extra_block_devices) > 0 ? "val" : "" # 4. Conditional improvements(空が返るとエラーだった?)
}
output "conditional2" {
value = var.region == "ap-northeast-1" ? [ "val1", "val2"] : [ "val1" ] # 4. Conditional improvements(以前はListやMapはreturnできなかった)
}
output "richtype" {
value = var.complex_values # 5. Rich value Types(これで複雑な値も一気に出力できるようになった)
}
output "userdata" {
value = local.userdata # 6. Template Syntax(ヒアドキュメントをテンプレートっぽく使えるようになった?)
}
diffを取ってみる
+しか無いのはv0.12にしか無いもののつもりです。
@@ -7,25 +7,44 @@
]
}
+# 5. Rich Value Types(modulesで変数を定義しておいてmodules { complex_values = { Label = "web" AZ = "a" } }みたいに渡せる)
+variable "complex_values" {
+ type = object({
+ Label = string
+ AZ = string
+ })
+
+ default = {
+ Label = "web"
+ AZ = "a"
+ }
+}
+# 6. Template Syntax(ヒアドキュメントの中にforとか変数を入れられるっぽい)
locals {
+ packages = [
+ "httpd",
+ "nginx",
+ ]
+
userdata = <<EOL
#cloud-config
repo_update: true
repo_upgrade: all
packages:
- - httpd
- - nginx
+%{ for package in local.packages ~}
+ - ${package}
+%{ endfor }
runcmd:
- - service httpd start
- - service nginx start
- - chkconfig httpd on
- - chkconfig nginx on
+%{ for package in local.packages ~}
+ - service ${package} start
+ - chkconfig ${package} on
+%{ endfor }
EOL
}
provider "aws" {
- region = "${var.region}"
+ region = var.region # 1. First-ClassExpressions(変数を""でくくらなくてよくなった)
}
resource "aws_instance" "web" {
@@ -33,33 +52,48 @@
ami = "ami-0f9ae750e8274075b"
instance_type = "t2.micro"
- tags = "${merge(map("Name", "HelloWorld"), map("Label", "web") )}"
-
- user_data = "${local.userdata}"
+ #tags = merge({ Name = "HelloWorld" }, { Label = "web" }) # 1. First-ClassExpressions(v0.11版を見れば分かるが以前はmap関数を使う必要があった)
+ tags = merge({ Name = "HelloWorld"}, var.complex_values) # 5. Rich Value Types(定義を参照してほしいがつまりは構造体が作れるようになった)
+
+ user_data = local.userdata # 6. TemplateSyntax(定義参照、ここは使ってるだけ)
- ebs_block_device = "${concat(var.extra_block_devices)}"
+ dynamic "ebs_block_device" {
+ for_each = var.extra_block_devices #2. For(dynamicの識別子(ここではebs_block_device)に1ループずつオブジェクトが入る)
+ content {
+ device_name = ebs_block_device.value.device_name
+ volume_type = ebs_block_device.value.volume_type
+ volume_size = ebs_block_device.value.volume_size
+ }
+ }
}
output "ips" {
- value = "${aws_instance.web.*.private_ip}"
+ value = [
+ for instance in aws_instance.web: #2. For(listやmap型であればforeachでループ回せる)
+ instance.private_ip #resource複数定義やaws_instance全部に対して、みたいなことはできなさそう
+ ]
}
output "devices" {
- value = "${var.extra_block_devices[0]}"
+ value = var.extra_block_devices[*].device_name # 3. Generalized Splat Operator(List型でcountは使えなかったが使えるようになった)
}
output "ids" {
- value = "${aws_instance.web.*.id}"
+ value = aws_instance.web[*].id # 3. Generalized Splat Operator(v0.11はaws_instance.web.idみたいに定義出来てた(らしい)がこれがリスト型だと直感的にわからないのでv0.12からかけないようになった)
}
output "conditional1" {
- value = "${length(var.extra_block_devices) > 0 ? "val" : ""}" # エラーになると思ったらならなかった
+ value = length(var.extra_block_devices) > 0 ? "val" : "" # 4. Conditional improvements(空が返るとエラーだった?)
}
-#output "conditional2" {
-# value = "${var.region == "ap-northeast-1" ? [ "val1", "val2"] : [ "val1" ]}" # planの時点でエラー
-#}
+output "conditional2" {
+ value = var.region == "ap-northeast-1" ? [ "val1", "val2"] : [ "val1" ] # 4. Conditional improvements(以前はListやMapはreturnできなかった)
+}
+
+output "richtype" {
+ value = var.complex_values # 5. Rich value Types(これで複雑な値も一気に出力できるようになった)
+}
output "userdata" {
- value = "${local.userdata}"
+ value = local.userdata # 6. Template Syntax(ヒアドキュメントをテンプレートっぽく使えるようになった?)
}
upgradeツールを試してみる
新文法を試したときに使ったv0.11版のtfファイルをアップグレードのツールに通してみます。
$ cp ./v011/ec2.tf ./v012-auto-upgrade/
$ cp ./v011/terraform.tfstate ./v012-auto-upgrade/
$ cd ./v012-auto-upgrade
$ terraform 0.12upgrade
yes
$ cd ../
出来上がったファイルとv0.11のファイルをdiffしてみます
$ diff -u ./v011/ec2.tf ./v012-auto-upgrade/ec2.tf
@@ -1,13 +1,23 @@
-variable "region" { default = "ap-northeast-1" }
+variable "region" {
+ default = "ap-northeast-1"
+}
+
variable "extra_block_devices" {
- type = "list"
+ type = list(string)
default = [
- { device_name = "/dev/sdf", volume_type = "gp2", volume_size = "8" },
- { device_name = "/dev/sdg", volume_type = "gp2", volume_size = "8" },
- ]
+ {
+ device_name = "/dev/sdf"
+ volume_type = "gp2"
+ volume_size = "8"
+ },
+ {
+ device_name = "/dev/sdg"
+ volume_type = "gp2"
+ volume_size = "8"
+ },
+ ]
}
-
locals {
userdata = <<EOL
#cloud-config
@@ -22,38 +32,62 @@
- chkconfig httpd on
- chkconfig nginx on
EOL
+
}
provider "aws" {
- region = "${var.region}"
+region = var.region
}
resource "aws_instance" "web" {
- count = 2
- ami = "ami-0f9ae750e8274075b"
- instance_type = "t2.micro"
-
- tags = "${merge(map("Name", "HelloWorld"), map("Label", "web") )}"
-
- user_data = "${local.userdata}"
-
- ebs_block_device = "${concat(var.extra_block_devices)}"
+count = 2
+ami = "ami-0f9ae750e8274075b"
+instance_type = "t2.micro"
+
+tags = merge(
+{
+"Name" = "HelloWorld"
+},
+{
+"Label" = "web"
+},
+)
+
+user_data = local.userdata
+
+dynamic "ebs_block_device" {
+for_each = concat(var.extra_block_devices)
+content {
+# TF-UPGRADE-TODO: The automatic upgrade tool can't predict
+# which keys might be set in maps assigned here, so it has
+# produced a comprehensive set here. Consider simplifying
+# this after confirming which keys can be set in practice.
+
+delete_on_termination = lookup(ebs_block_device.value, "delete_on_termination", null)
+device_name = ebs_block_device.value.device_name
+encrypted = lookup(ebs_block_device.value, "encrypted", null)
+iops = lookup(ebs_block_device.value, "iops", null)
+snapshot_id = lookup(ebs_block_device.value, "snapshot_id", null)
+volume_size = lookup(ebs_block_device.value, "volume_size", null)
+volume_type = lookup(ebs_block_device.value, "volume_type", null)
+}
+}
}
output "ips" {
- value = "${aws_instance.web.*.private_ip}"
+value = aws_instance.web.*.private_ip
}
output "devices" {
- value = "${var.extra_block_devices[0]}"
+value = var.extra_block_devices[0]
}
output "ids" {
- value = "${aws_instance.web.*.id}"
+value = aws_instance.web.*.id
}
output "conditional1" {
- value = "${length(var.extra_block_devices) > 0 ? "val" : ""}" # エラーになると思ったらならなかった
+value = length(var.extra_block_devices) > 0 ? "val" : "" # エラーになると思ったらならなかった
}
#output "conditional2" {
@@ -61,5 +95,6 @@
#}
output "userdata" {
- value = "${local.userdata}"
+value = local.userdata
}
+
なるほどなるほど、ということでplan
$ terraform plan
Error: Invalid default value for variable
on ec2.tf line 7, in variable "extra_block_devices":
7: default = [
8: {
9: device_name = "/dev/sdf"
10: volume_type = "gp2"
11: volume_size = "8"
12: },
13: {
14: device_name = "/dev/sdg"
15: volume_type = "gp2"
16: volume_size = "8"
17: },
18: ]
This default value is not compatible with the variable's type constraint:
element 0: string required.
???????
なんでエラー出るの?と思いましたが、
-list(string)
+list(map(string))
に変更したら通りました。
アップグレードツールは完全ではなさそうですね。
(これってほんとはIssue送るべき内容なのかな?)
修正後にterraform planしましたが差分はありませんでした。
なのでterraform.tfstateの出力が変わることの影響はあまり気にしなくていいのかもしれません。
とはいえ、v0.11とv0.12でそれぞれapply後のterraform.tfstateをdiffしてみましたが、ほとんどすべての行が変わっており、不安なのでplanして確認は必須ですね。
あとはterraformのバージョン情報が変わっていなかったのが若干気になりました。
載せられなかったこと
新しいHCLでは、JSON:HCLが完全に1:1で対応するようになったとか
まとめ
- 依存しているところは全部いっぺんにv0.12に上げないと死ぬかも
- 0.12upgradeサブコマンドは万能じゃない
- 新しいHCLはプログラミング言語の概念に近づいたので慣れる必要がありそう
- これ書いてるうちにbeta2が出た・・・(しかもinitで完全互換のプラグインが落としてこれるって言ってる・・・)
俺たちのlater this(2018) summerはこれからだぜ!