2023年に買ってよかったものを書いておきます。
2023年も色々細かい買い物してたんですが、かなり印象に残ってるのは1つしかないので、それを書いておきます。
Cleer Arc 2
イヤホンは外耳炎になりがちなのでヘッドホンを使っていたんですが、これに切り替えてからは外出先でも家でも気軽に使えるようになって、めちゃくちゃはかどっています。
耳をふさがないので外音も通常通り聞こえますし、運動中でも重宝しています。
2023年に買ってよかったものを書いておきます。
2023年も色々細かい買い物してたんですが、かなり印象に残ってるのは1つしかないので、それを書いておきます。
イヤホンは外耳炎になりがちなのでヘッドホンを使っていたんですが、これに切り替えてからは外出先でも家でも気軽に使えるようになって、めちゃくちゃはかどっています。
耳をふさがないので外音も通常通り聞こえますし、運動中でも重宝しています。
最近は自分向けのPDFリーダーアプリを開発しています。
このアプリではSwiftDataを使って読書履歴をモデル化して管理しているのですが、これを踏まえてちょっと自分のメモがてらにTODOアプリを例にしたメモを残しておきます。
Swiftを触り始めて3日目ぐらいなので、書き間違えや嘘などがあるかもしれません。
その場合はコメントなどで教えてもらえると助かります。
@Query
で動的な条件は付与できない@Query
で定義したフィルターを利用するまず今回のサンプルとなるタスクを一覧表示するViewと、Modelを定義します。
以下のような仕様で作成しておきます。
import SwiftUI import SwiftData @Model final class Task { var name: String init(name: String) { self.name = name } } struct TodoView: View { @Environment(\.modelContext) private var context @State private var keyword: String = "" @Query(sort: \Task.name) private var tasks: [Task] var body: some View { VStack { HStack { TextField("検索...", text: $keyword) .padding() .background(primaryColor) .cornerRadius(5) } .buttonStyle(PlainButtonStyle()) .padding(.horizontal) List(tasks) { task in Text(task.name) } } } } // Preview struct TodoView_Previews: PreviewProvider { static var words: [String] = [ "Summery", "Country", "Perk", "Relief", "Virus", "Hunter", "Photocopy", "Liberal", "Sugar", "Practice", ] static var container: some ModelContainer { let configuration = ModelConfiguration(isStoredInMemoryOnly: true) let container = try! ModelContainer(for: Task.self, configurations: configuration) // プレビュー用データを作成 words.forEach { word in let task = Task(name: word) container.mainContext.insert(task) } return container } static var previews: some View { TodoView() .modelContainer(container) } }
この段階では、検索用のTextFieldがありますが絞り込むロジックを書いていません。
そのため絞り込むためのロジックを書いていきます。
キーワード検索を絞り込むためにはどういったやり方があるでしょうか?
最初は@Queryのfilter
に絞り込むためのロジックを書けばいけると思っていたのですが、ここではkeyword自体を参照することができませんでした。
// エラー @Query(filter: #Predicate<Task> { $0.name.contains(keyword) }, sort: \Task.name) private var tasks: [Task]
そして小一時間ほど公式ドキュメントを眺めたりしながら試行錯誤した結果、Computed Propertiesなプロパティを定義することで実現することができました。
// 成功 @Query(sort: \Task.name) private var tasks: [Task] private var filteredTasks: [Task] { guard !keyword.isEmpty else { return tasks } return tasks.filter { $0.name.contains(keyword) } }
tasks
をそのまま返すというプロパティを定義することで動的なキーワード検索を比較的楽に実現することができました。
もし同じように悩んでいる方がいれば参考にしてみてください。
皆さん、こんにちは!
この記事はSRE Advent Calendar 2023の16日目の記事になります。
今回の話は、k8sクラスタでのコンポーネントの継続的アップデートを行うための工夫についての紹介です。
SREとして信頼性や安定性を担保するためにアップデートの話は必要不可欠化と思うので今回の話にしました。
紹介例がおうちクラスタのマニフェストになっていますが、実際の業務にも転用できるので参考になれば幸いです。
システムの信頼性や安定性を担保するためにもアプリケーションやサービス、ツールの継続的なアップデートは重要です。
アップデートを行うことで、セキュリティの向上、バグ修正や機能改善、パフォーマンスや安定性の向上が見込めます。
そのため、Kubernetesクラスタでは、クラスタ自体のバージョンアップや、各種コンポーネントのアップデートはこまめに行うことが重要です。
Kubernetesのコンポーネントで継続的にアップデートを行う場合には以下のような課題が挙げられます。
今回は、Argo CD, Helm, Renovateを使って課題を解消・低減する方法について紹介します。
Argo CD, Helm Charts, Renovateといったツールを組み合わせることで解消・低減することができます。
これらのツールは、異なる側面からアプローチし、Kubernetesの運用を簡素化し、自動化します。
Argo CDは、GitOpsなCDツールでリポジトリにあるマニフェストの状態を監視し、クラスタへ手動もしくは自動でリソースの作成や変更を反映してくれます。
そして、Argo CDはKustomizeやHelmといったツールもサポートしており、それらのマニフェストを指定することもできます。
この組み合わせでコンポーネントを管理すれば、Argo CDで自動デプロイする仕組みを構築することが可能です。
例として、実際のおうちクラスタで管理しているWordPressのArgo CD Applicationを張っておきます。
apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: wordpress finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default sources: - ref: home repoURL: https://github.com/corrupt952/home - path: manifests/wordpress/base repoURL: https://github.com/corrupt952/home - chart: mariadb repoURL: https://charts.bitnami.com/bitnami targetRevision: 14.1.4 helm: releaseName: mariadb valueFiles: - $home/manifests/values/mariadb.yaml - chart: wordpress repoURL: https://charts.bitnami.com/bitnami targetRevision: 18.1.27 helm: releaseName: wordpress valueFiles: - $home/manifests/values/wordpress.yaml destination: server: "https://kubernetes.default.svc" namespace: wordpress syncPolicy: syncOptions: - CreateNamespace=true automated: prune: true selfHeal: true
アップデートは、Helm Chartsのバージョン(例だとtargetRevision
)を上げるだけで比較的容易に行うことができます。
この組み合わせの時点で、挙げた課題についてはほぼ解消・低減できていますが、アップデート作業も楽にしたいのでRenovateを組み合わせます。
Argo CD+Helmで管理されているリポジトリにRenovateを追加することで、アップデートをRenovateが自動化してくれます。
以下の例では、Argo CDやKubernetesマニフェストを読み取って各種コンポーネントをアップデートするための設定です。
{ $schema: "https://docs.renovatebot.com/renovate-schema.json", extends: [ "github>corrupt952/.github:default.json5", ], argocd: { fileMatch: ["manifests/argocd-config/base/.*\\.yaml"], }, kubernetes: { fileMatch: ["manifests/.*/base/.*\\.yaml"], } }
さらにRenvoateのAutomergeをパッチやマイナーバージョンにのみ適用することで、Pull Requestを手動でマージしなくてもクラスタへ反映することができるようになります。
実際のマニフェストや設定は以下のリポジトリにまとめてあります。
参考にする場合は以下を見てみてください。
ただし、このレポはモノレポ運用しているため、AnsibleのPlaybookが混じっていることに注意してください。
Argo CD, Helm, Renovateを組み合わせてコンポーネントの継続的なアップデートを行う方法を紹介しました。
より楽により安全にアップデートするためにも今回紹介した方法が参考になれば幸いです。
それではよい週末・年末を :)
前回の続きでContainerized Data Importer(以下、CDI)を導入してUbuntuの仮想マシンを動かします。
マニフェストの取得コマンドは省きしますが、マニフェストをkustomization.yamlから参照するように定義しておきます。
--- apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: cdi resources: - https://github.com/kubevirt/containerized-data-importer/releases/download/v1.57.0/cdi-operator.yaml - https://github.com/kubevirt/containerized-data-importer/releases/download/v1.57.0/cdi-cr.yaml
前回同様にArgo CD Applicationを定義して、CDIをデプロイします。
マニフェストは以下のようになります。
--- apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: cdi finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default sources: - path: manifests/cdi/base repoURL: https://github.com/corrupt952/home destination: server: "https://kubernetes.default.svc" namespace: cdi syncPolicy: syncOptions: - CreateNamespace=true automated: prune: true selfHeal: true
PVCを定義する方法もありますが、今回はDataVolume
を定義してUbuntuのイメージをベースとしたデータボリュームを定義しておきます。
今回はUbuntu 22.04を使うので以下のような書き方にしました。
url
やstorageClassName
は環境に合わせて変更してください。
--- apiVersion: cdi.kubevirt.io/v1beta1 kind: DataVolume metadata: name: ubuntu-volume spec: source: http: url: "https://cloud-images.ubuntu.com/jammy/20231027/jammy-server-cloudimg-amd64.img" pvc: storageClassName: nfs accessModes: - ReadWriteOnce resources: requests: storage: 8Gi
Argo CDでデプロイしても良いのですが、動作確認のためだけなのでkubectl apply
を直接実行して反映します。
この方法でもPVCは作成されるので作成されたかどうかは、以下のコマンドで確認できます。
# DataVolumeの確認 kubectl get dv # PersistentVolumeClaimの確認 kubectl get pvc
DataVolumeの方でSuccessedと出ていれば問題ありません。
PVCがPendingのままの場合は、storageClassName
がクラスタ内で利用可能なStorageClass`になっているのかは確認しましょう。
VirtualMachine
を定義します。
DataVolumeであるubuntu-volume
をdisk0
という名前でマウントして使います。
また今回はbeestrapというISUCON向けのBootstrappingレポの動作確認も行いたいので
Serviceを定義してCockpitやNetdataのポートを解放しておきました。
--- apiVersion: kubevirt.io/v1 kind: VirtualMachine metadata: name: ubuntu labels: kubevirt.io/os: linux spec: running: true template: metadata: labels: kubevirt.io/size: small kubevirt.io/domain: ubuntu spec: domain: cpu: cores: 2 devices: disks: - name: disk0 disk: bus: virtio - name: cloudinitdisk cdrom: bus: sata readonly: true interfaces: - name: default masquerade: {} machine: type: q35 resources: requests: memory: 2048M networks: - name: default pod: {} volumes: - name: disk0 persistentVolumeClaim: claimName: ubuntu-volume - name: cloudinitdisk cloudInitNoCloud: userData: | #cloud-config hostname: ubuntu ssh_pwauth: true password: ubuntu chpasswd: expire: false --- apiVersion: v1 kind: Service metadata: name: ubuntu labels: kubevirt.io/os: linux spec: ports: - name: ssh port: 22 targetPort: 22 - name: cockpit port: 9090 targetPort: 9090 - name: netdata port: 19999 targetPort: 19999 selector: kubevirt.io/domain: ubuntu
こちらもDataVolumeと同様にkubectl apply
で直接反映します。
1分ほど起動に時間がかかるかもしれませんが、起動できればsshで接続することもできます。
kubectl virt ssh --local-ssh ubuntu@ubuntu
またVNCで接続したい場合はプロキシーとしてコマンドを実行するか、VNCクライアントを直接起動することもできます。
以下の例では、tigervncのクライアントがインストールされているWSL2の環境でクライアントを起動します。
kubectl virt vnc ubuntu
これで諸々の動作確認が終わりました。 Serviceで定義したポートに関しては仮想マシン内にCockpitなどをインストール後にport-forwardなどで動作確認すると接続できます。
おうちクラスタにKubeVirtをデプロイした時の話を書いておきます。
実際に変更したコミットは以下にあるので参考にしてください。
公式のInstalling KubeVirt on Kubernetesを元に必要なファイルを書いていきます。 まずは以下のコマンドをkustomization.yamlに書き換えてArgo CDでデプロイできるようにします。
# Point at latest release $ export RELEASE=$(curl https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt) # Deploy the KubeVirt operator $ kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/${RELEASE}/kubevirt-operator.yaml # Create the KubeVirt CR (instance deployment request) which triggers the actual installation $ kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/${RELEASE}/kubevirt-cr.yaml # wait until all KubeVirt components are up $ kubectl -n kubevirt wait kv kubevirt --for condition=Available
↓ kustomization.yamlに書き換える
--- apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: kubevirt resources: - https://github.com/kubevirt/kubevirt/releases/download/v1.1.0/kubevirt-operator.yaml - https://github.com/kubevirt/kubevirt/releases/download/v1.1.0/kubevirt-cr.yaml
おうちクラスタではArgo CDを使ってマニフェストを反映しています。 なので、Argo CD Applicationを作成してデプロイします。
--- apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: kubevirt finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default sources: - path: manifests/kubevirt/base repoURL: https://github.com/corrupt952/home destination: server: "https://kubernetes.default.svc" namespace: kubevirt syncPolicy: syncOptions: - CreateNamespace=true automated: prune: true selfHeal: true
後はデフォルトブランチにプッシュすれば、弊宅では反映されます。
これで諸々の作業は完了です。 krew経由でvirtctlをインストールしておくと楽なのでオススメしておきます。
弊宅のクラスタネットワークはサブネットが異なるため、ただのsshだけでは接続ができないのでvirtctlを使ってsshをしています。
kubectl virt ssh cirros@testvm
似たようなネットワーク構成の人がいれば参考にまでにどうぞ
少し前にRails 7.0.8がリリースされましたが、その前のバージョンである7.0.7.2でマイグレーションした結果のdb/schema.rb
が異なるということについて質問されたので、金曜夜に解説した内容を文章にまとめたものになります。
db/schema.rb
についたりつかなかったりするdb/schema.rb
の結果が異なる7.0.8からマイグレーションファイルのバージョンが6.1の場合、datetimeなカラムの精度は指定なしでマイグレーションされるように修正されました。
これにより、Rails 7.0.7.2と7.0.8の間でカラムの定義に違いが生じる場合があります。
7.0.7.2以前の7系と7.0.8とでdatetimeなカラムの作成方法が異なります。
そのため、7.0.8の修正が入る前の7系のRailsと、7.0.8のRailsのどちらのタイミングでマイグレーションしたかによって、カラムの定義が異なる可能性があります。
以下のようなマイグレーションファイルが定義されている場合、それぞれのバージョンでスキーマにどういった影響があるのかを確認してみます。
class Hoge< ActiveRecord::Migration[6.1] def change change_column :users, :inactive_at, :datetime, default: '2023-10-27 14:50:04' end end
それぞれのバージョンでマイグレーションした場合のテーブル定義は以下のようになっており、inactive_at
のdatetimeの精度指定が異なることが分かります。
# Rails 7.0.7.2でマイグレーションした場合のdatetimeなカラムの定義 > show create table users \G; *************************** 1. row *************************** Table: users Create Table: CREATE TABLE `users` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `inactive_at` datetime(6) DEFAULT '2023-10-27 14:50:04.000000', `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 # Rails 7.0.8でマイグレーションした場合のdatetimeなカラムの定義 > show create table users \G; *************************** 1. row *************************** Table: users Create Table: CREATE TABLE `users` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `inactive_at` datetime DEFAULT '2023-10-27 14:50:04', `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 1 row in set (0.000 sec)
このカラム定義の違いが、実際のアプリケーションにどのような影響を与えるのでしょうか?
例えば、7.0.7.2でマイグレーションしていたAさんの環境と、7.0.8にアップグレードした後にデータベースを作り直したBさんの環境とでは、rails db:schema:dump
の結果が異なります。
この挙動によっては、ローカルだけではなく特定のテスト環境や場合によっては本番環境を含めて、書く環境ごとにdatetimeの精度が異なるという可能性がありえます。
本記事では、Rails 7.0.8と7.0.7.2の間でdatetimeなカラムのschema.rbが異なることについて解説しました。
アプリケーション開発やマイグレーションを行う際には、Railsのバージョンによるカラム定義の違いに注意する必要があります。
正確なカラムの定義を把握し、適切にマイグレーションを行うことで、問題なくアプリケーションを運用することができます。
主に個人向けで作っているサービスでLogin with Notionを実装する必要があったので、その時の動作検証を行った時の作業ログです。
Login with Notionと仰々しく言っていますが、端的に言えばNotionと連携するためにOAuthを利用するだけの話です。
Notion公式ドキュメントに従って、Public Integrationを作成します。
今回の動作検証時のIntegrationの設定は、以下のようになっています。
適当
と書いてあるところは適当に値を設定しています。
Name
... 適当Content Capabilities
... Read content
User Capabilities
... Read user information including email addresses
Company name
... 適当Website or homepage
... 適当Privacy policy
... 適当Terms of use
... 適当Support email
... 適当Redirect URIs
... 動作確認のためにhttp://localhost:3000/callback
Sinatraを使って簡易的に検証します。
今回は使っていないですが、OmniAuthが使える状況であればomniauth-notionというGemが存在するので、それを使うと楽かもしれません。
Gemfileは以下のようにSinatraを使うためにsinatra
とpuma
を導入し、Sinatra::Reloaderを使いたいのでsinatra-contrib
も導入しておきます。
APIリクエストはNet::HTTP
を使いますが、Faradayなどを使った方が間違いなく楽なのでお好みで使ってください。
# frozen_string_literal: true source "https://rubygems.org" gem 'sinatra' gem 'sinatra-contrib' gem 'puma'
本格的な実装の前に今回作成するアプリケーションの大まかな仕様を決めておきます。
/
... ログインしているユーザーの名前を表示。ログインしていない場合は/login
へリダイレクト/login
... Public IntegrationのAuthorization URL
のリンクを配置/logout
... セッションをクリアし、/
へリダイレクト/callback
... Authorization URLアクセス後に呼び出されるコールバックなので、渡された情報を元にアクセストークンとセッションの生成を行うapp.rb
というファイルを作成して、Sinatraの設定を書きます。
# frozen_string_literal: true require 'uri' require 'net/http' require 'sinatra' require 'sinatra/reloader' class Application < Sinatra::Base CLIENT_ID = ENV['NOTION_CLIENT_ID'] CLIENT_SECRET = ENV['NOTION_CLIENT_SECRET'] REDIRECT_URI = 'http://localhost:3000/callback' # Notionで設定したRedirect URLsと同じである必要アリ configure do # セッションを有効化 # Cookieストアなので注意 enable :sessions set :port, ENV.fetch('PORT', 3000) set :bind, ENV.fetch('BIND', '0.0.0.0') # ローカルで実行するなら不要 # Hot reloadを有効化 register Sinatra::Reloader end def authorization_url uri = URI.parse('https://api.notion.com/v1/oauth/authorize') uri.query = URI.encode_www_form( client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, response_type: 'code', scope: 'all' ) uri.to_s end end Application.run!
ログイン画面を実装します。
とはいっても、Authorization URLするだけなので、/
へアクセスされた場合に/login
へリダイレクトするのも実装しておきます。
class Application < Sinatra::Base ... get '/' do redirect '/login' if session[:access_token].nil? # セッションにアクセストークンがない場合は/loginへリダイレクト 'TBD' end get '/login' do "<a href=\"#{authorization_url}\">Login with Notion</a>" # Authorization URLのリンクを表示 end end
この状態でアプリケーションを ruby app.rb
で起動し、localhost:3000へアクセスすると、/login
へリダイレクトされます。
Notionからのコールバックを実装する前に先にログアウト処理を実装しておきます。
今回はセッションをクリアするだけでOKです。
class Application < Sinatra::Base ... # POSTの方がよいがGETのが動作検証が楽なのでGET get '/logout' do session.clear redirect '/' end end
Notionからのコールバックを実装します。 アクセストークンを発行したいので Create a token に従います。
class Application < Sinatra::Base ... # FYI: https://developers.notion.com/reference/create-a-token get '/callback' do code = params[:code] uri = URI.parse('https://api.notion.com/v1/oauth/token') bearer_token = Base64.strict_encode64("#{CLIENT_ID}:#{CLIENT_SECRET}") # Client ID,Secetを結合してBase64でエンコード http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true response = http.post( uri.path, # Payload URI.encode_www_form( grant_type: 'authorization_code', redirect_uri: REDIRECT_URI, code: code ), # Headers { 'Authorization' => "Basic #{bearer_token}", 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8', 'Notion-Version' => '2022-06-28', } ) # 各種情報をセッションに保存 session[:access_token] = JSON.parse(response.body)['access_token'] # アクセストークン session[:user_id] = JSON.parse(response.body)['owner']['id'] # ユーザID redirect '/' end end
これでログイン処理の実装は終わりです。
後はユーザ名を表示すれば検証は終わりです。
/
でユーザ名を表示するようにします。
ユーザ情報を取得するには、Retrieve a userに従ってAPIリクエストします。
class Application < Sinatra::Base ... get '/' do redirect '/login' if session[:access_token].nil? uri = URI.parse("https://api.notion.com/v1/users/#{session[:user_id]}") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true response = http.get( uri.path, # Headers { 'Authorization' => "Bearer #{session[:access_token]}", 'Notion-Version' => '2022-06-28', } ) results = JSON.parse(response.body)['results'] user = results.first <<~HTML Hello, <b>#{user['name']}</b>! <br /> <a href="/logout">Logout</a> HTML end ... end
ここまで実装すれば一連の流れを画面上で操作でき、動作検証ができることが分かりました。
Login with Notionを使ったサンプルをあまり見かけないので放流だけしておきました。
検証目的での実装なのでセキュリティは非常に脆弱なコードなので、間違ってもそのまま流用しないでください。