限定公開のGKE上でセキュアなGithub Actionsのrunnerを構築
モチベーション
Github Actionsのセルフホストランナーでは、long pollingによりrunner側でingressのfirewallを設定せずにrunnerをデプロイ出来るというのを最近知ったので、GKEで検証していこうと思います。
構成
ざっくりですがこんな感じ。
GKEは限定公開のクラスタとして構築し、踏み台サーバからGKEにリクエストを送ります。
Github Actionsとの通信のためにVPCにはCloud NATをアタッチします。
前提条件
terraformで構築するため、予めインストールしておくこと。(検証はv1.0.0)
構築手順
GCPサービスデプロイ
VPC
まずGKE、踏み台サーバをデプロイさせるためのVPCを準備します。
resource "google_compute_network" "private_network" {
name = var.private_network_name
project = var.project_id
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "private_network_subnet" {
name = "${var.private_network_name}-subnet"
ip_cidr_range = var.private_network_subnet
region = var.region
network = google_compute_network.private_network.self_link
private_ip_google_access = true
secondary_ip_range {
range_name = "services"
ip_cidr_range = var.gke_services_subnet
}
secondary_ip_range {
range_name = "pods"
ip_cidr_range = var.gke_pods_subnet
}
}
podとserviceのIP range確保のため、サブネットの中にセカンダリCIDRを設定します。
Firewall
踏み台サーバにSSH接続するため、network tagがbastionのGCEインスタンスに対し22番portでのアクセスを許可します。
resource "google_compute_firewall" "private_network_allow_ingress_my_instance" {
name = "${var.private_network_name}-allow-ingress-bastion-instance"
network = google_compute_network.private_network.self_link
source_ranges = ["0.0.0.0/0"]
allow {
protocol = "icmp"
}
allow {
protocol = "tcp"
ports = ["22"]
}
target_tags = ["bastion"]
}
worker nodeからcontrol planeへのアクセスを許可します。
resource "google_compute_firewall" "private_gke_network_allow_egress_masternode" {
name = "private-gke-network-allow-egress-masternode"
network = google_compute_network.private_network.self_link
direction = "EGRESS"
destination_ranges = [var.gke_cluster_control_plane_ip_range]
allow {
protocol = "tcp"
ports = ["443", "10250"]
}
}
control planeからwork nodeへのアクセスを許可します。
resource "google_compute_firewall" "private_gke_network_allow_igress_masternode" {
name = "private-gke-network-allow-igress-masternode"
network = google_compute_network.private_network.self_link
source_ranges = [var.gke_cluster_control_plane_ip_range]
allow {
protocol = "tcp"
ports = ["9443"]
}
}
Cloud NAT
Action RunnerがGithubへアクセスできるようにCloud NATを作成します。
resource "google_compute_router" "nat_router" {
name = "${var.private_network_name}-router"
network = google_compute_network.private_network.self_link
}
resource "google_compute_address" "nat_address" {
name = "nat-address"
}
resource "google_compute_router_nat" "nat" {
name = "${var.private_network_name}-nat"
nat_ip_allocate_option = "MANUAL_ONLY"
router = google_compute_router.nat_router.name
source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
min_ports_per_vm = 64
nat_ips = [google_compute_address.nat_address.self_link]
}
踏み台サーバ
パブリックIPアドレスを持つ踏み台サーバを作成します。
resource "google_compute_address" "bastion_static_ip" {
name = "bastion-address"
}
resource "google_compute_instance" "my_pri_instance" {
project = var.project_id
zone = var.zone
name = "bastion"
machine_type = "e2-micro"
tags = ["bastion"]
boot_disk {
initialize_params {
image = "debian-cloud/debian-11"
}
}
network_interface {
subnetwork = google_compute_subnetwork.private_network_subnet.name
subnetwork_project = var.project_id
access_config {
nat_ip = google_compute_address.bastion_static_ip.address
}
}
service_account {
scopes = ["cloud-platform"]
}
metadata_startup_script = "apt-get install kubectl"
}
Private Google Access(Nice to Have)
Private Google Accessとは、プライベートVPC内のリソースがパブリックIPを持たずにGCPサービスにアクセスできる機能です。今回はCloud NATを構築するのでmustではないですが、セキュアな通信を実現するためは必須な機能であるためコードだけ記載しておきます。
resource "google_dns_managed_zone" "google_apis" {
project = var.project_id
name = "google-apis"
dns_name = "googleapis.com."
visibility = "private"
private_visibility_config {
networks {
network_url = google_compute_network.private_network.id
}
}
}
resource "google_dns_record_set" "google_apis_cname" {
project = var.project_id
managed_zone = google_dns_managed_zone.google_apis.name
name = "*.${google_dns_managed_zone.google_apis.dns_name}"
type = "CNAME"
ttl = 300
rrdatas = ["restricted.googleapis.com."]
}
resource "google_dns_record_set" "google_apis_a" {
project = var.project_id
managed_zone = google_dns_managed_zone.google_apis.name
name = "restricted.googleapis.com."
type = "A"
ttl = 300
rrdatas = ["199.36.153.4", "199.36.153.5", "199.36.153.6", "199.36.153.7"]
}
プライベートVPCではPGAのIPアドレスへのアクセスを許可していないため、穴あけを行います。
resource "google_compute_firewall" "private_gke_network_allow_egress_google_apis" {
name = "private-gke-network-allow-egress-google-apis"
network = google_compute_network.private_network.self_link
direction = "EGRESS"
destination_ranges = ["199.36.153.4/30"]
allow {
protocol = "all"
}
}
GKE
いよいよGKEの構築です。
resource "google_container_cluster" "primary" {
name = var.cluster_name
location = var.zone
initial_node_count = 1
network = google_compute_network.private_network.name
subnetwork = google_compute_subnetwork.private_network_subnet.name
remove_default_node_pool = true
private_cluster_config {
enable_private_nodes = true # 各ノードのパブリックIPを無効化
enable_private_endpoint = true # マスターノードのパブリックエンドポイントを無効化
master_ipv4_cidr_block = var.gke_cluster_control_plane_ip_range
}
ip_allocation_policy {
cluster_secondary_range_name = "pods"
services_secondary_range_name = "services"
}
master_authorized_networks_config {
cidr_blocks {
cidr_block = google_compute_subnetwork.private_network_subnet.ip_cidr_range
display_name = var.private_network_name
}
}
}
resource "google_service_account" "default" {
account_id = "service-account-id"
display_name = "Service Account"
}
resource "google_container_node_pool" "node_pool" {
name = "my-node-pool"
cluster = google_container_cluster.primary.id
node_count = 4
node_config {
machine_type = "e2-medium"
# Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles.
service_account = google_service_account.default.email
oauth_scopes = [
"https://www.googleapis.com/auth/cloud-platform"
]
}
timeouts {
create = "30m"
update = "20m"
}
}
Github Actions Runnerデプロイ
Github token取得
いよいよGithub Actions RunnerをGKEにデプロイしていきます。
Github tokenが必要になるため取得します。
https://github.com/settings/tokens/new にアクセスし、repoにチェックを入れgenerete tokenをクリックします。
表示されたtokenは後で使用するので保存しておいてください。
github actions定義ファイル作成
適当なリポジトリを作成し、以下のような定義ファイルを作成しておきます。
name: github action test
on:
push:
branches: [ "main" ]
jobs:
test:
name: test
runs-on: self-hosted
steps:
- id: test
run: |
sleep 100
echo hello world
踏み台サーバ設定
踏み台サーバにSSH接続します。
クラスタの認証情報を取得します。
gcloud container clusters get-credentials private-gke --zone=<zone>
cert namagerデプロイ
action runnerをデプロイするため、最初にcert managerをデプロイします。
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.8.2/cert-manager.yaml
action runners controllerデプロイ
続いて、action runners controllerをデプロイします。
kubectl apply -f \
https://github.com/actions/actions-runner-controller/\
releases/download/v0.22.0/actions-runner-controller.yaml --server-side
ContainerCreating
のまま止まっているため、github tokenを登録するsecretを作成します。
kubectl create secret generic controller-manager \
-n actions-runner-system \
--from-literal=github_token=<token>
無事、Runningになりました。
runnerデプロイ
最後にrunnerをデプロイします。
cat << EOS > runner.yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
name: runner
namespace: actions-runner-system
spec:
replicas: 3
template:
spec:
repository: <ユーザ名>/<リポジトリ>
EOS
確認
デプロイに成功すると、runner podが3台立ち上がります。
この状態でgithub actionsのworkflowを起動すると、runner podがactiveになる事がわかります。
Appendix
変数は以下を使用しました。ご参考まで。
variables.tf
variable "region" {
type = string
default = "us-central1"
}
variable "zone" {
type = string
default = "us-central1-a"
}
variable "project_id" {
type = string
default = "project-id"
}
variable "private_network_name" {
type = string
default = "private-network"
}
variable "gke_cluster_control_plane_ip_range" {
type = string
default = "172.16.0.0/28"
}
variable "cluster_name" {
type = string
default = "private-gke"
}
variable "private_network_subnet" {
type = string
default = "10.10.20.0/24"
}
variable "gke_services_subnet" {
type = string
default = "10.10.21.0/24"
}
variable "gke_pods_subnet" {
type = string
default = "10.20.0.0/16"
}
参考
Discussion