Swiftで動的にクラス作ってメソッド呼ぶ。しかも全く型情報がない状態で。要はjavascriptみたいなことがしたい

まとめ

  • NSSClassFromString使う
  • 引数が3つ以上だとperformSelectorは使えない
  • SwiftだとNSInvocationは使えないので代わりにIMPを使う
  • Class methodとInstance methodで呼び方ちょっと違う
  • performSelectorの返り値の型はUnmanaged<AnyObject>takeUnretainedValueを使う

引数が多いときのやり方で参考にしたobjcのコード

github.com

+ (id)initGraphRequestWithGraphPath:(NSString *)graphPath{
    Class FBSDKGraphRequestClass = NSClassFromString (@"FBSDKGraphRequest");
    id graph = [FBSDKGraphRequestClass alloc];
    
    SEL aSelector = NSSelectorFromString(@"initWithGraphPath:parameters:");
    IMP imp = [graph methodForSelector:aSelector];
    id (*func)(id, SEL, id, id) = (void *)imp;
    
    graph = func(graph, aSelector, graphPath, @{});
    return graph;
}

www.b2cloud.com.au

Unmanaged<AnyObject>の取り出し方

stackoverflow.com

var unmanagedObject: Unmanaged = someFunctionThatReturnsAnUnmanagedObject() var newValue: Type = unmanagedObject.takeRetainedValue as Type

takeRetainedValueってやつを使うみたい。でも、アプリがEXC_BAD_ACCESSかなんかだったかで落ちた。 takeUnretainedValueだったら大丈夫だった。

ふーむ。allocしたのに、release漏れてるからとかなんだろうか。Swiftでは普通allocなんて書かないからあんまり普段考えないことだけど。

SwiftでIMPを呼ぶ

stackoverflow.com

unsafeBitCastを使う。

NSClassFromStringでClass methodを呼ぶ

stackoverflow.com

kubernetes meetup Tokyo #4に参加してLT発表してきた #k8sjp

k8sjp.connpass.com

ECSからGKEに乗り換えたい

感想

  • Google Japanの食堂でながーい会場で発表した。人めっちゃいた。
  • kubernetesのserviceまわり、OnlyLocalのことが少し理解できた
  • RBACっていうIAMみたいな機能の話がためになった
  • ServiceAccountもなんなのか分かった
  • ただ、歓談の時間がほとんどなくてあんまり人と話すチャンスがなかった
  • 歓談のときに、kubernetesにしようと思ってたけど当時は難しそうすぎて、docker swarmにしたけど、そろそろkubernetesにしようかなーみたいなこと話せてよかった
  • あと、openshift使ってる人の話も聞けて参考になった
  • etcdが落ちた時、復旧する時にどれを神にすればいいか辛そうという話をしました
  • meetupうちでもやりたいなーって話しかけに行ったんですが400人ぐらい毎回集まっちゃうからでかい会場じゃないと厳しいとのこと、残念><
  • オーケストレーションはkubernetesがデファクトではありますが、みたいな雰囲気をなんとなく感じました
  • AWS上にkubernetesたてるにしても、etcdとかmasterのapi-serverも冗長化するみたいな風にしてやれば意外といけそうではって雰囲気。kube-awsってやつで H/A etcdになるように更新してるみたい

GKEからCloud SQLに接続する方法

Deisのpostgresをoff-clusterにする(Cloud SQLを利用) - まーぽんって誰がつけたの?Deisのpostgresをoff-clusterにする(postgresql on GCEを利用) - まーぽんって誰がつけたの?でCloud SQLをdeisのdatabaseとしようともがいてたけど解決した話。

Cloud SQLはRDSと違う

Cloud SQLAWSでいうRDS的なやつ。ただちょっと違うのが、同じVPC的な論理的に同じネットワーク内に存在することができずに、別ネットワークになってしまう。 なので、GKE(Google Container Engine)のネットワークとCloud SQLはインターネットを経由しないとつながらないということになる。

GCPが用意してるCloud SQL接続方法

1. 接続してくるグローバルIPアドレスを許可する

GKEのCluster内に存在するNodeは単なるGCEのインスタンスなので一つ一つはグローバルIPを持っている。なので、このIPを指定してあげる。 ただ、Nodeが増えたり減ったりしたときに常にIPアドレスを管理しないといけずあまりよい方法とはいえない :no_good:

2. SSLで接続する

IPアドレスを許可しない場合、SSL証明書を使って接続する。 基本的にdriverはSSL接続は対応してるからまぁこれでもいいんだけど、たまに対応してないミドルウェアなんかがあって取り扱いがちょっと面倒。特にDeisの場合、そういうオプションは用意されてないので、PRをして変更を受け入れてもらわないといけないという状況だった :rolling_eyes:

3. CloudSQL Proxyを使う

これが一番よい方法。Googleが用意してるProxyで、binaryやDocker Imageでinstallして起動するだけで接続できるようになる。Cloud SQL Proxyを起動する際に、Cloud SQLに接続可能なAPI Key的なものと、Cloud SQLインスタンス名を渡してあげるだけで接続をproxyしてくれる。 あとは、普通にproxyが起動しているインスタンス上で、127.0.0.1に対して接続しにいけばCloud SQLにつながってくれる :100:

ただ、単純なVMインスタンスであればこれでいいが、GKE(Kubernetes)上で使う場合は、ちょっと一工夫がいる :wrench:

公式で紹介されてるGKEからCloud SQLに接続しに行く方法

Kubernetesでは、PodというのがContainerの集まりで、それらを何個起動するかとかRolling Updateリリースとかを管理しているDeploymentというリソースがある。 で、公式で紹介されてるのは、DeploymentのPodにCloudSQL ProxyをDocker Imageからcontainerを立てる方法。

https://cloud.google.com/sql/docs/mysql/connect-container-engine

こんな風に、podの中にwordpressっていうimageでcontainerを起動させてるとしたら、そこに、cloud sqlのproxyのimageを追記する。 そうすると、wordpressのcontainerからは、127.0.0.1で接続しにいける。ただ、これだと、Deis本体に手を入れる必要がある。

- image: wordpress:xxx
  name: wordpers
    ・ 略
    ・
- image: gcr.io/cloudsql-docker/gce-proxy:1.09
  name: cloudsql-proxy
  command: ["/cloud_sql_proxy", "--dir=/cloudsql",
            "-instances=[INSTANCE_CONNECTION_NAME]=tcp:[PORT]",
            "-credential_file=/secrets/cloudsql/credentials.json"]
  volumeMounts:
    - name: cloudsql-instance-credentials
      mountPath: /secrets/cloudsql
      readOnly: true
    - name: ssl-certs
      mountPath: /etc/ssl/certs
    - name: cloudsql
      mountPath: /cloudsql
volumes:
  - name: cloudsql-instance-credentials
    secret:
      secretName: cloudsql-instance-credentials
  - name: ssl-certs
    hostPath:
      path: /etc/ssl/certs
  - name: cloudsql
    emptyDir:

で、実際に、DeisのChartという設定ファイルを書き換えることで、proxy経由で無事接続することができた🎉 これはPRチャンスということで、Contribution.md読むと、PR出す前にSlackで一言相談してみてねとのことで、入ろうとするがlimitエラーかなんかでジョインできない。Deisの問い合わせから頼む入れてくれって送って入れてもらった。

これはDeisにPRチャンス!! 意気揚々とチャンネルに書き込んだ😎

f:id:masato47744:20170422024919p:plain

すると、フィーチャーフラグとかつけてくれたらいいよ!でも、なんでわざわざそんなことするの?みたいに言われて、あっさり別の解決策を教えてもらった。わざわざ同じpod内に作る必要なんてないとのこと。

f:id:masato47744:20170422024931p:plain

Cloud SQL Proxyを単に一つのDeploymentとServiceとして起動する

Kubernetes上で、あるPodから別のDeploymentのPodには接続しにいけないのかなーって思ってたのが間違いだった。実は、Kubernetes上では、Pod間の通信はServiceを利用すれば簡単にできる。しかもNameSpaceをまたがっていても!

KubernetesがCluster内のDNSもいい感じに設定していてくれて、Service名.NameSpace名というホスト名でつながることができる!!

最終的な構成はこういう感じ。

NameSpace deis
                       <host: pg-sqlproxy.sql-proxy>
 | deis-controller |-----------|
                               |
-------------------------------|---
NameSpace sql-proxy            |
                               ▼
                         | Service: pg-sqlproxy |
                               |
                               ▼
                         | Pod: gce-proxy |
                               |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~
Cloud SQL                      |
                               ▼
                         | PostgreSQL |

この概念を利用すれば、Deisの1アプリとして、Cloud SQL Proxyをdeployしたっていい。Communityではそういうやり方も教えてもらった。

f:id:masato47744:20170422025008p:plain

まとめ

Deisのことを調べていくとKubernetesのことを知っていかないといけないんだけど、知れば知るほど奥が深いというか至れり尽せりな機能が用意されててほんとすごい🚢

Proxy Protocolを知りました

Proxy ProtocolはL4におけるx-forwarded-forみたいなもの

2010年ごろからあるみたいだったけどお恥ずかしながら知らずに生きてきた。 HTTPヘッダが覗けるロードバランサー(L7ロードバランサー)ならx-forwarded-forとかx-real-ipとかにIPを追記していくことでclientのIPアドレスを保持することができる。 HTTP通信以外のL4ロードバランサーの場合はそれができないので、Proxy Protocolというプロトコルを使ってclient ipを保持するという仕組み。

もとは、ロードバランサーのソフトウェアであるHAProxyが策定したものだが、それをAWSなど有名どころが採用したため標準的なものとして採用されている。

AWSのELBはどうなってたか

AWSの場合は、ELBでHTTPリスナーにすれば自動的にL7ロードバランサーになっているので気づいていないこともあると思う。もちろん、ALBはHTTPとHTTPSのみしか対応しておらずリクエストのパスで振り分けもできるのでL7ロードバランサーである。

HTTP以外、例えばMySQLとかSMTPとかを負荷分散する場合には、L4ロードバランサーになるが、その場合も、コマンドラインからProxy Protocolを有効にすることができるようです。 http://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/enable-proxy-protocol.html

f:id:masato47744:20170422024616p:plain

GCPではSSLプロキシ負荷分散の場合しか使えなかった

とにかく、Client IPが欲しくて欲しくてたまらなかったので、deis/routerにuse proxy protocolというのがあってなんかよく分からんけどこれをtrueにすればいけるかと思ったけど、ログにエラーが吐かれるのでproxy protocolとはなんぞやを調べ始めはじめたのがきっかけ。

https://cloud.google.com/compute/docs/load-balancing/tcp-ssl/#update_proxy_protocol_header_for_the_proxy

f:id:masato47744:20170422024628p:plain

Deis/routerでwhitelistでIP制限する方法

Security Considerations - Deis Workflow Documentationで公式にIP制限する方法が紹介されてるが、403が出る。

なんで403になっちゃうのかをここを調べてみた。

公式通りにやっても403

公式ページにもあっさりこうすればできると書いてあるが、確かに403になってしまう。

$ kubectl --namespace=deis annotate deployments/deis-router router.deis.io/nginx.enforceWhitelists=true
$ kubectl --namespace=deis annotate deployments/deis-router router.deis.io/nginx.defaultWhitelist="0.0.0.0/0"

deis/routerが生成するnginx.conf

deis/routerくんはただのnginxなので、生成されたnginx.confを見てみる。

・・中略
    server {
        listen 8080;
        server_name deis.35.189.144.204.nip.io;
        server_name_in_redirect off;
        port_in_redirect off;
        set $app_name "deis/deis-controller";




        allow 1.2.3.4;

        deny all;


        vhost_traffic_status_filter_by_set_key deis/deis-controller application::*;

        location / {



            proxy_buffering off;
            proxy_buffer_size 4k;
            proxy_buffers 8 4k;
            proxy_busy_buffers_size 8k;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_set_header X-Forwarded-Proto $access_scheme;
            proxy_set_header X-Forwarded-Port $forwarded_port;
            proxy_redirect off;
            proxy_connect_timeout 10;
            proxy_send_timeout 1200;
            proxy_read_timeout 1200;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;






            proxy_pass http://10.3.243.160:80;
        }

    }
・・・略

単に、virtual hostでallow 1.2.3.4;してるだけ。 アクセスログを見てみると、clientのIPがこちらのIPになってない。ここが問題のようだ。

2017/04/03 10:40:27 [error] 81#0: *739 access forbidden by rule, client: 10.146.0.2, server: deis.35.189.144.204.nip.io, request: "GET /v2/ HTTP/1.1", host: "deis.35.189.144.204.nip.io"
[2017-04-03T10:40:27+00:00] - deis/deis-controller - 10.146.0.2 - - - 403 - "GET /v2/ HTTP/1.1" - 406 - "-" - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" - "deis.35.189.144.204.nip.io" - - - deis.35.189.144.204.nip.io - - - 0.000

Client IPを保存するには??

kubenetes v1.5から、beta機能として、loadbalancerがclient IPをpreserveするような機能がついたので、その設定をいけば、client ipがrealなものになる。 やり方は、

$ kd annotate services/deis-router service.beta.kubernetes.io/external-traffic="OnlyLocal"

403になる原因としては、client ipがLoadbalancerのものか、kubernetesの何かのものになってしまったため。Deisの問題でもなく、GCPの問題でもなく、kubernetesの問題だった。以前からissueにあがっており、1.5からGCPとAzureのみ対応できた模様。AWSのELBに関しては、proxy protocolが使えるのであまり問題になっていなかった模様。

LoadbalancerにGCPのfirewall指定すればいいのでは?

この方法もkubernetesのannotateをすることで実施することができた。

単にfirewall設定をするだけでいけた。 https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/ をみると、loadBalancerSourceRanges で設定できるとある。

When using a Service with spec.type: LoadBalancer, you can specify the IP ranges that are allowed to access the load balancer by using spec.loadBalancerSourceRanges.

annotateで指定してあげると、即座にfirewallにsource rangeが反映される。

$ kd annotate services/deis-router service.beta.kubernetes.io/load-balancer-source-ranges="5.6.7.8/32"

これを打つと、annotationsにこれが追加される。

f:id:masato47744:20170422024306p:plain

そうすると、もともとあったfirewall ruleが更新される。

試しに、この状態で、service.beta.kubernetes.io/external-traffic=“OnlyLocal"を削除したら、接続できなくなった。 これがないとどちらにしろ、LBでfirewallを指定しても、deis-routerのnginxでIP制限したとしてもIP制限は働かないということになる。

まとめ

  • GKE上のDeisでIP制限するにはGCPのfirewallを使うか、deis/routerのnginxでIP制限するかの2通りのやり方がある
  • service.beta.kubernetes.io/external-traffic="OnlyLocalこれがないとClientIPが保存されない

deis-routerなしでKubernetesのIngressでDeisを動かす

Clusterを作る時にデフォルトでついてくるLBを外す

gloucd container clusters createをするときに、--disable-addons "HttpLoadBalancing"をつける。これをしないとGLBCがIngress Controllerとなってしまうため。コマンドラインからしか外せない。

cluster作成後どうなってるか

このときはkube-systemのnamesapce配下にkubernetesが動くために必要なものが起動されてる。addonを外したので、l7-default-backendなどは存在しない。

フラグ付きでDeisをインストール

$ helm install deis/workflow --namespace deis \
 --set global.experimental_native_ingress=true \
 --set controller.platform_domain=example.com

global.experimental_native_ingress=trueがポイント。

deis/routerの代わりにingressが作成されている。

$ kd describe ing
Name:           controller-api-server-ingress-http
Namespace:      deis
Address:
Default backend:    default-http-backend:80 (<none>)
Rules:
  Host              Path    Backends
  ----              ----    --------
  deis.example.com
                    /   deis-controller:80 (<none>)
Annotations:
Events: <none>

この段階でGCP側にこの辺が追加になる。

Ingress Controllerをインストール

今は、addonで外しているのでIngress Controllerがkubernetes上にいない状態。helmコマンドでIngress controllerをインストールする。今回は、treafikというLBのソフトウェアを利用する。

$ helm install stable/traefik --name deis-ingress-001 --namespace kube-system

これをすると

  • GCP上に80,443のHTTPを通すFirewallが作られ
  • TCP:80,443のL4ロードバランサーが作られ
  • treafikのimageがdeployされてそのL4 LBと結びつく

あとは、このとき作成されたLBのExternal IPとdeis.example.comをひけるAレコードを作成する。

Deisでアプリケーションをdeploy

通常通りregisterして、アプリケーションをdeployしたりすると以下のような動きをしてルーティングが行われる。

  • アプリケーションのnamespaceが追加
  • 該当のnamespaceにアプリケーションのdeploymentが追加
  • 該当のnamespaceにClusterIP型のserviceが追加
  • 該当のnamespaceにIngressも追加されて、そのClusterIP型のserviceを向く
     internet
        |
     [ LB ]
        |
   [ Traefik ]
   ____|____________________
  |                        |
[ deis/controller   ]     [ App ]

cluster作成時に作られるaddonのGLBCでやってみたが

これがうまくいかなかった。 まず、さっきと同じようにhelm installしたあとのFirewallとIngressの状態がちょっと違う。 まず、FirewallはGCP内のロードバランサーたちが所属するCIDRから、Nodeへの特定TCPポートを開くルールが作られる。これはhealthcheck用のようだ。

で、Ingressはdeis-controllerサービスが見つかりませんよーというエラーが出てくる。

$ kd describe ing
Name:           controller-api-server-ingress-http
Namespace:      deis
Address:        130.211.37.103
Default backend:    default-http-backend:80 (10.0.2.3:8080)
Rules:
  Host              Path    Backends
  ----              ----    --------
  deis.example.com
                    /   deis-controller:80 (<none>)
Annotations:
  forwarding-rule:  k8s-fw-deis-controller-api-server-ingress-http--90b3b4269724c70
  target-proxy:     k8s-tp-deis-controller-api-server-ingress-http--90b3b4269724c70
  url-map:      k8s-um-deis-controller-api-server-ingress-http--90b3b4269724c70
  backends:     {"k8s-be-31752--90b3b4269724c7ab":"Unknown"}
Events:
  FirstSeen LastSeen    Count   From            SubObjectPath   Type        Reason  Message
  --------- --------    -----   ----            -------------   --------    ------  -------
  3m        3m      1   loadbalancer-controller         Normal      ADD deis/controller-api-server-ingress-http
  2m        2m      1   loadbalancer-controller         Normal      CREATE  ip: 130.211.37.103
  2m        2m      3   loadbalancer-controller         Warning     Service Could not find nodeport for backend {ServiceName:deis-controller ServicePort:{Type:0 IntVal:80 StrVal:}}: Could not find matching nodeport from service.
  2m        2m      3   loadbalancer-controller         Normal      Service no user specified default backend, using system default

deis-controllerはtype:ClusterIPというクラスター内部IPのみで接続できるサービスとして公開されるようになってるが、このClusterIPとIngressとGLBCだと相性が悪く接続に失敗するようだった。ドキュメントにちゃんと書いとけよ的なissueもあがってた。

なので、現状は、GLBCをIngress ControllerとしたGKE上ではNative Ingressの利用は難しそうだった。。

まとめ

やってみるとまだ対応されてない(grafanaとかのルーティングがない)ところがあって、フラグについてるExperimentalは伊達じゃないと感じた。なので、やっぱりdeis/routerを使う方法にすることにした。

GKEだとデフォルトでIngress Controller(=GLBC)がいる

おまえ、そこにいたのか

kubernetesでIngressでやる場合、Ingress Controllerが必要なんだけど、GKEの場合はマスターノード、つまりGCPのマネージドな領域でクラスター作成時に一緒に作られるようになってた。

で、これをGLBC(GCE Load-Balancer Controller) Cluster Addonという。

Ingress Controllerを作るときはgcenginxのclassが標準で用意されてたけど、このgceっていうやつはGLBCのことで、ただ、GLBCGKEだと最初からaddonされるから作らなくてよいということだった。

自分で作りたい場合は、cluster作成時に、コマンドとかAPI経由でのみ、外せる。GUIコンソールからだと無理だった。

GLBCとは

なんとなく、GCPにおいてあるロードバランサーのことだろって思ってたけど違う

これはkubernetes側が勝手に名前をつけたやつで、GCPのリソースを組み合わせてL7のロードバランサー相当のものを作る命令のあつまりみたいなもの。まさにcontroller。あと、ルーティングしたときにどこにもあてはまらないリクエストがきたときにデフォルトの404ページを出してくれるだけのバックエンドのセット。

Ingressを作ると、GLBCがたたかれて、GLBCがGCPAPIをたたいて、L7のロードバランサーを実現するために、GCPでL7ロードバランサーをするための設定をしてくれる。

f:id:masato47744:20170422023108p:plain

で、そのGLBCはどこにいるかっていうとGKEの場合は、cluster作成時にマスターノードに作られていたって話でした。kube-systemっていうnamespaceが基本的にcluster作成時にできるものなんだけど、ここにGLBC相当のものがいないのではまった。ただ、さっき言った404出してくれるl7-default-backendはNodeの方にいるのでこの管理画面に表示されてるのが分かる。

f:id:masato47744:20170422023135p:plain