たゆたふ。

定まる所なく揺れ動き、いろいろやってみたメモ。など

Github actions のコスパについて考えた

Github actions はとても便利だ。テストやビルドを自動化するのに活用している。
パブリックリポジトリだと無料で実行環境が利用できるのがありがたい。
その無料の実行環境 Github-hosted runner では重すぎる処理を実行したくて Github actions の Self-hosted runner 環境を作った話は前回のエントリで書いた。

hero.hatenablog.jp

環境構築の動機となった目的は果たしたものの、作った環境はコスト性能比的にも良い選択だったのだろうか?
と思ってちょっと調べてみた。

今回はそれについて記す。

レイヤ数の多い大きな Docker イメージのビルドをギリギリ Github-hosted runner で実行していたのだけど、マルチアーキテクチャビルドをしようとして遂に処理できなくなった。仕方がないので、Self-hosted runner 環境を作ってビルドできるようにした。
イメージのビルドにはプログラムのコンパイルなど計算量の多い処理が含まれているのでメモリよりも CPU の速度が全体の処理速度を律速すると思われる。
目的のビルドを行うためには Github-hosted runner 環境より高性能な Self-hosted runner 環境にしないといけない。
かといってオーバースペックにして余計なコストもかけたくない。

ということで、Github-hosted runner の環境と同じ 2 コア CPU を利用できるいくつかのサービスを比較してみた。 同様の構成で最も安く高性能な CPU を利用できるのはどれなんだろうか? と。
ターゲットとしている Docker イメージのビルドではそれほど巨大なメモリが必要というわけではなさそうだったので比較対象構成のメモリ量の差は気にしないこととした。

なお、本エントリでの AWS ついての記述については 2022 年 5 月初旬のバージニアリージョンにおけるスペックや料金である。 Github についても 2022 年 5 月初旬時点でのスペック及び料金である。

Github-hosted runner の CPU と料金

まず Github-hosted runner の CPU と料金について確認した。

Linux の環境のスペックについてドキュメントには次の様に記載されている。

  • 2-core CPU
  • 7 GB of RAM memory
  • 14 GB of SSD disk space

数量はわかるがどのような CPU の 2-core なのかがわからない。
そこで次の様な Github workflow で lscpu を実行してみた。

name: lscpu
on:
  push:
    branches:
      - test-workflow
jobs:
  lscpu:
    runs-on: ubuntu-latest
    steps:
      - run: lscpu

その結果、次の型式の CPU であることがわかった。

  • Intel(R) Xeon(R) Platinum 8272CL CPU @ 2.60GHz

パブリックリポジトリなら無料で使えるが、プライベートリポジトリなら有料で $0.008/min かかる。 時間あたりに換算すると $0.48/hour になる1

コスト意識高く AWS EC2 インスタンスを利用している人なら、意外と高いと思う額だと思う。
まあ、結構な無料枠があるので個人利用なら実質無料で使っている人が多いと思う。

lscpu の詳しい結果を見る

Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Byte Order:                      Little Endian
Address sizes:                   46 bits physical, 48 bits virtual
CPU(s):                          2
On-line CPU(s) list:             0,1
Thread(s) per core:              1
Core(s) per socket:              2
Socket(s):                       1
NUMA node(s):                    1
Vendor ID:                       GenuineIntel
CPU family:                      6
Model:                           85
Model name:                      Intel(R) Xeon(R) Platinum 8272CL CPU @ 2.60GHz
Stepping:                        7
CPU MHz:                         2593.907
BogoMIPS:                        5187.81
Hypervisor vendor:               Microsoft
Virtualization type:             full
L1d cache:                       64 KiB
L1i cache:                       64 KiB
L2 cache:                        2 MiB
L3 cache:                        35.8 MiB
NUMA node0 CPU(s):               0,1
Vulnerability Itlb multihit:     KVM: Mitigation: VMX unsupported
Vulnerability L1tf:              Mitigation; PTE Inversion
Vulnerability Mds:               Mitigation; Clear CPU buffers; SMT Host state unknown
Vulnerability Meltdown:          Mitigation; PTI
Vulnerability Spec store bypass: Vulnerable
Vulnerability Spectre v1:        Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:        Mitigation; Retpolines, STIBP disabled, RSB filling
Vulnerability Srbds:             Not affected
Vulnerability Tsx async abort:   Mitigation; Clear CPU buffers; SMT Host state unknown
Flags:                           fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti fsgsbase bmi1 hle avx2 smep bmi2 erms invpcid rtm mpx avx512f avx512dq rdseed adx smap clflushopt avx512cd avx512bw avx512vl xsaveopt xsavec xsaves md_clear


余談だけど、Geekbench BrowserIntel(R) Xeon(R) Platinum 8272CL の測定結果を探すと Azure 上で測定されたものばかりだった。この CPU はひょっとして Azule 専用?と思った。そうなら Github というのはやはり Microsoft 傘下なんだなと2

Fargate の CPU と料金

続いて Fargate について調べてみた。
次のスペックで Github runner を実行して、workflow 内で lscpu を実行してみた。

  • Runtime version: 1.40
  • cpu: 2048 core (= 2vCPU)
  • mem: 8192 MB (= 8GB)

メモリについては選択できる値が決まっているので Github に近い値を選択した。

試してみると Fargate はいくつかの CPU の型式が混在しているらしい。起動のたびにランダムに割り当てられる様子。
数回の試行で次の2種類の CPU を確認した。

  • Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz
  • Intel(R) Xeon(R) Platinum 8175M CPU @ 2.50GHz

lscpu の詳しい結果を見る

Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz だった場合の結果

Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Byte Order:                      Little Endian
Address sizes:                   46 bits physical, 48 bits virtual
CPU(s):                          2
On-line CPU(s) list:             0,1
Thread(s) per core:              2
Core(s) per socket:              1
Socket(s):                       1
NUMA node(s):                    1
Vendor ID:                       GenuineIntel
CPU family:                      6
Model:                           85
Model name:                      Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz
Stepping:                        7
CPU MHz:                         3099.801
BogoMIPS:                        4999.98
Hypervisor vendor:               KVM
Virtualization type:             full
L1d cache:                       32 KiB
L1i cache:                       32 KiB
L2 cache:                        1 MiB
L3 cache:                        35.8 MiB
NUMA node0 CPU(s):               0,1
Vulnerability Itlb multihit:     KVM: Vulnerable
Vulnerability L1tf:              Mitigation; PTE Inversion
Vulnerability Mds:               Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Vulnerability Meltdown:          Mitigation; PTI
Vulnerability Spec store bypass: Vulnerable
Vulnerability Spectre v1:        Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:        Mitigation; Retpolines, STIBP disabled, RSB filling
Vulnerability Srbds:             Not affected
Vulnerability Tsx async abort:   Not affected
Flags:                           fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves ida arat pku ospke

Intel(R) Xeon(R) Platinum 8175M CPU @ 2.50GHz だった場合の結果

Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Byte Order:                      Little Endian
Address sizes:                   46 bits physical, 48 bits virtual
CPU(s):                          2
On-line CPU(s) list:             0,1
Thread(s) per core:              2
Core(s) per socket:              1
Socket(s):                       1
NUMA node(s):                    1
Vendor ID:                       GenuineIntel
CPU family:                      6
Model:                           85
Model name:                      Intel(R) Xeon(R) Platinum 8175M CPU @ 2.50GHz
Stepping:                        4
CPU MHz:                         3106.294
BogoMIPS:                        4999.99
Hypervisor vendor:               KVM
Virtualization type:             full
L1d cache:                       32 KiB
L1i cache:                       32 KiB
L2 cache:                        1 MiB
L3 cache:                        33 MiB
NUMA node0 CPU(s):               0,1
Vulnerability Itlb multihit:     KVM: Vulnerable
Vulnerability L1tf:              Mitigation; PTE Inversion
Vulnerability Mds:               Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Vulnerability Meltdown:          Mitigation; PTI
Vulnerability Spec store bypass: Vulnerable
Vulnerability Spectre v1:        Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:        Mitigation; Retpolines, STIBP disabled, RSB filling
Vulnerability Srbds:             Not affected
Vulnerability Tsx async abort:   Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Flags:                           fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves ida arat pku ospke


このスペックのバージニアリージョン(us-east-1)での料金は次の通り。

  • cpu: $0.04048/core・hour x 2core = $0.08096/hour
  • mem: $0.004445/GB・hour x 8GB = $0.03556/hour
  • strage: $0.000111/GB・hour x 14GB = $0.001554/hour
  • total: 0.08096 + 0.03556 + 0.001554 = $0.118074/hour ≒ $0.118/hour

Fargate Spot だと単価が下がるので次の値段になる。

  • cpu: $0.0128568/hour x 2 = $0.0257136/hour
  • mem: $0.00141177/GB・hour x 8 = $0.01129416/hour
  • strage: $0.000111/GB・hour x 14GB = $0.001554/hour
  • total: 0.0257136 + 0.01129416 + 0.001554 = $0.03856176/hour ≒ $0.039/hour

最大 70% 割引とのことだが、計算してみると約 68.24% OFF となっている。
ちなみに現状 Spot が使えるのは x86_64 のみで、Arm64 では提供されていない。

Codebuild の CPU と料金

比較対象として Github actions と同様 CI/CD に特化したサービスと位置づけられる CodeBuild も調べてみる。
CodeBuild には CPU、メモリともに Github-hosted runner とほぼ一致スペックのものが提供されないので、CPU コア数の一致する次のコンピューティングインスタンスタイプで確認した。

  • general1.small (2 vCPU, 3 GB メモリ)

イメージは aws/codebuild/standard:5.0 (Ubuntu 20.04) を利用した。

CodeBuild も1種類の CPU で統一されているわけではなかった。 数回試行した結果、次の CPU のいずれかであった。

  • Intel(R) Xeon(R) Platinum 8275CL CPU @ 3.00GHz
  • Intel(R) Xeon(R) Platinum 8124M CPU @ 3.00GHz

lscpu の詳しい結果を見る

Intel(R) Xeon(R) Platinum 8275CL CPU @ 3.00GHz の結果

Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Byte Order:                      Little Endian
Address sizes:                   46 bits physical, 48 bits virtual
CPU(s):                          2
On-line CPU(s) list:             0,1
Thread(s) per core:              2
Core(s) per socket:              1
Socket(s):                       1
NUMA node(s):                    1
Vendor ID:                       GenuineIntel
CPU family:                      6
Model:                           85
Model name:                      Intel(R) Xeon(R) Platinum 8275CL CPU @ 3.00GHz
Stepping:                        7
CPU MHz:                         3614.580
BogoMIPS:                        5999.99
Hypervisor vendor:               KVM
Virtualization type:             full
L1d cache:                       32 KiB
L1i cache:                       32 KiB
L2 cache:                        1 MiB
L3 cache:                        35.8 MiB
NUMA node0 CPU(s):               0,1
Vulnerability Itlb multihit:     KVM: Vulnerable
Vulnerability L1tf:              Mitigation; PTE Inversion
Vulnerability Mds:               Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Vulnerability Meltdown:          Mitigation; PTI
Vulnerability Spec store bypass: Vulnerable
Vulnerability Spectre v1:        Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:        Mitigation; Full generic retpoline, STIBP disabled, RSB filling
Vulnerability Srbds:             Not affected
Vulnerability Tsx async abort:   Not affected
Flags:                           fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves ida arat pku ospke

Intel(R) Xeon(R) Platinum 8124M CPU @ 3.00GHz の結果

Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Byte Order:                      Little Endian
Address sizes:                   46 bits physical, 48 bits virtual
CPU(s):                          2
On-line CPU(s) list:             0,1
Thread(s) per core:              2
Core(s) per socket:              1
Socket(s):                       1
NUMA node(s):                    1
Vendor ID:                       GenuineIntel
CPU family:                      6
Model:                           85
Model name:                      Intel(R) Xeon(R) Platinum 8124M CPU @ 3.00GHz
Stepping:                        4
CPU MHz:                         3412.932
BogoMIPS:                        5999.99
Hypervisor vendor:               KVM
Virtualization type:             full
L1d cache:                       32 KiB
L1i cache:                       32 KiB
L2 cache:                        1 MiB
L3 cache:                        24.8 MiB
NUMA node0 CPU(s):               0,1
Vulnerability Itlb multihit:     KVM: Vulnerable
Vulnerability L1tf:              Mitigation; PTE Inversion
Vulnerability Mds:               Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Vulnerability Meltdown:          Mitigation; PTI
Vulnerability Spec store bypass: Vulnerable
Vulnerability Spectre v1:        Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:        Mitigation; Full generic retpoline, STIBP disabled, RSB filling
Vulnerability Srbds:             Not affected
Vulnerability Tsx async abort:   Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Flags:                           fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves ida arat pku ospke


1時間あたりの料金は次のとおり。

  • general1.small: $0.005/min x 60min = $0.3/hour

なお、general1.small には 100min/月(= 1.67 hour/月)の無料枠が設定されており、この範囲内では無料。

Self-hosted runner (c6i.large) の CPU と料金

先日構築した Docker イメージのビルド用の Self-hosted runner 環境で利用している AWS EC2 についても確認した。
インスタンスタイプはコンピューティング最適化の c6i.large(2vCPU, 4GiB メモリ)を利用している。

次の CPU を使っていることがわかった。

  • Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz

lscpu の詳しい結果を見る

Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Byte Order:                      Little Endian
Address sizes:                   46 bits physical, 48 bits virtual
CPU(s):                          2
On-line CPU(s) list:             0,1
Thread(s) per core:              2
Core(s) per socket:              1
Socket(s):                       1
NUMA node(s):                    1
Vendor ID:                       GenuineIntel
CPU family:                      6
Model:                           106
Model name:                      Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
Stepping:                        6
CPU MHz:                         3502.020
BogoMIPS:                        5799.96
Hypervisor vendor:               KVM
Virtualization type:             full
L1d cache:                       48 KiB
L1i cache:                       32 KiB
L2 cache:                        1.3 MiB
L3 cache:                        54 MiB
NUMA node0 CPU(s):               0,1
Vulnerability Itlb multihit:     Not affected
Vulnerability L1tf:              Not affected
Vulnerability Mds:               Not affected
Vulnerability Meltdown:          Not affected
Vulnerability Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl and seccomp
Vulnerability Spectre v1:        Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:        Mitigation; Enhanced IBRS, IBPB conditional, RSB filling
Vulnerability Srbds:             Not affected
Vulnerability Tsx async abort:   Not affected
Flags:                           fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid avx512f avx512dq rdseed adx smap avx512ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves ida arat avx512vbmi pku ospke avx512_vbmi2 gfni vaes vpclmulqdq avx512_vnni avx512_bitalg tme avx512_vpopcntdq rdpid md_clear flush_l1d arch_capabilities


このインスタンスタイプの1時間あたりのオンデマンド料金は $0.085/hour。 スポットインスタンスだと大体 50〜60% OFF 程度の割引率なのでざっくり半額になると期待していいと思う3

比較

Github-hosted runner の実行環境とそのスペックに相当する実行環境を作れる AWS サービスのそれぞれの CPU と1時間あたりの料金は下記の表の通り。
いずれも CPU のブランドは Intel(R) Xeon(R) Platinum。
AWS の料金はバージニアリージョン(us-east-1)のもの。
Geekbench の値は Geekbench Browser から取ってきた値で、同じ型式の CPU の結果でメモリ等近しい構成の Multi-Core Score。CPU の性能はクロック数のみで決まるものでもないので比較の指標として追加した。あくまで参考値として。

環境 料金
($/hour)
CPU クロック数 コア数 Memory Geekbench
(参考値)
Github-hosted runner 0.480 8272CL 2.60GHz 2 7GB 1608
Fargete 0.117 8259CL
8175M
2.50GHz
2.50GHz
2 8GB 1900
--
Fargete Spot 0.039 8259CL
8175M
2.50GHz
2.50GHz
2 8GB 1900
--
CodeBuild (general1.small) 0.300 8275CL
8124M
3.00GHz
3.00GHz
2 3GB 2329
2256
EC2 c6i.large(オンデマンド) 0.085 8375C 2.90GHz 2 4GB 2872
EC2 c6i.large(SPOT) 0.043 8375C 2.90GHz 2 4GB 2872

やはり Github-hosted runner 環境が最も料金が高い。CPU 性能的にはほぼ同等と思われる Fargate の 4 倍以上となっている。比較的高価と思っていた CodeBuild よりも高い。

単純に時間あたりの料金を比べるとそうなのだが、ちゃんとコスパを比較するなら無料枠を考慮する必要がある。
無料枠を考慮して時間と利用料の関係をグラフ化すると次の様になる。

利用料比較グラフ

パブリックリポジトリでの利用や無料枠に収まる範囲での利用しかないなら Github-hosted runner は最安で最も手間も少ないのは言うまでもない。

無料枠を超えている場合、目安として Github の Free アカウントなら総利用時間が 45 時間/月、Pro アカウントやオーガニゼーション(Team アカウント)なら 60 時間/月を超えていると Self-hosted runner の利用を検討してもいいかと思う。 Fargate は Github-hosted runner よりちょっと性能が良さそうってところだが、ほぼ同じ性能を安く調達したいだけなら魅力的だ(EC2 より起動が速いので)。
コストを抑えるには workflow の実行毎に Self-hosted runner のインスタンスを起動し、終わったら止めることが前提になるが、自動化すればその手間は負担にならないと思う。

単純に Github-hosted runner でも実行できる処理のコスト改善をするだけなら上記の通り。

ただ、利用時間が長い場合、それは実行回数が多いためではなく1回の処理時間が長くなっているのではないだろうか。そうならばより性能の高いインフラで Self-hosted runner 環境を作ったほうが良いだろう。
例えば Linter の実行など軽い処理を Github-hosted runner で実行して無料枠を有効に消費しつつ、ビルドやコンパイルなど比較的重くて時間がかかっている処理は Self-hosted runner の利用を利用するといいかもしれない。 ユニットテストなどはコア数の多いインスタンスを使って並列実行すれば大きな時間短縮も狙えると思う。
クラウドサービスを利用すると実際には通信料や構築と運用という目に見えにくいコストもかかるが、それでもコストと処理時間の両方を削減できる可能性もあると思う。
こうゆう用途には処理内容に適した EC2 インスタンスを選択して Self-hosted runner を運用するのがいいと思う4Github-hosted runner と同じ金額を払うならもっと性能の良いインスタンスを利用できるし、スポットインスタンスを使えば更にコストを下げられる5

まとめ

Github-hosted runner では重くて実行できなくなった Docker イメージのビルドのために Self-hosted runner の実行環境を作ったがもっと良い選択肢があったのではと気になって調べてみた。

結果的には ECS の EC2 ノードで Self-hosted runner コンテナのタスクを運用するという自分の選択がベストかなと思われた。

Github actions は便利だけど無料枠を超えて使っている場合、利用料は決して安くないということも再認識した。
とはいえ別の環境を構築、運用するにも手間暇がかかるのでコストとバランス次第だと思う。

Self-hosted runner 環境を構築するなら私的には現時点で次の結論を得た。 - 処理に見合った CPU 性能を求めるなら EC2 のスポットインスタンスで ECS タスクを利用する - Github-hosted runner と同程度の性能でよく、コストを下げたいなら Fargate Spot を使う

なお、本エントリで挙げた利用料には通信料金など運用に伴い発生するすべての料金を含んでいるわけではない。
また、私の計算間違いで値が不正確な可能性もある。
したがって実際の運用でかかる料金について一切保証できないので悪しからず。
と言い訳を用意しつつ締めくくる。

参考


  1. Github の Free アカウントで 2000 分/月(= 33.3 時間/月)、Pro アカウントでは 3000 分/月(= 50 時間/月)の無料枠がある。プライベートリポジトリでもこの枠内の利用であれば無料。

  2. 更に余談だけど、Gitlab の CI/CD で提供されている SaaS 環境はドキュメントGCP の n1-standard-1 を使っていると記載されている。

  3. バージニアリージョンの 2022 年 5 月初旬時点。

  4. さらに利用が増えれば Github-hosted runner より CodeBuild の方が安くなるが、乗り換える気があるならその前に Self-hosted runner を導入しているだろう。その手間ひまかけて作ったその環境をわざわざより高額な CodeBuild に乗り換えることもまあないかなと思う。

  5. $0.48/hour も出せばオンデマンド料金でも AWS EC2 の t3.2xlarge(8vCPU 32GiB $0.3328/hour)や c6i.2xlarge(8vCPU 16GiB $0.34/hour)などが使える。スポットならざっくり半額と考えればさらに高価だが高性能なインスタンスの利用も検討できるかもしれない。

ECS で Github Actions Self-hosted runner を動かす

前回のエントリで Github Actions の Self-hosted runner を試したことを書いた。

hero.hatenablog.jp

で、もともと何がやりたかったかというと、Github workflows の実行環境ではビルドできないほど大きな次の Docker イメージのビルド。イメージのサイズは約 10 GB にもなる。

qiita.com

毎回 EC2 インスタンスを起動しその中で Self-hosted runner を起動するのも面倒なので ECS で環境構築した。
それについて記す。

実現したいこと

もともとは Github workflow で Docker イメージをビルドし、それを GHCR1 に Push していた。
これまでなんとかビルドできていたが、イメージが大きくなりすぎて難しくなってきた。そもそもの欲張ったイメージの仕様上、小さくするのも難しい。
さらに Apple Silicon の MBP に乗り換えたので Arm 向けのイメージもビルドしたくなり、マルチアーキテクチャのビルドを追加したら遂にビルドできなくなってしまった2

それでビルド環境の増強でなんとかしたい。というのが要件だ。

整理すると次を実現したい。成果物である Docker イメージはこれまで同様 GHCR に Push したい。

  1. 大きな Docker イメージをビルドしたい
  2. x86_64 と arm64 の両方のアーキテクチャ向けにビルドしたい
  3. できるだけビルド時間は短くしたい
  4. 手間を少なく運用したい
  5. コストをできる限り減らしたい

MUST 要件は 1、2 のみ。 残りはより良いソリューションとするための検討課題。

Self-hosted runner を採用したこと

「大きな Docker イメージをビルドしたい」ので Github がホストするランナー3よりパワフルな環境を用意する必要がある。

当初は Self-hosted runner ではなく AWS Codebuild で実現しようかとも考えた。慣れているし。
しかし検討する中で、Self-hosted runner の方が「手間を少なく運用したい」を満たしやすいと考えた。

ビルドがいつも想定内の実行時間でうまく動けばいいのだろうけど、時間がかかったりエラーが発生するケースがあるかもしれない。 そうすると進捗やログの確認がしやすいほうがよい。CodeBuild だと、AWS Console を開いて見ることになる4けど、Self-hosted runner なら Github だけで完結する。

ということで Self-hosted runner を使うことに決めた。
また通常運用では「管理を Github 内で完結させるという」のも今後の検討の方針とした。

実行環境は EC2 ノードの ECS にした

さて、Self-hosted runner を使うとするとそれを動かす環境はどうしよう。
オンプレでリソースを用意なんてできないのでクラウドサービスを使う。
各種サービスの無料枠では Github の提供しているもの以上の環境を得られそうにないので、そこの課金はやむなしと考える。が、「コストをできる限り減らしたい」。
とすると利用する時間だけ起動し終わったら速やかに止めるようにしたい5

結論としては以下のように考えて AWS ECS を EC2 ノードで使うこと選択した。

シンプルには前回のエントリでやったように EC2 インスタンス上で実行すればいい。いちいちインスタンスの中に入って操作するのは面倒なら専用の AMI を作ればインスタンス起動時に Github runner を自動起動するものが作れるだろう。
しかし一方で、今後 Github runner もアップデートされていくだろう。それを考えると AMI よりも Docker イメージのメンテナンスの方がローカルマシン上でも動作確認できるし「手間を少なく運用したい」をより満たすだろう。 ということで、コンテナで運用することにした。

AWS でコンテナを動かす環境はいくつかあるが、 EKS クラスターを作るのは要件に対して大げさすぎし、クラスターが存在するだけで課金されるので「コストをできる限り減らしたい」的に NG。 App Runner も Web システムを動かすことに特化されすぎているし、 Lambda は最大 15 分でタイムアウトしてしまう。実行したい処理はおそらくその制限内では収まらない。
ということで ECS を使うことにした。

ECS を使うなら Fargate の方がクラスターノードのリソース管理が不要でいいかと思った。 が、Fargate の良くないところは CPU やメモリの数量は選べるが、その性能は選択できないところ。 実際に Fargate で提供される CPU と Github actions の実行環境のそれを比べてみると同等かやや劣っていた。 Github よりパワフルな環境を手に入れるための Self-hosted runner なのでこれでは本末転倒。

ということでインスタンスタイプで性能の良いものを選択できる EC2 を利用することとした。 実際、構築してみるとタスクを起動するとキャパシティプロバイダとオートスケーリンググループにより自動的にインスタンスクラスターに追加できる。そしてタスクを削除すると追加されたインスタンスが停止される(タスクがなくなれば、インスタンスも 0 台になる運用も可能)。直接的にインスタンスの運用はしなくていいので手間的には Fargate と変わりなかった。

ECS で Docker イメージをビルドするには

Github からは公式のイメージは公開されていないので Github runner のイメージは自分で作成した。

Docker in Docker でイメージをビルドするにはコンテナを特権モードで動かす必要がある6。タスク定義で privileged: true を設定し、Docker をインストールしたイメージを作れば良い。
ECS コンテナ内で Docker を使うにはもう1つ方法がある。ホストマシン側の Docker エンジンにコンテナからアクセスする方法があり今回はそれを利用した。コンテナ側に Docker エンジンインストールするとイメージが大きくなりそうだし、特権モードも必要ないし。

やり方は次の通り。

Docker イメージに次を含める。

  1. Docker エンジンは不要だけどコマンドは必要なので docker-ce-cli はインストールする
  2. GID=994docker という名前のユーザグループを作る
  3. docker を実行するユーザを作る。その際、ユーザグループ docker に入れる

Dockerfile の抜粋で示すと次の通り。

ARG USERNAME=user
ARG GROUPNAME=user
ARG UID=1000
ARG GID=1000
ARG DOCKER_GROUPNAME=docker
ARG DOCKER_GID=994

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
            docker-ce-cli

## Add user and workdir
RUN groupadd -g ${GID} ${GROUPNAME} && \
    groupadd -g ${DOCKER_GID} ${DOCKER_GROUPNAME} && \
    useradd -m -s /bin/bash -u ${UID} -g ${GID} -G ${DOCKER_GID} ${USERNAME}
USER ${USERNAME}

docker ユーザグループの GID が決め打ちすぎるのが気になるが、とりあえずココが原因でエラーになったりしてないので良しとする。

そして、タスク定義のコンテナ定義でホストの /var/run/docker.sock を同じパスでマウントする7

これで ECS のコンテナ内でホスト側(つまり、コンテナ自身の起動に利用されている)Docker エンジンを使える。
あとは、actions/runner: The Runner for GitHub Actions から runner を取ってきてインストールすれば Docker ビルドできる Self-hosted runner イメージの出来上がり。

Dockerfile 全体はこちら => Dockerfile

ちなみにこのイメージも x86_64 と Arm64 で使いたいのでマルチアーキテクチャでビルドしている。
ビルドステップが少ないのでこちらは Github hosted な runner で十分ビルドできる。

作ったもの

作ったものの全体は次の様の通り。

ECS cluster for Github self-hosted runners

これを次のように使っている。

  1. 予め起動用の Lambda 関数で Github runner の ECS タスクを起動する
  2. ビルド対象のリポジトリで workflow を Push などのトリガーで実行する
  3. workflow の Job は x86/arm64 それぞれのインスタンス上の Github runner タスクが並行して Docker イメージをビルドし GHCR に Push する
  4. 後続の Job でマルチプラットフォームとしてマニフェストを作成し、GHCR に Push する
  5. 終わったら停止用 Lambda 関数を使って Github runner の ECS タスクを止める

前述したが ECR クラスターのノードとなる EC2 インスタンスの起動/停止は直接操作していない。Capacity Provider と Auto Scaling Group がよしなに用意してくれる。

これらを CDK で構築した。そのリポジトリは次の通り。

github.com

これで要件の「大きな Docker イメージをビルドしたい」と「x86_64 と arm64 の両方のアーキテクチャ向けにビルドしたい」を実現した。Buildx を使ったマルチプラットフォームビルドだといずれかのイメージがエミュレーション環境でビルドされるので時間がかかるのだが、それぞれネイティブな環境を用意したのでエミュレーションオーバーヘッドによる遅さはなくなった。1時間以上かかっていたビルドが約 30 分に短縮できた。これには C6i/g.large というパワフルなインスタンスを使ったおかげもあるが、スポットインスタンスを使うことで、「できるだけビルド時間は短くしたい」を満たしながら「コストをできる限り減らしたい」もできていると思う。 はじめはタスクの起動/停止を簡便化する Lambda を用意していなかったのだが「手間を少なく運用したい」ので追加した。

Self-hosted runner とそれを使った workflow の動き

前回のエントリ(Github Actions の Self-hosted Runner をやってみた)では単に Self-hosted runner を起動してハロワ程度を試しただけだったが、実際に使うものを構築しながらもう少し詳しく調べてみた。

Self-hosted runner のマシンは外部にポートをさらさなくて良い

ドキュメントにも書かれているのだけど、runner は HTTPS long poll により Job の情報を受け取り常に runner 側から Github に通信する。runner にインターネット側から接続できるようにする必要はない。
実際、インターネットとの間に NAT がある家庭用ネットワークの中でも動く。

とすると AWS でも外部からの切断を遮断された Private Subnet で運用できると思う。
しかし、コストを減らすために Public Subnet で運用している。

Private subnet に置いても wrokflow の中で各種ライブラリパッケージや Docker イメージ等をダウンロードすると思うので runner からはインターネット側に通信できる必要がある。
そのためには NAT Gateway が必要になるが NAT Gateway は存在するだけで料金がかかるし、それ経由の通信が割高だからだ。
まあ、Public IP が付くとはいえ、外部からの接続は制限しているし、起動しているのは Job 実行の間だけだし良いだろう。

workflow の実行時に Self-hosted runner は起動していなくても良い

workflow から Self-hosted runner を使う Job を実行する前に runner を起動しておいたほうがスムーズではあるば、予め起動しておくことは MUST ではない。

workflow の Job は一旦キューイングされる。なので、キューイング時点で runner が起動していなくても runner が起動した時点でキューから Job を取得し処理される。なおドキュメント によると最大 24 時間キューに留めることができる。

今は手動で Self-hosted runner のタスクを上げ下げしているが、運用の仕方がパターン化してくれば自動化してもいいかもしれないと思っている。runner を起動する Job を追加して ephemeral (Job を処理したら自ら終了する)モードの runner の起動すれば Job 終了とともに runner も終了するので起動/停止が自動化できそう。

自動アップデート機能搭載

Github runner は自動アップデート機能を備えている。
以前はうまく起動していた runner の ECS タスクが起動直後に落ちるようになったので調べると起動時にアップデートがあれば自動的にダウンロードして、再起動していた。
再起動のためにプロセスが終了するのでその時点でコンテナも終了し、タスクが落ちていたというわけだ。

それ自体は便利な機能なのだけど、コンテナでの運用を考えるとそのためにコンテナが落ちるのは困る。 自動アップデートを無効化するには runner を設定する際に --disableupdate オプションを付ければよい。

まとめ

Self-hosted runner を利用してビルドできるようになった。
それだけでなくビルド時間も1時間以上 → 約 30 分と短縮できた。

管理も Github 側で Job の進行状況も確認できるし、ログも参照できる。
運用の手間も最小にできたと思うし、自動化の目処もある。
安いリージョン(us-east-1)での運用とスポットインスタンスの利用でコストも抑えられたと思う。

参考


  1. GitHub Container Registry

  2. エラーが発生したりはないが、ビルド時間が Github action の実行時間制限を超えてしまった。時間がかかるのは Arm64 向けのビルドが QEMU のエミュ環境になるからだと思われる。

  3. Supported runners and hardware resources

  4. AWS Console はログインが維持される時間が長くないのでしょっちゅうログイン操作を求められている。なので進捗やログ確認目的で開くのはめんどくさい。

  5. この点においてはリソース確保と開放を自動でやってくれる CodeBuild は便利だと思う。

  6. Fargate はそもそも特権モードでコンテナ起動できないのも採用できない理由だった。しかしkaniko という OSS を使うと特権モードでなくてもイメージをビルドできるらしい(Amazon ECS on AWS Fargate を利用したコンテナイメージのビルド | Amazon Web Services ブログ)。kaniko でイメージをビルドする Github action もあるようなのでそのうち試してみたい。

  7. https://github.com/HeRoMo/ecs-github-actions-runner/blob/f980828be1b49e5590f174471e20b9f105c83027/lib/constructs/EcsGithubActionsRunner.ts#L148-L155

Github Actions の Self-hosted Runner をやってみた

個人的に時々更新している次の Docker イメージがある。

qiita.com

そのサイズ、約 10GB。 もちろんビルドにも時間がかかる。

これまで Github workflow でビルドし、 Github Container Registory (ghcr) に push して公開していた。
もともとビルドに1時間以上かかっていたが、x86_64 に加えて Arm64 向けにもビルドしようとしてできなくなった。Github workflow はデフォルトでは6時間でタイムアウトし、強制終了となる。その6時間の内に終わらなくなってしまったのだ。
Public なリポジトリだと無料なので少々時間がかかってもまあいいかと長過ぎるビルド時間を改善せず放置していたのだがビルドできないのは困る。

AWS CodeBuild に乗り換えようかなとも思ったのだけど、以前から気になっていた Github Actions の Self-hosted Runner を試してみた。

Self-hosted Runner を試す

まずはどんなものか知るために EC2 インスタンスを立てて試してみた。
本エントリではそれについて記す。

リポジトリでの登録

最初に Self-hosted runner を利用したいリポジトリの Setting で新たな self-hosted runner を登録する。
次の図の①②③の順でクリックするだけ。

Github Actions の Runners 管理ページ
Github Actions の Runners 管理ページ

③の緑のボタンをクリックすると次のページが表示される。

Runner の新規登録
Runner の新規登録

このページには Download 方法と Configure 方法が記載されている。 Download 方法はインストール手順、Configure 方法は起動方法と思って OK。

ちなみに macOSWindows は x64 アーキテクチャしかないが、 Linux は x64、ARM、ARM64 が選択できる。

重要なのはアンダーラインを引いた token の値1。後で必要なのでメモしておく。

インストール

EC2 インスタンスSSH で接続して前述のページに記載されている通りのコマンド2を実行するだけ。 迷うところはない。

mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.290.1.tar.gz -L https://github.com/actions/runner/releases/download/v2.290.1/actions-runner-linux-x64-2.290.1.tar.gz
tar xzf ./actions-runner-osx-x64-2.290.1.tar.gz

起動

tar ファイルを開いたら、前述のページの Configure に記載されているコマンドを実行する。 そのままコピー&ペーストで構わない。
./config.sh 実行後、いくつか質問されるがとりあえず全部デフォルトでいいのでリターンしていくだけ。

[ec2-user@ip-172-31-26-219 actions-runner]$ ./config.sh --url https://github.com/HeRoMo/jupyter-langs --token XXXXXXXXXXXXXXXXXXXXXXXXXXXXX

--------------------------------------------------------------------------------
|        ____ _ _   _   _       _          _        _   _                      |
|       / ___(_) |_| | | |_   _| |__      / \   ___| |_(_) ___  _ __  ___      |
|      | |  _| | __| |_| | | | | '_ \    / _ \ / __| __| |/ _ \| '_ \/ __|     |
|      | |_| | | |_|  _  | |_| | |_) |  / ___ \ (__| |_| | (_) | | | \__ \     |
|       \____|_|\__|_| |_|\__,_|_.__/  /_/   \_\___|\__|_|\___/|_| |_|___/     |
|                                                                              |
|                       Self-hosted runner registration                        |
|                                                                              |
--------------------------------------------------------------------------------

# Authentication


√ Connected to GitHub

# Runner Registration

Enter the name of the runner group to add this runner to: [press Enter for Default]

Enter the name of runner: [press Enter for ip-172-31-26-219]

This runner will have the following labels: 'self-hosted', 'Linux', 'X64'
Enter any additional labels (ex. label-1,label-2): [press Enter to skip]

√ Runner successfully added
√ Runner connection is good

# Runner settings

Enter name of work folder: [press Enter for _work]

√ Settings Saved.

[ec2-user@ip-172-31-26-219 actions-runner]$ ./run.sh

√ Connected to GitHub

Current runner version: '2.289.3'
2022-04-23 09:34:07Z: Listening for Jobs

./run.sh 実行後、Listening for Jobs と表示されるが、これで Github に接続し、ワークフローの Job の実行を待っている状態。

先程 token を取るのに利用した Github の管理ページを確認すると次のように Self-hosted runner が idle 状態となっている。

Idle な runner
Idle な runner

ip-172.31.26.219 は runner の名前。何も指定していないので EC2 インスタンスのホスト名がそのまま使われている。
その横の 'self-hosted''Linux''X64' はこの runner を使う場合に指定するラベルでワークフローの runs-on で指定する。

タスクの実行

試しに test-workflow といブランチを作り次のようなワークフローを追加してみる。 self-hosted runner で実行するために runs-on: self-hosted と指定している。 ここはラベル 'Linux' または 'X64' を指定しても同じ。

name: Hello World
on:
  push:
    branches:
      - test-workflow
jobs:
  hello-world:
    runs-on: self-hosted
    permissions:
      contents: read
      packages: write
    steps:
      - run: echo "hello-world"
      - run: uname -a
      - run: sleep 60 # すぐに終了するとスクショが取りにくいので

これを Github に Push すると ./run.sh を実行したターミナルには Job 実行を示すログが出色される。

[ec2-user@ip-172-31-26-219 actions-runner]$ ./run.sh

√ Connected to GitHub

Current runner version: '2.290.1'
2022-04-23 10:22:54Z: Listening for Jobs
2022-04-23 10:25:48Z: Running job: hello-world # ここから実行ログ
2022-04-23 10:26:58Z: Job hello-world completed with result: Succeeded # 成功終了したことを示すメッセージ

Github の管理ページではタスクの実行中は次のように Active と表示される。

Active な runner
Active な runner

あら簡単。

ワークフローの実行が終了すると、runner は再び Idle 状態に戻る。

Runner の終了

./run.sh を実行したターミナルで ctrl + c で runner を止めることができる。 止めると管理ページでは次のように Offline となる。

Offline な runner
Offline な runner

まとめ

Github Action の Self-hosted runner を試してみた。
思ったより随分簡単だった。

Github 側から Runner への接続はしないようなので Runner からインターネットへの通信ができれば動作できる様子。
ごく簡単なワークフローしか試さなかったが、Docker を使うなら予め使える環境を用意する必要があるだろう。

運用を考えると Runner 自体も Docker で動かしたいし、次回は ECS で Runner を動かしてみるつもり。

参考


  1. の有効期限は1時間。このスクリーンショットのはとっくに無効となっている。

  2. GitHub Actions Runner のリポジトリ actions/runnerReleases にも記載されている。

Alfred の AlfredTweet をなんとか動かした

Mac を使っていると Alfred はとても便利。というか個人的には必須ツールと思っている。

www.alfredapp.com

無料で使っていても十分便利なのだけど、Powerpack に課金すると更に便利になる。
というのも Powerpack では Workflows がアンロックされるからだ。

www.alfredapp.com

Workflows はスクリプトにより Alfred を機能拡張できる仕組みで、自分で新しい Workflow を作成できるし、他のユーザが作成して公開しているものを利用できる。

例えば、Alfredtweet をインストールすると Alfred の UI から直接ツイートできる。

www.packal.org

しかし、Alfredtweet は PHP で実装されているため、Monterey で PHP が OS に含まれないようになって動かなくなって残念に思っていた。
Alfred 4.6 で Alfred 自体が homebrew でインストールした PHP のパスも検知するようになった。
それによって多くの PHP を使った workflow は何にもしなくても動くようになったが、Alfredtweet はダメだった。
最初に Twitter アカウントの認証設定があるのだけどそれが動かないのでどうしようもない。

どうも作者もメンテを放棄している様子だし、別の代替を探してみた1のだけど見つけられなかった。

諦めていたのだけど、少し変更して動かせたのでメモしておく。

Alfredtweet のインストール

インストール自体は普通に AlfredTweet | Packal からダウンロードした alfredtweet.alfredworkflow を Alfred で開くだけ。

Twitterのアカウント認証設定

ツイートするためには Twitter アカウント認証を通す必要がある。
Alfred で AlfredTweet コマンドを実行するとできるはずなのだけど、動かない。
ログを見ると php コマンドが見つからないとエラーが出ている。

Workflows 管理画面で AlfredTweet を開いて、次のワークフローブロックをダブルクリックで開く。

alfredrweet

次のダイアログが開く。動かない原因は、ここに直書きされている php コマンドらしい。

書き換え前

これを次の様に Homebrew でインストールしたフルパスに書き換えてやる。

書き換え後

書き換えたら「save」ボタンで閉じる。

Alfredtweet のアカウント設定

上記の作業後、Alfred を開いて AlfredTweet とコマンドする。

セットアップコマンド

すると以前の様にブラウザで次の認証ページが開いて Twitter アカウントとの連携ができるようになる。

認証ページ

Twitter で権限認可したら遷移するページにトークンが表示される。再び Alfred を開いて AlfredTweet の引数にトークンを渡して実行すると AlfredTweet にアカウント設定できる。

はずなのだが、ここでも動かない。 ログを見るとディレクトリが見つからないというエラーが出ているので、該当のディレクトリを次の様に作ってやる。

mkdir -p "~/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data"

ふう、これでなんとか動いたようだ。 なにやら次のようなメッセージが通知されたのだけど、これは無視しても OK だった。

エラーメッセージ

tweet コマンドの修正

さてと、試しにツイートしてみるかと Alfred を開いて tweet コマンドを使ってみるとまだエラーで動かない。

ログを確認すると次の箇所が悪いらしい。

Tweet

開いてみると案の定、php コマンドを使おうとしている。

変更前

それを次の様に書き換えるとツイートできるようになった。

変更後

他のコマンドも動かない

Alfredtweet には他にも follow/unfollowblock/unblock などの操作もできる。
これらを開いてみると、書かれている Script は PHP なのに Language には /bin/bash が設定されているので、/opt/homebrew/bin/php に変更すると動くようになる。

Language が bash になっている

他にタイムラインやメンション、リストなどを Alfred 上で確認するコマンドもあるのだけど、それらはデータは取ってこれているようだけど、レスポンスを正しくパースできなくなて目的通りには動かない。

まあ、ツイート以外は Twitter アプリ上で操作するほうが便利だと思うのでこれらのコマンドの不具合は放置することした。

なんとか動いたが、そのうち動かなくなるのだろうな。 まあ、ツイート自体もそんなに頻繁にする方ではないのだけど。


  1. alfred-twitter-toolkitというのを見つけたのだけど、つぶやくことはできないっぽい。作者自ら'AlfredTweet is far more powerful' って書いてるし。

AWS Systems Manager を触ってみた

AWS Systems Manager (SSM)は AWS 上、オンプレミスに関係なく SSM エージェントが稼働するサーバインスタンスを一元管理するためのサービス。

存在は知っていて、パラメータストアのような一部の機能は使っていたものの、AWS コンソールからオンプレサーバを操作するっていうのがどこまでできるのか気になっていた。でやってみた。

このエントリは次の 3 日目の記事となる。

qiita.com

マネージドインスタンスの登録

本エントリでは AWS の外、つまり、AWS 以外のデータセンターや自宅などで稼働するサーバを物理、仮想を問わずオンプレサーバと呼ぶことにする。

SSM より管理されるサーバはマネージドインスタンスと呼ばれる。 マネージドインスタンスには SSM エージェントをインストールする必要がある。

  1. アクティベーションの作成
  2. SSM エージェントのインストール(その手順の中でアクティベーション

順にやってみた。

アクティベーションの作成

最初に AWS の SSM コンソールでアクティベーションを作成する。 ここでいう「アクティベーションを作成する」とは、SSM エージェントをアクティベーションする際の「アクティベーションコードを発行する」ことである。

以下に手順を示す。

  1. AWS Systems Mangerのコンソールのアイティベーションにアクセスする。
    SSM - アクティベーション
  2. 右上の「アクティベーションの作成」をクリックする。
  3. 必要な項目を入力しアクティベーションを作成する

    SSM − アクティベーションの作成

    • デフォルト値のまま登録しても良い。
    • IAM ロールは「システムの作成したデフォルトコマンド実行ロール」を選択すると、初回実行時にAmazonEC2RunCommandRoleForManagedInstancesが作成される。
    • デフォルト値のままだとインスタンス制限 1 となっているのでアクティベーションできるインスタンスが 1 つだけとなってしまう。複数のインスタンスを登録するときにはインスタンス制限の数を設定する。
    • 実運用の際にはアクティベーションの有効期限を設定したほうが良いと思われる。
  4. 登録後表示される Activation CodeActivation IDを控えておく。これがエージェントのインストールの際に必要となる。
    • 再び参照できないのでなくさないようにする必要がある。
    • なくしたらまたアクティベーションを作成すれば良いだけだが、面倒。

SSMエージェントのインストール

SSM エージェントは次の OS にインストールできる。

各 OS へのインストールは次の資料の手順が示されている。

このエントリでは Ubuntu 18.04 Server で試してみた。 Ubuntu の場合のインストール手順は次の通り。

mkdir /tmp/ssm
curl https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/debian_amd64/amazon-ssm-agent.deb -o /tmp/ssm/amazon-ssm-agent.deb
sudo dpkg -i /tmp/ssm/amazon-ssm-agent.deb
sudo service amazon-ssm-agent stop
sudo amazon-ssm-agent -register -code "activation-code" -id "activation-id" -region "region"
sudo service amazon-ssm-agent start

この 5 行目で SSM エージェントのアクティベーションを行っている。 その際に先に作成したアクティベーションActivation CodeActivation IDを利用する。

試したところ全くつまずくところはなく、すんなりインストールできる。 設定に成功するとしばらくして次の様に AWS コンソールでマネージドインスタンスとして表示される。

SSM - マネージドインスタンス
マネージドインスタンスになれば、AWSコンソールからの操作を行うことができる。

シェルコマンドの実行

SSM エージェントを通して任意のシェルコマンドを実行できる。
ハローワールド代わりに、次のコマンドを実行してみた。

date
ls -l /etc/

マネージドインスタンス上でコマンドを実行するには[アクション]-[ランコマンド]を利用する。 以下に手順を示す。

コマンドのドキュメント

実行できるコマンドは[共有リソース]-[ドキュメント]で定義され、管理されているようだ。
任意のシェルコマンドを実行させるには AWS-RunShellScript ドキュメントを利用する。

ということで、最初に次の様に AWS-RunShellScript を選択する。

SSM ランコマンド ドキュメントの選択

コマンドのパラメータ

続いてコマンドのパラメータを設定する。 ここは、選択したコマンドにより異なるが、AWS-RunShellScript の場合、次のような UI になっており、実行するシェルコマンドを直接記述する。

SSM ランコマンド コマンドのパラメータ

ターゲット

次に実行対象のマネージドインスタンスを選択する。

SSM ランコマンド ターゲットの指定

直接指定もできるが、マネージドインスタンスに設定したタグで指定すれば、複数インスタンスを一度に設定できるのであろう。

その他の設定

他に次の設定できる。詳細は省略。

  • コメント
  • 実行レートの制御
  • 出力オプション 次の出力先を指定できる。択一でなく複数出力も可能
  • SNS 通知

最後にはコンソールからではなく CLI で実行する場合のオプション設定済みの CLI コマンドも表示される。

実行

実行すると次の様に実行ステータスが表示される。

![SSM ランコマンド実行状況

SSM ランコマンド実行状況

全体の進行状況が表示されるとともに、個々のターゲットでの実行時の出力も確認できる。

結果の出力

コマンドの実行結果を確認する。 AWS コンソールの SSM ランコマンドのコマンド履歴で確認できる。

また、出力先に S3 を指定しているとコマンドの標準出力がファイルに出力される。 この例の場合、次の内容のファイルが指定バケットの指定ディレクトリに出力される。

2018年 12月 02日日曜日 16:15:24 JST
合計 796
drwxr-xr-x 4 root root    4096 11月 29  2017 X11
-rw-r--r-- 1 root root    2981 11月 29  2017 adduser.conf
drwxr-xr-x 2 root root    4096 11月  5 20:53 alternatives
〜 以下略 〜

期待通りの結果が出力されている。

Dockerコンテナの起動

さて、個々のコマンドを実行してできることを確認するよりも Docker コンテナを起動できれば、各サーバのソフトウェア環境にかかわらず処理を実行させることができるであろうと考えて試してみた。

EC2 インスタンスではなく物理マシン上の Ubuntu 18.04 LTS server に SSM エージェントをインストールして、Docker コンテナを起動するまでを試みた。

Docker は apt でインストールする

SSM エージェントを通して次のコマンドを実行してみる。

echo $PATH

すると出力結果は次の通り。これが SSM エージェントに通っている PATH ということになる。

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

一方、Ubuntu 18.04 LTS server では最近追加された snap の利用を促される。 Docker も snap でインストールするよう促されるが、それに従うと /snap/bin/docker にインストールされてしまう。が、SSM エージェントのパスに /snap/bin が追加されるわけでもなく Docker コマンドが見なからないという結果になった。

apt でのインストールもできるので、そちらでインストールすると パスの通ったところに Docker がインストールされるので snap ではなく apt でインストールする必要がある。

Dockerコンテナの起動には AWS-RunShellScript を使う

SSM には AWS-RunDockerAction ドキュメントというその用途用のドキュメントが存在する。しかしそれを利用した場合、次の様にイメージの pull には成功しているようだが、コンテナの起動には失敗してしまった。

Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
f17d81b4b692: Pulling fs layer
82dca86e04c3: Pulling fs layer
046ccb106982: Pulling fs layer
046ccb106982: Verifying Checksum
046ccb106982: Download complete
f17d81b4b692: Download complete
82dca86e04c3: Verifying Checksum
82dca86e04c3: Download complete
f17d81b4b692: Pull complete
82dca86e04c3: Pull complete
046ccb106982: Pull complete
Digest: sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341
Status: Downloaded newer image for nginx:latest
docker: Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused "exec: \"\": executable file not found in $PATH": unknown.
failed to run commands: exit status 127

一方、汎用のシェルコマンド実行用の AWS-RunShellScript ドキュメントを利用して次を実行すると Docker コンテナの起動に成功した。

項目 設定値
コマンドのドキュメント AWS-RunShellScript
commands docker run --name some-nginx -d -p 8080:80 nginx

これは nginx を起動するものだが、別マシンからの nginx へのアクセスも可能であった。

AppArmer

もう1つ問題があった。

Ubuntu18.04 では AppArmer が有効になっている。
そのため、起動したコンテナを止められない事象が発生した。

次のコマンドで AppArmer をとめるとコンテナを停止できる。

sudo aa-status # 状態の確認
systemctl disable apparmor.service --now
service apparmor teardown

上記の実行後、次の設定で起動した Docker コンテナを停止できた。

項目 設定値
コマンドのドキュメント AWS-RunShellScript
commands docker stop some-nginx

実運用の際には AppArmer を止めるか、適切な設定が必要になるだろう。

まとめ

  • SSM エージェントを Ubuntu にインストールして、SSM コンソールから操作できることを確認した。
  • 任意のコマンドの実行が可能で、Docker コンテナの起動も可能。
  • しかし、AppArmer による制限があるので注意が必要。

また、主には Ubuntu で試したのだけど、SSM エージェントは Raspbian にもインストール可能となっている。任意のシェルコマンド実行までは動作することを確認した。Docker コンテナ起動については未実施。

参考

Gitbook の環境を Dockerで作った

しばらくきちんとしたドキュメント作成から遠ざかっていたのだけど、仕様書なるものを書く事になった。

基本的に文章を書くのは Markdown 形式に寄せていくようにしているので Gitbook を使うことにした。最終的に PDF にすることも簡単であろうと。

しかし、PDF 出力でハマった。その記録。

要件

目的はシステムの仕様書を書くということを踏まえ次の要件で環境を作ることにした。

  1. Markdown で記述できること
  2. UML も記載したい
    • 最低でも、クラス図、シーケンス図は書きたい
  3. PDF で出力できること

実現方法の検討

1. Markdownで記述できること

「1. Markdown で記述できること」は Gitbook を採用した時点で満たせる。
最初から Gitbook を使うつもりだったので、検討するまでもない。 ただ、ドキュメントの量が大きくなってくるとビルドに時間がかかりそうなのが気がかりだったので代替プロダクトを一応探してみた。
mdBookという Rust 製の Gitbook クローンを見つけたのだけど、mdBook 自体も言語から発展途上感が強かったので、今回は見送った。

で、当初の方針通り Gitbook を採用して進めることとした。

2. UMLも記載したい

これについては Gitbook のプラグインで解決できそうなので、どれを使うかということで探した。

mermaidを利用したものがお手軽で良さそうだったのだけど、クラス図をサポートできるものが見つからなかったので PlantUMLを利用したもので検討した。

PlantUML を利用するプラグインはいくつもある。ググってすぐ出てきたのは、plantumlなのだけど、それを改良したっぽい umlを使って見ることにした。

動作はするのだけど、gitbook serve で使っていると、UML の出力が遅くて快適に使えない。PlantUML で UMLレンダリングする際に Java でできた PlantUML を動かすために JVM の起動から始まるのが遅い原因だと思われる。

そこで、PlantUML サーバをつかって UMLレンダリングするタイプのplantuml-cloudを使ってみることにした。狙い通り UMLレンダリングは速くなった。 速度改善と同時に PlantUML に必要な graphvizのインストールも不要になった。 オフライン時も使えるようにしたいのでローカルで PlantUML のサーバである plantuml-serviceを動かすことにした。Docker イメージも提供されているので、次の Docker コンテナを動かす Docker Compose ファイルを作った。

  • plantuml-service
  • nginx
  • Giitbook

nginx は plantuml-service をリバースプロキシするために入れている。というのも plantuml-service の待受ポート番号(1608)と plantuml-cloud の接続ポート番号(80) が一致しておらず、変更もできなかったのだ。それで仕方なくリバプロして Gitbook からのリクエストを受けられるようにした。

docker-compose.yml は次の通り。

version: "2.2"
services:
  plantuml:
    image: 'bitjourney/plantuml-service:1.3.3'

  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - plantuml

  gitbook:
    image: hero/docker-gitbook:latest
    volumes:
      - ./gitbook:/gitbook
      - ./dest:/gitbook_dest
    ports:
      - 4000:4000
    depends_on:
      - nginx
    command: 'serve'

2018/11/08追記 plantuml-cloudに送っていたプルリクがマージされ、ポート番号が指定できるようになった。これに伴い nginx によるリバプロは不要になり、次の docker-compose.yml のように plantuml と gitbook だけでよくなった。

version: "2.2"
services:
  plantuml:
    image: 'bitjourney/plantuml-service:1.3.3'

  gitbook:
    image: hero/docker-gitbook:latest
    volumes:
      - ./gitbook:/gitbook
      - ./dest:/gitbook_dest
    ports:
      - 4000:4000
    depends_on:
      - plantuml
    command: 'serve'

この環境でplantuml-cloudを使うには次のように book.json で plantuml-cloud を設定する。

{
    "plugins": ["plantuml-cloud"],
    "pluginsConfig": {
      "plantuml-cloud": {
        "protocol": "http",
        "host": "plantuml",
        "port": 1608
      }
    }
}

3. PDFで出力できること

Gitbook の PDF 出力は calibreに依存している。 calibre をインストールしている他の Docker イメージを参考にしながら次のような Dcokerfile を作った。

FROM node:10.12.0-alpine
LABEL maintainer="HeRoMo"

ENV GLIBC_VERSION 2.28-r0
RUN apk add --update curl && \
    curl -Lo /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub && \
    curl -Lo glibc.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-${GLIBC_VERSION}.apk" && \
    curl -Lo glibc-bin.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-bin-${GLIBC_VERSION}.apk" && \
    apk add glibc-bin.apk glibc.apk && \
    /usr/glibc-compat/sbin/ldconfig /lib /usr/glibc-compat/lib && \
    echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
    apk del curl && \
    rm -rf glibc.apk glibc-bin.apk /var/cache/apk/*

ENV LD_LIBRARY_PATH $LD_LIBRARY_PATH:/opt/calibre/lib
ENV PATH $PATH:/opt/calibre/bin
ENV CALIBRE_INSTALLER_SOURCE_CODE_URL https://raw.githubusercontent.com/kovidgoyal/calibre/master/setup/linux-installer.py
RUN apk update && \
    apk add --no-cache --upgrade \
    bash \
    ca-certificates \
    gcc \
    mesa-gl \
    python \
    qt5-qtbase-x11 \
    wget \
    xdg-utils \
    libxcomposite \
    xz && \
    wget -O- ${CALIBRE_INSTALLER_SOURCE_CODE_URL} | python -c "import sys; main=lambda:sys.stderr.write('Download failed\n'); exec(sys.stdin.read()); main(install_dir='/opt', isolated=True)" && \
    rm -rf /tmp/calibre-installer-cache

RUN yarn global add gitbook-cli svgexport

COPY ./start.sh /usr/bin/
RUN chmod 755 /usr/bin/start.sh

ENTRYPOINT ["start.sh"]

さて、このイメージを使って PDF を作って見ると、PDF はできるにはできるが、UML の図が表示できない。

plantuml-service では SVGUMLレンダリングしているので、PNG ならどうだろうかと試そうにも plantuml-service は SVG のみサポートなので使えない。PDF 作成時のみローカルで PlantUML を動作するようにして試してみたがダメだった。

イメージを小さくしようと alpine ベースのイメージをベースにしていたが、そのかわり calibre やそれが依存している glibc を自前でビルドするようにしていたらそこに何らかの不足があるようだ。

試しに Debian ベースのイメージをベースに作り直した。それでは calibre もパッケージマネージャでインストールできる。UML を含む PDF の出力も問題なく出力できる。

最終的に gitbook の Dockerfile は次のようになった。

FROM node:10.13.0-slim
LABEL maintainer="HeRoMo"

# install apt packages
RUN apt-get update -y && \
    apt-get install -y \
      bzip2 \
      calibre && \
    apt-get autoremove -y && \
    apt-get clean && \
    rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/*

RUN yarn global add gitbook-cli svgexport

COPY ./start.sh /usr/bin/
RUN chmod 755 /usr/bin/start.sh

ENTRYPOINT ["start.sh"]

シンプルになった。イメージのサイズは約 900MB と少々大きくなったけど。

リポジトリは次の通り。

github.com

Dockerhub はこちら。

https://hub.docker.com/r/hero/gitbook/

GAS を Typescript でリファクタした

以前、GAS を Webpack と Babel でモダンに実装した話を書いた。

hero.hatenablog.jp

このエントリを要約すると次の通り。

  • Togglで記録した作業時間をRedmineの作業時間に登録する Google スプレドッドシートアドオンを作った。
  • google\/clasp を利用して、ローカルでコーディング、バージョン管理した。
  • ES6 で JS を書いて webpack + babel で GAS 用にビルドするようにした。

Claspが Typescriptをサポートした

まあまあモダンな JS で GAS を実装できる構成を確立したことで、今後はやりやすくなるぞと思ってから、半年ほど。

Clasp に驚くアップデートがあった。v1.5.0 で Typescript をサポートしたのだ。

qiita.com

clasp push コマンドを実行すると、JavaScript のコードを対応する GAS プロジェクトファイルにアップロードしてくれるのだが、Typescript のコードも自動的にコンパイルしてアップロードしてくれるようになったのだ。

もちろん、HTTP 通信には UrlFetchApp.fetch を使わないといけないとか GAS プラットフォームの制約はあるものの、コードのシンタックスは Typescript のものが使える。

つまり、次が使える。

型以外は webpack+babel の構成でも実現していたので、それほど目新しさはないが、webpack も babel も必要なく、これまで通りのコマンドを使うだけでトランスパイルしてアップロードしてくれる。

Toggl2rm を移行してみた

これは便利そうだということで、以前作ったToggl2rmを移行してみた。

移行前後のコードは次の通り。

依存モジュールが激減

移行前はこんな数のモジュールに依存していた。

  • @google/clasp
  • babel-core
  • babel-loader
  • babel-preset-env
  • babel-preset-gas
  • eslint
  • eslint-config-airbnb-base
  • eslint-plugin-googleappsscript
  • eslint-plugin-import
  • file-loader
  • gas-webpack-plugin
  • html-webpack-plugin
  • webpack

それが、移行後はたったの 3 つ。

トランスパイルに必要な一切を @google/clasp が担保してくれるので、 どんどんアップデートされる webpack や babel を追いかける必要もない。 その他必要なモジュールの組み合わせにも悩まされることもない。

@types/google-apps-script によって GAS 固有のクラス群にも型チェックできるようになるし、VSCode ならばコード補完が使えるのでドキュメントを確認する回数も減るだろう。

いいことしかない。

コードの書き換え

もともとのコードが ES6 だったこともありコードの書き換えも少なかった。

  • 拡張子を *.ts に変更する
  • 各関数の引数と戻り値に型アノテーションを追加
  • tslint のエラー箇所を修正
  • 変数 global を削除

ほぼこれだけで移行完了だった。

結構、関数コメントに引数や戻り値について書き込んでいたけれど、やはりコードで定義できるほうが確実だと思った。 そして、コード補完が感動的に便利。

いいことしかない。

まとめ

clasp で Typescript を使うのは簡単。

趣味レベルで使っている GAS のコードは作った後はそれほど頻繁にはいじらないと思う。そういうコードほど型アノテーションは恩恵があると思う。 また、ビルドの依存モジュールも少なくてコード以外のもののメンテナンスが最小化される。

GAS の実装で Typescript を使わない理由が見当たらない。