tech.guitarrapc.cóm

Technical updates

terraform for_each で data をいい感じで渡す

Terraform のfor_each は data でも使えます。 だから何だって感じですが、data でよく aws_iam_policy を拾うことがあるかと思いますが、for_each が使えれば 複数のpolicy を aws_iam_groupaws_iam_useraws_iam_role といったリソースに紐づけるあるあるがシンプルに定義できそうです。(できます)

一個一個書いてもいいんですけど、attach 程度は for_each でまとめてしまいたいですよね。

for_each くせ強くてうっかり忘れがちなのでメモしておきます。

目次

TL;DR

単純に書くか、あるいは型をうまく使えるか、どっちらか書きやすい方で書けばいいでしょう。

  • おとなしく [*] 経由でアクセスして primitive な list として認識させる
  • for_each の中の型を for を介することで認識させる

環境

  • terraform: 0.12.19
  • aws provider: 2.43.0

やりたいこと

IAM GroupにPolicy をアタッチすることを考えます。 aws_iam_group_policy_attachment を使うと複数のポリシーをいい感じに Group にアタッチできるのでこれを使うのが鉄板かと思います。

では Groupに data から取得した policy arnをいい感じに当てられてないか考えましょう。

事前定義

group と policy を次のように定義しておきます。

resource "aws_iam_group" "Administrators" {
  name = "Administrators"
  path = "/"
  lifecycle {
    prevent_destroy = true
  }
}

data "aws_iam_policy" "AdministratorAccess" {
  arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}
data "aws_iam_policy" "ReadonlyAccess" {
  arn = "arn:aws:iam::aws:policy/ReadonlyAccess"
}

この group と policy のアタッチをどう書くといいでしょうか。

うまくいく方法

うまくいく方法から、2つほど思いつきます。

[*] 経由で指定する

シンプルに [*].arn を使って、複数リソースからのarnプロパティを指定します。

resource "aws_iam_group_policy_attachment" "Administrators" {
  for_each   = toset(data.aws_iam_policy.AdministratorAccess[*].arn)
  group      = aws_iam_group.Administrators.name
  policy_arn = each.value
}

複数の data リソースを取得するなら concat()すればできます。

resource "aws_iam_group_policy_attachment" "Administrators" {
  for_each   = toset(concat(data.aws_iam_policy.AdministratorAccess[*].arn, data.aws_iam_policy.ReadonlyAccess[*].arn))
  group      = aws_iam_group.Administrators.name
  policy_arn = each.value
}

[*] 外して toset([data.aws_iam_policy.AdministratorAccess.arn, data.aws_iam_policy.ReadonlyAccess.arn]) ではだめなのがムズカシイ

The given "for_each" argument value is unsuitable: "for_each" supports maps and sets of strings, but you have provided a set containing type dynamic.

for で型を指定する

arn は 型が stringと決まり切っているけど terraform 的には dyanmic になるので困ります。 GitHubに似たようなことで困っている人がいます。

github.com

ここで示されているように、setmap でdynamic として認識されているときに、型を明示するために一度 for を回しています。 この方法をつかうのは、次のようなエラーの場合です。

Error: The given "for_each" argument value is unsuitable: "for_each" supports maps and sets of strings, but you have provided a set containing type dynamic.

示されている方法は今回のケースでもうまくいきます。

resource "aws_iam_group_policy_attachment" "Administrators" {
  for_each = {
    for k in [
      data.aws_iam_policy.AdministratorAccess.arn,
    ] : k => k
  }
  group      = aws_iam_group.Administrators.name
  policy_arn = each.value
}

記述が冗長なのがいやですが、for を介することで型が明確に決まるので、単純に for_each にプロパティを充てただけだと dynamic などと言われてエラーの時はこの方法で突破できます。

複数の data を並べるのも問題ありません。

resource "aws_iam_group_policy_attachment" "Administrators" {
  for_each = {
    for k in [
      data.aws_iam_policy.AdministratorAccess.arn,
      data.aws_iam_policy.ReadonlyAccess.arn,
    ] : k => k
  }
  group      = aws_iam_group.Administrators.name
  policy_arn = each.value
}

ダメな方法

幾つか思いつく方法がやってみるとだめなことありませんか? ちょっと直感と反しててむむっとなります。

data.aws_iam_policy.AdministratorAccess

安直にやるとだめ。まぁそれはそうです。

resource "aws_iam_group_policy_attachment" "Administrators" {
  for_each   = data.aws_iam_policy.AdministratorAccess
  group      = aws_iam_group.Administrators.name
  policy_arn = each.value.arn
}
Error: Unsupported attribute

  on modules/iam/group.tf line 27, in resource "aws_iam_group_policy_attachment" "Administrators":
  27:   policy_arn = each.value.arn
    |----------------
    | each.value is "AdministratorAccess"

This value does not have any attributes.


Error: Unsupported attribute

  on modules/iam/group.tf line 27, in resource "aws_iam_group_policy_attachment" "Administrators":
  27:   policy_arn = each.value.arn
    |----------------
    | each.value is "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": \"*\",\n      \"Resource\": \"*\"\n    }\n  ]\n}"   

This value does not have any attributes.


Error: Unsupported attribute

  on modules/iam/group.tf line 27, in resource "aws_iam_group_policy_attachment" "Administrators":
  27:   policy_arn = each.value.arn
    |----------------
    | each.value is "/"

This value does not have any attributes.


Error: Unsupported attribute

  on modules/iam/group.tf line 27, in resource "aws_iam_group_policy_attachment" "Administrators":
  27:   policy_arn = each.value.arn
    |----------------
    | each.value is "arn:aws:iam::aws:policy/AdministratorAccess"

This value does not have any attributes.


Error: Unsupported attribute

  on modules/iam/group.tf line 27, in resource "aws_iam_group_policy_attachment" "Administrators":
  27:   policy_arn = each.value.arn
    |----------------
    | each.value is "Provides full access to AWS services and resources."

This value does not have any attributes.


Error: Unsupported attribute

  on modules/iam/group.tf line 27, in resource "aws_iam_group_policy_attachment" "Administrators":
  27:   policy_arn = each.value.arn
    |----------------
    | each.value is "arn:aws:iam::aws:policy/AdministratorAccess"

This value does not have any attributes.

arn を指定してみる

複数の property が入ってくるなら arn を指定すればよさそうと思うと、string なのでダメです。

resource "aws_iam_group_policy_attachment" "Administrators" {
  for_each   = toset(data.aws_iam_policy.AdministratorAccess.arn)
  group      = aws_iam_group.Administrators.name
  policy_arn = each.value
}
Error: Invalid function argument

  on modules/iam/group.tf line 25, in resource "aws_iam_group_policy_attachment" "Administrators":
  25:   for_each   = toset(data.aws_iam_policy.AdministratorAccess.arn)
    |----------------
    | data.aws_iam_policy.AdministratorAccess.arn is "arn:aws:iam::aws:policy/AdministratorAccess"

Invalid value for "v" parameter: cannot convert string to set of any single
type.

setかmapでないとダメなのでそれはそうです。 toset([]) を使って指定してもダメなのが、はじめにむむっと思うのではないでしょうか。

resource "aws_iam_group_policy_attachment" "Administrators" {
  for_each   = toset([data.aws_iam_policy.AdministratorAccess.arn])
  group      = aws_iam_group.Administrators.name
  policy_arn = each.value
}
Error: Invalid for_each set argument

  on modules/iam/group.tf line 10, in resource "aws_iam_group_policy_attachment" "Administrators":
  10:   for_each   = toset([data.aws_iam_policy.AdministratorAccess.arn])

The given "for_each" argument value is unsuitable: "for_each" supports maps
and sets of strings, but you have provided a set containing type dynamic.

PowerShell の -PipelineVariable を使おう

PowerShell v5 で追加された PipelineVariable に関して、本では説明していたのですがブログに書いていませんでした。 いい感じの例があったので、説明しておこうと思います

目次

TL;DR

  • PipelineVariable を使うと、パイプラインの中で一度 $x = $_ と書いて変数を保持していた処理が不要になります
  • Sort-Object のような集計系のCmdlet を使うとパイプラインの後続にわたる値の挙動が変わるので注意しましょう
  • Az モジュール使いにくい

対象のPowerShellコード

いい感じの Pipeline Variable の例があります。

この例を通して PipelineVariable を見てみましょう。

PipelineVariable とは

このコードの | % -pv vnet { $_ } は Pipeline Variable を使っています。

Pipeline Variable というのは、パイプラインからオブジェクトを次のパイプラインに送出するときに、そのオブジェクトを指定した変数に保持する機能です。

公式ではこう言っています。

新しい共通パラメーター PipelineVariable が追加されました。 PipelineVariable を使用すると、パイプされたコマンド (またはパイプされたコマンドの一部) の結果を変数として保存し、パイプラインの残りの部分に引き渡すことができます。

docs.microsoft.com

そのパイプライン時点のオブジェクトを変数に保持して何が嬉しいかというと、パイプラインがつながっている限り、直後ではない後続のパイプラインでもそのオブジェクトを変数経由で利用できます。 どういうことでしょうか?

PowerShellでは、通常パイプライン直後のスクリプトブロックでは流れてきたオブジェクトを 自動変数$_ 経由で読めます。

PS > ps | %{$_} | Get-Member


   TypeName: System.Diagnostics.Process

しかし、その次のパイプラインのスクリプトブロックでは、$_ の中身は前のパイプラインの中身に変わります。 例えば次のように、%{$_.Name} とProcess型のName プロパティ出力すると、後続のパイプライン | %{$_} では Process型ではなく String型に 変わります。

PS> ps | %{$_.Name} | %{$_} | Get-Member

   TypeName: System.String

PipelineVariable を使わないと、一連のPipelineの中であるパイプラインにおける変数を保持して後続に渡すときにハッシュテーブルや適当な入れ物に入れて渡すなど手間がかかります。

# こんなことはしたくない!
PS> ps | %{@{ps=$_;name=$_.Name}} | %{$_.ps} | Get-Member

PipelineVariables を使う例

↑の例以外にも、パイプラインの中でさらにパイプラインを書く時に、一番初めにパイプラインに入った$_ を思うように取れなくて一瞬引っ掛かったという人は多いのではないでしょうか?

何もしない無駄にそれっぽいのを例にします。 末尾の %{$_} では Process型が欲しいのですが、当然 String が来ます。

PS> ps | %{$_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$_}}
pwhs
pwhs

この時にPipelineVariable を使わない場合、一時変数に入れてからやることが多いと思います。

PS> ps | %{$ps=$_; $_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$ps}}

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
    149   404.51     462.63      54.20    6820   1 pwsh
     71    55.12      96.09       1.95   13816   1 pwsh

PipelineVarible はこういった「パイプラインの後続で今のコンテキスト ($_) を使いたい」というシーンで機能します。 PipelineVarible に置き換えてみましょう。

PS> ps -pv ps | %{$_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$ps}}

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
    149   404.51     462.63      54.20    6820   1 pwsh
     71    55.12      96.09       1.95   13816   1 pwsh

%{$ps=$_;} と書いていた処理を、パイプラインを開始する前の ps コマンド時点に -pv ps と持ってきました。 このように、「パイプラインの中で $_ をいちいち変数に受け取っていた」という人は結構楽に書けるようになるはずです。

Aggregateする処理ではPipelineVariables の利用を気を付ける

Sort-Object のようにパイプラインをせき止める Aggregation 系の処理では、後段のパイプラインの結果は前段と変わります。 例えば先ほどのコードに意図的に Sort-Object を入れるとわかります。

PS> ps -pv ps | sort | %{$_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$ps}}

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     31    29.86       9.57       0.42    9476   1 YourPhone
     31    29.86       9.57       0.42    9476   1 YourPhone

pwsh を拾っているはずなのに、YourPhone というプロセスに変わってしまいました。 YourPhone Sort-Object から渡った最後のオブジェクトに相当します。

PS> ps | sort | select -Last 1

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     31    29.86       9.57       0.42    9476   1 YourPhone

では、Sort-Object のようなパイプラインを一度せき止めるCmdletを挟みたい場合、どうすればいいのでしょうか? 簡単です、Sort-Object で PipelineVariable に割り当ててください。

PS> ps | sort -pv ps | %{$_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$ps}}

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
    148   403.27     461.28      59.45    6820   1 pwsh
     71    55.39      96.36       2.00   13816   1 pwsh

元コードからみる PipelineVariable

さぁ、PipelineVariable については概ね理解できたと思います。 元のコードを再度提示してみてみましょう。

AzVirtualNetwork | % -pv vnet { $_ } | % { $_.subnets } | select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix | sort vnet, snet

このコードの PipelineVariable は、先ほどの ps と違って一度 %{$_} を介しているように見えます。 どういうことか見てみましょう。

-pv vnet は早められる

もし、PipelineVariables を使うときに自分が % -pv vnet {$_} のように、ただ後続に $_ を流すだけの処理で PipelineVariablesを使うように書いていたら、その-pv、前段のコマンド時点に持っていくことができます。

AzVirtualNetwork -pv vnet | % { $_.subnets } | select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix | sort vnet, snet

元コードでは %{$_} でパイプラインを通るたびに新しく $vnet$_ を割り当てているのですが、それは AzVirtualNetwork | 時点でやっています。 ps の例で見せたように一番初めの Cmdletを実行した結果はパイプラインを通るのですが、その時点で PipelineVariable としてキャプチャできます。

単純な % {$_} をやるようなパイプラインが減るのは可読性、速度面の両面から嬉しいので検討するといいでしょう。

Select @{l={};v={}} をやめる

PipelineVariable とは関係ありませんが、Select @{l={};v={}} を使って PSObjectを生成している部分があります。

PSObject の生成方法としては、Select @{l={};v={}} 以外にも [PSCustomObject]@{} があります。1

Select-Object を使ったPSObject生成のメリットは、「プロパティの合成ができる」ことです。プロパティの合成は、ハッシュテーブルの後に書かれている addressprefix がそれにあたります。

select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix

一方で、[PSCustomObject]@{} を使うとハッシュテーブルから直接PSObjectに型変換します。 このやり方ではプロパティの合成はできず、自分で全プロパティに関してハッシュテーブルを定義しないといけません。 とはいえ、le のようなマジックキーに比べるとシンプルで速度も速く、可読性は高いでしょう。

% {[PSCustomObject]@{vnet=$vnet.name;snet=$_.name;addressprefix=$_.addressprefix}}

初めのコードをPSCustomObject に切り替えるとこうなります。

AzVirtualNetwork | % -pv vnet { $_ } | % { $_.subnets } | % {[PSCustomObject]@{vnet=$vnet.name;snet=$_.name;addr=$_.addressprefix}} | sort vnet, snet

まとめる

「-pv を早める」、「PSCustomObject に切り替える」の2つを組み合わせてみましょう。

AzVirtualNetwork -pv vnet | % { $_.subnets } | % {[PSCustomObject]@{vnet=$vnet.name;snet=$_.name;addr=$_.addressprefix}} | sort vnet, snet

幾分ワンライナー長い!読みたくない!感は減りました。

Azure環境の事前準備

もしコードを試す場合、Azureに VNet と Subnet が必要です。 せっかくなので、Az モジュールでサクッと組んでみましょう。

VNet作るだけなら料金かからないですしね。

AzureRm をアンインストールする

おわこん!それなのに Visual Studio で勝手に入る。

foreach ($module in (Get-Module -ListAvailable AzureRM*).Name |Get-Unique) {
   write-host "Removing Module $module"
   Uninstall-module $module
}

docs.microsoft.com

Az のインストール

代わりに Az モジュールを入れます。

Install-Module -Name Az -AllowClobber -Scope CurrentUser

docs.microsoft.com

vnet とかの準備

あとは Azure環境に ReousrceGroup、VirtualNetwork、Subnetを作ります。

$location = 'Japan East'
$rg = New-AzResourceGroup -Name test -Location 'Japan East'
$vnet = New-AzVirtualNetwork -Name test -ResourceGroupName $rg.ResourceGroupName -Location 'Japan East'  -AddressPrefix 10.0.0.0/16
$subnet = Add-AzVirtualNetworkSubnetConfig -Name a -VirtualNetwork $vnet -AddressPrefix 10.0.0.0/24
$vnet | Set-AzVirtualNetwork

これでコードを試せます。

AzVirtualNetwork | % -pv vnet { $_ } | % { $_.subnets } | select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix | sort vnet, snet

後片付け

リソースグループごとさくっと消せば全部消えます。

Remove-AzVirtualNetwork -Name $rg.Name

蛇足

Add-AzVirtualNetworkSubnetConfig で$vnet のプロパティを変更して (out相当の処理!?)、$vnet | Set-AzVirtualNetwork でVirtualNetwork に変更を適用しているの、とても書きにくいやり方ですね。

PowerShell っぽくないというか Azure に見られる特有に感じるのですが... 気のせいでしょうか。 Az モジュール、コマンドも探しにくく、Cmdletから使い方が予想できない使い方になってて、PowerShell の書く経験としては最悪に感じます。すごい、悲しい。

PowerShell 的には Addを使うと対象のオブジェクトに追加されることが多いので、それを期待している人は多いでしょう。Add-Content とか。

今回の場合、Add-AzVirtualNetworkSubnetConfig というCmdlet実行時点で $vnet に割り当てがされるのを期待するような気がほんわりします。(しない感じもある)

Add-AzVirtualNetworkSubnetConfig -Name a -VirtualNetwork $vnet -AddressPrefix 10.0.0.0/24

あるいは、VirtualNetworkSubnetConfig を作って、VirtualNetworkにAddするとか。(こっちのほうが納得感とCmdletからの予測ができそう)

$subnet = New-AzVirtualNetworkSubnetConfig  -Name a -VirtualNetwork $vnet -AddressPrefix 10.0.0.0/24 #実際には-VirttualNetwork パラメーターはない
Set-AzVirutalNetwork -SubnetConfig $subnet # こんなCmdlet もない

リソースを逐次分離したくてこうなったと思うのですが使い勝手が悪いのは Az Module でも改善されてないのでした。 az cli のほうが使いやすいので、私はもっぱら az cli です。

なお、AWS の Cmdlet 設計は秀逸で、どのCmdlet も aws cli と比較してもわかりやすい印象があります。 ただ、やはり型の扱いは若干めんどうさが表に出ていますが。


  1. ほかにもAdd-Member などいくつか方法がありますが本題ではないので省略します。

Datadog Log Management で Kubernetes の external-dns やkube2iam のログレベルを適切に扱いたい

Kubernetes のログを見るといえば、Datadog Log Mangement が楽なのですが時々ログメッセージが適切なレベルで扱われません。

そんなときによくやる「ログメッセージからレベルになるフィールドを取り出して Log Status として認識させる」ことを見てみましょう。

f:id:guitarrapc_tech:20200108100711p:plain
Before

f:id:guitarrapc_tech:20200108100721p:plain
After

目次

TL;DR

  • Custom Pipeline を使って、ログメッセージを構造化とLog Status の変更を行えばok。
  • ログメッセージがkey=value で構造化されているとさくさく構築できるので神

問題

Kubernetes に限らず、Log Management でログを取り込んだ時に意図と違ったログレベルとして認識されることがあります。

たとえば Kubernetes で external-dns を使っているとき、実行するものがないときは All records are already up to date というメッセージ出力されるのですが、Datadog Log Management では Status が Error となっています。

level=info msg="All records are already up to date"

しかしこのログは、Message に level=info とある通りエラーログではありません。 これをInfoとして扱いたいのでさくっと対処しましょう。

ログを見てみる

まずはログがどう認識されているのか見てみましょう。

f:id:guitarrapc_tech:20200108100351p:plain
何も実行するものがないときのログ

対象のログを見てみると、Log Status が Error となっていることがわかります。

f:id:guitarrapc_tech:20200108100325p:plain
Log Status が Error

また、ログも構造化されていません。

f:id:guitarrapc_tech:20200108100217p:plain
ログが構造化されていない

対処方針

external-dns のログは、level key でログレベルを表現しています。 こういったログへの追加対応は、Log Management のPipeline に追加のカスタムパイプラインを追加することでできます。 パイプラインには様々な処理(Processor)を設定できます。 今回はデフォルトのパイプラインで設定されたログレベルを書き換えたいので、Log Status Remapper を用いることでログに含まれる level キーの値を Log Status として認識させることができます。

Use this Processor if you want to assign some attributes as the official status. For example, it can transform this log:

f:id:guitarrapc_tech:20200108094843p:plain
before

f:id:guitarrapc_tech:20200108094853p:plain
after

docs.datadoghq.com

対応方針は次の通りです。

  • 新規パイプランを作る
  • ログメッセージがただの文字列として扱われているのでJSON として認識されるように構造化
  • 構造化したデータから対象の level key を拾ってLog Status を書き換える

パイプライン対応

Custom Pipeline を追加して、2つProcessorを追加していきます。

Custom Pipeline の追加

Datadog Agent のPipeline の後ろに新規Pipeline を追加します。

パイプラインを作成するときに、対象となるログを絞り込みます。 今回は All records are already up to date となっているログを絞りたいので、Source とメッセージで指定してみましょう。

source:external-dns message:"*All records are already up to date"

Processor の追加: Grok Parserで構造化する

パイプラインができたら、具体的な処理単位であるProcessor を追加します。 まずは、ただの文字列になっているログを構造化 (JSON) 変換するため、Grok Parserを用います。

https://docs.datadoghq.com/logs/processing/processors/?tab=ui#grok-parser

変換方法を考えるため、対象にしたいログメッセージを見てみましょう。

time="2020-01-07T09:49:07Z" level=info msg="All records are already up to date"

今回のログは 「key=value がスペースで羅列されている構造」とわかります。 こういった、文字列が key{何かの文字}value で構成されている場合、%{data::keyvalue} を使うだけでサクッと構造化できます。

external_dns_rule %{data::keyvalue}

docs.datadoghq.com

これでいい感じのJSON に変換されてました。

{
  "time": "2020-01-07T09:49:07Z",
  "level": "info",
  "msg": "All records are already up to date"
}

f:id:guitarrapc_tech:20200108095407p:plain
Grok Parser で構造化

Processor の追加: Log Status Remapperでログステータスとする

構造化したデータからステータスをとります。 もう一度Processor を追加して、今度は Log Status Remapper を使ってログステータスの差し替えを行います。

https://docs.datadoghq.com/logs/processing/processors/?tab=ui#log-status-remapper

level キーの値を使うので、対象キーに level を指定します。

f:id:guitarrapc_tech:20200108095722p:plain
Log Status Remapper で level キーを用いる

これでおしまいます。

Pipelineの結果を確認

Log Explorer でログの結果を見てみると、いい感じに Status が変わったことが確認できます。

f:id:guitarrapc_tech:20200108100010p:plain

先ほどなにもデータがなかった ATTRIBUTES にも、変換した構造化データがのっていることがわかります。

このパターンはちょくちょく使うので、さくっとできると捗るでしょう。

REF

github.com

GitHub Actions で .NET Framework プロジェクトをビルドする

Visual Studio 拡張 のプロジェクトは、今でも .NET Framework です。 .NET Framework ということは、基本的に Windows でのビルドになります。

今回、AppVeyor でやっていたビルドを GitHub Actions に移行したのでメモ。

最終的に、次のように .NET Framework のビルドが GitHub Actions で完結します。

f:id:guitarrapc_tech:20200106103428p:plain
.NET Framework のビルドがGitHub Actions で成功した図

目次

TL;DR

  • .NET Framework のアプリも GitHub ホストランナーで Windows OS を選べば問題ない。
  • Visual Studio 拡張のような変なビルドでも問題ないので安心してビルドできる
  • GitHub Actions は AppVeyor にトドメを刺しそう (good bye

リポジトリ

VS拡張で提供している OpenUserSecrets をVS2019 対応するついでに AppVeyor から GitHub Actions に移植します。

github.com

事前知識

GitHub Actions での書き方など一通りの注意は公式Docみるかまとめたので参照してください。

tech.guitarrapc.com

CIサービスの変化

  • Before: AppVeyor
  • After: GitHub Actions

前提として、AppVeyor / GitHub Actions のいずれにおいても Windows 依存のビルドは Docker Image でのビルド実行ではなくホストマシンでの実行となります。

それぞれの定義を見ていきます。

AppVeyor の定義

OpenUserSecrets/appveyor.yml at 362cc778821bf9724176ed9439c8c4f6c84e5e8a · guitarrapc/OpenUserSecrets · GitHub

image: Visual Studio 2017
version: '1.0.{build}'  
shallow_clone: false    
pull_requests:   
  do_not_increment_build_number: false  

configuration: Release   
platform: Any CPU    

before_build:    
 - nuget restore src/OpenUserSecrets.sln  

build:   
  project: src/OpenUserSecrets.sln   

artifacts:   
- path: '**\*.vsix'

AppVeyor は、イメージに Visual Studio などのツールが入っているので Visual Studio 2017Visual Studio 2019 を選んでおきます。

www.appveyor.com

GitHub Actions

OpenUserSecrets/build.yml at master · guitarrapc/OpenUserSecrets · GitHub

name: build

on: [push]

jobs:
  build:

    runs-on: windows-2019

    steps:
    - uses: actions/checkout@v1
    - uses: warrenbuckley/Setup-Nuget@v1
    - run: nuget restore $Env:GITHUB_WORKSPACE\src\OpenUserSecrets\OpenUserSecrets.csproj
    - uses: warrenbuckley/Setup-MSBuild@v1
    - run: MSBuild.exe $Env:GITHUB_WORKSPACE\src\OpenUserSecrets\OpenUserSecrets.csproj -p:Configuration=Release
      timeout-minutes: 5
    - uses: actions/upload-artifact@v1.0.0
      with:
          name: artifacts
          path: src\OpenUserSecrets\bin\Release\OpenUserSecrets.vsix

GitHub Actions は、host によってインストールされているツールが変わります。

help.github.com

help.github.com

windows-2019 は Visual Studio および Microsoft.VisualStudio.Workload.VisualStudioExtension が入っているので、このホストイメージで問題ありません。

パスを解決する

こういったCI でビルドするときに妙なはまり方をしやすいのが「PATH」です。 特にツールのパスは、「どこにインストールされたのか興味がない」のに、パスがとっていないと気にする必要があります。 そのため、こういったツールを利用するときはパスを通すのが定石です。

.NET Framework のビルドは、「パッケージをNuGet で復元する」「msbuildでビルド」というよくある2段階を踏んで実行されます。 この2つで使うツールを、GitHub Actions でパス解決しつつ実行する方法を考えましょう。

nuget.exe のパス解決

NuGet のパッケージリストアはnuget.exe を使って行います。 nuget.exe のパス解決は、uses: warrenbuckley/Setup-Nuget@v1 で行えるのでぜひ利用しましょう。

github.com

これで nuget restore csprojのパス で NuGetのパッケージリストアが行えるようになりました。

MSBuild.exe のパス解決

MSBuild は通常 Visual Studio を一緒に入っています。 これを解決するツールとして vswhere があるのですが、そんなものを使わず warrenbuckley/Setup-MSBuild を使いましょう。パスに入れてくれます。

github.com

これで MSBuild.exe csprojやslnのパス でビルドが実行できるようになりました。

NuGet のパッケージリストアを行う

基本的に、現在のリポジトリのチェックアウトパスに興味ありません。 環境変数 GITHUB_WORKSPACE を使うといい感じにcheckout したときのベースパスが解決されます。

これで、csproj のパスが $Env:GITHUB_WORKSPACE\src\OpenUserSecrets\OpenUserSecrets.csproj とわかりました。

キャッシュを考えそうですが、今回のような即座にパッケージリストアが完了する場合は考えなくてもいいでしょう。

これでいい感じで NuGet のパッケージリストアが実行できました。

f:id:guitarrapc_tech:20200106102930p:plain

ビルドを行う

ビルド時のcsprojパスも NuGet と同じで GITHUB_WORKSPACE を使えばokです。

また、今回は Visual Studio 拡張をビルドしたら、そのビルドパッケージを配布するのでリリースビルドを行います。

MSBuild実行時時にConfigurationプロパティを Release に切り替えればokです。

これでいい感じで MSBuild が実行されました。

f:id:guitarrapc_tech:20200106103003p:plain

デフォルトの csproj は、CIでビルドするときに 次のセクションで紹介する devenv 初期化が走って限界である 6 hour までタスクがタイムアウトしません。 timeout-minute: 5 は、ビルドが5分以上かかること自体が異常とみなしてタイムアウトを仕掛けています。

Visual Studio 拡張ビルド時のdevenv 初期化をスキップする

Visual Stduio 拡張は、クラスライブラリなどと違って 「ビルド時に Visual Stduio )devenv.exe) の初期化を行う」動きをします。 CI 的にはdevenvの初期化なんてされてほしくないわけで、実際永遠に終わりません、厄介! (6 hour timeout でビルド失敗する悲劇が起こる)

f:id:guitarrapc_tech:20200106105526p:plain
6時間タイムアウトでビルドが失敗した図

対策はいくつか考えられます。

github.com

私のオススメは、PropertyGroup で DeployExtension を false にして初期化を行わないことです。 csprojに <DeployExtension Condition="'$(GITHUB_ACTIONS)' != ''">False</DeployExtension> を設定しておけば GitHub Actions でのみ無効化されます。

Debug と Release ビルドで無効化したいので次のようになるでしょう。

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <!-- Stop Initialize Visual Studio Experimental Instance on CI -->
    <DeployExtension Condition="'$(GITHUB_ACTIONS)' != ''">False</DeployExtension>
    <!-- 省略 -->
  </PropertyGroup>

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <!-- Stop Initialize Visual Studio Experimental Instance on CI -->
    <DeployExtension Condition="'$(GITHUB_ACTIONS)' != ''">False</DeployExtension>
    <!-- 省略 -->
  </PropertyGroup>

これはオススメしません。失敗するはず。(このPRも最終的に取り消して↑の手法に切り替わってる)

github.com

パッケージをアップロードして GitHub Webから取得できるようにする

GitHub Release に置くことも考えそうですが、Visual Studio 拡張はMarket Placeからの配布が基本なので今回は Release ページには載せないことにします。 となると、ビルドした Action 毎にビルド成果物である .vsix ファイルがWeb上から取得できればok です。

こういった時に使えるのが、Artifacts です。 ビルドで vsix が生成されるパスは、csproj のあるパスからみて bin\Release\OpenUserSecrets.vsix とわかっているので、指定すればok です。

upload-artifact アクションは、環境変数 GITHUB_WORKSPACE のパスで実行されるので、Artifacts の path は リポジトリルートからみた指定でok です、親切!

期待通りアップロードされています。

f:id:guitarrapc_tech:20200106103023p:plain

いい感じで取得できることがわかります。

f:id:guitarrapc_tech:20200106102846p:plain

おまけ

今回の OpenUserSecrets のアップデート (1.1.0) は、この記事にあったNuGet のパッケージをインストールしてくださいメッセージへの対処や VS2019対応です。

shuhelohelo.hatenablog.com

REF

基本はこれ。パス解決の考慮がないのでCI的には取り回しが悪いので注意。

github.community

GitHub Actions ことはじめ

GitHub Actions 以前調べたのですが、いろいろあって個人プロジェクトでサクッとビルドするのみに使っていました。 今回改めて調べを進めたのでメモ。

幾つかのリポジトリを GitHub Actions に移行したけど、記事にしようとまとめてたらやった内容以上に調べることになってめちゃめちゃ時間かかった。

目次

TL;DR

  • とりあえず公式ドキュメント読めばok

help.github.com

  • 公式ドキュメントにないのはだいたいフォーラムみればok

github.community

  • 基本は Azure pipeline だけど、結構使いやすい感じになってる。
  • Azure Pipelines と違って、step の name を省略するとコマンドがそのまま表示されるので name は基本省略で行ける。 (これ重要)
  • public repo なら Github Actions もいい感じ。Windows や macOS 依存はこっちに移植する。
  • private repo は、GitHub を新料金プランにしているか次第。CircleCI の Performance Plan のユーザー課金が納得いかないのでこっちに移行していきたい。
  • クラウドホストランナーが基本でよさそうだけど、Kubernetes 上でのCDを考えると セルフホストわんちゃんある。
  • AppVeyor でやってたことはほぼ移行できる。
  • CircleCI もだいたい行ける。Orb だけ代替あるかが問題になるぽ。

トレンド

GitHub Actions の今のスタイルがブログでアナウンスされた2019/Aug/8 ぐらいからトレンド上がってるんですね。

github.blog

github.blog

f:id:guitarrapc_tech:20200106074554p:plain
世界

f:id:guitarrapc_tech:20200106074613p:plain
日本

f:id:guitarrapc_tech:20200106074630p:plain
USA

GitHub Actions の基本

基本的な情報を裏取りする。

使用条件

  • Org は新GitHub Pricing に移行していること (旧プランの場合 Actions 自体使えない)

f:id:guitarrapc_tech:20200106001331p:plain

旧プランで使えないのは、最近の GitHub の新サービスあるあるなのですが、なるほど困るケース結構多い。

旧プランと新プランは、リポジトリ数課金 or 人数課金の違い。 小規模な会社で30リポジトリ100ユーザーとかだと、新旧で桁が違うレベルで料金が変わるため移行に踏み切れないのはままある。

  • 旧プランは、人数無制限、リポジトリ数で段階的なプラン選択
  • 新プランは、人数課金、リポジトリ数制約なし

旧プランのほうがお得なケースは多数あってなかなか新プランに移行が難しいケースも見かけるので実は難しいやつ。

使用制限

並列数やAPI、実行時間で制限がある。

help.github.com

  • 1リポジトリあたりの同時並列は20
  • 1時間当たり リポジトリの全アクションで1000 APIリクエスト
  • ワークフローは実行時間6時間が上限 (指定しないと6時間たつまでタイムアウトしないので長すぎ
  • 無料、有料で同時実行上限がある
GitHub plan Total concurrent jobs Maximum concurrent
macOS jobs
Free 20 5
Pro 40 5
Team 60 5
Enterprise 180 15

マイニングがダメとかその辺は当然あるので見ておきましょう。 Serverless Computing がダメなのもそれはそうなんだけど、いい感じにやってみた的にやりそうな人も出てきそうなので規約でしばるのでいいですね。

料金

OSS は無料で並列度高いので気軽に利用できていい感じ。CircleCI より緩くて Azure Pipeline や Travis 並みに楽。

会社など Private Repo だと、Team でも 10000min/month しかつかないので、十分ではなく追加購入必要になる感じ。 従量課金の体系はシンプルで、GitHub ユーザー課金ある前提と合わせて追加なので、もともと GitHub 使ってたこと考えると CircleCI の Performance Plan より納得度は圧倒的にある。だからこそ新料金プランに限定するんでしょうが。

github.co.jp

help.github.com

  • パブリックリポジトリ : 無料
  • プライベートリポジトリ : 含まれる利用時間 + 追加購入

含まれる利用時間

利用時間は毎月リセットされるけど、ストレージはリセットされない。

2020/4/13 までは以下でしたが、2020/4/14~ 変更に

~2020/4/13プラン 1か月あたり利用時間 ストレージ
Free 2,000分 500 MB
Pro 3,000分 1 GB
Team 10,000分 2 GB
Enterprise 50,000分 50 GB
2020/4/13~プラン 1か月あたり利用時間 ストレージ
Free 2,000分 500 MB
Team 3,000分 2 GB
Enterprise 50,000分 50 GB
GitHub One 50,000分 50 GB

利用時間的には、GitHub Team が 旧Pro 相当になった感じ。

ストレージ価格がおもむろに出てきたけど、public は無料。 private の場合に、アカウントの持つGitHub packages とGitHub Actions アーティファクトの利用合計らしい。 制限を超えてアカウントが利用していて、$0 を超えるLimit spendingを設定していると料金がかかるので注意がいりそう。 そんなに大きなサイズのArtifact を設定するなという話なのですが、要注意。

アカウント > Settings > Billing > Cost management > Limit spending で設定できる。デフォ $0

ビリングの storage に関してを見ておくほうがよさそう。 ちょっと細かいので随時確認しよ。

https://help.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions#calculating-minute-and-storage-spending

OS別時間消費

Linux を 1として、Windows や macOS は利用時間の倍率がついているという考え。

Operating system Minute multiplier
Linux 1
macOS 10
Windows 2

追加購入

倍率に応じてなので、結果としてOS別に価格差が生じる。

Windows たっか。(あるある macOS やばい、これは追加になるとセルフホストまったなしになりそう。

ホストランナー スペック 料金
Linux 2 cores, 7GB $0.008 1分あたり
Windows 2 cores, 7GB $0.016 1分あたり
macOS 2 cores, 7GB $0.08 1分あたり
セルフホスト - Free

ホストランナーの指定

GitHub Actions は Azure Pipeline 同様にホストランナー1をどこで実行するか選択できる。

help.github.com

マネージド、セルフホストの選択

ホストランナーはマネージド、セルフホストが選択できる。

  • GitHub ホストランナー :マシンメンテナンス、アップグレードがマネージドに提供される
  • セルフホストランナー : 物理、仮想、コンテナ、オンプレミス、クラウド に自分でインストールする

通常はクラウドホストランナー (GitHub が提供するホスト) を選ぶし、それが最も手早く楽。

セルフホストランナーは既にビルドを実行する環境がある、けどビルドの管理をしたくない、何かしらマシンに事前にインストールが必要など融通をきかせたいときに便利。ようは Azure Pipeline の Self Hosted Agent と一緒。

help.github.com

例えば、自分たちのKubernetesクラスターにセルフホストランナーを入れてビルドをすることで、クラスターの認証への考慮を減らしてクラスターへのデプロイすることもできる。

github.com

個人のプロジェクトで考えるので、ここでは GitHub ホストランナー を前提としてみていく。

ハードウェアリソース

GitHub ホストランナーは、 Azure VM (Standard DS_v2) 上で動作するので、スペックはVMと同じ、素直。

  • 2コアCPU
  • 7 GBのRAMメモリー
  • 14 GBのSSDディスク容量

f:id:guitarrapc_tech:20200104221842p:plain
DSv2-series

インストールされるツール

OS 毎に一覧になってるので入ってないツールはインストールして対応で。

help.github.com

IP

GitHub の IP とは違うのが注意で、以下のURL のIPレンジではない。

https://api.github.com/meta

github.community

全て Azure VM でのホスト、かつ現状は us-east-2 region なのでIPもそこ縛り。2

IPレンジは毎週更新してJSONで公開されているが、このリンクはまさかの Download Center で面倒な状況。

www.microsoft.com

サポート回答もこのURLで、常に最新のjsonに直接アクセスできるリンクは用意しない気配。

github.community

直URL も日付入りで、決まったURLへ定期取得するのは想定されてなさそう。

https://download.microsoft.com/download/7/1/D/71D86715-5596-4529-9B13-DA13A5DE5B63/ServiceTags_Public_20191216.json

100日さかのぼってみたが、今のところ更新ない or URL生成ルール違うのか404。(ルールあるのかなぁ)

OSの選択

GitHub ホストランナーの実行可能な環境は3種類あり、Azure Pipeline と同じ。 ほとんどのケースではLinux 選んでおけばいいけど、.NET Framework のような Windows 固定、Xcode のような macOS 固定もあるので用途に応じて。

  • Linux
  • Windows
  • macOS

Azure Pipeline 同様にマトリックスビルドには対応しているので、OSS で各種OSむけのビルドは CircleCI より書きやすく、Travis CI の代替になりえる筋はある。

https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/configuring-a-workflow#configuring-a-build-matrix

実行権限

CIではツールインストールなど権限を必要とすることが多い。 Linux/Windows いずれにおいても管理者権限があるので権限で悩むことはないかな。

  • Linux, macOS: パスワードレス sudo 状態
  • Windows: UAC無効管理者状態

ファイルパス

実パスを気にせず、GitHub が提供している環境変数でアクセスできる。

  • $HOME : ユーザーパス。ログイン認証情報を含めてokらしい。
  • $GITHUB_WORKSPACE : アクションとシェルコマンドの実行パス、変更可能。
  • $GITHUB_EVENT_PATH : webhook イベントのPOSTペイロードへアクセスできる。

ホスト仮想マシン

ファイルパスは静的ではないため、GitHub が提供している環境変数を使う必要がある。

  • home
  • workspace
  • workflow/event.json

コンテナ

静的にパスを利用するため、USER を指定すると $GITHUB_WORKSPACE にアクセスできなくなる。 /github Path Prefix を予約しているので自分でボリュームマウントするときは気を付ける。

  • /github/home
  • /github/workspace
  • /github/workflow/event.json

環境変数

CircleCI のように spin up した環境の変数は出してくれない。Azure Pipeline と一緒で不親切、正直なんでやねんっていつも思ってる。

自分で拾う必要があるので、適当にデフォルトの環境変数は把握しておく。

GitHub が使う環境変数は、GITHUB_ prefixがついており、 GITHUB_ prefixを使用して環境変数またはシークレットは設定できない。(エラーになる)

help.github.com

シークレット

Settings > Secrets からシークレットを設定できる。

f:id:guitarrapc_tech:20200105004654p:plain

Azure Pipeline 同様に、ログからは自動的にマスクされる。 制約として、ワークフローで最大100シークレット、容量は64K 。

help.github.com

構造化データを値にするのは避けるべきというのは、あるある注意。

GitHubがログのシークレットを確実に削除するよう、構造化データをシークレットの値として使用することは避けてください。たとえば、JSONやエンコードされたGit blobを含むシークレットは作成しないでください。

GITHUB_TOKEN

GitHub Actions で何気に一番うれしいやつ。GitHub への操作多いので、Token 自動的に生成してくれるの最高。

前提として、write アクセスがあるユーザーになってるので注意。

リポジトリに対してwriteアクセスがあるユーザなら、誰でもシークレットの作成、読み取りおよび使用ができます。

GitHub Apps をインストールするアクセストークンとして、GITHUB_TOKEN が自動生成される。 ジョブごとに60分後に期限切れになる一方で、ワークフローはデフォルト6hまでタイムアウトにならないので注意がいりそう (このデフォルトが長い)

help.github.com

権限は一通り書き込み権限があり、リリースにパッケージ投げたりコメント書いたりは十分。 逆に言うと結構強いので注意がいりそう。(もちろんOrg / Team はない)

Fork リポジトリからは read があるけど write がない。PRでトリガーされるワークフローで書き込み操作は軽くはまることはあり得る。

Permission Access type Access by forked repos
checks read/write read
contents read/write read
deployments read/write read
issues read/write read
metadata read read
packages read/write read
pull requests read/write read
repository projects read/write read
statuses read/write read

コンテキスト

コンテキストはかなり便利。

help.github.com

github コンテキストは REST API でとれるレスポンス/webhook レスポンスに近く、そのコミットの情報が結構とれる。 コミットメッセージまででるので、skip ci の自作とかはこれで。 環境変数 GITHUB_ でほとんどとれるとか書いてるけど、取れるのはよくCIにある情報程度で、少し深くなるとコンテキスト触るしかない。

env や secrets もコンテキストの一種なので、めちゃめちゃ使う。

コンテキストは、steps じゃなくても参照できるのが重要で、jobs.if でも利用できるのでコンテキストが一致するかで job自体を実行するかとかも制御できる。

Artifact

なるほど、アーティファクトで job 間のデータ永続は Azure Pipeline まんま。

GitHub Actions のアーティファクトとは、ワークフロー実行中に生成されるファイル、またはファイルのコレクション。

ワークフロー内の各ジョブが異なるインスタンスで実行されるため、ワークフロー内でジョブ間でデータを受け渡すのには必須。 ビルドパッケージを置いたり、ログを置いたり何かと使うやつ。

help.github.com

アーティファクトはアップロードされると zip になるけど、ダウンロードは生データが取れるので取り回しは楽。

  • ジョブが終了する前にデータをアップロードして、ワークフロー実行の内部にファイルを保存することができる。 アーカイブをアップロードするときは、名前を付ける。
  • ファイルをアップロードすると、同じワークフロー実行内の別のジョブでダウンロードできる。 アーカイブをダウンロードするとき、アーカイブは名前で参照できます。 アップロードされているアーティファクトをダウンロードできるのは、同じワークフロー実行中だけ。

ワークフロー終了後にジョブごとに Artifact をWebから取得できるので便利。

f:id:guitarrapc_tech:20200105000841p:plain

トリガーイベント

一つ以上のイベントをトリガーに設定できる。 pushや Issue、PR、webhookなどの各種イベント以外にも定期実行も可能。 ここが CircleCI では厳しかったので GitHub Actions でうれしいところ。

GITHUB_SHAGITHUB_REF 環境変数に現在のイベントに応じた状態がでるのでなるほど。(イベントごとに変わる) wiki ページの作成、更新は gollum イベントらしい。

help.github.com

ただ、ワークフローから別のワークフローはトリガーできないので、コメントなり適当なイベントを仲介する必要がある。

実行しているワークフローのアクションからは、新しいワークフローの実行をトリガーできない。

push イベントでファイルの変更を完全に終えず、REST API を使って取得が必要なのもなるほど。

GitHub Actionsが利用できるwebhookのペイロードには、commitオブジェクト中のadded、removed、modified属性は含まれない。

Fork されたリポジトリではデフォルトでは動かない。 もし実行するようにしても、Fork されたリポジトリの GitHub Actions は ベースリポジトリではトリガーされず、Fork先のリポジトリで許可が必要なのは普通のCIでよい。

Cache

なるほど、CircleCI とよく似てるけど、微妙に違う。

マッチングルールは、key で完全一致を見て、失敗したら restore-keys を上から順に前方一致で検索なのでよくあるとおり「長いキーから順に書き並べる」のが王道。さらに restore-keys で複数一致したら最近のキャッシュが利用されるので素直な印象。

  1. key の完全一致
  2. restore-keys を上から評価
  3. より最新のもの

help.github.com

Cache と Restore がセットになってるのはうれしいですね、actions/cache しておくと、そのパスに変更合った時にJobの終了時に自動的に post action としてキャッシュされるのは普通に便利。 この時キャッシュキーがすでにあればスキップされるので、意図通り。

www.kaizenprogrammer.com

OS別なら、${{ runner.os }} をキーに入れるのもあるある。 キャッシュ上書きがなく、7日でキャッシュ消えるものの任意のキャッシュクリアがないので、環境変数などで cache-version を定義してキャッシュキーに含めるのも王道。

pushとpull_request イベントだけ使えること、7日間以上アクセスされていないキャッシュエントリは削除されることに注意。 また、リポジトリのキャッシュサイズ制限が2GB と小さいのもはまる。

  • 400MBを超えないキャッシュ内の個々のファイル。 この制限を超えた場合、400 - Bad Requestが返さる。
  • リポジトリ内のすべてのキャッシュの合計サイズが2GBを超えない。

相変わらず Windows \ と macOS/Linux / でパス文字 の解釈が違って key の指定がコケるのはつらい。

github.com

Actions

CircleCI Orbs じゃないけど、似たような処理の塊は Actions と呼ばれていて、Marketplace で公開されている。

github.com

Actionsでやっていることを知りたいときは、追っていくと GitHub リポジトリにたどりつくので、やってることも読めるしいい感じになってる。

Actions について概要はドキュメントがあるのでみればok

help.github.com

Actions を使うときは、GitHub Web 上で YAML を編集する時なら右にポップアップが出る。

f:id:guitarrapc_tech:20200106005534p:plain

シンタックスエラーを検出したり、インテリセンスとかも利くので、こっち使うのが楽。

f:id:guitarrapc_tech:20200106005612p:plain
GitHub Web上ならシンタックスエラーも検出

f:id:guitarrapc_tech:20200106005645p:plain
インテリセンスも利く

通知

Web と Email が設定できる。

help.github.com

f:id:guitarrapc_tech:20200105004804p:plain

なるほどビルドがコケると通知される。

f:id:guitarrapc_tech:20200105004620p:plain

YAML

実際書くときは、YAML書いてみて、わからないものを調べるという流れで慣れていっている。 ではYAMLで実際にどう書くのかざくっと使うものを見てみる。

Azure Pipeline 風味が残ってるような感じで、もうちょっと砕けててほど良さもある。

Getting started

はじめての YAML テンプレートは、Actions 選択したときに選択できるテンプレートを使うのが楽。

help.github.com

YAMLシンタックス

どういう要素があるのかは、まずはシンタックスを把握すればok。

help.github.com

最低限の定義はこんな感じ。何の意味もないやつ。

on: push
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo Hello

次の要素が必須なのがわかる。

  • name : Workflowの名前でリポジトリのActions画面で表示される名前。省略するとパスになるので付けたほうがいい。
  • on: 必須。実行するイベントやブランチなどの指定をする。
  • jobs: 必須。とりあえず jobs: でok
  • jobs..runs-on: 動作させるホストランナーの指定。GitHub hosted か Self-hosted かはここで決まる。
  • jobs..steps: 必須。とりあえず steps: でok。jobs を書くときは、最低1つの steps が必要。
  • jobs..steps.step : steps の中は最低一つのstep が必要。例は run step を使っている。

on

実行するトリガーの定義をここでする。 GitHub Actions が他のCIと決定的に違うのが、ここでGitHub イベントと楽にバインドできることだと思う。

# push のみ
on: push
# push と pull request なら
on: [push, pull_request]

ブランチも含めた制御ができるのはいい感じ。

on:
  # Trigger the workflow on push or pull request,
  # but only for the master branch
  push:
    branches:
      - master
  pull_request:
    branches:
      - master
  # Also trigger on page_build, as well as release created events
  page_build:
  release:
    types: # This configuration does not affect the page_build event above
      - created

定期実行は、schedule イベントで実行できる。

on:
  schedule:
    # * is a special character in YAML so you have to quote this string
    - cron:  '*/15 * * * *'

env

グローバルに、全Job、全step から参照する環境変数はここで指定する。 特定のジョブでなら jobs.<job_id>.env でいいし、特定のStep でなら jobs.<job_id>.steps.env があるので、あまり使いたくない。

jobs.<job_id>.needs

依存する job を指定できる。大事。 ここで指定しないと並列に回るので、直列で回すときは CircleCI 同様依存関係を作る必要がある。

jobs:
  prepare:
    runs-on: ubuntu-latest
    steps:
      - run: echo prepare

  build:
    runs-on: ubuntu-latest
    needs: prepare
    steps:
      - run: echo build

jobs.<job_id>.runs-on

実行するホストのOSをあらかじめ定義された値から選んで使う。

  • Windows Server 2019: windows-latest または windows-2019
  • Ubuntu 18.04: ubuntu-latestまたはubuntu-18.04
  • Ubuntu 16.04: ubuntu-16.04
  • macOS Catalina 10.15: macos-latest, macOS-10.15
jobs:
  build:
    runs-on: ubuntu-latest

jobs.<job_id>.container

ジョブをコンテナで実行したいならここで指定する。

jobs:
  my_job:
    runs-on: ubuntu-latest
    container: node:10.16-jessie

何気にイメージ指定するだけじゃなくて、細かくDockerの実行時パラメーターも調整できる。

jobs:
  my_job:
    runs-on: ubuntu-latest
    container:
      image: node:10.16-jessie
      env:
        NODE_ENV: development
      ports:
        - 80
      volumes:
        - my_docker_volume:/volume_mount
      options: --cpus 1

jobs.<job_id>.services

追加のコンテナを動かすときは services を用いる。 例えば、CI上で Database 起動させてテストするとかはこれ。

jobs:
  container-job:
    runs-on: ubuntu-latest
    container:
      image:  node:10.16-jessie
    services:
      redis:
        image: redis
        ports:
          - 6379/tcp

job も actionもコンテナで実行しているときに services のコンテナの参照をするときは、ホスト名でok。

そのステップがホストで実行しているときに services のコンテナの参照をするときは、localhost + マッピングしているホストのポート でアクセスする。 ホストでマッピングされたポートは ${{ job.services.<service_name>.ports[<port>] }} でとれる。 上のredisなら、${{ job.services.redis.ports['6379'] }}

jobs.<job_id>.if

ジョブの実行自体をここで制御できる。circleci のwhen を job で指定するみたいなやつ。 if ではコンテキスト参照できるので、特定のコミットメッセージの場合はジョブを実行しないとかが書ける。

jobs:
  my_first_job:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request' && github.event.action == 'unassigned'

あと、if ではコンテキスト参照するときに ${{ }} で囲まなくていい。 演算子とか書式はドキュメント見るのがいい。

help.github.com

job じゃなくて step を 条件で実行制御したい場合は、jobs.<job_id>.steps.if があるので、step に対して if を付ければok

jobs:
  my_first_job:
    runs-on: ubuntu-latest
    steps:
     - name: My first step
       if: github.event_name == 'pull_request' && github.event.action == 'unassigned'
       run: echo This event is a pull request that had an assignee removed.

jobs.<job_id>.steps

ここにやりたい処理を書いていく。 uses で、GitHub Actions を利用したり runs でシェルでコマンド実行したりできる。

uses は、step で特定のコンテナ実行とかもできるのでこれは結構便利。

  • パブリック Repository の任意のリリースやブランチ、コミットを指定できる。
  • 自分のリポジトリで定義した action の参照
  • public registory の docker をステップで実行
jobs:
  my_first_job:
    runs-on: ubuntu-latest
    steps:
      # use GitHub Actions
      - uses: actions/setup-node@v1
      # use a specific version tag of a public repository
      - name: use AWS Repo
        uses: actions/aws@v2.0.1
      # use a action in workflow repository
      - uses: ./.github/actions/my-action
      # use a docker in public registory
      - uses: docker://gcr.io/cloud-builders/gradle

jobs.<job_id>.steps.run

一行、複数行、所定のパスで実行というのがよく使うパターン。 複数行は、安定の | なのでまぁ大丈夫そう。

jobs:
  my_first_job:
    runs-on: ubuntu-latest
    steps:
      # single line
      - run: sudo apt-get update
      # multi line
      - run: |
          sudo apt-get install google-cloud-sdk && \
          kubectl

他のシェルを指定するには shell: で指定する。

jobs:
  my_first_job:
    runs-on: ubuntu-latest
    steps:
      - run: sudo apt-get update
        shell: bash
      - run: Get-Location
        shell: pwsh

Windows の run、あるいはpowershellpwsh などを指定して PowerShell がシェルの場合、自動的に頭に $ErrorActionPreference = 'stop' が追加されて Fail fast になるのと、末尾に if ((Test-Path -LiteralPath variable:\LASTEXITCODE)) { exit $LASTEXITCODE } を追加して実行結果でrunステップが失敗するように処理が差し込まれているので注意。

jobs.<job_id>.steps.with

いわゆるパラメーター。 Actions とかでパラメーター渡すときは with の中に map (KeyValue) を書くことになる。 この with で指定したキーは、INPUT_ prefix つきで大文字に変換されて Actions 内部から参照できる。 first_name というキーで指定したなら INPUT_FIRST_NAME で値に参照できる。

jobs:
  my_first_job:
    runs-on: ubuntu-latest
    steps:
      - name: My first step
        uses: actions/hello_world@master
        with:
          first_name: Mona
          middle_name: The
          last_name: Octocat      

環境変数

step のrun実行時に渡したいなら、env: で指定する。

jobs:
  my_first_job:
    runs-on: ubuntu-latest
    steps:
      - name: Hello world
        run: echo Hello world $FIRST_NAME $middle_name $Last_Name!
        env:
          FIRST_NAME: Mona
          middle_name: The
          Last_Name: Octocat

step のrunの中でいじるなら、シェルに従って環境変数を触ればいい。

# windows
$env:PATH += ";$pwd\build\lib"

step 間で環境変数を保持したい場合は、set-env を echo 出力する。Azure Pipeline に似た感じだけど微妙に違う。

::set-env name={name}::{value}

action_state という環境変数に yellow という値をいれる場合はこうなる。

echo "::set-env name=action_state::yellow"

先ほどの PATH をstep間で保持するならこう。

# windows
$env:PATH += ";$pwd\build\lib"
echo "::set-env name=PATH::$env:PATH"

PATH 環境変数への追加は、 echo "::add-path::/path/to/dir" が用意されている。 echo "::set-env name=PATH::${PATH}:/path/to/dir" でも動くけど、楽なほうで。

help.github.com

シークレット

シークレットの利用は、secrets コンテキスト経由で利用できる。 例えば SuperSecret というキーで登録したなら、${{ secrets.SuperSecret }} 。 利用する際は、with 構文か env など環境変数経由で参照する。

steps:
  - name: Hello world action
    with: # Set the secret as an input
      super_secret: ${{ secrets.SuperSecret }}
    env: # Or as an environment variable
      super_secret: ${{ secrets.SuperSecret }}

GITHUB_TOKEN も シークレットコンテキスト経由で利用できます。

name: Pull request labeler
on:
- pull_request
jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/labeler@v2
      with:
        repo-token: ${{ secrets.GITHUB_TOKEN }}

ログの表示で任意の文字列をマスクするために ::add-mask::{value} もある。

Artifact

アップロード操作は$GITHUB_WORKSPACEを起点に path で指定できます。 常に相対パスでいいので、絶対パスを気にする機会が減っていい感じです。

アップロードしてダウンロード

jobs:
  job_1:
    name: Add 3 and 7
    runs-on: ubuntu-latest
    steps:
      - shell: bash
        run: |
          expr 3 + 7 > math-homework.txt
      - name: Upload math result for job 1
        uses: actions/upload-artifact@v1
        with:
          name: homework
          path: math-homework.txt

  job_2:
    name: Multiply by 9
    needs: job_1
    runs-on: windows-latest
    steps:
      - name: Download math result for job 1
        uses: actions/download-artifact@v1
        with:
          name: homework
      - shell: bash
        run: cat homework/math-homework.txt

Cache

サンプルはいくつかあるのでそれをみるのが手っ取り早い。

github.com

.NET Core の nuget を例で考えます。(.NET Framework は忘れましょう)

.NET Core は Package References にしていると csproj にpackage 情報に入るのが厄介です。 もし csproj が一つならこれでいいでしょう。

steps:
  - uses: actions/cache@v1
    with:
      path: ~/.nuget/packages
      key: ${{ env.cache-version }}-${{ runner.os }}-nuget-${{ hashFiles('src/project.csproj') }}
      restore-keys: |
        ${{ env.cache-version }}-${{ runner.os }}-nuget-${{ hashFiles(''src/project.csproj') }}
        ${{ env.cache-version }}-${{ runner.os }}-nuget-

一つだけプロジェクトがあるならならいいのですが、複数の csproj に分離していると当然パッケージとバージョンの同一性を担保する方法が lock ファイルに比べて面倒です。 CirclrCI でやったように、csproj を拾ってきて md5 あたりを拾ってキャッシュキーにする感じになるでしょう。 CircleCI の例を載せておきます。

steps:
  - run:
      name: Calculate cache key for csproj
      command: |
        {
          md5sum $(find << parameters.search_path >> -name << parameters.target_file_pattern >> | sort -r)
        } > ~/cache-key-source-<< parameters.project >>
      working_directory: << parameters.working_directory >>
  - save_cache:
      name: Cache nuget pacakges
      key: nuget-<< parameters.cache_key >>-<< parameters.project >>-{{ checksum "~/cache-key-source-<< parameters.project >>" }}
      paths:
        - "~/.nuget"

nuget-cache-orb/nuget_save_cache.yml at 77c2c0cafe089314ae3d3d08473d429372737289 · guitarrapc/nuget-cache-orb · GitHub

なお、nuget は、macOSは Xamarin などでキャッシュサイズが半端なくなるので、こういうのを避けるために NuGet Package のパスを明示的に設定するのはあり。 その場合は、NUGET_PACKAGES 環境変数にパスを指定して、actions/cache の path にも指定する。

env:
  NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
  - uses: actions/cache@v1
    with:
      path: ${{ github.workspace }}/.nuget/packages
      key: ${{ env.cache-version }}-${{ runner.os }}-nuget-${{ hashFiles('src/project.csproj') }}
      restore-keys: |
        ${{ env.cache-version }}-${{ runner.os }}-nuget-${{ hashFiles(''src/project.csproj') }}
        ${{ env.cache-version }}-${{ runner.os }}-nuget-

TIPS

細かいけど知っておくといい TIPS がたまってきたのでメモ。

step の name

step ごとに name を指定することで、GitHub 上の表示を設定できる。

f:id:guitarrapc_tech:20200106010550p:plain
step ごとの name を付ける

省略するとコマンドがそのままでるので、CircleCI のように基本的に name 指定しないのがよさそう。

f:id:guitarrapc_tech:20200106010530p:plain
step ごとの name を省略する

Azure Pipelie は、コマンドの内容ではなく command@1 とかのモジュール名の表示になってたので、GitHub Actions で改善してて本当に良かった、神。

タイムアウト

timeout-minutes で指定できる。 デフォルトが 6hour と長いので、指定した方がいいケースが多い。

タイムアウトは、job と step ごとの両方に指定可能。

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - run: sleep 300
        timeout-minutes: 3

skip ci / ci skip

標準ではない。 なので、job の if で制御してるっぽいけど、手元でやるとエラーになるのが不思議。

github.com

Organization の他の private repo の参照

ssh-agent Action を使えば SSH キーで known_hosts 設定したりできるけど、あんまり使いたくはない。

github.com

自動生成される Token でその制御はできないので、専用の Token を生成するのが一番手っ取り早い。

- uses: actions/checkout@v1  
  with:
    repository: organization_name/repo_name
    token: ${{ secrets.ACCESS_TOKEN }}

stackoverflow.com

Badge

サポートされてた。ドキュメントないけど。

srz-zumix.blogspot.com

いい感じの Action どこ

安定の awesome で。

github.com

ローカルで構文チェックするCLIとかないの

Circle CLI のようなローカル実行向けのCLI はないです。 残念。


  1. GitHub Actionsランナーアプリケーションがインストールされた、GitHubがホストする仮想マシン

  2. 今後増える可能性があるとのことですが、本当に増やすのかな?