複数のファイルをまとめてkubectl apply する
kubectl にはその機能がなかったのでfindしてxargsする
find . -name "*.yaml"|xargs -I {} kubectl apply -f {}
kubectl execの仕組みを追える範囲で追ってみた
kubernetesを利用しているとき、 kubectl exec -it "pod_name" /bin/bash で目的のコンテナにbashログインすることができますが、これがなぜ動いているのか理解できていなったので調べてみました。
kubectl execとsshと何が違うのか?(sshと同じセキュリティ対策が必要ではないのか??)と聞かれたときに違うとは思うんだけどどう違うのかうまく答えられなくて気になってしまったのがきっかけです。
概要
調べてみたところ既に質問していた人がいました。
このフォーラムによると、kubectl exec は以下のように流れていくようです。
- kube-apiserver(以下apiserver)にPOSTリクエストを投げる( /v1/namespaces/{ns}/pods/{pod}/exec )
- このリクエストには SPDYへのアップグレードが含まれている(SPDYによる多重化通信が必要なため)
- apiserverはPodに接続するため、kubeletとの通信を確立させる
- kubeletは有効期限の短いtokenを発行し、CRIにリダイレクトさせる
- CRIハンドラーがリクエストを実行し、docker exec APIコールを行う
kubectl execコマンドは最終的にはPodが存在するNodeでdocker exec コマンドが実行される、と覚えておけばよさそうです。(CRIインタフェースに対応していればコンテナランタイムは問わないので、docker execとは限りませんが)
ソースコードを追ってみる
Google Forumに書かれていることが本当なのかわからなかったので、念のためソースコードを追ってみます。最近仕事でGo使いだしたのできっと読めるはず・・
kubectl
POSTして
kubernetes/exec.go at master · kubernetes/kubernetes · GitHub
SPDY通信してそう。
kubernetes/exec.go at master · kubernetes/kubernetes · GitHub
/v1/namespaces/{ns}/pods/{pod}/execでPOSTしている箇所は見つけることができました
api-server
/v1/namespaces/{ns}/pods/{pod}/execで実行されるコードを見つけることはできませんでした。。
しかし、おそらくapiserverとkubeletが通信するendpointを作成しているコードを見つけることができました。
kubernetes/strategy.go at 152b09ac550d50deeeff7162093332b4f7f0397d · kubernetes/kubernetes · GitHub
loc := &url.URL{
Scheme: nodeInfo.Scheme,
Host: net.JoinHostPort(nodeInfo.Hostname, nodeInfo.Port),
Path: fmt.Sprintf("/%s/%s/%s/%s", path, pod.Namespace, pod.Name, container),
RawQuery: params.Encode(),
}
/exec/{namespace}/{name}/{container} というpathでNodeと通信するURLを作成していそうです。Nodeに存在しているのはkubeletのはずなので、次はkubeletで /exec/{namespace}/{name}/{container} というエンドポイントで待ち受けているコードを探してみます。
kubelet
https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/server/server.go#L352
// getExec handles requests to run a command inside a container. なんかそれっぽい!
https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/server/server.go#L707
func (s *Server) getExec(request *restful.Request, response *restful.Response) {
~~~
url, err := s.host.GetExec(podFullName, params.podUID, params.containerName, params.cmd, *streamOpts)
~~~
}
hostのgetExecを読んでいます。Hostに何が入るのかわからないですが、getExecを実装している箇所をさらに検索します。
hostは省略しますが、
func (kl *Kubelet) GetExec(podFullName string, podUID types.UID, containerName string, cmd []string, streamOpts remotecommandserver.Options) (*url.URL, error) { container, err := kl.findContainer(podFullName, podUID, containerName) if err != nil { return nil, err } if container == nil { return nil, fmt.Errorf("container not found (%q)", containerName) } return kl.streamingRuntime.GetExec(container.ID, cmd, streamOpts.Stdin, streamOpts.Stdout, streamOpts.Stderr, streamOpts.TTY) }
Kubelet.streamingRuntimeのGetExecが呼ばれています。
streamingRuntimeの定義はこれです
runtime, err := kuberuntime.NewKubeGenericRuntimeManager( ~~~ ) if err != nil { return nil, err } klet.containerRuntime = runtime klet.streamingRuntime = runtime
streamingRuntime.GetExecでは下記のGetExecが実行されることになります
func (m *kubeGenericRuntimeManager) GetExec(id kubecontainer.ContainerID, cmd []string, stdin, stdout, stderr, tty bool) (*url.URL, error) { ~~ resp, err := m.runtimeService.Exec(req) ~~ }
ここでは、runtimeServiceのExec(req) を実行しています。runtimeServiceが何なのか調べます。
// gRPC service clients runtimeService internalapi.RuntimeService
runtimeService internalapi.RuntimeService とあるので、 internalapi を見てみると
internalapi "k8s.io/kubernetes/pkg/kubelet/apis/cri
ついにCRIまでたどり着きました。
switch containerRuntime { case kubetypes.DockerContainerRuntime: // Create and start the CRI shim running as a grpc server. streamingConfig := getStreamingConfig(kubeCfg, kubeDeps, crOptions) ds, err := dockershim.NewDockerService(kubeDeps.DockerClientConfig, crOptions.PodSandboxImage, streamingConfig, &pluginSettings, runtimeCgroups, kubeCfg.CgroupDriver, crOptions.DockershimRootDirectory, !crOptions.RedirectContainerStreaming) if err != nil { return nil, err } if crOptions.RedirectContainerStreaming { klet.criHandler = ds } // The unix socket for kubelet <-> dockershim communication. klog.V(5).Infof("RemoteRuntimeEndpoint: %q, RemoteImageEndpoint: %q", remoteRuntimeEndpoint, remoteImageEndpoint) klog.V(2).Infof("Starting the GRPC server for the docker CRI shim.") server := dockerremote.NewDockerServer(remoteRuntimeEndpoint, ds) if err := server.Start(); err != nil { return nil, err } ~~~~
criHandlerにDockerServiceが設定されています。
DockerServiceの定義はこれです。 ここで詰まってしまったのですが、kubeletがexec用にtokenを発行しているという手掛かりを元にコードを検索してみます。
endpoints := []struct { path string handler restful.RouteFunction }{ {"/exec/{token}", s.serveExec}, {"/attach/{token}", s.serveAttach}, {"/portforward/{token}", s.servePortForward}, }
見つかりました。
exec/{token} というURLでserveExec を実行しています。どこかでこの streaming packageのNewServer(config, runtime)が実行されていると思うのですが見つかりませんでした。 GetExecが実行されてからどこかでこのserveExecにたどり着くんだと思います。
remotecommandserver.ServeExec( resp.ResponseWriter, req.Request, s.runtime, "", // unused: podName "", // unusued: podUID exec.ContainerId, exec.Cmd, streamOpts, s.config.StreamIdleTimeout, s.config.StreamCreationTimeout, s.config.SupportedRemoteCommandProtocols)
serveExecはremotecommandserverのServeExecを呼び出します。
importに
remotecommandserver "k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
が含まれているので remotecommandを見に行きます。
func ServeExec(w http.ResponseWriter, req *http.Request, executor Executor, podName string, uid types.UID, container string, cmd []string, streamOpts *Options, idleTimeout, streamCreationTimeout time.Duration, supportedProtocols []string) { ~~ err := executor.ExecInContainer(podName, uid, container, cmd, ctx.stdinStream, ctx.stdoutStream, ctx.stderrStream, ctx.tty, ctx.resizeChan, 0) ~~ }
executorのExecInContainerが実行されています。
func (a *criAdapter) ExecInContainer(podName string, podUID types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error { return a.Runtime.Exec(container, cmd, in, out, err, tty, resize) }
ExecInContainerはRuntimeのExec(container, cmd, in, out, err, tty, resize)を実行しています。 dockershimにExecがあればそれが処理の実態になっていそうです。
dockershim
func (r *streamingRuntime) Exec(containerID string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error { return r.exec(containerID, cmd, in, out, err, tty, resize, 0) } // Internal version of Exec adds a timeout. func (r *streamingRuntime) exec(containerID string, cmd []string, in io.Reader, out, errw io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error { container, err := checkContainerStatus(r.client, containerID) if err != nil { return err } return r.execHandler.ExecInContainer(r.client, container, cmd, in, out, errw, tty, resize, timeout) }
Execから execHandlerのExecInContainerを呼んでいます。
func (*NativeExecHandler) ExecInContainer(client libdocker.Interface, container *dockertypes.ContainerJSON, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error { done := make(chan struct{}) defer close(done) createOpts := dockertypes.ExecConfig{ Cmd: cmd, AttachStdin: stdin != nil, AttachStdout: stdout != nil, AttachStderr: stderr != nil, Tty: tty, } execObj, err := client.CreateExec(container.ID, createOpts)
exec handler のExecInContainerで docker execしてるコードにたどり着きました。長かった。。ほとんど自己満足ですが、前述のフォーラムの情報は間違ってなさそうだと思えるところまで調べることが出来ました。
CreateExecの中身も気になりましたがさすがに気力なくなりました。。 getExecからServceExecのところは完全に流れを見失ってしまったのでいずれ補完したいです。
Goは書いてると大量のコードを書くことになってつらいですが、読むのは楽ですね。途中詰まってしまいましたが、API通信を挟むところでコードだけでは追いきれなくなってしまうのは言語問わずそうなのでGoの問題ではないと思います。
参考
api-server, kubeletについてはこちら
Kubernetes: 構成コンポーネント一覧 - Qiita
CRIってなんだっけ?というの調べるのにはここを参照しました
GKEでIstioのmetricsをStackdriverに連携する方法
すごくわかりにくいのですが、mixier配下のoperatorconfigディレクトリにあるyamlを適用すればとりあえず連携できました。
istio/stackdriver.yaml at master · istio/istio · GitHub
現在のreleaseバージョン(1.0.5)ではエラーが出るので注意が必要です。 このPRで修正されていました。 github.com
ちなみに、Istio on GKE(https://cloud.google.com/istio/docs/istio-on-gke/overview) を利用する場合は、何の設定もいらないようです。 いまのところIstio on GKEの制約が強すぎてやりたいことができないので、このままOSS版を利用する予定です。
KubernetesのCronjobを手動実行する方法
出来ないことはないだろうと思って調べてみたら方法はあった。 環境変数とか一部設定を変えてすぐ動作確認したいときに便利です。
kubectl create job <job-name> --from=cronjob/<cronjob-name>
関連Issueはこれ。
GKE環境でPCI-DSS準拠のシステムを構築した事例
GCP, GKEを使ってPCI-DSS準拠のシステムを作った事例。この動画を見て気になったところをまとめてみました。仮想通貨販売所の話は興味なかったので省略 www.youtube.com
動画メモ
PCI-DSS準拠の方針
課題
リソース不足
解決策
- PCI-DSSの対象とするスコープを減らす。個人情報、カード情報。
- GCP Project、VPCを別にする
- ログイン可能なユーザーを限定する
- 専用のG SuiteドメインからOneLoginを利用してログイン
- One Loginはパスワード要件を満たすため
アーキテクチャの全体像
クレジットカード情報、個人情報はGCPプロジェクトのみで扱うようになっている。他の情報は全て別システム。
HTTPSアクセスはFastly経由。Triple DES, TLS1.1を無効化したかったため。
FastlyのLogging endpointsを BigQuery, TCP to Kuberntes Clusterに指定している。
Fastly- Fluentd Cluster
Deployment Containers
fastlyのログを整形してStackDriverに送信している
CronJobs:
BigQueryに格納されたFastlyのログからアクセス数が異常なIPをブロックする(またはその逆)のgolangプロセス
Payment Proxy Cluster
Deployment Containers
payment-proxy: 本体。 golang
digest: Argon2 hash計算用サービス in ruby Sinatra
cloudsql-proxy: そのまま
nginx: ReverseProxy
ossec: 侵入検知プロセス(IDS) Nginxのログ監視・結果を出力
Virus Scan Cluster
Deployment Containers clamav: 他clusterにいる各containerは立ち上げ時にclamscan
Vulnerability Scan Cronjob
苦労した点良かった点
- 事例が見つからない。オンプレ前提の項目が多くどう満たせばよいかわからなかった。→頑張って事例作った
- ファイル変更検知をbastionからdocker diffすることで行える
- 本番のStackdriver Debugger使える
- SSHも自動的に二段階認証になる
- スケーラビリティもついてくる
おまけ
外部のコンサルタントを交えて行われるPCI-DSS対応のための審査をタスクとルールブックはGithub Issueで管理
感想
PCI-DSS監査対象になるシステムを最小にして、専用のGCPプロジェクト・ネットワーク、GSuiteアカウントで運用しているということでした。これ自体はまぁそうなるよなーという感じ。
cloudsql-proxy使っているということは、CloudSQLのエンドポイントはグローバルに開いていてサービスアカウントでの認証になっているはずで監査で何か言われないのかちょっと気になった。去年の発表だし、いまはプライベートIPモード使っているのかな。
ウイルススキャン、侵入検知、ファイル変更検知はオンプレやIaaSベースではよく聞くけど、コンテナベースのアーキテクチャでどうやるのかあまりイメージついてなかったので勉強になりました。特にファイル変更検知をdocker diffで簡単にできるのは気づかなかった。
監査をGithub Issueで管理するように進めてくれるのは、担当者レベルでがんばってもなかなか難しそう。会社からのバックアップがあったのだと思うので、よい文化だなーと思いました。
今までこういうメモは社内wikiに雑にまとめていたのですが、転職回数増えてくると社内wikiに書いてもいつか見れなくなってしまうしなー、と思うようになってきたので、自分のブログに載せていくことにしました。
StackDriver Monitoringにカスタムメトリクスを送信する
ここに書いてあることが全てなのですが、僕の理解力が低いのか送信するまでに色々手間取ったので備忘のため残しておきます。 cloud.google.com
Rubyのサンプルコードで説明していきます。 Ruby用のSDKをインストールして、スクリプトではSDKをrequireしてあるのが前提です。
gem install google-cloud-monitoring
require "google/cloud/monitoring"
メトリクスの作成
カスタムメトリクスを送信するためには、まずメトリクスの定義をStackDriverに送信する必要があります。 事前定義なしでいきなり送りつけることも可能ですが、その場合はStackDriver側で色々なデフォルト値が利用されます。メトリクスの定義は後から変更できないものが多いようなので、基本的には事前に定義しておいた方がよさそうです。
metric_client = Google::Cloud::Monitoring::Metric.new formatted_name = Google::Cloud::Monitoring::V3::MetricServiceClient.project_path([project_id]) label1 = Google::Api::LabelDescriptor.new( key: "label_test", description: "カスタムメトリクスへのラベル付与テスト", value_type: Google::Api::LabelDescriptor::ValueType::STRING ) md = Google::Api::MetricDescriptor.new( name: "my_first_custom_metric", type: "custom.googleapis.com/custom_metrics_test/test_metric", description: "カスタムメトリクスの説明テスト", display_name: "はじめてのカスタムメトリクス", metric_kind: Google::Api::MetricDescriptor::MetricKind::GAUGE, value_type: Google::Api::MetricDescriptor::ValueType::INT64, unit: "", labels: [label1] ) metric_client.create_metric_descriptor(formatted_name, md)
type(custom.googleapis.com/custom_metrics_test/test_metric)がこのメトリクスを一意に特定するkeyになります。 metrics_kind, value_typeでメトリクスの種別を定義します。
labelはメトリクスを送信するときに付与することができるラベルの定義です。この場合は"label_test"というkeyに任意の文字列を付与することができます。 MetricDescriptorインスタンスを作ってそれを送信すればokです。
上記をスクリプトを実行すると、StackDriver上にカスタムメトリクスが作成され、Metric Explorerで探して表示することができます。
次は、メトリクスの定義ができたので、そこに書き込んでみます。
metric = Google::Api::Metric.new( type: "custom.googleapis.com/custom_metrics_test/test_metric", labels: {"label_test" => "ラベル付与テスト!!!!"} ) resource = Google::Api::MonitoredResource.new( type: "global", labels: {"project_id" => @project_id} ) point = Google::Monitoring::V3::Point.new( interval: Google::Monitoring::V3::TimeInterval.new( end_time: Google::Gax.time_to_timestamp(Time.now) ), value: Google::Monitoring::V3::TypedValue.new(int64_value: 123456) ) ts = Google::Monitoring::V3::TimeSeries.new( metric: metric, resource: resource, points: [point] ) time_series = [ts] metric_client.create_time_series(formatted_name, time_series)
メトリクスの送信は、TimeSeriesの配列を送信します。TimeSeriesにはMetric, Resource, Pointsという属性があります。
Metricは、送信するメトリクスの定義を指定します。この例では事前に定義した"custom.googleapis.com/custom_metrics_test/test_metric"を指定しています。labelを付与することができます。
Resourceは、監視対象のリソースを指定します。StackDriverで事前に規定されており、global以外にもgce_instance, gee_podなどがあります。このメトリクスを収集したリソースを指定するのがよいと思います。
Pointは、intervalとvalueという属性を含んでいます。valueが記録されるメトリクスの値です。(ここまで長かったですね。。)intervalにはstart_time,end_timeという属性があり、記録するメトリクスのtimestampを指定することができます。メトリクスのtypeでDELTA(差分)を選んだ場合はstart_time, end_time両方の指定が必要になりますが、今回の場合は、end_timeだけでよいです。利用する言語にもよるのですが、Google.ProtoBufで定義されているTimestamp型にする必要があるので注意が必要です。
ここまで成功すれば、StackDriverのコンソールにカスタムメトリクスが表示されることが確認できます。
カスタムメトリクスは有料リソースなのに注意ですが、うまく利用すれば柔軟な監視を構築することができそうです。
Volumeを使わずにDockerコンテナに安全にCredentialを渡す
コンテナ化されたアプリケーションからGCP,AWSなどクラウド上のリソースにアクセスする場合、Credentialを適切な方法でコンテナ内に渡す必要があります。
ほとんどのケースではVolumeMountで大丈夫なのですが、Volumeを使わずに実施する必要があって悩んだのでメモしておきます。
前提
コンテナにCredentialを渡す場合、Dockerfileに記述してしまうのが一番早いのですがそれは好ましくありません。 コンテナ実行時にCredentialを引き渡すのが推奨されている方法のようです。 これはDockerfileに記述してしまうと、Credentialが変わるたびにビルドが必要になってしまうためです。
方法
コンテナ起動時にCredentailを渡す必要があるので、entrypointに処理を挟みます。 Credentialは環境変数にセットすることにしました。
Dockerfile内で
COPY docker-entrypoint.sh /usr/local/bin RUN chmod +x /usr/local/bin/docker-entrypoint.sh ENTRYPOINT ["docker-entrypoint.sh"]
と記述して任意のスクリプトを実行できるようにしておきます。 docker-entrypoint.sh はこれです。
#!/bin/sh set -e if [ -n "$CREDENTIAL" ]; then mkdir /.secrets printf '%s' "$CREDENTIAL" > /. secrets/crendential.json fi exec "$@"
アプリケーションは、CMDに実行するために必要なコマンドを記載し、アプリケーション内で /. secrets/crendential.json を読み込めばokになります。
docker run する場合はこうなります。
CREDENTIAL = `cat path/to/credential.json` export $CREDENTIAL docker run -e CREDENTIAL=$CREDENTIAL image
docker-composeの場合はこんな感じ
- environment: - "CREDENTIAL=${CREDENTIAL}"