Google Kubernetes EngineにNAT付きでConcourse CIをたてる
(この記事は、Recruit Engineers Advent Calendar 2018 16日目の記事です)
こんにちは。リクルートでエンジニアをしている加藤です。 フロントエンドをやったり、バックエンドをやったり、プロダクティビティ的なことをやったりと、いろんなことをやっています。最近は、KubernetesとGCPを触る機会が増えてきたのと、あとJenkinsがもういい加減辛くなってきてConcourse CIを触り始めました。つまりこの記事は、 最近やったこと全部盛り な内容になっております。すこし長いですがお付き合いください。
この記事でやること
この記事では、GKE(Google Kubernetes Engine)に、NAT付きでConcourseを立てる方法をご紹介します。今回想定している状況は、
- ファイヤウォールの内側にいる、プライベートのサービス(GitlabやGithubEnterperiseなど)につなぎに行きたい
- Concourseには特定のIPからのみ接続を許可したい
- Concourseへの接続はHTTPSにしたい
というものです。エンタープライズの開発環境では、よくある状況かと思います。 全体構成は以下のようになります。
この構成を作るため、以下の順で進めていきます。
- NAT Gateway付きのGKEクラスタを作る
- Let's EncryptのTLS証明書をGKEクラスタで管理する
- ConcourseをGKEクラスタ上で動かす
- Cloud Armorで接続元IPを制限する
この記事で例示されるコードは、後ほどリポジトリにアップして、記事に追記します。
Concourse CI?
Concourse CIは、pivotalによって開発されている、オープンソースのCIツールです。ググるとJenkins vs Concourseみたいな記事が出るくらいには人気があります。特徴的な点としては、パイプラインの状態可視化と、設定ファイルによる明示的なタスク定義があります。ConcourseCIについては、数ある紹介記事・比較記事にお任せします。個人的にはMediumにある公式の紹介記事が丁寧に書いてあっておすすめです。
参考 - JenkinsとConcourse CIを比較 - Concourse official articles on Medium
0. 前提
⚠️ これ以降の手順は、以下の項目を前提として進みます。ご注意ください ⚠️
- GCPプロジェクトを持っていること
- ローカルマシンにGoogle Cloud SDKがインストールされていて、
gcloud
コマンドが利用可能であること gcloud
コマンドでログインしていて、GCPプロジェクトを設定していること- ローカルマシンにkubectlがインストールされていて、
kubectl
コマンドが利用可能であること
1. NAT Gateway付きのGKEクラスタを作る
GKEクラスタからのリクエストの送信元IPを固定するには、NAT Gatewayが必要です。今回、NAT Gatewayの役割を果たしてくれるのは、GCE(Google Compute Engine)のVMインスタンスです。このVMインスタンスをNATインスタンス
と呼びます。GKEクラスタのアウトバウンドのトラフィックは、全てNATインスタンスを経由するようにします。これによって、GKEクラスタからのリクエストの送信元IPは、NATインスタンスのものになります。プライベートサービスのファイヤウォールには、NATインスタンスのIPを許可する設定を追加してあげれば、GKEクラスタからの接続を許可することができます。
本章では、リソースのデプロイにCloud Deployment Managerを利用します。各リソース定義のフォーマットは、yamlをベースとしてjinja2で書かれています。サポートされているリソース定義については、Cloud Deployment Managerドキュメントのサポートされるリソースタイプから全て辿ることができます。
1.1. ネットワークリソースの定義
ネットワークリソースを定義します。必要なのは、以下の3つです。
構成のベースとなるVPCネットワークは、構成全体を囲むいわゆるグループのようなものです。network
というリソースで表現されます。GKEクラスタのサブネットと、NATインスタンスのサブネットは、ネットワークに属するグループです。subnetwork
というリソースで表現されます。
resources: ######## Network ############ - name: devops-nat-network type: compute.v1.network properties: autoCreateSubnetworks: false ######### Subnets ########## ######### For Cluster ######### - name: devops-subnet type: compute.v1.subnetwork properties: network: $(ref.devops-nat-network.selfLink) ipCidrRange: 172.16.0.0/12 region: asia-northeast1 ########## For NAT Instance ########## - name: devops-nat-subnet type: compute.v1.subnetwork properties: network: $(ref.devops-nat-network.selfLink) ipCidrRange: 10.1.1.0/24 region: asia-northeast1
GCPでは、ネットワークはCIDRとリージョンを持たず、サブネットがCIDRとリージョンを持っています。同じネットワークに属するサブネット同士は、CIDRの範囲が被ってはいけないので注意してください。この辺りは少し混乱するかもしれませんが、一旦はグルーピングがしたいんだな、くらいの理解で大丈夫です。さらに詳しくはVirtual Private Cloud(VPC)ネットワークの概要を読んでください。AWSと比較して違いが知りたいという方は【AWSしかやったことない人向け】AWSとGCPのネットワークの違いを理解してみようが非常に丁寧でおすすめです。
この設定で、以下のように枠だけの状態ができました。
各リソースのname
は適当につけました。今回はConcourseのための構成なので、全てにdevops
というprefixをつけています。後からどんな用途のリソースかわかればいいと思いますので、状況に応じて好きなものをつけてください。
1.2. NATインスタンスとGKEクラスタの定義
########## NAT Instance ########## - name: devops-nat-vm type: compute.v1.instance properties: zone: asia-northeast1-a canIpForward: true tags: items: - nat-to-internet machineType: https://www.googleapis.com/compute/v1/projects/{{ env["project"] }}/zones/asia-northeast1-a/machineTypes/f1-micro disks: - deviceName: boot type: PERSISTENT boot: true autoDelete: true initializeParams: sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-9-stretch-v20181113 networkInterfaces: - network: projects/{{ env["project"] }}/global/networks/devops-nat-network subnetwork: $(ref.devops-nat-subnet.selfLink) accessConfigs: - name: External NAT type: ONE_TO_ONE_NAT ##asia-northeast1リージョンに外部IPアドレスを予約してから入れる natIP: metadata: items: - key: startup-script value: | #!/bin/sh # -- # --------------------------- # Install TCP DUMP # Start nat; start dump # --------------------------- apt-get update apt-get install -y tcpdump apt-get install -y tcpick iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE nohup tcpdump -e -l -i eth0 -w /tmp/nat.pcap & nohup tcpdump -e -l -i eth0 > /tmp/nat.txt & echo 1 | tee /proc/sys/net/ipv4/ip_forward ########## FIREWALL RULES FOR NAT Instance ########## ## アウトバウンドトラフィック - name: devops-nat-vm-firewall type: compute.v1.firewall properties: allowed: - IPProtocol : tcp - IPProtocol : udp - IPProtocol : icmp - IPProtocol : esp - IPProtocol : ah - IPProtocol : sctp sourceTags: - route-through-nat network: $(ref.devops-nat-network.selfLink) ## インバウンドトラフィック - name: devops-nat-vm-ssh type: compute.v1.firewall properties: allowed: - IPProtocol : tcp ports: [22] sourceRanges: ## デバッグのため、特定のネットワーク内からのみNATへのSSHを許可 - YYY.YYY.YYY.YYY/ZZ network: $(ref.devops-nat-network.selfLink)
########## GKE CLUSTER CREATION ########## - name: devops type: container.v1.cluster metadata: dependsOn: - devops-nat-network - devops-subnet properties: cluster: name: devops network: devops-nat-network subnetwork: devops-subnet nodePools: - name: default-pool initialNodeCount: 1 config: tags: - route-through-nat # マシンタイプについてはhttps://cloud.google.com/compute/docs/machine-types?hl=ja machineType: n1-highmem-2 imageType: ubuntu autoscaling: enabled: true minNodeCount: 1 maxNodeCount: 3 zone: asia-northeast1-a
この設定で、以下のように枠に箱が入った状態ができました。
1.3. ネットワークルーティングの定義
########## GKE MASTER ROUTE ########## - name: devops-master-route type: compute.v1.route properties: destRange: $(ref.devops.endpoint) network: $(ref.devops-nat-network.selfLink) nextHopGateway: projects/{{ env["project"] }}/global/gateways/default-internet-gateway priority: 100 tags: - route-through-nat ########## NAT ROUTE ########## - name: devops-route-through-nat metadata: dependsOn: - devops - devops-nat-network type: compute.v1.route properties: network: $(ref.devops-nat-network.selfLink) destRange: 0.0.0.0/0 description: "route all other traffic through nat" nextHopInstance: $(ref.devops-nat-vm.selfLink) tags: - route-through-nat priority: 800
この設定で、以下のように箱の間に道を通すことができました。
1.4. 静的IPアドレスを取得する
静的なIPアドレスを取得します。GCEのVMにデフォルトで割り振られるIPアドレスは動的なもので、インスタンスを再起動したりすると切り替わってしまいます。NATインスタンスの場合、これではあまり嬉しくありません。
静的なIPアドレスを取得する詳しい方法は公式のドキュメントを参照してください。アドレスを取得する際には、先ほど定義したGKEクラスタと同じリージョンを選択します。静的なIPアドレスが取得できたら、1.2
で空欄にしていた、NATインスタンス設定のnatIP
に追記します。
取得したIPを許可するよう、プライベートのサービスのファイヤウォールに設定します。これで、クラスタからプライベートのサービスに繋がるようになりました。
1.5. 設定をデプロイする
ここまでで、基本的な構成の設定が完了しました。あとは、これをデプロイするだけです。デプロイには、gcloud
コマンドを使います。cluster-with-nat-route.yaml
というファイルに上記の設定がひとまとめにされていて、このデプロイにdevops-with-nat
という名前をつける場合、以下のようなコマンドになります。
$ gcloud deployment-manager deployments create devops-with-nat --config cluster-with-nat-route.yaml
処理には少し時間がかかります。処理が完了したらgcloud
コマンドで、ローカルマシンのkubectl
をGKEクラスタに繋げます。
$ gcloud container clusters devops get-credentials
1.6. Helmをインストールする
この章の仕上げとして、GKEクラスタでHelmが使えるようにセットアップします。 Helmは、Kubernetes用のパッケージマネージャです。これ以降の2章、3章では、GKEクラスタへのデプロイに、Helmを利用します。
まず、ローカルマシンにHelmクライアントをインストールします。手順はリンク先を参照してください。完了すると、helm
コマンドが使えるようになります。
GKEクラスタでHelmを利用するには、適切な権限が付与されたサービスアカウントが必要です。Helmのデフォルト設定に従い、tiller
という名前でサービスアカウントを作成します。
apiVersion: v1 kind: ServiceAccount metadata: name: tiller namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: tiller roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: ServiceAccount name: tiller namespace: kube-system
上記設定がhelm.yaml
という名前の場合、GKEクラスタへの適用は次のように行います。
$ kubectl apply -f helm.yaml
サービスアカウントが作成できたら、ローカルマシンのhelm
コマンドを初期化します。
$ helm init --upgrade --service-account tiller
2. Let's encryptのTLS証明書をGKEクラスタで管理する
GKEクラスタができたので、TLS証明書の準備に移ります。TLS証明書は、Let's Encryptで取得します。GKEクラスタで証明書管理を行う場合、cert-manager という超便利ツールを使います。
2.1. cert-managerをインストールする
cert-managerは、すでにhelmのパッケージが公開されており、コマンド一つでデプロイすることができます。今回は、cert-manager用に新しくnamespaceを切ります。
$ kubectl apply -f k8s/namespace.yaml $ helm install --name cert-manager --namespace cert-manager stable/cert-manager
2.2. Let's EncryptへTLS証明書作成依頼を発行する
今回、ドメインはAWSのRoute53で取得しているという想定です。その他のドメインプロバイダの場合、Issuer
のspec.acme.dns01
の書き方が変わります。dns01でのチャレンジに使えるドメインプロバイダと設定の書き方は、cert-managerのドキュメントに書いてあります。
apiVersion: v1 kind: Secret metadata: name: prod-route53-credentials-secret namespace: cert-manager type: Opaque data: secret-access-key: YOUR_AWS_SECRET_ACCESS_KEY(base64 encoded) --- apiVersion: certmanager.k8s.io/v1alpha1 kind: Issuer metadata: name: letsencrypt-prod namespace: cert-manager spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: your_email_here@example.com privateKeySecretRef: name: letsencrypt-prod dns01: providers: - name: prod-dns route53: region: ap-northeast-1 accessKeyID: YOUR_AWS_ACCESS_KEY_ID secretAccessKeySecretRef: name: prod-route53-credentials-secret key: secret-access-key --- apiVersion: certmanager.k8s.io/v1alpha1 kind: Certificate metadata: name: mycert namespace: cert-manager spec: secretName: mydomain-net-tls issuerRef: name: letsencrypt-prod commonName: '*.mydomain.net' acme: config: - dns01: provider: prod-dns domains: - '*.mydomain.net'
上記設定がcert.yaml
という名前の場合、GKEクラスタへの適用は次のように行います。
$ kubectl apply -f cert.yaml
証明書の発行には少し時間がかかります。申請状況の経過は次のコマンドで確認できます。
$ kubectl describe cert mycert -n cert-manager
Failed等の表示が途中でみられるかもしれませんが焦らず待ってみてください。完了するとmydomain-net-tls
というSecretが作成されます。作成されたかどうかは、以下のコマンドで確認できます。
$ kubectl get secret -n cert-manager
2.3. namespaceを跨いでワイルドカード証明書を使い回す
ワイルドカード証明書を、クラスタ内のnamespaceを跨いで使いまわしたい、というのは非常にわかりやすい要求です。Let's EncryptのAPIコールには件数制限があるということと、namespaceごとに証明書を取る手間を考えると、誰もがそうしたいと思うでしょう。しかし、cert-managerで取得した証明書をnamespaceを跨いで使うには、少し工夫が必要です。
cert-managerにはClusterIssuer
というのがあり、これを使うとクラスタ内の異なるnamespaceで証明書を使いまわせそうに見えるのですが、悲しいことにそうはいきません。このIssueにもある通り、ClusterIssuer
自体は使いまわせても、取得した証明書は使いまわせないという悲しい仕様になっています。
そこで、kubedを使います。一度Issuer
で取得した証明書の変更を監視しておいて、対象となるnamespaceに都度連携する方式です。
kubedは、helmでインストールすることができます。
$ helm repo add appscode https://charts.appscode.com/stable/ $ helm install appscode/kubed --name kubed --namespace kube-system \ --set apiserver.enabled=false \ --set config.clusterName=devops
kubedのインストールが完了したら、先ほど作成されたmydomain-net-tls
に、kubedが監視するためのアノテーションを貼ります。
$ kubectl annotate secret mydomain-net-tls -n cert-manager kubed.appscode.com/sync="app=kubed"
この章の仕上げとして、Concourseをデプロイするnamespace を作り、そこにkubedで証明書を連携します。
apiVersion: v1 kind: Namespace metadata: name: concourse labels: app: kubed
kubedが連携先として認識するため、labelにapp=kubed
を設定しています。kubectlで設定を反映します。
$ kubectl apply -f concourse-namespace.yaml
証明書が連携されているかどうか確認して完了です。
$ kubectl get secret mydomain-net-tls -n concourse
ここまでで、構成は以下のようになっています。だいぶできてきました。
3. ConcourseをGKEクラスタにデプロイする
3.1. 静的なIPアドレスを取得してドメインに紐づける
Concourse用に静的なIPアドレスを取得します。1.4.
の手順と同じ方法で取得し、concourse
という名前をつけます。この名前はConcourseの設定で使います。取得したIPアドレスを、AWS Route53でドメインに紐づけます。今回は、concourse.mydomain.net
という名前を紐づけた想定で進めます。
3.2. ソースをfetchして設定を書く
Helmで提供されているConcourseのパッケージに、設定を追加してデプロイします。まずソースをfetchしてきます。
$ helm fetch --untar stable/concourse
新しくconcourse
というディレクトリが作られます。この中にある、values.yaml
がデフォルトの設定ファイルになります。コピーして持ってきましょう。
$ cp ./concourse/values.yaml ./myvalues.yaml
この中に、変更する箇所がいくつかあります。設定ファイル全体をここで見るのは長すぎるので、ピックアップして見ていきます。設定ファイルには大きく分けてconcourse
という項目と、web
という項目があります。concourse
にはアプリケーション内の設定を書き、web
にはkubernetesリソース周りの設定を書くようになっています。
concourse: web: logLevel: error bindPort: 8080 # IPを紐づけたドメイン externalUrl: https://concourse.mydomain.net tsa: logLevel: info bindPort: 2222 peerIp: #ここはあとで追加する
web: service: type: NodePort ingress: enabled: true annotations: #取得した静的IPの名前 kubernetes.io/ingress.global-static-ip-name: concourse hosts: #IPを紐づけたドメイン - concourse.mydomain.net tls: #cert-managerで取得してkubedで連携した証明書 - secretName: mydomain-net-tls #IPを紐づけたドメイン hosts: - concourse.mydomain.net
3.2. デプロイして設定をアップデートする
$ helm install --name concourse --namespace concourse -f concourse.yaml ./concourse
concourseのworkerはwebのpeerIp
がないとエラーを吐き続けるのですが、これはデプロイ後でないとわかりません。
$ kubectl get svc concourse-web -n concourse
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE concourse-web NodePort XXX.XXX.XXX.XXX <none> 8080:30979/TCP,2222:31674/TCP 18s
のXXX.XXX.XXX.XXX
を、myvalues.yaml
のpeerIp
に設定して再度デプロイします
$ helm upgrade -f myvalues.yaml concourse ./concourse
ここまでで、以下のような状態まできました。あと一息です。
3.3. 動作確認
Concourse自体はすぐに立ち上がるのですが、LBが立ち上がるまで少し時間がかかります。GCPのコンソール上で状態を確認しつつ、全てのリソースが立ち上がったら、接続できるかどうか確認します。
今回、ユーザ名とパスワードは変更していないので、デフォルト設定のままです。ユーザ名:test
、パスワード:test
でログインしてみましょう。
webから
https://concourse.mydomain.net
にアクセスして、右上にあるlogin
ボタンからフォームを開きます。
flyから
Concourseでは、fly
というCLIを使って設定の操作を行います。ローカルマシンにflyをインストールして、以下のコマンドでログインします。
$ fly -t myci login -c https://concourse.mydomain.net -u test -p test
4. Cloud Armorで接続元IPを制限する
最後に、Cloud Armorを使って、接続元のIPを制限します。Cloud Armorは現在Beta版として提供されているセキュリティサービスです。LBに組み込むことで、GCP上のリソースをDDoS攻撃から守ってくれる他、ファイヤウォールの設定を書くこともできます。本来、kubernetesのnetwork policyを書くのが王道だと思いますが、今後設定を使いまわしたいというのと、あと単純に使ってみたかったという理由なので許してください。
設定は、GCPのコンソール上から行うことができます。メニューから、ネットワークセキュリティ > Cloud Armor
を選択します。
ポリシーの作成
ボタンを押すと、設定編集画面が開きます。デフォルトアクションを拒否
にして、許可ルールを追加していきます。ターゲットへのポリシーの適用
で、先ほど作られたLBを選択したら完了です。
実際に、許可されたアドレス外からアクセスしようとすると、403 Forbidden
が表示されるので試してみてください。
さて、ここまでで全ての構成が完成しました! 長かった...。お付き合いいただきありがとうございました。
まとめ
この記事では、エンタープライズの開発環境によくある状況を想定して、GKEにConcourse CIを立てる方法をご紹介しました。GCPのネットワーク設定や、cert-managerとkubedの合わせ技による証明書の管理などは、かなりややこしい部分だと思います。かなり駆け足になってしまいましたので、随時記述を補足していきたいと思います。
この記事が何かのお役に立てば幸いです。
※残課題※
以下残課題は、対応完了次第追記します。