CloudFormationでGitLab Runner環境を作ったときのメモです。
自分には以下の前提がありました。
- GitLabはすでにEC2で動いている
- 大きめのインスタンスを常時動かすともったいないのでAutoScalingしたい
- GitLabはIPアドレス制限していて、そのレベルを下げたくない
- GitLabのいるVPCはピアリング接続等でプライベートIPアドレスに空きがない
## AutoScaling
GitLabのAWSでAutoScalingさせるドキュメントを読みました。
上のドキュメント内でbastion instanceと呼ばれているマシンがジョブ一覧をポーリングし、ジョブがあればアイドル状態のマシンを使って実行します。GitLabのpush ではなくbastion instanceのpullで動くところが重要です。いつかFargate対応されそうですが2019年5月時点ではまだありません。
CloudCraftで描いた図を載せます。IPv6対応もしてあるのですがIPv4と同じ構成なので省略しました。Bastionをパブリックサブネット、Runnerをプライベートサブネットに分けています。
図に入れられなかったセキュリティグループの設定はこちらです。
### Bastionインスタンス
#table1向き | プロトコル | ポート範囲 | ソース |
---|
インバウンド | TCP | 22 | GitLab |
アウトバウンド | すべて | すべて | すべて |
インバウンドの22番は問題が起きたときの/var/log
の確認のために開けてあります。
### Runnerインスタンス
#table2向き | プロトコル | ポート範囲 | ソース |
---|
インバウンド | TCP | 2376 | 192.168.0.0/27(あとv6も) |
アウトバウンド | すべて | すべて | すべて |
インバウンドの2376番はBastionインスタンスがDocker Daemonとの通信で使います。インバウンドソースはBastion Networkの CIDRです。
## chialab/aws-autoscaling-gitlab-runner
chialab/aws-autoscaling-gitlab-runner/blob/master/runner.yml
参考にしたテンプレートです。ロールの部分とBastionインスタンス(このテンプレートではManager)の部分で24時間分くらい助かりました。VPCを外から与えるのが自分のテンプレートと違うところです。
テンプレートを載せようと思ったのですがそうもいかないので、リソースの一覧を出して詰まったところをまとめます。
スタックのパラメータでセキュリティグループidを渡してSecurityGroupIngressで作成した2つのElastic IPからの接続を許可します。「どこを許可するのか」は下の詰まったところで後述します。
## 詰まったところ
テンプレートで必要になるのはRunnerトークンで、これはGitLabの設定画面で確認できるRegistrationトークンとは別ですRunnerを登録した際に発行されるトークンなので、事前にGitLab APIでRunnerを登録してトークンを発行しておきます。トークンが間違っているとインスタンス作成時の疎通確認で失敗します。
AWS::CloudFormation::Initを使うとcfn-initができます。
- AWS CloudFormation のメタデータの取得と解析
- パッケージのインストール
- ディスクへのファイルの書き込み
- サービスの有効化/無効化と開始/停止
テンプレートの行数が一番多い部分になりましたが先のリポジトリのおかげで楽ができました。
### ネットワーク
#### ネットワークACLとセキュリティグループの違い
これを把握できていなかったためACLのインバウンドを制限してしまいパッケージインストールなどの外部アクセスができなくなりました。セキュリティグループとネットワークACLの比較の以下の部分を読んで解決しました。
#table3セキュリティグループ | ネットワークACL |
---|
ステートフル:ルールに関係なく、返されたトラフィックが自動的に許可されます | ステートレス:返されたトラフィックがルールによって明示的に許可されます |
ACLのインバウンドを制限すると外部へのリクエストは通れますがレスポンスが通れません。外部アクセスが必要な場合ACLインバウンドは許可します。
#### パブリックIPアドレスの関連付け
Elastic IPの関連付けはインスタンスの作成後になります。Bastionインスタンスの作成中AWS::CloudFormation::Initの設定でGitLabにリクエストを送るのですが、テンプレートで指定しているElastic IPはこの時点ではまだついていません。サブネット側でMapPublicIpOnLaunchをtrueにすると起動時にパブリックIPv4アドレスが割り当てられるのでcfn-init中も外部と通信できます。
ここまでの問題はAWSコンソールではBastionインスタンス作成時のエラーとして現れるためデバッグに時間がかかりました。
## GitLabとの通信内容
ジョブの中で特別なことをしない限りGitLabへの通信は以下の4種類です。
#table4送信元 | ポート | 内容 |
---|
Bastion | 443 | ジョブを引き受ける |
Bastion | 443 | ジョブのログを送る |
Bastion | 443 | ジョブの結果を送る |
Runner (NAT) | 443 | リポジトリを GET する |
これらを止めないようにネットワークを設定すれば期待どおりに動作します。
## 動作確認
次のgitlab-ci.ymlのパイプラインを動かしてみます。
以下はログです。
- 一度失敗したパイプラインなのでブラウザのリトライ要求で始まります
- IPアドレスとホストはマスクしています
- 日付は
[14/May/2019:03:27:45 +0000]
を[03:27:45]
に短くしています
IdleTimeを300秒、IdleCountを0に設定していたため、2台のマシンは5分後にそれぞれシャットダウンされました。
ジョブ57と58は同じステージなので並行に実行されるはずですが、58は57 よりも後に実行されています。これはBuildステージに入った時にアイドル状態だったマシンがジョブ76を実行していた1台だけだったからです。ジョブ57は既存マシンに割り当てられたため開始が早く、ジョブ58は新しいマシンの起動を待つため開始が遅れました。次のTestステージではBuildステージで使った2台のマシンがアイドル状態で存在していたので同時に始まっています。