ラズパイ4でKubernetesクラスタを再構築したときに様々詰まった話
いつもGKEでやっていたのだけど、おうちで好き勝手に動かせるクラスタが欲しいと思い、構築
元々勤務している学校のサーバー室に、こっそりk8sクラスタとして仕掛けていたのだが、回収して自宅で再度構築し直した
その時に、様々つまづいたので、備忘録として
OSのインストール
OSはUbuntu 22.04 LTSを使用
Raspbery Pi Imagerを使用したところ、このようなメッセージが
「検証に失敗しているだけで、動かせはするやろ」と思ったのだが、動かず
ラズパイのACTランプが4回点滅を繰り返しているので、調べてみたところ
緑色のACTライトが4回点滅する場合は、コード「start.elf」が見つからなかったことを示しています。Start.elfは、ブートフォルダの先頭にある関数です。この機能が見つからない場合、RaspberryPiは起動しません。
どうやら本当に書き込めていないらしい。
別のmicro SDカードを刺してみたところ、問題なく動いたので、純粋にmicro SDが壊れていたみたい。
試しに使ったmicro SDがシリコンパワー製のもので、読み書き速度に難があったことから、あきばお〜でサンディスクの64GB micro SDカードを新規調達した。
gpgキー?
Dockerのインストールで、
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
をやったところ、
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).
と出てきた。
どうやら、最近のaptでは、apt-key
が非推奨らしい。
ただ、一応動いているといえば動いているので、一旦無視して、気が向いたら対応する。
kubeletが動いてくれない
クラスタ構築後、Worker nodeを kubeadm join
で追加したはいいのだが、nodeがいつまでも Ready
にならず、 NotReady
のままになってしまう。
clusteradmin@master:~$ kubectl get nodes The connection to the server 10.0.100.1:6443 was refused - did you specify the right host or port?
それどころか、kubectl
が時たま Connection Refused
で使えなくなる始末。
なんでやと思ったのだけど、似たような感じの事例で困っている人の記事を発見。
最近のUbuntuでは、cgroupsがv1からv2になった関係で、containerd周りでコケているらしい?
とりあえず、記事に書いてある通り、cgroupsをv1に戻す対応をした。
Raspberry Pi + Ubuntuな環境でcrgoupsのバージョンを指定する際は、/boot/firmware/cmdline.txt
にsystemd.unified_cgroup_hierarchy=0
を追記して、再起動
参考: raspberry pi os - Enable cgroups v1 in RaspberryPi OS - Raspberry Pi Stack Exchange
公式Docsを見てみると、/etc/containerd/config.toml
のSystemdCgroup
という項目をTrueにするとのこと。
確かに、現状の設定ファイルはこのようになっている
clusteradmin@master:~$ cat /etc/containerd/config.toml | grep Systemd SystemdCgroup = false
これ直すと、普通に動いたのかしら
追記:動いた
flannelのPodが立ち上がらない
コンテナ間通信のプラグインとして、flannelを使っているのだが、flannelのpodがいつまでもcrashしてしまう
clusteradmin@master:~$ kubectl get pods --all-namespaces NAMESPACE NAME READY STATUS RESTARTS AGE kube-flannel kube-flannel-ds-bzqn6 0/1 Error 2 (20s ago) 27s kube-system coredns-787d4945fb-97f5n 0/1 ContainerCreating 0 10m kube-system coredns-787d4945fb-qbdc7 0/1 ContainerCreating 0 10m kube-system etcd-master 1/1 Running 27 (2m53s ago) 11m kube-system kube-apiserver-master 1/1 Running 8 (2m15s ago) 10m kube-system kube-controller-manager-master 1/1 Running 10 (3m11s ago) 11m kube-system kube-proxy-d6jld 1/1 Running 5 (3m37s ago) 10m kube-system kube-scheduler-master 1/1 Running 25 (3m4s ago) 9m38s
logにも、それっぽいエラーが出力されていた
clusteradmin@master:~$ kubectl logs kube-flannel-ds-bzqn6 --namespace=kube-flannel Defaulted container "kube-flannel" out of: kube-flannel, install-cni-plugin (init), install-cni (init) I0102 14:14:59.696494 1 main.go:204] CLI flags config: {etcdEndpoints:http://127.0.0.1:4001,http://127.0.0.1:2379 etcdPrefix:/coreos.com/network etcdKeyfile: etcdCertfile: etcdCAFile: etcdUsername: etcdPassword: version:false kubeSubnetMgr:true kubeApiUrl: kubeAnnotationPrefix:flannel.alpha.coreos.com kubeConfigFile: iface:[] ifaceRegex:[] ipMasq:true ifaceCanReach: subnetFile:/run/flannel/subnet.env publicIP: publicIPv6: subnetLeaseRenewMargin:60 healthzIP:0.0.0.0 healthzPort:0 iptablesResyncSeconds:5 iptablesForwardRules:true netConfPath:/etc/kube-flannel/net-conf.json setNodeNetworkUnavailable:true} W0102 14:14:59.696794 1 client_config.go:617] Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work. I0102 14:14:59.733776 1 kube.go:126] Waiting 10m0s for node controller to sync I0102 14:14:59.733890 1 kube.go:431] Starting kube subnet manager I0102 14:15:00.734163 1 kube.go:133] Node controller sync successful I0102 14:15:00.734886 1 main.go:224] Created subnet manager: Kubernetes Subnet Manager - master I0102 14:15:00.735003 1 main.go:227] Installing signal handlers I0102 14:15:00.736299 1 main.go:467] Found network config - Backend type: vxlan I0102 14:15:00.736395 1 match.go:206] Determining IP address of default interface I0102 14:15:00.739258 1 match.go:259] Using interface with name eth0 and address 10.0.100.1 I0102 14:15:00.739424 1 match.go:281] Defaulting external address to interface address (10.0.100.1) I0102 14:15:00.739649 1 vxlan.go:138] VXLAN config: VNI=1 Port=0 GBP=false Learning=false DirectRouting=false E0102 14:15:00.748235 1 main.go:327] Error registering network: operation not supported W0102 14:15:00.748933 1 reflector.go:347] github.com/flannel-io/flannel/subnet/kube/kube.go:432: watch of *v1.Node ended with: an error on the server ("unable to decode an event from the watch stream: context canceled") has prevented the request from succeeding I0102 14:15:00.749578 1 main.go:447] Stopping shutdownHandler...
vxlanなるものが悪さをしているらしい。
vxlanとは?も含めて、この記事が参考になった。
podは複数のnode上にあるため、それらを同一のネットワーク内で接続するのに、vxlanという機能を使用しているらしいが、Ubuntu21.10あたりから含まれなくなったらしいので、ネットワークを作成できないみたい
ラズパイに関しては、linux-modules-extra-raspi
というパッケージをインストールすることで、解決した。
それでも立ち上がらないflannel
またしてもflannelのpodが立ち上がらないので、再びログを洗った
clusteradmin@master:~$ kubectl logs kube-flannel-ds-5cq5f --namespace=kube-flannel Defaulted container "kube-flannel" out of: kube-flannel, install-cni-plugin (init), install-cni (init) I0102 14:21:39.663540 1 main.go:204] CLI flags config: {etcdEndpoints:http://127.0.0.1:4001,http://127.0.0.1:2379 etcdPrefix:/coreos.com/network etcdKeyfile: etcdCertfile: etcdCAFile: etcdUsername: etcdPassword: version:false kubeSubnetMgr:true kubeApiUrl: kubeAnnotationPrefix:flannel.alpha.coreos.com kubeConfigFile: iface:[] ifaceRegex:[] ipMasq:true ifaceCanReach: subnetFile:/run/flannel/subnet.env publicIP: publicIPv6: subnetLeaseRenewMargin:60 healthzIP:0.0.0.0 healthzPort:0 iptablesResyncSeconds:5 iptablesForwardRules:true netConfPath:/etc/kube-flannel/net-conf.json setNodeNetworkUnavailable:true} W0102 14:21:39.663887 1 client_config.go:617] Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work. I0102 14:21:39.700428 1 kube.go:126] Waiting 10m0s for node controller to sync I0102 14:21:39.700527 1 kube.go:431] Starting kube subnet manager I0102 14:21:39.707598 1 kube.go:452] Creating the node lease for IPv4. This is the n.Spec.PodCIDRs: [172.16.0.0/24] I0102 14:21:40.701102 1 kube.go:133] Node controller sync successful I0102 14:21:40.701695 1 main.go:224] Created subnet manager: Kubernetes Subnet Manager - master I0102 14:21:40.701743 1 main.go:227] Installing signal handlers I0102 14:21:40.702648 1 main.go:467] Found network config - Backend type: vxlan I0102 14:21:40.702964 1 match.go:206] Determining IP address of default interface I0102 14:21:40.705451 1 match.go:259] Using interface with name eth0 and address 10.0.100.1 I0102 14:21:40.705623 1 match.go:281] Defaulting external address to interface address (10.0.100.1) I0102 14:21:40.705858 1 vxlan.go:138] VXLAN config: VNI=1 Port=0 GBP=false Learning=false DirectRouting=false E0102 14:21:40.707784 1 main.go:327] Error registering network: failed to acquire lease: subnet "10.244.0.0/16" specified in the flannel net config doesn't contain "172.16.0.0/24" PodCIDR of the "master" node I0102 14:21:40.707957 1 main.go:447] Stopping shutdownHandler...
こちらは、あまり情報が見つからなかったのだが、中国語(!?)で記事が見つかった。
flannelのpodを作成するときに使用するyamlファイルの中に、このような設定項目がある
kind: ConfigMap apiVersion: v1 metadata: name: kube-flannel-cfg namespace: kube-flannel labels: tier: node app: flannel data: cni-conf.json: | { "name": "cbr0", "cniVersion": "0.3.1", "plugins": [ { "type": "flannel", "delegate": { "hairpinMode": true, "isDefaultGateway": true } }, { "type": "portmap", "capabilities": { "portMappings": true } } ] } net-conf.json: | { "Network": "10.244.0.0/16", "Backend": { "Type": "vxlan" } }
この中の"Network": "10.244.0.0/16",
という部分で、podのCIDRを指定しているのだが、Kubernetesクラスタを作成した際のオプションで--pod-network-cidr=172.16.0.0/16
と指定しており、ここが一致していないのが原因だった。
そのため、合わせた上でapply
し直したところ、うまく動き出した。
clusteradmin@master:~$ kubectl get pods --all-namespaces NAMESPACE NAME READY STATUS RESTARTS AGE kube-flannel kube-flannel-ds-ssg4b 1/1 Running 0 18s kube-system coredns-787d4945fb-2bqlx 1/1 Running 0 3m5s kube-system coredns-787d4945fb-wx6w8 1/1 Running 0 3m5s kube-system etcd-master 1/1 Running 29 3m21s kube-system kube-apiserver-master 1/1 Running 0 3m15s kube-system kube-controller-manager-master 1/1 Running 0 3m17s kube-system kube-proxy-mvk7d 1/1 Running 0 3m5s kube-system kube-scheduler-master 1/1 Running 27 3m18s
Workerの方はどうなの?
今回、Workerの方もラズパイ4 + Ubuntu22.04という環境のため、cgroupsのバージョンを落とす + linux-modules-extra-raspi
のインストールを行った上でkubeadm join
を実行する
感想
バージョンが新しくなったりして、いつまでも同じ情報が使えるわけではないなという所感
Kubernetesのエコシステムは巨大で、エラーの目星をつけるのもとても大変だった・・・
大学祭のシステムをフルGCPで運用してみた
10/31〜11/01に、東洋大学 赤羽台キャンパスの大学祭「INIAD-FES」はオンラインで開催となりました。 その際のシステムを、GCPでホストしたので、その時のシステムの構成や、構成してみての感想をまとめてみたというお話です。
なおここでは開発したシステムなどの詳細には触れません
↓にも同じ内容を掲載しています qiita.com
どんな大学祭?
今年はCOVID-19の影響が続く中、多くの大学祭がオンライン上での開催を余儀なくされました。 INIAD-FESも例外ではありませんでしたが、多くの大学祭でライブ配信+Webページ上での掲示にとどまったものになっているのに対して、Webページ上で(できるだけ)キャンパスを再現→ライブ配信や静的コンテンツを掲載するという形としました。 できるだけ、キャンパス内の雰囲気を感じてもらおうというコンセプトです。
使ったサービスたち
Google Kubernetes Engine
最も要となるものです。
などをホストしました。 これらは、ユーザーが触れるシステムであり、VMを直接は触りたくない(運用が大変なので)・負荷分散を行いたいという理由で、GKEを使用しました。
GKEの負荷分散は、Google ManagedなSSL証明書を無料で使用できるので、とても便利ですが、もうちょっと発行早くしてくれないかなぁって所感です。 なお、static-ipは先に予約しておいて、予めDNSレコードに登録しておいたほうが後が幸せ。
自分はDockerを雰囲気で使ってる人間なので、当初は「え、ただの仮想マシン的なやつでしょ?」と思って、入構管理システムでcronを叩くようにしたら爆死しました。 DjangoのContainerではDjangoしか動かないんですね・・・
なお、こんな感じのyamlを書いてあげれば、CronJobとしてGKEに登録が可能です。 確かに、これはこれでお行儀がいいなと感じました。
apiVersion: batch/v1beta1 kind: CronJob metadata: name: sendmail namespace: health-reception spec: schedule: "0 4,8,14 * * *" jobTemplate: spec: template: spec: containers: - name: sendmail image: dummy args: - python3 - manage.py - send_alert_mail env: restartPolicy: OnFailure
GCPのロードバランサーは、WebSocketに対応しているんですが、なんかやたらめったら切断されるなぁって思ったら、設定されたタイムアウト時間で一旦切断されるんですね。 configを書いて、serviceの設定に適用するようにすれば、ひとまず問題を回避できました。
↓のbeta.cloud.google.com/backend-config
で設定を適用しています。
ingressの方へはreadinessProbeと同じく、serviceの設定が伝わるようで、特に設定しなくても大丈夫でした。
apiVersion: cloud.google.com/v1beta1 kind: BackendConfig metadata: name: venue-backend-config namespace: venue-backend spec: timeoutSec: 3600 --- apiVersion: v1 kind: Service metadata: annotations: beta.cloud.google.com/backend-config: '{"ports": {"3000":"venue-backend-config","443":"venue-backend-config"}}' labels: app: venue-backend name: venue-deployment-service namespace: venue-backend spec: externalTrafficPolicy: Cluster ports: - name: 80-to-80-tcp nodePort: 31377 port: 80 protocol: TCP targetPort: 80 - name: 3001-to-3000 nodePort: 31401 port: 3001 protocol: TCP targetPort: 3000 - name: 3000-to-3000-tcp nodePort: 30323 port: 3000 protocol: TCP targetPort: 3000 - name: 3002-to-3000-tcp nodePort: 30472 port: 3002 protocol: TCP targetPort: 3000 selector: app: venue-backend sessionAffinity: None type: NodePort
ただ、WebSocketはそれだけでポートを1つ占有し続けることになるので、あまりお行儀は良くないのかもしれませんね。
Google Cloud Storage
静的コンテンツのホストをしました。 キャッシュが行われて、変更が即座に反映されないというのが、ありがたいのだろうけどちょっと泣かされました。 単純なWebサイトであればホストできるんじゃね?って思ったんですが甘かった・・・
Cloud SQL
Postgresで、会場サイトや入構管理システムのデータを保持しました。
最初はDeployment内にPostgresのコンテナをもたせていたのですが、GKEのPersistent VolumeがReadWriteMany
に対応しておらず、スケールできないことが開発途中で判明したので、急遽の変更です。
できるだけPaaSを使いましょうねってことですね。
Cloud Memorystore
Railsのチャット関係で、ActionCableを採用したことから、Redisのために使用しました。 結構高いので、一瞬躊躇したのですが、Cloud SQLと同じく、Deployment内に持たせているとPersistent Volumeの問題でスケールできなくなるため、採用 純粋なRedisだと思ってたら、Rails6の仕様との兼ね合いでトラブりました(参考)
Cloud Build
これは初めて使いました。
自分はCI/CDは使ってこなかったのですが、開発メンバー からの圧力により が多め(当社比)で、デプロイの自動化は必要となったための採用です。
GitlabのCI/CDは無料でできる範囲に制限があること(途中で使い切りました)、GKEやGCRなどGCPのサービスへの権限をかんたんに付与できるということで、とても重宝しました。
なお、GCPサービスへの権限付与は、サービスアカウントを有効化するだけです、簡単ですね。
運用してみての感想
GCPかなり活用したつもりですが、実際にはVPCを意識していないなど、結構問題アリアリな感じです。 個人でGCP運用しているのにも活かせる知見は得られたんじゃないかなぁと思います。
あと、GKEで運用していた部分をGAEに置き換えた場合というのも、ちょっと興味あります。 そもそもGKEで運用するべきだったのかという辺りも、また探ってみたいと思いました。
at INIAD生の皆様 INIAD-FES実行委員会、エンジニアが深刻なレベルで不足中です。 実行委員会で↑みたいな大規模システムの運用、してみませんか?
WindowsでもRailsの開発環境をそろえてみた
Railsの開発は、基本MacBook Proで行っているので、何の感慨もなく環境構築できるのですが、WindowsだといろいろUNIX系OSとの差異が目立って、開発環境を準備できておらず、家のWindows DesktopがTwitter専用マシンと化していたので、この際開発環境を構築してみたのでメモです。
当初はWSL(無印)で頑張ろうとしていたんですが、Dockerがうまく動かなかったので、Insider Previewを有効化した上でWSL2を使用しています。
WSL2はWindows 10のbuild 2004から利用可能になります、リリースまで秒読み段階みたいなのですが、COVID-19の影響を受けているようで、リリースされそうだけどされないような状態になっています。。。 もう4月終わるよ
なお、IDEはRubyMineを使用します。
WSL2を有効化
にいい感じの方法が載っていたのでそのまま(おい
Ubuntuをインストール
数日前に、Ubuntu 20.04 LTSがリリースされましたが、人柱にはなりたくないので、おとなしく18.04 LTSを入れることにします。
Windows側にDocker Desktopをインストール
Ubuntu on WSLでは、Docker darmonは動かさず、Windows OS上で動いているDocker daemonを使用する形式のようです。
そこで、Windows側にDocker Desktopをインストールして、起動しておきます。
WSL2で動くLinuxディストリビューションが、Settings→Resources→WSL INTEGRATIONに表示されているので、トグルをオンにしておきます。
Ubuntu上でDockerを動かしてみる
早速、動かしてみます。
うん、いい感じですね!
rbenvをインストール
・・・中身Ubuntuなので、特に思うこともなく、インストールします
RubyMineにWSL上のRubyを読み込ませる
VSCodeでいうRemote的なものを使って、RubyMineでもUbuntuに入ったRubyのSDKを読み込ませます。
Settings→Languages & Frameworks→Ruby SDK and Gemsを開き、「+」→「New Remote」
Linuxディストリビューションを選択しますが、ここで注意したいのがRubyのパスです。
rbenvでインストールしたRubyは、どこにあると思いますか?見てみましょう。
(kentaro
はユーザー名なので、適宜置換してください)
kentaro@DESKTOP1:~$ which ruby /home/kentaro/.rbenv/shims/ruby
~/.rbenv/shims/ruby
が、Windows側から見えない(というより、shims
というディレクトリは実際には存在しないようです)、RubyMineではエラーになってしまいます。
そこで、それぞれのバージョンごとに、パスを指定してあげます。
/home/kentaro/.rbenv/versions/{Rubyのバージョン}/bin/ruby
ターミナルもWSLを使うようにする
Dockerコマンドを叩いたりするのは、Ubuntu上で行います。
なので、RubyMineから見えるTerminalもUbuntuのものにしたいですよね。
「Tools」→「Terminal」の「Shell path」をC:\Windows\System32\bash.exe
にします。
うん!いい感じですね!
まとめ
説明になってるようでなってないのですが、備忘録ということなので・・・
これで、Windows環境でも開発を進めるのが楽になります。
早くbuild 2004が正式リリースされて欲しい・・・
おまけ:Windows⇔WSLのファイルの扱い
最近のWSLでは、(WSL1も共通で)/mnt
以下に、Windowsマシンのディレクトリがマウントされるようになっているようです。
なので、開発のプロジェクトなどはWindows OS側に置いて、Ubuntu側にはそこへのリンボリックリンクを置いておくという運用にしています。 (MacとプロジェクトをDrive経由で同期したいという需要もあるため)
Mac Catalyst 書初め
こんにちは 最近になってようやくSwiftUIを触り始めたので、この機会にということで、MacCatalystにも挑戦してみました
すごくざっくり言えば、iOSとMacのアプリが同じコードで動くということです 実際触ってみた感想で言えば、夢を見過ぎるべきでないということですかね
というわけで、導入してみましょう
何を作る?
所用でTwitterのAPIの申請を作成することになったんですが、その申請って英語なんですよね
英語力が死んだ自分には、書くのはしんどいので(そもそもあれ大して読まれてないって説もありますがどうなんですかね)翻訳ソフトに頼りますが、Google翻訳は信用してないので、NICTのAPIを使うことにします そして、あのWebサイト、時間制限があるんですよね(24分間)
かといって、メモとかに残すのもだるいので、ネイティブアプリでAPIを叩く感じにします
プロジェクトの作成
プロジェクトの作成方法としては、iOSと一緒です
User Interface
をSwiftUI
にするのを忘れないであげてください
Deploy Info
のTarget
のMac
にチェックを入れます
メッセージが出るのでEnable
をクリックします
プロジェクトのMac対応は以上です
Deviceの一覧に、MacとiPhone/iPadが共存しているの、なんか新鮮ですよね
両方で動くんだという実感が湧きます
SwiftUI
import SwiftUI struct ContentView: View { @State private var rawText = "" @State private var translatedText = "" var translationController = TranslationController(apiKey: "YOUR API KEY", apiSecret: "YOUR API SECRET", apiUserName: "YOUR USER NAME") var body: some View { VStack{ TextField("翻訳元", text: $rawText) .padding(.horizontal) Button(action: { let (parameter, url) = self.translationController.generateParameter(fromLang: "ja", toLang: "en", text: self.rawText) self.translationController .oauthClient! .client .post(url, parameters: parameter, completionHandler: {result in switch result{ case .success(let response): self.translatedText = self.translationController.getTranslatedText(result: response) case .failure(let error): print(error.description) break } }) }) { Text("翻訳") }.padding() Text(self.translatedText) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
import Foundation import SwiftyJSON import OAuthSwift class TranslationController{ var apiKey = "" var apiSecret = "" var apiUserName = "" var apiBaseUrl = "https://mt-auto-minhon-mlt.ucri.jgn-x.jp" var oauthClient:OAuth1Swift! init(apiKey:String?, apiSecret:String?, apiUserName:String?){ if let apiKey = apiKey{ self.apiKey = apiKey } if let apiSecret = apiSecret{ self.apiSecret = apiSecret } if let apiUserName = apiUserName{ self.apiUserName = apiUserName } self.oauthClient = OAuth1Swift(consumerKey: self.apiKey, consumerSecret: self.apiSecret) } func generateParameter(fromLang:String, toLang:String, text:String) -> (OAuthSwift.Parameters, String){ var parameter = OAuthSwift.Parameters() parameter["key"] = self.apiKey parameter["name"] = self.apiUserName parameter["type"] = "json" parameter["text"] = text return (parameter, "\(self.apiBaseUrl)/api/mt/generalNT_\(fromLang)_\(toLang)/") } func getTranslatedText(result:OAuthSwiftResponse) -> String{ guard let resultString = result.string else{ return "" } let data = JSON.init(parseJSON: resultString) if let resultText = data["resultset"]["result"]["text"].string{ return resultText } return "" } }
コード読んでくださいって感じなんですが、ただ単に、翻訳元のテキストを投げて翻訳結果を取得するってだけですね
ハマったところ
NICTのAPIはOAuth1認証です(OAuth2ではありません)
自分でシグネチャを生成するのは面倒だと判断したので、OAuthSwiftを使いましたが、そいつがエラーに
iOSだと動くのに、なんでやねんという感じなんですが、(OAuthSwift自体はMac対応を謳っています)sudo gem update cocoapods
でCocoapodsをアップデートして、(念の為)pod install
したら上手く動きました
バージョンの問題なんですかね
というわけで完成
うん!いい感じですね!(UIが絶望的に下手っぴですが)
まとめ
SwiftUIは、これまでよりは表現が制約されますが、よしなにやってくれるというのがとても強いと思いました
AutoLayout未だに意味分かんねぇもん・・・
後は、ライブラリ関係がMac Catalystに対応してくれるといいんですが、なかなか時間かかるでしょうね・・・
そういう意味では、MacとiOSが同じコードで動く!なんて、あまり夢を見過ぎるべきでないと感じました(どちらかというと、iPadOSとmacOSのペアなんですが、正直iPadOSとiOSの違いが自分にはよくわからない・・・)
ではでは〜