TECH PLAY

株式会社ラクス

株式会社ラクス の技術ブログ

935

こんにちは、フロントエンドチームの亀ノ上です。 最近は画像生成AIやテキスト生成AIなど、AIによる自動生成に関する話題をよく目にします。特にここ最近は ChatGPT の勢いが凄まじく、毎日のようにニュースを見かけるような気もしています。 今回はそんな ChatGPT でも使用されている 言語モデル である GPT-3 を用いて、 Nuxt3 で簡単なテキスト生成アプリを作成します。 Nuxt3について Nuxt3とは Nuxt3の機能 Nuxt3インストール インデックスページの設定をする フロントエンド開発 バックエンド開発 OpenAIのAPI Keysを作成 フロントエンドとバックエンドを連携 おわりに 参考 Nuxt3について Nuxt3とは Nuxt3 とは、 Vue.js アプリケーションを構築するための フレームワーク です。特徴としては、高速な開発、シンプルな設計、高機能な機能などがあげられます。 Nuxt3の機能 今回 Nuxt3 を選択した理由として、 Nuxt3 には、サーバーサイド レンダリング や API Routes などの機能が備わっています。 API Routes は、アプリケーション内で API エンドポイントを定義し、簡単に呼び出すことができる機能です。この機能を使うことで、 API 開発とフロントエンド開発を効率的に行うことができます。 Nuxt3インストール それではまずは Nuxt3 をインストールしていきます。 公式 npx nuxi init gpt3-app 🎉 Another prime Nuxt project just made! Next steps: 📁 ` cd gpt3-app` 💿 Install dependencies with `npm install` or `yarn install` 🚀 Start development server with `npm run dev` or `yarn dev` インストールが終わったら、 ディレクト リを移動してパッケージをインストールします。 cd gpt3-app `npm install` or `yarn install` 今回は yarn install ですすめます。 ... success Saved lockfile. $ nuxt prepare Nuxi 3 . 1 . 2 ✔ Types generated in .nuxt ✨ Done in 113 .76s. パッケージのインストールが終わったら、 Nuxt を起動してみます。 yarn dev Nuxi 3 . 1 . 2 Nuxt 3 . 1 . 2 with Nitro 2 . 1 . 2 > Local: http://localhost:3000/ > Network: http://xxx.xx.xx.xxx:3000/ ℹ Vite client warmed up in 1388ms ✔ Nitro built in 421 ms http://localhost:3000/ にアクセスします。 Nuxt の画面が表示されていればOKです! インデックスページの設定をする 次にコードの方を修正していきます。 現在表示されている画面は app.vue の内容になります。 app.vue < template > < div > < NuxtWelcome /> </ div > </ template > まずは自身で作ったページを表示するために pages/index.vue を作成します。 pages/index.vue ファイルは、アプリケーションの / ルートに自動的に設定されます。 pages/index.vue < template > < h1 > Hello Nuxt3! </ h1 > </ template > 次に app.vue を変更します app.vue < template > < div > < NuxtPage /> </ div > </ template > <NuxtPage /> は pages/index.vue が存在する場合は、 pages/index.vue を自動的に レンダリング します。 それでは一度 yarn dev で画面を確認してみましょう。 問題なく表示されていますね。 フロントエンド開発 今回の GPT-3 アプリは、キーワードを入力して、テキストを生成するといったアプリにしたいと思いますので。キーワードの入力エリアと、ボタンを作成します。 pages/index.vue < script setup> const keyword = ref ( '' ) ; const handleClick = () => { // ここに生成する処理をかく } </ script > < template > < h1 > GPT-3 APP </ h1 > < div > < input type = "text" v-model= "keyword" > </ div > < div > < button type = "button" @click= "handleClick" > テキスト生成 </ button > </ div > </ template > Nuxt3 はもちろん Vue.js ベースの フレームワーク なので Vue3 の機能をフル活用できます。 v-bind や v-model といったディレクティブから ref() などの API も使用できます。ただ、 <script setup> で import { ref } from 'vue' しなくて良いのか気になった方がいるかもしれません。 Nuxt3 では自動 import の機能が搭載されています。この機能により、 API の import 文を記述することが不要になります。また、 API だけでなく、作成した コンポーネント なども自動で import してくれるので、これにより少ないコード量で Webアプリケーションを開発することができます。 バックエンド開発 次に、 server/api/generate.post.js を作成しますが、少しだけ API Routes について説明します。 冒頭でも少しお話しましたが、 Nuxt3 は API Routes をサポートしています。 API Routes は、サーバーサイドで動作する API エンドポイントを指し、外部のデータソースからデータを取得したり、データを更新することができます。 その API Routes を実装するための ディレクト リが server/api であり、この ディレクト リ内で API を簡単に実装することができます。 それではコードを書いていきます。 server/ api /generate.post.js export default defineEventHandler(async ( event ) => { const { prompt } = await readBody( event ); const payload = { model: "text-davinci-003" , prompt , temperature: 0.7, top_p: 1, frequency_penalty: 0, presence_penalty: 0, max_tokens: 300, n: 1, } ; const response = await fetch( "https://api.openai.com/v1/completions" , { headers: { "Content-Type" : "application/json" , Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? "" } ` , } , method: "POST" , body: JSON.stringify(payload), } ); const json = await response.json(); return json; } ) 処理は以下のような流れとなっています。 フロント側から渡されたリク エス トデータを取得 OpenAI の API に渡す payload を作成 fetch メソッドで OpenAI からレスポンスオブジェクトを取得 レスポンスオブジェクトから json データを取得しフロントに返す まず GPT-3 にテキスト生成してもらうキーワードをフロント側から受け取るのですが、リク エス トデータの body を readBody(event) で取得することができます。 その後フロント側から渡されたキーワードと定義した payload を fetch メソッドへ定義して応答を待ちます。 OpenAIの API Keysを作成 バックエンドの処理で、 fetch メソッドのヘッダーに API Key を設定していました。 OpenAI の API を使用するには API Key が必要なので作成していきます。 まずは 公式サイト へ行き右上の SIGN UP をクリックしてください。 ログインができたら右上のアイコンから View API Keys をクリックします 画面中央付近にある Create new secret key をクリックします 表示された API Key をコピーして下さい。右の緑のアイコンからもコピーできます。 コピーしたら .env を作成しコピーした API Key をはりつけます OPENAI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxx フロントエンドとバックエンドを連携 それでは最後にフロントエンドとバックエンドを連携して確認してみましょう。 app.vue から作成した API を実行します。 app.vue const generateText = ref( '' ) const handleClick = async () => { const { data } = await useFetch( '/api/generate' , { method: 'POST' , } ) generateText.value = data.value.choices [ 0 ] .text } < template > < h1 > GPT-3 APP </ h1 > < div > < input type = "text" v-model= "keyword" > </ div > < div > < button type = "button" @click= "handleClick" > テキスト生成 </ button > </ div > < div > < h2 > 生成テキスト </ h2 > < div > {{ generateText }} </ div > </ div > </ template > 大まかな流れとしては以下の通りです。 template で表示するために、リアクティブな変数 generateText を宣言。 useFetch で API から生成テキストを取得。 generateText にセットし、画面に表示する。 useFetch は Nuxt3 が提供しているデータを取得するための関数です。もちろん自動 import なため、 import 文は不要です。今回は POST リク エス トのため method:'POST' を指定しています。 GET リク エス トの場合は第2引数を省略できます。 useFetch で返ってくるデータですが、 data の他には error, pending, execute, refresh が含まれます。今回は data のみを使用するので、分割代入で取得しています。また、 data は、リアクティブデータである Ref で返ってくるので、 data.value で取り出すことができます。 data.value には GPT-3 で生成されたデータが含まれており、 data.value.choices[0].text で取り出します。取り出したデータは generateText にセットします ただ、現在は入力したキーワードを渡せていません。 GPT-3 から期待したテキストが生成されるように、キーワードから文章を加工して、渡すようにします。 < script setup> const keyword = ref ( '' ) ; const generateText = ref ( '' ) const prompt = computed (() => ` 日本語で回答して下さい。 ${keyword.value} について最大150文字で説明してください。 ` ) const handleClick = async () => { const { data } = await useFetch ( '/api/generate' , { method: 'POST' , body: { prompt } } ) generateText.value = data.value.choices [ 0 ] .text } </ script > < template > < h1 > GPT-3 APP </ h1 > < div > < input type = "text" v-model= "keyword" > </ div > < div > < button type = "button" @click= "handleClick" > テキスト生成 </ button > </ div > < div > < h2 > 生成テキスト </ h2 > < div > {{ generateText }} </ div > </ div > </ template > それでは実行してみましょう(最大1分ほどかかる場合があります) キーワードを元にテキストの生成ができました! おわりに 今回触ってみて感じましたが、 GPT-3 アプリ作成は Nuxt3 の機能を理解するのにちょうどよい題材だと思いました。自動 Import についてや、特に API Routes について知識を深めることができていい経験になりました。 デザインを整えたり、生成履歴を残して ChatGPT 風にしたり、テキスト生成の待機中の状態を表示したり…など、まだまだできることが多いと思います。よかったら参考にいろいろ試してみて下さい。 参考 vercel.com エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは、tatsumiです。 私は普段、チャットディーラーAIという製品の開発に携わっています。 www.chatdealer.jp 先日、チャットディーラーAIに シングルサインオン の機能を実装したのですが、機能開発を行うまでは シングルサインオン の仕組みについて何となくでしか理解していませんでした。 機能開発を行うにあたって、 シングルサインオン の仕組みを調べたので、当ブログでまとめたいと思います。 シングルサインオン(SSO)とは? シングルサインオン(SSO)の認証方式と仕組み 代行認証方式 リバースプロキシ方式 エージェント方式 フェデレーション方式 まとめ シングルサインオン (SSO)とは? シングルサインオン (SSO)とは、1つのIDとパスワードで複数のサービスにログインできる仕組みのことです。 通常であれば、サービスごとに個別にログインが必要で、IDやパスワードもそれぞれで設定・管理しなければならず、ユーザーにとって大きな負担となります。 シングルサインオン を利用すれば、このような負担が解消されます。 シングルサインオン (SSO)の認証方式と仕組み シングルサインオン を実現する方法として、5つの認証方式があります。 それぞれの仕組みと特徴を紹介します。 代行認証方式 代行認証方式は、ユーザーが利用する端末に導入したエージェントが、対象システムのログイン画面を監視して、ログイン画面が起動したらユーザーの代わりにID/パスワードを自動入力して認証する仕組みです。 リバースプロキシ方式 リバースプロキシ方式は、リバースプロキシと呼ばれる中継サーバを介して認証を行う方式です。 PCからリバースプロキシサーバに対してWeb認証を実施すると、リバースプロキシサーバから認証済みの Cookie が発行されます。 その後、ログインしたい各サービスにおいて、リバースプロキシサーバを経由し、対象のシステムにアクセスすることで シングルサインオン でのログインを行います。 エージェント方式 エージェント方式は、Webサーバや アプリケーションサーバ にエージェントを導入する方式です。 代行認証方式では、ユーザーが利用する端末にエージェントを導入しますが、エージェント方式の場合は対象のシステム側にエージェントを導入します。 初回ログイン時にSSOサーバから Cookie が発行されます。 2回目以降のログイン時や他システムへのログイン時には、システム側のエージェントがユーザーが保持している Cookie を利用してSSOサーバーにユーザー認証を行うことで シングルサインオン を実現します。 フェデレーション方式 フェデレーション方式は、対象サービス(SP)と認証を行うサービス(IdP)の間で認証情報をやり取りする方式です。 IdPにログインすれば、各サービスはIdPから認証情報を取得することで、ユーザーは各サービスをログインなしで利用することができます。 フェデレーション方式では、 SAML や OpenID Connectといった プロトコル がよく利用されます。 ラク スのサービスでは、楽楽精算、楽楽販売、メールディーラー、チャットディーラーAIといったサービスで シングルサインオン を利用することができますが、何れも SAML 認証によるフェデレーション方式で シングルサインオン を実現しています。 まとめ 今回の記事で紹介した通り、一口に シングルサインオン と言っても様々な方式が存在しています。 導入する際は、それぞれの方式のメリット/デメリットを把握した上でどの方式を採用するか判断した方がよいでしょう。 次回は、私が開発に携わっているチャットディーラーAIを含め、 ラク スのその他サービスで実装されている SAML 認証について詳しく解説したいと思います。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは! ラク ス1年目の koki _matsuraです。 今回は 掲示 板アプリ作成を通して、SvelteKitの基礎的な部分 をご紹介させていただきます。 目次は下記のようになっています。 はじめに Svelteとは SvelteKitとは 掲示板アプリ作成 アプリの概要 環境構築 SvelteKit データベース Prisma テーブル作成 ルーティング作成 新規登録画面 ログイン画面 スレッド投稿画面 スレッド一覧画面 スレッド詳細画面 ログアウト機能 エラー画面 終わりに はじめに Svelteとは WebアプリケーションやUIを構築するための JavaScript フレームワーク です。有名なものでは「React」や「Vue」が挙げられます。 Svelteにはこれらの フレームワーク と比べて下記のような特徴があります。 仮想DOMを用いない こちらが最も大きな特徴となります。 まず、仮想DOMとはその名前の通りプログラムの中で作られる仮のDOMのことを言います。従来の フレームワーク では変更点があれば毎回DOM全体を構築していましたが、仮想DOMは下図のように変更前と変更後の仮のDOMを比較し、差分を検出することで変更部分のみを構築でき、表示速度も速くなります。 しかし、Svelteでは仮想DOMを用いていません。従来のように変更のたびに全DOMを再構築しているわけでもありません。 代わりに コンパイル 時に特殊なことをします。 具体的にはビルド時にDOMの構造を解析し、変更の可能性がある部分をVanilla JSのコードとして落とし込む最適化を行います。そして、データに変更があれば該当のJSコードを動かし、実際のDOMを変更します。なので、仮想DOMを必要とせずに差分のみを変更することができ、高速な レンダリング を行えます。 また、JSファイルに全てのロジックが記述されているのでランタイムが不要となり、軽量です。 これらのことから分かる通り、Svelteはビルド時にJSコードを生成する「 コンパイラ 」です。 シンプルな構文で記述できる Svelteではボイラープレートを記述する必要がなかったり、変数や関数などのアプリケーションの状態を自動的に管理するため状態管理専用ライブラリが入りません。 よって、記述するコードがほかの フレームワーク より少なくて済みます。 また、テンプレートベースの シンタックス を使用してUIを記述するため、 JavaScript とHTMLの マークアップ を分離することができ、かなりコードが見やすくなります。 真にリアクティブである Svelteは、必要なデータを持つ変数を定義し、その変数が変更された時、自動的に画面に反映するといった仕組みを備えています。 このリアクティブな仕組みにより、変更を手動で書かなくて済み、結果的にコードを書く量が減り、開発効率が向上するといったメリットがあります。 SvelteKitとは Svelteを使ってWebアプリケーションを開発するための フレームワーク です。ReactにとってのNext、VueにとってのNuxtのようなものです。 開発する上で必要となる環境を自動でセットアップし、アプリケーション開発の作業を簡素化してくれます。具体的には ルーター や SSR の基本的なものからビルドの最適化、プリロード、柔軟な レンダリング の設定など高速なページロードをするための機能です。 多くのサポートがあるため、開発者はアプリケーションの開発に集中することができます。 掲示 板アプリ作成 ここからは実際にSvelteKitを使って、簡単な 掲示 板アプリを作成します。 ORMとして Prisma を使います。 アプリの概要 作成する 掲示 板アプリで開発する機能は次のようになっています。 ユーザー登録 ログイン・ログアウト スレッド投稿 コメント投稿 必要となるページは次のようになっています。 新規登録画面 ログイン画面 スレッド一覧画面 スレッド詳細画面 スレッド投稿画面 エラー画面 かなりシンプルな 掲示 板アプリです。 環境構築 SvelteKit 早速ですが、SvelteKitでプロジェクトを作成しましょう。ターミナルを開き、プロジェクトを作成する ディレクト リに移動し、下記のコマンドを打ちます。 プロジェクト名は「board-app」としましたが、お好きな名前で構いません。 npm create svelte@latest [プロジェクト名] 作成時にテンプレートや、JSかTSか、ESLintを使うか、Prettierを使うか、テストにPlaywright、Vitestを使うか聞かれます。今回の場合はテンプレートに「Skeleton project」を選択し、JSかTSかは「TypeScript」を選択します。その他は好きに選んでいただいて構いません。 プロジェクトが作成されれば、次やるべきことがターミナルに表示されます。 僕のターミナルには下記のように表示されました。 Next steps: 1: cd board-app 2: npm install (or pnpm install, etc) 3: git init && git add -A && git commit -m "Initial commit" (optional) 4: npm run dev -- --open プロジェクトに移動し、「npm install」をしましょう。Nextを使ってると、そのまま4に行けるのですがSvelteKitの場合は一旦、インストールを挟まないといけないようです。 「npm run dev -- --open」をコマンドで打つと、自動で開きます。下記のような初期画面が開いていれば成功です。 SvelteKitの環境構築は終わりです。 データベース 僕はデータベースにSupabaseを使います。特に理由はありません。なので、他の方法でデータベースを作成していただいても大丈夫です。 もし、Supabaseをこれから使いたいという方は僕が過去に書いた記事を参考にしていただければと思います。 この記事のSupabaseでプロジェクトを作成する部分までで大丈夫です。テーブルの定義は Prisma で行います。 ※ Supabaseのプロジェクト作成の際に設定するパスワードを忘れないようにしてください。 tech-blog.rakus.co.jp Prisma Prisma をインストールします。下記のコマンドをターミナルに入力します。 npm install prisma --save-dev npx prisma init コマンド入力後にプロジェクトに prisma ディレクト リが作成され、中には「schema. prisma 」が作成されていると思います。 providerは合ったものにしてください。僕はSupabaseを使用しているので postgresql となります。 urlは「.env」ファイルから「DATABASE_URL」という変数の値を持ってきています。 DATABASE_URLは未設定ですが、テーブル作成の際にSupabaseから取得してきたいと思います。 // schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } 一旦、必要なものは揃ったので環境構築は終わりです。 テーブル作成 Svelteを使ってガンガン開発していきたいところですが、まずはテーブルの作成をしていきます。先ほど自動で作成された「schema. prisma 」を開きます。 今回の 掲示 板アプリに必要なのは「User」「Post」「Comment」です。それぞれユーザのテーブル、投稿されたスレッドのテーブル、スレッドに対するコメントのテーブルになっています。 「schema. prisma 」に下記のようにテーブル定義します。 // 省略 model User { id String @id @default(uuid()) name String @unique password String authToken String @unique created_at DateTime @default(now()) Comment Comment[] Post Post[] } model Post { id Int @id @default(autoincrement()) userId String content String created_at DateTime @default(now()) Comment Comment[] user User @relation(fields: [userId], references: [id]) } model Comment { id Int @id @default(autoincrement()) userId String postId Int content String created_at DateTime @default(now()) post Post @relation(fields: [postId], references: [id]) user User @relation(fields: [userId], references: [id]) } テーブルが定義されたので、データベースに登録したいところですが、「DATABASE_URL」が未設定なのでその値を取得します。 Supabaseのプロジェクトのサイドバーから「Project Settings」-> 「Database」に移動します。下記のような画面が表示されていれば大丈夫です。 画面の下の方にデータベースの URI がありますのでコピーしましょう。 この URI の[YOUR-PASSWORD]と書かれている部分に自分が設定したパスワードを入力します。 そして、「.env」のDATABASE_URLにペーストします。 // .env DATABASE_URL= [コピーしたデータベースのURI] 準備が整ったので、ターミナルで下記のコマンドを打ちます npx prisma migrate dev --name init 成功すれば、 prisma ディレクト リ下にmigrationsフォルダが作成されます。また、Supabaseの方にもテーブルが作成されていることも確認できます。 データベースを操作する時には「PrismaClient」が必要となるので、 src 下に lib/prisma.ts を作成して、「 prisma .ts」に次のように記述ます。 // src/lib/prisma.ts import { PrismaClient } from "@prisma/client" ; export const db = new PrismaClient () これで、 Prisma によるデータベース操作の際はこのファイルをインポートすればすぐに行えます。 ルーティング作成 では、最初にアプリケーションのルーティングを作っていきます。 SvelteKitは src/routes がデフォルトのルートです。つまり、routes直下に配置されたページは「/」で表示されます。 src/routes/example の直下に配置されたページは「/example」で表示されます。 そして、ページを表すファイルは「+page.svelte」で定義できます。 実際に初期起動時に表示されたページは src/routes/+page.svelte の内容となっています。 すごくシンプルなので簡単にルーティングを作成できます。 改めてこの 掲示 板アプリで必要な画面は下記のようになっています。 新規登録画面 ログイン画面 スレッド一覧画面 スレッド詳細画面 スレッド投稿画面 特に複雑な構造になっていないので、下記のように src/routes 以下にフォルダを作成していきましょう。 一覧画面はデフォルトのルートに表示するため、フォルダは作成しなくて問題ありません。 詳細画面となる detail/[postId] の[postId]は動的なルーティングに対応します。 例えば、「/detail/1」や「detail/sample」などを入力しても detail/[postId] 直下にある「+page.svelte」が表示されます。 ルーティングは完成しました。 新規登録画面 最初に新規登録画面を作成いたします。 /register 下に「+page.svelte」を作り、下記のような登録フォームを作ります。 デザインは全く取り入れてないので少し見にくいかもです。 // register/+page.svelte < h1 > ユーザー登録 < /h1 > < form method = "post" action = "?/register" > < label > 名前: < input name = "name" type= "text" / > < /label > < label > パスワード: < input name = "password" type= "password" / > < /label > < button > 登録する < /button > < /form > 当然ながらこれではボタンを押してもリク エス ト先がないため400系のエラーしか返ってきません。 サーバサイドのロジックを提供するために必要なのは「+page.server.ts」です。同じ ディレクト リ内に作るだけで大丈夫です。 余談ですが、SvelteKitのページは「+page.ts」「+page.server.ts」によって支えられています。「+page.ts」はクライアントサイドで実行されます。クライアント側のロジックやユーザインタ ラク ションの処理に用いるのが推奨されます。「+page.server.ts」はサーバー側で実行されます。 SSR のロジックや、データベースからデータを取得する処理などを記述することが推奨されます。 「+page.server.ts」に次のように記述します。 // register/+page.server.ts import { fail , redirect } from "@sveltejs/kit" ; import type { Actions } from "./$types" ; import bcrypt from 'bcrypt' import { db } from "$lib/prisma" ; export const actions: Actions = { register : async ( { request } ) => { const data = await request.formData (); const name = data. get( "name" ); const password = data. get( "password" ); if (typeof name !== "string" || typeof password !== "string" || ! name || ! password ) { return fail ( 400 , { message: "名前・パスワードは必須です。" } ) } const user = await db.user.findUnique ( { where: { name } } ) if ( user ) { return fail ( 400 , { message: "既に存在するユーザーです。" } ) } await db.user.create ( { data: { name , password : await bcrypt.hash ( password , 10 ), authToken: crypto.randomUUID (), } , } ) throw redirect ( 303 , '/login' ) } } actionsには様々なサーバー側の処理を記述します。型はActionsです。Actionsのインポート先である ./$types はSvelteKitの コンポーネント や関数の型情報を提供しています。そして、Actionsはイベントや条件に対応して実行される関数です。 上記のコードでは「register」が定義されています。これは「+page.svelte」のformタグのactionの値に対応します。 引数にはrequestを入れていますが、他には cookie やsetHeaderなどサーバーサイドでの処理に必要な情報が提供されます。 具体的なコードの説明としては、下記のようになります。 1. request.FormData()でformで入力した名前とパスワードのデータを取得します。 2. data.get([inputのname値])で各々のデータを取得します。 3. 型のチェックと入力のチェックをします。もし、未入力の場合はfail関数を返します。第一引数に ステータスコード を第二引数に任意のデータをオブジェクトで入れます。 4. チェックが問題なければ、 Prisma を使って、既に存在する名前かデータベースに検索をかけます。存在しているならfail関数を返します。 5. 存在していないユーザーならCreateで名前とハッシュ化したパスワード、 トーク ンを登録します。 6. 全て完了すれば、redirect関数でログイン画面にリダイレクトさせます。 SvelteKitはデータベースへの接続を自動で切ってくれるのでdisconnectの処理を入れなくても問題ありません。 「+page.server.ts」での処理も書き終わりました。 登録ができて、ログイン画面のURLに飛ばされれば大丈夫です。僕は名前「user」パスワード「password」で登録すると、Supabaseの方にも反映されていました。 正常処理は実装できましたが、存在するユーザー名を入力した時に返されてくるfail関数の処理を実装できていません。 「+page.svelte」に下記のコードを追加するだけで簡単に登録バリデーションが実装できます。 // register/+page.svelte < script lang = "ts" > import type { ActionData } from "./$types" export let form: ActionData < /script > < h1 > ユーザー登録 < /h1 > { # if form?.message } < p class= "error" > { form?.message } < /p > { / if } /* HTML省略 */ < style > .error { color: red ; } < /style > actionはリク エス ト処理後、次の更新まで対応のページのformプロパティにデータを返します。つまり、fail関数の第二引数のデータはformに返されます。なので、form.messageでバリデーションメッセージを取り出せます。 実際に存在する名前を入力すると、次のように表示されると思われます。 これで新規登録画面が作成できました。 ログイン画面 ログイン画面を作ります。 まずは /login 下に「+page.svelte」を作成し、ログインフォームを作ります。 登録フォームとほとんど一緒です。今回は「+page.server.ts」からくるバリデーションメッセージを想定して、formを用意しておきます。 // login/+page.svelte < script lang = "ts" > import type { ActionData } from './$types' ; export let form: ActionData < /script > < h1 > ログイン画面 < /h1 > { # if form?.message } < p class= "error" > { form.message } < /p > { / if } < form method = "POST" action = "?/login" > < label > 名前: < input name = "name" type= "text" / > < /label > < label > パスワード: < input name = "password" type= "password" / > < /label > < button > ログイン < /button > < /form > < style > .error { color: red ; } < /style > このログイン機能ではセッションを使うので、「+page.server.ts」では、引数にrequestだけでなくcookiesを取り入れます。 // login/+page.server.ts import { db } from "$lib/prisma" ; import { type Actions , fail , redirect } from "@sveltejs/kit" ; import bcrypt from "bcrypt" ; export const actions: Actions = { login: async ( { request , cookies } ) => { const data = await request.formData (); const name = data. get( "name" ); const password = data. get( "password" ); if (typeof name !== "string" || typeof password !== "string" || ! name || ! password ) { return fail ( 400 , { message: "名前とパスワードを入力してください" } ) } const user = await db.user.findUnique ( { where: { name } } ) if ( ! user ) { return fail ( 400 , { message: "名前またはパスワードを間違えています" } ); } const correctPassword = await bcrypt.compare ( password , user.password ) if ( ! correctPassword ) { return fail ( 400 , { message: "名前またはパスワードを間違えています" } ) } const authenticatedUser = await db.user.update ( { where: { name } , data: { authToken: crypto.randomUUID () } } ) cookies. set( 'session' , authenticatedUser.authToken , { path: '/' , maxAge: 60 * 60 * 24 * 30 , } ) throw redirect ( 303 , '/' ) } } 前半部分は入力に対してのバリデーション、そして次に名前でUserテーブルに検索をかけ、存在すれば、パスワードを一致するか比較する。 パスワードも一致していれば、該当のユーザーのauthTokenを更新する。 そして、最後にcookiesにauthTokenをセットします。setの第一引数には cookie の名前、第二引数には値を、そして第三引数にはオプションをセットします。オプションにはいろいろありますが、今回はアプリ全体で使用したいのでpathを「/」にし、有効期限は1ヶ月にしました。 ログイン画面で名前に先ほど登録した名前、パスワードでログインしましょう。 成功すれば初期画面にリダイレクトされ、 cookie が登録されるはずです。 ログイン機能ができましたが、今のままではログインを通らずに一覧を見れてしまうため、 cookie の意味がありません。 なので、ログインと新規登録画面以外には cookie が登録されていない場合には遷移できないようにします。 「ログイン・新規登録画面以外に cookie 登録されていない場合」のような処理を書くのはあまりにも非効率です。一度に全ての画面に処理を反映したいですよね。 そのような時はルート ディレクト リ src/routes に「+layout.server.ts」を作成します。そして下記のようなコードを記述してください。 // +layout.server.ts import { redirect } from "@sveltejs/kit" ; import type { LayoutServerLoad } from "./$types" ; export const load: LayoutServerLoad = async ( { url , cookies } ) => { const session = await cookies. get( 'session' ) if( ! session && ( url.pathname !== '/login' && url.pathname !== '/register' )) { throw redirect ( 303 , '/login' ) } } 「+layout.server.ts」は作成された ディレクト リ以下の全てに処理を入れることができるファイルです。なので、遷移するたびに挟みたい処理などはlayoutファイルを使うことが推奨されます。 後にHeader作成時にも使いますが「+layout.svelte」を使うと同様にその ディレクト リ以下、全てにファイルの情報が共有されます。共通部分を作るときに推奨されます。 load関数はページが読み込まれる前に行う処理を記述できます。 cookiesにsessionが登録されていないときにログイン画面と登録画面以外に遷移すると強制的にログイン画面にリダイレクトされるようにしました。 これでログインせずに一覧へ遷移することはできなくなりました。 スレッド投稿画面 スレッドの投稿画面を実装します。 順序的には一覧画面から実装するべきかもしれませんが、一覧は投稿がなければ表示するものがないので、先に投稿できるようにします。 post/ 下に「+page.svelte」「+page.server.ts」を作成します。 「+page.svelte」は次のようなコードにします。コードは登録画面やログイン画面とあまり変わりません。 // post/+page.svelte < script lang = "ts" > import type { ActionData } from "./$types" export let form : ActionData < /script > < h1 > スレッド投稿 < /h1 > { # if form?.message } { form.message } { / if } < form method = "POST" action = "?/post" > < label > 内容: < input name = "content" type= "text" / > < /label > < button > 投稿する < /button > < /form > 「+page.server.ts」は少し考えなければなりません。 スレッドを投稿するためには、postテーブルにデータを渡すのですが、登録にはuserIdが必要となります。もちろん、userテーブルからauthTokenで検索をすれば解決する話です。しかし、コメント投稿の際にも同じようにuserIdが必要になります。このような部分は共 通化 しましょう。 src 下に「hooks.server.ts」を作成します。「hooks.server.ts」はサーバーがリク エス トを受けるたびに実行されます。このファイル内でユーザー情報を検索し、アプリケーション内で共有できるようにすれば、何度もuserを検索するコードを書く手間が省けます。 「hooks.server.ts」は次のように記述してください。 // src/hooks.server.ts import { db } from "$lib/prisma" ; import type { Handle } from "@sveltejs/kit" ; export const handle: Handle = async ( { event , resolve } ) => { const session = event.cookies. get( 'session' ); if ( ! session ) { return await resolve ( event ) } const user = await db.user.findUnique ( { where: { authToken: session } , select: { id: true , name: true } } ) if ( user ) { event.locals.user = { id: user.id , name: user.name } } return await resolve ( event ) } userテーブルから検索をするためにsessionの値が必要となるのでcookiesを取得します。また、検索をするときはユーザー情報を全て取り出すのではなく、IDと名前だけ取り出します。 そして、event.localsにuser情報を格納します。localsに格納した情報はアプリ内で使うことが可能になります。 TypeScriptで書いている方はuserを型定義しないといけません。「app.d.ts」を次のように書き換えてください。 // src/app.d.ts declare global { namespace App { // interface Error {} interface Locals { user: { id: string , name: string } } // interface PageData {} // interface Platform {} } } export {} ; これにより、「+page.server.ts」は次のように書くだけで投稿することができるようになります。 // post/+page.server.ts import { db } from "$lib/prisma" ; import { fail , redirect , type Actions } from "@sveltejs/kit" ; export const actions: Actions = { post : async ( { request , locals } ) => { const data = await request.formData () const content = data. get( "content" ); if (typeof content !== "string" || ! content ) { return fail ( 400 , { message: "タイトルと内容は必須入力です。" } ) } if ( ! locals.user ) return fail ( 400 , { message: "登録されていないユーザーです。" } ) await db.post.create ( { data: { userId: locals.user.id , content } } ) throw redirect ( 303 , '/' ) } } sessionを取得せずに、localsからuserIDを取り出して、postテーブルのuserIdに割り当てています。 投稿が成功すれば、一覧へリダイレクトします。 これで投稿画面が完成しました。 スレッド一覧画面 スレッドを一覧できる画面を実装します。 一覧では、それぞれのスレッドを コンポーネント を使って表示してみたいと思います。 src/lib に「Thread.svelte」を作成してください。コードは次のようにしてください。 // lib/Thread.svelte < script lang = "ts" > export let id : number ; export let content: string ; < /script > < div class= "thread" > < div class= "thread-content" > < h2 > { content } < /h2 > < a href = "detail/{id}" > スレッドの詳細を見る < /a > < /div > < /div > < style > .thread { display: flex ; flex-direction: row ; width: 100 % ; margin: 10px auto ; box-shadow: 0 10px 20px rgba ( 0 , 0 , 0 , 0.19 ), 0 6px 6px rgba ( 0 , 0 , 0 , 0.23 ); } .thread-content { width: 100 % ; margin-left: 10px ; } .thread-content a { display: inline-block ; font-size: 14px ; color: blue ; text-decoration: none ; } < /style > 少しだけデザインをしてみました。目的は コンポーネント を作ることなので正直デザインはどちらでも大丈夫です。 大事なポイントはIDとContentを変数で宣言し、それをHTML内で使用していることです。ReactのPropsと同じです。 では一覧を作成していきます。 src/routes 下にデフォルトで作成されていた「+page.svelte」のコードを下記のように書き換えてください。 // +page.svelte < script lang = "ts" > import { goto } from "$app/navigation" ; import type { PageData } from "./$types" ; import Thread from "$lib/Thread.svelte" ; export let data: PageData < /script > < main > < h1 > スレッド一覧 < /h1 > < button type= "button" on:click = { () => goto( "/post" ) } > 投稿する < /button > { #each data.threads as thread } < Thread id = { thread.id } content = { thread.content } / > { /each } < /main > Thread コンポーネント は他と同じようにインポートするだけで使えるようになります。 PageData型はサーバーサイドで生成されたデータを、クライアント側で使用するためのものです。なので、dataにはサーバー側からスレッド一覧のデータが送られてくることを想定しています。 HTMLには「投稿する」ボタンとeach文でスレッド コンポーネント を並べていきます。Propsと同じでidにthread.idを、contentにthread.contentを渡しています。 「投稿する」ボタンをクリック時には、goto関数で投稿画面へナビゲーションをしています。 次に、「+page.server.ts」を作成して次のようなコードにしましょう。 // +page.server.ts import { db } from "$lib/prisma" import type { PageServerLoad } from "./$types" ; export const load: PageServerLoad = async () => { const threads = await db.post.findMany ( { orderBy: { id: 'desc' } , } ); return { threads } } データを取得したいタイミングは一覧表示前なので、PageServerLoad型を使います。 そして、postテーブルからID降順で全件取得するだけです。 うまくいけば、次のように投稿した内容がThread コンポーネント を介して表示されるはずです。 また、「スレッドの詳細を見る」を押すと、「/detail/[id]」に飛ぶことも確認できます。もちろん、詳細画面は未作成なので404エラーが表示されます。 スレッド詳細画面 詳細画面では、コメントを投稿することができるようにします。と言っても、特に新しいことをするわけではありません。 いつも通り、「+page.svelte」「+page.server.ts」を作成します。 これからやるべき事は load関数で詳細情報(タイトル・作成者・作成日時)とコメント(内容・作成者・作成日時)の取得 詳細画面にスレッドの情報と投稿されたコメントを表示 コメントフォームの作成 コメントを投稿するアクションの作成 今までやってきたことの振り返りみたいな感じですね。 「load関数で詳細情報とコメントの取得」からやっていきます。 postテーブルから詳細情報を、commentテーブルからpostIdに該当するコメントを取得してくるのがシンプルでいいかもしれませんが、postテーブルが定義されている「schema. prisma 」を見返してみましょう。 model Post { id Int @id @default(autoincrement()) userId String content String created_at DateTime @default(now()) Comment Comment[] user User @relation(fields: [userId], references: [id]) } PostテーブルはCommentの情報を持っています。また、Userの情報も取り出せそうです。 なので、次のように書くことで複雑ではありますが一回の操作で欲しい情報を取得することが可能です。 // detail/[postId]/+page.server.ts import { db } from "$lib/prisma" import { fail } from "@sveltejs/kit" ; import type { PageServerLoad } from "./$types" export const load: PageServerLoad = async ( { params } ) => { const threadDetail = await db.post.findUnique ( { where: { id : Number ( params.postId ) } , include: { Comment : { orderBy: { id : 'desc' } , select: { content: true , created_at: true , user: { select: { name: true } } } } , user: { select: { name: true } } } , } ); if( ! threadDetail ) throw error ( 404 , { message: "存在しないスレッドです。" } ) return { threadDetail } } paramsには { postId : {id} } が入っているため、params.postIdで必要なIDを取得できます。 次にincludeを使って、Commentテーブルからはコメントと作成日時、作成者を取り出していますUserテーブルからはこのスレッドの作成者を取り出しています。 includeとは、関連するデータを一緒に取得するために用いられます。これによりクエリ数が減少し、パフォーマンスが上がるというメリットがあります。 スレッドの詳細情報とそれに付随するコメントをまとめたthreadDetailをクライアント側に返します。もし、存在しないスレッドの場合はエラーを投げます。エラーページについては後で書きます。 「 詳細画面にスレッドの情報と投稿されたコメントを表示」 をします。「+page.svelte」に次のコードを書いてください。 // detail/[postId]/+page.svelte < script lang = "ts" > import type { ActionData , PageData } from "./$types" ; type comment = { user: { name: string } content: string created_at: Date } export let data: PageData ; const threadAuthor : string = data.user.name const threadContent : string = data.threadDetail.content const threadCreatedAt: Date = data.threadDetail.created_at const comments : comment [] = data.threadDetail. Comment < /script > < div > < a href = "/" > < 一覧に戻る < /a > < /div > < h1 > スレッド詳細 < /h1 > < h2 > { threadContent } < /h2 > < p > 作成者: { threadAuthor } 作成日時: { threadCreatedAt } < /p > < h2 > コメント < /h2 > { # if comments.length } { #each comments as comment } < p > 名前: { comment.user.name } < /p > < p > 日時: { comment.created_at } < /p > < p > コメント: { comment.content } < /p > < br / > { /each } { : else } < p > コメントはありません < /p > { / if } サーバーから取得してきたデータは少々複雑になっているので、スレッド作成者、内容、作成日時、コメントの4つに分けます。 また、コメントの中にも名前、作成日時、内容があるのでcomment型を定義して、commentsに格納します。今回の場合、カスタムな型定義がこれだけなので同じファイルに書きましたが、複数ある場合は src/lib 下などに型定義用のファイルなどを作るようにしましょう。 スレッドの情報が取得できていれば、下記のように画面に表示されると思います。 続いて、「コメントフォームの作成 」をします。全然難しくありません。「+page.svelte」に次のコードを加えるだけです。 // detail/[postId]/+page.svelte < script lang = "ts" > // 省略 /* 追加 => */   export let form: ActionData ; < /script > /* HTML省略*/ < div > { # if form?.message } < p class= "error" > { form.message } < /p > { / if } < /div > < form method = "POST" action = "?/comment" > < input name = "comment" type= "text" / > < button > コメントする < /button > < /form > < style > .error { color: red ; } < /style > "コメントがありません"の下にコメント用のフォームが作成されたはずです。 あとは「コメントを投稿するアクションの作成」をすれば終わりです。「+page.server.ts」のload関数の下に次のコードを加えてください。 // detail/[postId]/+page.server.ts export const actions: Actions = { comment : async ( { request , locals , params } ) => { const data = await request.formData (); const comment = data. get( "comment" ); if (typeof comment != "string" || ! comment ) { return fail ( 400 , { message: "コメントは必須入力です。" } ) } await db.comment.create ( { data: { userId: locals.user.id , postId: Number ( params.postId ), content: comment } } ) } } 詳細画面で適当なコメントをしてみましょう。 ID降順なので、最新のものが上に来るようになっていれば問題ありません。 また、他のスレッド詳細を見てもコメントがないことを確認しておきましょう。 詳細画面も完成しました。 残すはログアウト機能とエラーページ作成です。しかし、やる事はそれほど難しくありません。おまけみたいな感じで聞いてもらえるとありがたいです。 ログアウト機能 ログアウトするというのは cookie 情報を消すということです。それだけです。 src/routes 下にlogoutフォルダとlogoutフォルダ下に「+page.server.ts」を作成してください。 「+page.server.ts」内で行うことは cookie を消して、ログイン画面にリダイレクトさせるだけです。念の為にもし、「/logout」に遷移した場合でもload関数で一覧へ戻るようにしておきます。 // logout/+page.server.ts import { redirect } from "@sveltejs/kit" import type { Actions } from "./$types" export const load: PageServerLoad = async () => { throw redirect ( 303 , '/' ); } export const actions: Actions = { logout: async ( { cookies } ) => { cookies. delete( 'session' ); throw redirect ( 303 , '/login' ) } } ログアウトの処理はできたのでログアウトボタンをヘッダーに配置したいと思います。ヘッダーは全ての画面で共有したいので、 src/routes 下に「+layout.svelte」を作成します。 「+layout.svelte」は次のようなコードにします。 // +layout.svelte < script lang = "ts" > import { page } from '$app/stores' < /script > { # if ! $page.data.user } < a href = "/login" > ログイン < /a > < a href = "/register" > 新規登録 < /a > { : else } < form action = "/logout?/logout" method = "POST" > < button > ログアウト < /button > < /form > { / if } < slot / > $app/storesは「+layout.server.ts」から返されるlocals.userの情報を含んでおり、ログインしていれば「$page.data.user」には値が入っているのでログアウトを表示し、値がない時には新規登録とログインへのリンクを表示します。 ログアウトボタンを押せば、 logout/+page.server.ts のlogoutが発火します。 これでログアウト機能ができました。 エラー画面 最後にエラー画面を作っていきます。SvelteKitでは簡単にエラー時に特定のエラー画面に飛ばすことができます。 先ほど、詳細画面を作成しているとき、存在しないIDを取得しようとすると「+page.server.ts」ではerrorを投げていました。 // detail/[postId]/+page.server.ts if( ! threadDetail ) throw error ( 404 , { message: "存在しないスレッドです。" } ) SvelteKitでは、loadの中でerrorが発生した場合、最も近くにある「+error.svelte」が呼び出されます。なので、 detail/[postId] に「+error.svelte」を作成することでカスタムなエラー画面を表示させることができます。 // detail/[postId]/+error.svelte < script lang = "ts" > import { page } from '$app/stores' ; < /script > < h1 > エラーページ < /h1 > < h3 > { $page.error?.message } < /h3 > pageからerrorに格納したmessageを取り出すことができます。とても簡単に実装できました。 実際に「detail/[存在しない投稿のID]」に訪れると、エラーページが表示されました。 ちなみに、エラーが発生した場所から最も近い「+error.svelte」が呼び出されるという特徴から、 src/routes/ 下に「+error.svelte」を作るとログイン画面や登録画面でerrorが投げられても src/routes/+error.svelte が表示されるようになります。 404エラー画面が完成しました。 しかし、500エラーには対応できていません。例えば、「/detail/sample」に訪れてください。postIdは数値を想定しているので文字列を入れられると500エラーが発生します。 「+error.svelte」が表示されると思います。 特に問題はないのですがデフォルトの文字列が表示されています。この文字列を変えたい場合には「hooks.server.ts」に次のコードを加えるだけです。 // src/hooks.server.ts export const handle: Handle = async ( { event , resolve } ) => { // 省略 } export const handleError: HandleServerError = ( { event } ) => { return { message: event.url.pathname + "で500エラーが発生!" , } ; } HandleServerErrorは予期せぬエラー発生時に実行されます。もう一度、「/detail/sample」に訪れると表示が変わると思います。 今回はパスを表示していますが、他にも色々な情報を表示できるのでお好きなように設定してください。 500エラーにも対応したエラー画面が完成しました。 これにて、シンプルすぎる 掲示 板アプリが完成しました!お疲れ様でした!! 本来であれば、自身の投稿やコメントを編集・削除できるべきなのですが、記事がさらに長くなってしまうことと、投稿を実装できていれば編集・削除はそれほど難しくないため省かせていただきました。ぜひ、独力で実装してみてください! 終わりに 今回は 掲示 板アプリ作成を通してSvelteKitの基礎をご紹介させていただきました。 と言っても、まだまだSvelte・SvelteKitには様々な機能がありますので、これを機にもっと勉強していただけると嬉しいです。 僕自身も最近になって勉強し始めたばかりなので、もっとSvelte・SvelteKitの知識を深めていくつもりです。 本当に長い記事になってしまいましたが、ここまで読んでいただいた方々、少しでも覗いていただけた方々、ありがとうございました! エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
はじめに こんにちは、 ラク スフロントエンド開発課の斉藤です。 記事タイトルはReact開発者なら知る人ぞ知る りあクト! TypeScriptで始めるつらくないReact開発 のパロディです。とてもわかりやすい入門書なのでReact初学者の方には学びの第一歩として自信を持ってオススメできます! さて今回は、モダンなフロントエンド技術を採用したうえで、極力シンプルで開発体験を損なわないような ディレクト リ構成を考えてみたので共有したく記事にしました。現在実際に運用しているのですが、今のところ大きな問題も無くチームからの不満も上がっていません。しかし、個人的に微妙な部分もあるのでそちらの紹介も行いたいと思います。 今回、構成を考えるにあたって重視したポイントは以下の3点です。 新しく参入するメンバーでもすぐに理解できるシンプルな構成にしたい テストやリファクタしやすい構成にしたい できればルールが厳しすぎず、快適な開発がしたい ラク スフロントエンドチームは現在急拡大中で、新卒や中途から新しく開発に参加してくれるメンバーも多くなってきました。したがって 新しく参入するメンバーでもすぐに理解できるシンプルな構成 にすることは MUST です。また、新規に開発する製品は長く開発が続いていく (はず) と見込まれるためプロジェクトの規模が大きくなっても、カオスになりにくくテストしやすい構成を目指します。 現在実際に運用しているのですが、今のところチームからの不満も上がっておらず、問題なく快適に開発できています。しかし個人的に微妙な部分もあるのでその辺りの紹介もしていければと思います。 前提として採用している主なライブラリは以下になります。 React v18 MaterialUI ReactQuery Recoil ReactHookForm zod 結論 src/ ├─ common/ ├─ components/ │ ├─ features/ │ │ ├─ generic/ │ │ ├─ todo/ │ │ │ ├─ container/ │ │ │ ├─ presenter/ │ │ │ ├─ hooks/ │ │ │ ├─ actions.ts │ │ │ ├─ todo.constant.ts │ │ │ ├─ todo.type.ts │ │ │ └─ todo.validation.ts │ ├─ ui/ │ └─ pages/ ├─ lib/ フロントエンドの ディレクト リ構成としてまず思いつくのがアトミックデザインですが、最近は bulletproof-react という リポジトリ の アーキテクチャ を参考にした 機能ごとに ディレクト リを分けるパターン が採用されつつあるように思います。例えば @sakito さんが以下の記事で機能ごとに ディレクト リを分ける構成を紹介しています。 zenn.dev アトミックデザインは明確に コンポーネント の責務を分けることができ、再利用性も高いのですが、メンバー間で コンポーネント の切り出し方に差異が生まれやすくカオスになりがちです。一方機能ごとに ディレクト リを分けるパターンは、どのように コンポーネント を切り出せばよいかがイメージしやすく、メンバー間で コンポーネント の分け方にブレが生まれにくいというメリットを感じました。したがって シンプルさ という観点で軍配が上がり、今回重視するポイントにもマッチしていたので機能ごとに分けるパターンをベースとすることにしました。 依存関係は以下のようになっています。 コンポーネント の依存関係 図の上側にある コンポーネント は下の コンポーネント importでき、UIは他のUIを、Containerは他のContainerをimportすることができます。このような依存関係の構成は以下の @yoshiko さんの記事を参考にさせていただきました。 zenn.dev ui ui ディレクト リの中身は以下のようになっています。 ui/ ├─ button/ │ ├─ Button.tsx │ └─ IconButton.tsx ├─ textField/ │ ├─ TextField.tsx │ └─ RhfTextField.tsx ├─ index.ts 基本的にはMUIの コンポーネント をラップし、独自にスタイルを当てた コンポーネント 群になり、 ドメイン を持たせないようにします。 コンポーネント が ドメイン を持つか、持たないかは、その コンポーネント をそのまま別のプロジェクトで使い回すことができるか、否かで判断します。またReactHookFormを使っている場合は、同じui ディレクト リ内にReactHookFormのロジックを付加した コンポーネント を配置します。 RhfTextField.tsx import { FieldValues , useController , UseControllerProps , } from 'react-hook-form' ; import { TextField , TextFieldProps } from './TextField' ; export type RhfTextFieldProps < T extends FieldValues > = TextFieldProps & UseControllerProps < T >; export const RhfTextField = < T extends FieldValues >( props: RhfTextFieldProps < T > ) => { const { name , control } = props ; const { field: { ref , ...rest } , fieldState: { error } , } = useController < T >( { name , control } ); return ( < TextField inputRef = { ref } { ...rest } { ...props } errorMessage = { error && error.message } / > ); } ; またui配下の コンポーネント はindex.tsで一気にexportしてしまいます。 components/ui/index.ts // button export * from "./button/Button" ; export * from "./button/IconButton" ; // textField export * from "./textField/TextFiled" ; export * from "./textField/RhfTextField" ; このようにすることで コンポーネント 使用側でuiパーツを一度にimportすることができます。 import { Button , RhfTextField } from "src/components/ui features features ディレクト リは以下のような構成になっています。 features/ ├─ generic/ ├─ todo/ │ ├─ container/ │ ├─ presenter/ │ ├─ hooks/ │ ├─ (actions.ts) │ ├─ (todo.constant.ts) │ ├─ (todo.type.ts) │ └─ (todo.validation.ts) features内の ディレクト リは画面仕様書単位で分けます。 この運用方法のメリットは誰が開発しても同じ切り分け方になるということです。これにより、実装に入る前にメンバー間で「どのように ディレクト リを分けるか」という議論をしなくてもよくなります。また機能の開発担当も画面仕様書単位で振り分けることで、features ディレクト リを横断して開発を行うことが少なくなりコンフリクトを防ぐことができます。 画面仕様書単位で分けると1つの機能が大きくなりすぎてしまう、という場合は画面仕様書の切り分けを先に検討します。そもそも画面仕様書の記述量が膨大だと読むコストが高く、仕様の把握漏れが頻発してしまいます。これは健全な状態ではないので先に画面仕様書の改善から行っていき、機能を小さく分けた上で開発を行います。 またContainer/Presenterパターンを採用するため、各feature ディレクト リの中にcontainer ディレクト リとpresetner ディレクト リを配置します。ロジックをcontainerやhooksに逃がすことでpresenterを純粋に保ち、テストしやすい構成にします。hooksには機能を実現するために必要な API を叩くカスタムフックや、複雑なロジックをまとめるカスタムフックを記述します。 hooks/ ├─ useFetchTodo.ts ├─ useUpdateTodo.ts ├─ useDeleteTodo.ts ├─ useToggleTodoStatus.ts hooksがあればcontainerは不要かと思われがちですが、ロジックを全てカスタムフックに書くと容易にfatなhooksになってしまうためcontainerがあるとコードも見通しやすくなり便利です。 actions.tsにはその機能で使うRecoilのロジックを記述します。また[feature].constant.tsにはその機能で使う定数、[feature].type.tsには型、[feature].validation.tsにはzodで定義する scheme を記述します。基本的にはこれらのファイルは無くてもよく、コードの見通しが悪くなってきたら作るというユルい運用をしています。 またfeaturesにはgenericというプロジェクト内共通の機能を担う ディレクト リも作成します。こちらもtodo ディレクト リと同じようにcontainer、presenter、actions.tsなどを持ちます。こちらの ディレクト リ内には ドメイン を持ち(=他のプロジェクトには流用できない)、アプリケーション内の様々な場所で汎用的に用いられる機能のロジックを記述します。例えばヘッダに表示するユーザープロフィール コンポーネント などはこちらに記述します。 pages pagesはurl単位で分けfeatures内のcontainerを呼び出すだけです。 import { TodoContainer } from 'src/components/features/todo/container/TodoContainer' ; export const Todo = () => { return < TodoContainer / > } ; 依存関係としてはrouter→pages→containerのようになっています。routerから直接containerを呼び出しても良いのですが、将来的にSuspenseやErrorBoudanryを導入したり、特定のページだけサイドバーを表示したいといった需要に対応しやすいようにこのような構成としました。 common commonにはプロジェクト内の様々な場所で呼び出される変数や型などを定義します。 common/ ├── constants.ts ├── cssVariables │ ├── color.ts │ ├── variables.ts │ └── zIndex.ts ├── messages.ts ├── reactQueryKeys.ts ├── recoilKeys.ts ├── types.ts └── utils ├── sum.ts ├── index.ts constants.ts constantsの記述例 export const FONT_SIZE_MAP = { XS: "10px" , S: "12px" , M: "14px" , L: "16px" , XL: "18px" } as const export const FRUIT = { apple: "りんご" , banana: "バナナ" , orange: "みかん" , } as const types.ts typesの記述例 export type Fruit = typeof FRUIT [ keyof typeof FRUIT ] ; export type ErrorResponse = /// cssVariables カラーコードやz-index、ヘッダの高さなどの css にかかわる変数を管理する ディレクト リです。styled-componentを使っているのでtsファイルで管理します。 messages.ts エラーメッセージやバリデーションメッセージを管理するファイルです。 reactQueryKeys.ts/recoilKeys.ts ReactQueryとrecoilは使用する際に一意なキーを設定しなければならないため1つのファイル内で一元管理し、重複しないようにします。 utils 汎用的に使える便利関数を管理する ディレクト リです。 lib ライブラリの初期設定などはこちらの ディレクト リに記述します。 lib/ ├── reactQueryClient.ts ├── zod.ts 改善したいポイント features配下の ディレクト リを画面仕様書単位で分けている点 画面仕様書単位で ディレクト リを分けるとチーム感での ディレクト リ構成の認識齟齬が起こりづらくなるというメリットがある一方、長くプロジェクトを続けていった場合に破綻してしまうのでは無いかという不安があります。プロジェクトの初期段階では機能の数も少ないため、画面仕様書の記述量も少なくすみますが、フェーズ2、3と時が経つごとに機能が追加され、画面仕様書も更新されていきます。画面仕様書が肥大化してきてファイルを分けようとなったとき、 ディレクト リも同時に分けなければルールの一貫性が損なわれてしまいます。しかし、実際に ディレクト リを分割するようリファクタする 工数 をそのときに用意できるかわからないため、有耶無耶になってしまいルールが破綻してしまう懸念があります。 src/common配下のconstantsファイルやrecoilKeysファイルの肥大化 定数やキーは開発が進むに連れて多く書かれるためすぐに肥大化してしまいます。特にrecoilキーなどは一元管理しないとキーの値がかぶりやすくなり、安易にファイルを分け辛く悩ましいです。 constantsファイルとtypesファイルが分かれていてめんどくさい 定数から型を生成することはよくありがちなので、そのままconstantsファイルに書いてしまいたくなることがよくあります。しかしファイル名がconstantsなので型が記述されていると違和感です。constantとtypeの両方を記述するファイルにしてもいいのですが、良いファイル名が思いつきません。また、同じファイルに書いてしまうとコードが容易に肥大化してしまうので面倒ですが分けています。 まとめ 総括としてモダンフロントエンドの つらくないけどちょっとつらい ディレクト リ構成 に落ち着いたと感じています。完璧とは言えませんが比較的開発体験がよく、シンプルな構成にすることはできたのではないかと思います。これから運用を続けていくことで知見を貯めていき、さらなる改善を重ねていきたいです。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
はじめに はじめまして。 ラク スの某インフラ担当者です。 今回は個人的に今後キーになりそうなネットワークの高速化技術について触れたいと思います。 具体的な実装方法やコマンドの話ではありませんので、「へぇ」という感じで読んでいただければ幸いです。 はじめに ネットワーク高速化の必要性 RDMA(Remote Direct Memory Access) RDMA概要 実際に検証してみた DPUについて というわけで ネットワーク高速化の必要性 現代のインフラ界隈は仮想化技術を使うことが当たり前になってきています。 仮想化といえば一昔前は大型のホストサーバを用意してESXiなどを入れた単純なハイパーバイザー型の仮想化が主流でしたが、 近年はより複雑な仮想化/集約が行われるようになりました。 HCI(Hyper Converged Infrastructure)による複数のコンピュートリソース/ストレージの集約だったり SDN(Software Defined Network)によるネットワーク仮想化だったり、 所謂 プライベートクラウド を構築するような事例も世の中にはたくさん増えてきています。 さらにはコンテナ、 kubernetes といったものも注目を集め始めており(ぱっと見はシンプルそうに見えますが)非常に高度な構造の仮想化が主流になろうとしています。 そんなこんなで、集約をしてサーバリソースを有効活用しよう!なんてキラキラした話が空中を飛び交っているわけですが、 ここで無視することが出来ないのがネットワークです。 VM が集約されれば トラフィック は当然集中しますし、HCIで複数ノードを集約した場合にはたくさんの通信がノード間を行き来します。 ネットワークもその集約化に追随できなくてはなりません。 また、AIやら ディープラーニング やらで GPU を使って高速に並列処理を回して処理するようなシステムも 近年話題になる事が多いですが、大掛かりで大規模な処理が増えれば トラフィック への影響も膨大なものとなるでしょう。 GPU で高速処理してるのにネットワークが遅く、結局 GPU が遊んでいるなんてあっては元も子もないので、 今後のIT業界においてはネットワークの高速化、広帯域化は必須と言えると思います。 そこで今回は、このネットワーク周りの処理の高速化について調べてみようと思います。 RDMA(Remote Direct Memory Access ) ネットワークの高速化技術の一つにRDMA(Remote Direct Memory Access )というものがあります。 RDMA概要 RDMAは元々はInfinibandの世界で実装されている技術になります。 Infinibandって何?と思った方もいらっしゃるかと思いますが、Infinibandというのはみんなが慣れ親しんでいるであろう イーサネット とはまた別の 世界線 の話で、 スーパーコンピュータの世界で使われているような超高速ネットワークになります。 RDMAの仕組みは簡単に言うと、データ転送の一連の処理は通常、アプリ⇒メモリ⇒OSにコピー⇒ネットワークカードといったようなコピー処理が必要になってくるが、 そのコピーが遅いしCPUも使うし大変だから、各サーバーのメモリ間をダイレクトに接続して転送しちゃえば良いんじゃ?といった塩梅。 間のメモリからOSに落としていく処理を回避するためCPUリソースもほとんど使わないし処理も高速というハナシ。ゼロ・コピーなどと呼ばれることもあるようです。 そして、この技術をInfinibandだけじゃなく、広く使われている イーサネット の世界にもどうにか持ち込めないか?というニーズも世の中に当然出てくるわけで、 そこで生み出されたのがRoCE(RDMA over Converged Ethernet )で、世に出てきたのは2010年頃のようです。(読み方は「ロッキー」と読むのが主流のようです) どうやってそんなことやるの?と思われるでしょうが、これを実現するにはまずRDMAに対応する専用のネットワークカードが必要になってきます。 この対応ネットワークカードで業界をけん引する有名な製品にMellanox制のConnectXシリーズがあります。 正確に言うと、Mellanoxは2020年に NVIDIA に買収されていますので、現在は NVIDIA の製品ブランドとなります。 あれ、聞いたことあるぞ?というゲーマーさんもいらっしゃると思いますが、はい、あのグラボで有名な NVIDIA さんです。 このRDMAに対応したネットワークカードを使い、かつ、kernelもそれに対応している必要があります。 近年の新しいkernelであればおおよそ対応している(?)と思われますので、ご興味があれば今使用されているOSが対応しているのか確認いただければと思いますが、 さらに加えて、 ロスレス なネットワークを前提として QoS などを設定、高速化NWで効率よく転送するためにMTUを変更する、 帯域がネックにならないように可能ならば25Gbpsや40Gbpsといった広帯域を確保したい、 経路のスイッチもRDMA対応している必要などもあり、実は導入難易度は結構高いです。 また、RoCEと通常の イーサネット ネットワークを混在させた場合の互換性もありません。 そのため、既存のシステムを変更していくというよりは、 RDMAに対応したシステム全体を別で構築したのちに移行するなどの対応が必要となってくるでしょう。 そこのハードルの高さこそありますが、新規システムを1から構築したい場合などはぜひ検討してみていただきたいものになります。 実際に検証してみた ということで、実は弊社ではRDMAを使った高速ネットワークを実際に導入/検証してみました。 正確には イーサネット での導入になりますのでRDMAというかRoCEになり、さらにそのバージョン2にあたるRoCEv2が実装 プロトコル になります。 HCIのシステムおよび VMware 製品を使用して高速なvSAN(※)のシステムを組んで VMware 社様と一緒に共同で ベンチマーク テストなどを実施しました。 ※vSAN 7.0 Update 2からRoCEv2に対応しています。また、vSANで組む場合はキャッシュディスクはNVMeにする必要があります。 正確な ベンチマーク 結果などは現時点での公表が難しいのですが、従来の TCP/IP の イーサネット よりも10~20%はIOPS(※)が上がることが分かりました。 (※read、writeの比率を変えつつ限界まで負荷をかけレイテンシが上がってきた際のIOPS。) ただ、ちょっとややこしいのですが、vSANの通信となると例えばストレージポリシーに従って二重書き込みや三重書き込みをするケース、 あとはキャッシュディスクからキャパシティディスクへデータを落とす処理なども存在するため単純にRoCEv2単体の性能を見る検証という話ではありません。 この件についての詳細はここでは長くなるため割愛しますが、 あくまでvSANなどネットワーク通信がたくさん発生するような場所でRDMAは有効に働く可能性があり、 ぜひ今後導入する場合は検討しておきたい事項の一つになる、という塩梅でとらえていただければ、、と思います。 恐らく、、より細かい数値は別の機会に皆様にお伝えできるはず、、(現在情報を色々まとめています。) なので少々お待ちください! DPUについて 似たような話でインフラ界隈で最近高速化の技術としてDPU(データ プロセッシング ユニット)というものが話題に上がるようになりました。 DPUはCPU、 GPU 、に並んで第3の刺客といった具合の存在で、簡単に言うと NIC にプロセッサを載せてネットワーク処理やストレージ管理、セキュリティ処理などをオフロードさせる仕組みのことを言います。 簡単に図示すると以下のような形です。 全部CPUがやってたやつを・・ こうやって分散すれば良くね? より一層CPUが本来やりたかったアプリケーションの処理に専念することができます。 DPUで有名なのは、ここにも NVIDIA さんの名がありますが、CPU界隈の猛者の Intel や AMD も当然ながら覇権を争っています。 CPUとDPUで親和性があった方が良いのかまだ未検証なので詳細は分かりませんが、ARMベースなのか x86 ベースなのか サーバのCPU アーキテクチャ に何を採用しているのか?というのがDPUの選択にも関わってくるかもしれません。 ちなみに、 VMware でもvSphere 8.0からDPUが対応されるようになり、RDMAと併用することでより一層の高速化を実現することが可能になってるようです。 (まあ併用というかDPU搭載のsmartNICは標準機能として今後RDMAも普通に対応していくんだと思います) VMware の詳しい記事はこちら docs.vmware.com 今後もし仮想基盤の新規導入を検討する場合は、ぜひ検討に入れたい内容になります。 というわけで 本当に雑多で申し訳ないのですが、ネットワーク高速化で今後キーになりそうな技術の簡単なご紹介をしました。 DPUについてもどうにか機会を見つけて検証してみたいと思っております。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
はじめに こんにちは!hy094です。 今回はZero-Runtime CSS -in-JSである「macaron」(macaron- css )の使い方を調べてみたので、 それをまとめたいと思います。 ※本記事は大部分が公式の GitHub と 公式ドキュメント の和訳で構成されています。 ※英語がとても苦手なので翻訳アプリを駆使して書いています。誤りがあったらこっそり教えていただけると嬉しいです。 はじめに macaron(macaron-css)って結局どうなの? macaron(macaron-css)って何? どんな特徴があるの? どうやってインストールするの? バンドラの設定は? どうやって使うの? 1. styled componentを作成する 2. スタイルを追加する 3. variantsを追加する 4. デフォルトのvariantsを設定する 5. コンポーネントをレンダリングする 6. サンプルコードを見てみる おわりに macaron(macaron- css )って結局どうなの? Vanilla-Extract の辛い部分である 動的なスタイルがやや当てにくい 同じファイル内でスタイルと コンポーネント の両方を記載できない といった問題が解決されていて、とてもよかったです。 (1点目は私の問題、2点目は好みの問題かもしれませんが・・・) 感覚的には styled-components と Stitches と Vanilla-Extract の いいとこ取り をしたライブラリといった印象ですね。 macaron(macaron- css )って何? 一言で言うと CSS -in-JS with zero runtime, type safety and colocation github.com 和訳すると「ゼロランタイム・型安全・コロケーションを実現する CSS -in-JS」です。 アイコンから察するに、おそらくお菓子のマ カロン で合っていると思います。 また、1.0.0のリリースが2022/12/06とかなり新しいライブラリとなっています。 正式名称が macaron-css なのか macaron なのか迷いどころですが、 本記事では公式サイトの macaron.js.org を信じて以降は macaron と呼称します。 公式サイトは以下を参照してください。 macaron.js.org どんな特徴があるの? macaronの特徴として、以下のようなものがあります。 ビルド時にスタイルを抽出する(ゼロランタイムである) スタイルと コンポーネント のコロケーション(繋がりが強くなる) TypeScriptのサポート styled-componentsとvanillaの両方の API をサポート Stitches のようなvariants API が利用できる 追加の設定など不要でReact,Solidで使用可能 esbuild,viteのサポート どうやってインストールするの? npm/yarnからインストールできます。 Reactの場合は以下のコマンドを実行します。 # npm npm install @macaron-css/core @macaron-css/react # yarn yarn add @macaron-css/core @macaron-css/react Solidの場合は以下のコマンドを実行します。 # npm npm install @macaron-css/core @macaron-css/solid # yarn yarn add @macaron-css/core @macaron-css/solid この後は yarn x React x Vite での環境を想定して記載します。 そのほかの書き方などは 公式ドキュメント )を参照してください。 バンドラの設定は? viteの プラグイン をインストールします。 yarn add @macaron-css/vite vite.config.js に以下のように記載します。 import { macaronVitePlugin } from '@macaron-css/vite' ; import { defineConfig } from 'vite' ; export default defineConfig ( { plugins: [ macaronVitePlugin (), // other plugins ] , } ); macaronVitePlugin() は他の プラグイン の設定よりも 前に 記載する必要があるとのことです。 どうやって使うの? 1. styled componentを作成する @macaron-css/react から styled をインポートし、styled componentを作成します。 import { styled } from '@macaron-css/react' ; const Button = styled ( 'button' , {} ); 2. スタイルを追加する コンポーネント に適用される基本スタイル(base styles)を追記します。 hoverやメディアクエリ、ネストされた セレクタ などを含めることが可能です。 macaronの全てのstyling API は入力としてstyle objectを受け取り、型安全です。 その恩恵としてオートコンプリートもされます。 import { styled } from '@macaron-css/react' ; const Button = styled ( 'button' , { // --- ↓add↓ --- base: { backgroundColor: 'gainsboro' , borderRadius: '9999px' , fontSize: '13px' , padding: '10px 15px' , ':hover' : { backgroundColor: 'lightgray' , } , } , // --- ↑add↑ --- } ); 3. variantsを追加する variantsとは、可変のスタイルを実装しやすくするための機能です。 例えば以下のようなものです。 // Buttonコンポーネントでvariantsを設定 const Button = styled ( "button" , { variants: { color: { violet: { color: "blueviolet" } , gray: { color: "gainsboro" } , } , } , } ); // 利用側でこう使える () => < Button color = { "violet" } > hello ! < /Button > macaronでもvariantsキーを利用し、variantsを追加できます。 また、追加できる数に制限ありません。 そしてもちろん、基本スタイルと同様にstyle objectを受け取ります。 これを活用することで、動的なスタイル変更が容易になります。 import { styled } from '@macaron-css/react' ; const Button = styled ( 'button' , { base: { backgroundColor: 'gainsboro' , borderRadius: '9999px' , fontSize: '13px' , padding: '10px 15px' , ':hover' : { backgroundColor: 'lightgray' , } , } , // --- ↓add↓ --- variants: { color: { violet: { backgroundColor: 'blueviolet' , color: 'white' , ':hover' : { backgroundColor: 'darkviolet' , } , } , gray: { backgroundColor: 'gainsboro' , ':hover' : { backgroundColor: 'lightgray' , } , } , } , } , // --- ↑add↑ --- } ); 4. デフォルトのvariantsを設定する defaultVariants を利用して、デフォルトのvariantsを設定できます。 この例の場合、何も指定しなければ color='violet' となります。 import { styled } from '@macaron-css/react' ; const Button = styled ( 'button' , { base: { backgroundColor: 'gainsboro' , borderRadius: '9999px' , fontSize: '13px' , padding: '10px 15px' , ':hover' : { backgroundColor: 'lightgray' , } , } , variants: { color: { violet: { backgroundColor: 'blueviolet' , color: 'white' , ':hover' : { backgroundColor: 'darkviolet' , } , } , gray: { backgroundColor: 'gainsboro' , ':hover' : { backgroundColor: 'lightgray' , } , } , } , } , // --- ↓add↓ --- defaultVariants: { color: 'violet' , } , // --- ↑add↑ --- } ); 5. コンポーネント を レンダリング する 通常のReact コンポーネント と同じように使用できます。 macaronでは styled-components のように コンポーネント と同じファイル内でスタイルを宣言できるため、より繋がりが強くなります。 これをmacaronは 真の コロケーション( true colocation)と表現しています。 import { styled } from '@macaron-css/react' ; const Button = styled ( 'button' , { base: { backgroundColor: 'gainsboro' , borderRadius: '9999px' , fontSize: '13px' , padding: '10px 15px' , ':hover' : { backgroundColor: 'lightgray' , } , } , variants: { color: { violet: { backgroundColor: 'blueviolet' , color: 'white' , ':hover' : { backgroundColor: 'darkviolet' , } , } , gray: { backgroundColor: 'gainsboro' , ':hover' : { backgroundColor: 'lightgray' , } , } , } , } , defaultVariants: { color: 'violet' , } , } ); // --- ↓add↓ --- () => < Button color = "gray" > Click me ! < /Button >; // --- ↑add↑ --- 6. サンプルコードを見てみる 最後に、公式の github に記載されているサンプルコードです。 今までの1~5に記載した内容で概ね理解できるかと思います。 import { styled } from '@macaron-css/react' ; const Button = styled ( 'button' , { base: { borderRadius: 6 , } , variants: { color: { neutral: { background: 'whitesmoke' } , brand: { background: 'blueviolet' } , accent: { background: 'slateblue' } , } , size: { small: { padding: 12 } , medium: { padding: 16 } , large: { padding: 24 } , } , rounded: { true : { borderRadius: 999 } , } , } , compoundVariants: [ { variants: { color: 'neutral' , size: 'large' , } , style: { background: 'ghostwhite' , } , } , ] , defaultVariants: { color: 'accent' , size: 'medium' , } , } ); // Use it like a regular solidjs/react component function App () { return ( < Button color = "accent" size = "small" rounded > Click me ! < /Button > ); } compoundVariants は組み合わせのvariantsです。 このサンプルコードでは、 color={'neutral'} かつ size={'large'} の場合に、 background: 'ghostwhite' が反映されます。 おわりに 新しく出てきたばかりで これから伸びていく感 をひしひしと感じるmacaron、いかがでしたでしょうか? 新しすぎることや日本語のドキュメント・記事が全くないこともあり業務で使うにはまだ早いと思いますが、 個人開発ではどんどん使っていきたいポテンシャルを感じました。 macaronは 次に来る CSS -in-JS かもしれません。興味が湧いた方はぜひ使ってみてください。 そして日本語の記事を書いてくれると僕が喜びます笑 それでは、ご覧いただきありがとうございました! エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは、tatsumiです。 システム開発 を行っていると、非同期処理を実装する場面は少なくないと思います。 ということで、今回は私自身の備忘録も兼ねて、Laravelでの非同期処理についてまとめてみました。 Laravelで非同期処理を行うには? Laravelでキューを使ってみよう キュー用のテーブルを作成する ジョブクラスを作成する ジョブクラスのタイムアウト時間/試行回数を設定する キューにジョブを投入する キューを非同期実行に変更する おまけ ShouldBeUniqueインターフェイスを実装する uniqueIdを定義する さいごに Laravelで非同期処理を行うには? Laravelで非同期処理を行う場合、キューを利用します。 まず、キューを利用する上での登場人物3名を紹介します。 キュー キューはある決まった処理を非同期で実行するための仕組みのことです。 キューにジョブを登録し、登録されたジョブから実行されていきます。 ジョブ ジョブは実行する処理自体のことを指します。 ワーカー ワーカーはジョブを処理するプロセスのことを指します。 キューについて理解していない方は、スーパーのレジを想像してみてください。 レジで順番待ちをしている人の列がキューになります。 また、一人一人の会計処理がジョブを指し、レジ打ちをしている店員がワーカーとなります。 思い返せば、飲食店の行列や役所や病院での待ちもキューで、日常生活の中にも多く存在しています。 キューとキューを利用する上での登場人物について理解したところで、早速実際にLaravelでキューを使ってみましょう。 Laravelでキューを使ってみよう キュー用のテーブルを作成する まずはLaravelでキューを利用する際に必要となるテーブルを作成しましょう。 テーブルの作成は下記コマンドで行うことができます。 $ php artisan queue:table $ php artisan migrate コマンドを実行すると、jobsテーブルとfailed_jobsテーブルが作成されます。 それぞれのテーブルの役割は以下の通りです。 jobs キューに登録されているジョブを管理します。 ジョブが登録されるとjobsテーブルに登録され、テーブルに登録されているジョブのうち、古いものから順に処理されます。 failed_jobs ジョブ実行の失敗を記録します。 ジョブの処理が何等かの理由で失敗した際に、failed_jobsテーブルに登録されます。 合わせてexceptinカラムにジョブ実行時に発生したexceptionと スタックトレース が出力されます。 ジョブクラスを作成する テーブルの用意ができたら、次にジョブクラスを作成しましょう。 ジョブクラスの作成は下記コマンドで行うことができます。 $ php artisan make:job SampleJob 実行すると、 app/Jobs フォルダの中に SampleJob.php が作成されます。 作成されたクラスには、 __construct() メソッドと handle() メソッドがあり、それぞれの役割は以下の通りです。 __construct() ジョブをキューに投入する際にジョブに対して引数を渡すことができます。 (キューへジョブを投入する方法は後述します) その引数を当メソッドで受け取ります。 handle() ジョブ上で実行したい処理を記述します。 上記の他に failed() メソッドでジョブ処理失敗時の処理を定義することができます。 ジョブ処理内で何等かのエラーが発生した際に、 failed() メソッドが呼び出され、引数としてジョブの失敗の原因となったThrowable インスタンス が渡されます。 ジョブクラスの タイムアウト 時間/試行回数を設定する ジョブクラスでは、ジョブの タイムアウト 時間や試行回数を指定することもできます。 ・ タイムアウト 時間  ジョブ1回あたりの タイムアウト 時間を指定できます。  指定する場合は、クラス変数として $timeout を定義します。  (指定しない場合は、デフォルト値の60秒となります。) public $timeout= タイムアウト時間(秒数指定);  ※指定した秒数でジョブ処理が終了しなかった場合は、失敗扱いとなります。 ・試行回数  ジョブが失敗した際に、何回まで再試行するかを指定できます。  指定する場合は、クラス変数として $tries を定義します。  (指定しない場合は、1度失敗すると再試行は行われずに即失敗扱いとなります。) public $tries = 試行回数;  ※試行回数上限に達した場合は、 failed() メソッドが呼び出され、ジョブ失敗扱いとなります。   (failed_jobsテーブルに登録される) タイムアウト 時間/試行回数はコマンドでも指定可能ですが、クラス変数の値が優先されるため注意! キューにジョブを投入する ジョブの実装が完了したら、いよいよ作成したジョブをキューに投入してみましょう。 キューにジョブを投入するには、 dispath() メソッドを使います。 SampleJob::dispatch($Jobへの引数) これで作成したジョブがキューに投入され、ワーカーによってジョブに実装した処理が実行されるようになります。 キューを非同期実行に変更する デフォルトでは、キューは同期実行されるようになっているため、非同期実行とするために .env の設定を変更します。 QUEUE_CONNECTION=database QUEUE_DIRVER=database これで基本的な準備は完了です! あとは実際に動かして、動作を確認してみましょう。 おまけ ここからはおまけです! dispath() メソッドでキューにジョブを投入すると、投入した分だけキューにジョブが溜まっていきます。 しかし、キューの中に同じジョブは常に一つだけにしたい!というケースもあると思います。 そんなときは、一意なジョブを作成することで解決できるので、おまけでは一意なジョブの作成方法について紹介します。 ShouldBeUnique インターフェイス を実装する まず一つ目の方法は、ジョブクラスに ShouldBeUnique インターフェイス を実装する方法です。 class SampleJob implements ShouldQueue, ShouldBeUnique { ・・・ } 上記のように、ジョブクラスに対して インターフェイス を実装するだけです。 この インターフェイス では、ジョブクラスへメソッドを追加する必要はありません。 この インターフェイス を実装すると、対象のジョブがキューに存在している、または処理中のタイミングで dispath() メソッドを実行しても、キューに投入されません。 ジョブの処理が完了した後、または再試行にすべて失敗した後、一意のジョブがロック解除され、キューに対象のジョブを投入できる状態となります。 もし、ジョブが実行される直前にロックを解除したい場合は、 ShouldBeUnique ではなく ShouldBeUniqueUntilProcessing インターフェイス を実装しましょう。 uniqueIdを定義する 二つ目の方法は、ジョブクラス内で特定の「キー」を定義する方法です。 public function uniqueId() { return 任意のID; } 上記のように、ジョブクラスに uniqueId() メソッドを実装します。 このメソッドを実装すると、先ほどの ShouldBeUnique インターフェイス を実装したときと同じように、同じIDを持つジョブは既存のジョブの処理が完了するまで`dispath()``メソッドを実行しても、キューに投入されません。 また、時間経過でジョブの一意ロックを解放することもできます。 その場合は、ジョブクラスに uniqueFor プロパティを定義します。 既存のジョブがuniqueForで指定した時間内に処理されない場合、一意ロックが解放され、同じキーを持つ別のジョブをキューに登録できるようになります。 public $uniqueFor = ジョブの一意ロックが解放されるまでの秒数; さいごに 本記事では、Laravelでのキューを利用した非同期処理についてまとめてみました。 ここで紹介した内容は、Laravelの公式ドキュメントにも載っているので、是非そちらも参照してみてください! readouble.com それでは良き PHP ライフを! エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
はじめに おはようございますこんにちはこんばんは。 筆者は PHP の経験がまだ2年に満たない程度なのですが、 PHP の比較は何かとクセがあるなぁと思いながらコーディングする日々です。宇宙船 演算子 や エルビス 演算子 など筆者もまだあまり使いこなせていない面白い 演算子 もありますので、学習も含めて改めて皆さんと一緒に比較 演算子 を見ていこうというのが本記事の趣旨となっております。 はじめに 大前提 ==や!=を書くのは控えましょう 型を揃えて比較しましょう 基本の比較演算子 曖昧な比較と厳密な比較 宇宙船演算子 三項演算子 エルビス演算子 null合体演算子 おわりに 参考文献 大前提 ==や!=を書くのは控えましょう 既存からあるものならともかく新規で生み出すのは止めましょう。思いも寄らない比較結果になる可能性があります。新規で書く場合は === と !== を使いましょう。逆に既存からあるものを === に置き換えるのは注意しましょう。一見大丈夫そうに見えても思わぬ値が入ってきて既存と判定が変わってしまう場合があります。 型を揃えて比較しましょう 1つ目の大前提で等しい・等しくないについては問題無くなりましたが、 > と < についてはまだ問題が残ります。例えばですが、 2 > "1" は想定通り true になります。実際 PHP はこの場合stringの "1" を数字としてみなし、比較を行います。基本的に数字型文字列を使っている場合は問題ないのですが、そもそも別の型同士を比較すること自体あまりしっくりこないですし、今後言語仕様に変更がある恐れも大いにあるのであくまで同じ型同士で比較することを心がけましょう。 基本の比較 演算子 演算子 説明 == 等しければtrue 曖昧な比較 === 等しければtrue 厳密な比較 != 等しくなければtrue 曖昧な比較 <> !=と同じ 曖昧な比較 !== 等しくなければtrue 厳密な比較 < 左辺が右辺より小さければtrue <= 左辺が右辺より小さいか等しければtrue > 左辺が右辺より大きければtrue >= 左辺が右辺より大きいか等しければtrue 曖昧な比較と厳密な比較 簡単にいうと曖昧な比較は == で厳密な比較は === です。 JavaScript と同じ感覚ですね。曖昧な比較は、 PHP が型の相互変換を行なってくれて比較結果を返してくれます。逆に厳密な比較は型についてもチェックを行います。型が違えばその時点で別物と見なすのです。当然と言えば当然な気もしてきますが、以下のような結果の差が出てきます。 <?php 1 == "1" // true 1 === "1" // false これはまだ簡単な方で null や 0 が絡み出すと一気に複雑になります。他言語を同時に書いている人はそのままの流れで == を使わないように気をつけましょう。 Java を使っている人にはstringの比較を == で行わずに equals() で行うのと同じような感覚で厳密な比較を使っていただけるとわかりやすいかなと思います。また、曖昧な比較で使用される型の相互変換にはルールが存在するので過去コードを読み解くときには 公式ドキュメント とにらめっこしましょう。 宇宙船 演算子 <=> これが宇宙船 演算子 です。全ての プログラミング言語 にあるわけではないので初めて聞いた方もいらっしゃるのではないでしょうか。そういう私も PHP を初めてから知りました。この 演算子 は以下のように判定されます。 パターン 結果 左辺が右辺より小さい -1 左辺が右辺より大きい 0 左辺と右辺が等しい 1 一応この 演算子 は文字列や配列に対しても使用できるのですがあまり直感的ではないですし、数値に対して使いましょう。便利そうに見えていつ使うんだろう?というような 演算子 ですね。実際、通常のコードに出てくることはあまりないと思います。なぜならこの比較結果によって3分岐するようなコードは設計から見直したほうが良い可能性が高いからです。sortを書く場合はとてもシンプルに書けるようになるのでまたその時に思い出してみてください。sortはひたすら大きい小さい等しいの比較を連続して行うので便利です。 ちなみになぜ宇宙船 演算子 かというと <=> の形が スターウォーズ に出てくる宇宙船に形が似ているなど諸説あるそうです。(タイ・ファイターで検索してみてください) 三項演算子 みんな大好き? 三項演算子 です。以下のように書きます。 (1) ? (2) : (3) 判定は以下の通りです。 パターン 結果 (1)がtrue (2) (1)がfalse (3) 少しわかりづらいのでコードっぽく書いてみましょう。 <?php $ おやつ = 洋菓子は好きですか () ? ケーキ : お饅頭 正確にはコードではないのですがこうするとわかりやすいです。「洋菓子は好きですか」関数がtrueならケーキがおやつ変数に入りますし、falseならお饅頭がおやつ変数に入ります。 三項演算子 を使わずに書くと以下の通りです。 <?php if ( 洋菓子は好きですか ()) { $ おやつ = ケーキ } else { $ おやつ = お饅頭 } こんなにも行数も書く量も変わってきます。もちろん最初のうちは 三項演算子 は読みづらいのですが慣れてくるととてもシンプルに見えてきます。変数代入だけでなくもちろん関数実行もできます。 <?php $ averageScore > 80 ? お小遣いプレゼント () : 参考書プレゼント () ここで注意点ですが、何でもかんでも 三項演算子 を使えばいいというわけではありません。あくまで見やすくするため、すなわち可読性を上げるために使用してください。慣れないうちは一旦普通にif-else文で書いてから 三項演算子 に変換してみてください。そこで悩んでしまったり、出来た式を見た時に理解に時間がかかりそうなコードになってしまった時は 三項演算子 を使う場面では無いということです。例えば以下のような時は基本的に 三項演算子 を無理に使用する必要はありません。 if文がネストしている else ifが入ってきて3分岐以上になる if文の中の行数が2行以上ある 特に PHP は他の言語と違い 三項演算子 を左から右に読んでいくので、1行に複数の 三項演算子 が積み重なっている場合は他言語使用者からすると全く違う結果になってしまいます。PHP8.0以上では()を使用して1つの 三項演算子 の範囲を明確にしてあげないとエラーになります。それでも読みにくいことは確かなので極力控えましょう。 エルビス 演算子 ?: PHP の公式ドキュメントには エルビス 演算子 というワードは出てこないのですが、いわゆる エルビス 演算子 というやつです。こちらは 三項演算子 の短縮記法の1つです。見ての通り 三項演算子 の条件式がtrueだった時の部分( 三項演算子 の紹介項目でいう(2)の部分)がギュッと圧縮された形になります。書き方は以下のような形。 <?php $ 貰えるお小遣い = $ テストの点数 ?: 1 微妙にわかりにくくなってしまったような気がしますが、テストの点数が1点以上(true)ならそのぶんのお小遣いをあげ、テストの点数が0(false)なら0は可哀想なので1だけお小遣いをあげます。要するにtrueだった場合は条件式の値がそのまま値になります。falseだった場合は 三項演算子 の場合と同じで : の右側が値として入ります。これを エルビス 演算子 を使わずに 三項演算子 で書くと以下のようになります。 <?php $ 貰えるお小遣い = $ テストの点数 ? $ テストの点数 : 1 $テストの点数 を2回書くことになってしまいました。これは確かに短縮したくなりますね。これも最初のうちはステップを踏むと書きやすいです。 if-else文で書く 簡単に 三項演算子 で書けそう! → 三項演算子 で書く 三項演算子 の条件式とtrueの時に同じこと書いてる! → エルビス 演算子 で書く 以上のステップがスムーズに書けた時は完璧です。気持ちいいですね〜。慣れてくると一発で 三項演算子 で書けたり、 エルビス 演算子 で書けたり、はたまたこれは普通にif-elseで書いた方が良いという判断が付くようになるのでif文を書くときは頭にこのステップを入れておきましょう。あくまで条件式の部分に入る値がfalsy(falseと同等の値)の時に右辺が値となることがポイントです。 PHP の場合はこの後出てくるnull合体 演算子 と少し紛らわしいので注意してください。 ちなみに エルビス 演算子 の由来は エルビス プレスリー の顔文字から来ているらしいです。( Wikipedia ) null合体 演算子 こちらも エルビス 演算子 とは別の 三項演算子 の短縮記法です。書き方は以下のような形。 <?php $ 野菜 = $ 冷蔵庫 [ '野菜室' ] ?? 八百屋さんに買い物 () 感覚としては エルビス 演算子 と同じです。ただ、 演算子 の名前にもあるように ?? の左辺がnullだったときに右辺が値となります。null合体 演算子 を使わずに 三項演算子 で書くと以下の通り。 <?php $ 野菜 = isset ( $ 冷蔵庫 [ '野菜室' ]) ? $ 冷蔵庫 [ '野菜室' ] : 八百屋さんに買い物 () エルビス 演算子 の条件式部分に自動で isset() を付けてくれているような感じです。比較 演算子 なのにtrue,falseの判定じゃなくなったと思うかもしれませんが、実際には見えない isset() の返り値であるtrue,falseで判定しているのです。これだけ聞くと PHP ではnullはfalsyなので エルビス 演算子 だけでいいと思われるかもしれませんが、これまた便利な場面があるのです。それは配列のキー操作をする場合です。 PHP のコードでやたらめったら isset() を使用してから配列操作を行なっているのを目にしたことはないでしょうか。一回 エルビス 演算子 を使用して以下のコードを実行してみましょう。 <?php $ refrigerator = [] ; $ vegetable = $ refrigerator [ 'vegetableRoom' ] ?: 'tomato' ; 実行結果は以下の通りです。 Warning: Undefined array key "vegetableRoom" in /tmp/preview on line 3 野菜室というキーは冷蔵庫配列に存在しないよ と怒られてしまうわけですね。   では、null合体 演算子 を使用して同じコードを実行してみましょう。 <?php $ refrigerator = [] ; $ vegetable = $ refrigerator [ 'vegetableRoom' ] ?? 'tomato' ; 結果はエラーが出ないはずです。このまま $vegetable を出力すると中身は'tomato'です。ということでnullチェックをするときはnull合体 演算子 を使用した方が安全にチェックできるというわけです。使い道としてWebアプリケーションを想定するとして、フロント側からパラメータが配列で渡ってくることが多いと思います。その時、値を取り出して処理を行うわけです。ただ、全ての値がきちんと送られてくるとは限りません。空欄可のアンケートフォームもあるでしょう。そんな時、何も値がない場合は内部的にはデフォルト値として一定の値を使おうという場面が出てくるわけです。こういった場面ではnull合体 演算子 はとても短く処理を書く事ができます。例のコードだと、野菜室キーは野菜を入れるときに初めてキーを作成する可能性もあるわけで、それを全て考慮してコードを書くのは面倒くさいですよね。 PHP ではピタッと使いどきがハマる場面が結構あると思いますので存在を頭の中に入れておきましょう! おわりに 本記事では PHP の比較 演算子 を改めて見ていきました。言語の公式ドキュメントを読むのって楽しいですよね。本来調べたかったことと別の発見があったりしてついつい読んでしまいます。 PHP にこんな機能があったんだとかこんな仕様なのかよ!と突っ込みたくなってしまったりと改めて基本の機能や仕様を見返してみると案外ベテランの方でも面白い発見があるかもしれません。 PHP は何かとクセのある言語ですがそれも含めて愛おしい言語だと思いますので今後もPHPerライフ楽しんでいきましょう。 参考文献 PHP: 比較演算子 - Manual エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
皆さんこんにちは。インフラエンジニアやってますkumakichi_kunです。 ついこの間、、、かと思いましたがそうでもないですね、、、 数年前に業務で Ansible Vault を導入しようとした際に私自身すごく困ったことがあるので、 コマンド例や注意ポイントなどをご紹介いたします。 Ansible Vaultってなに? ここから本題:Ansible Vaultの紹介 1. 1つのファイル丸ごと 2. 1つの変数のみ memo)Ansible Vaultの歴史というか流れのようなもの まとめ 暗号化の単位(というか範囲)は2パターンあるよ! それぞれメリットとデメリットがあるよ! 結論)暗号化する対象を見極めて、 1つのファイル丸ごとと1つの変数のみの形式をうまく使い分けようね! おわりに ちょっとした小技集(蛇足あるいは自分用のメモ) Ansible Vaultってなに? Ansible内で平文のまま扱いたくない情報を暗号化する機能 です。(Ansibleバージョン:v1.5以降?) ファイルの実体としては以下のような UTF-8 で エンコード されたテキストですが、 $ANSIBLE_VAULT;1.2;AES256;test 64623535646465656235363633623431396365666237653663633962616437663239343762356463 3833333531663736656533643734303763393835383663650a386231626230373937666331306536 36613136303934643733346566346366653434643039626535646637353230663839353234363039 6364353566643030620a323733616636666131656263373336383233303636376339316634363061 3262 ansible-playbook コマンドなどを実行する際には内部で以下のように認識される仕組みになります。 this_is_txt 公式ドキュメント(日本語版) 補足①:本記事でのansible実行環境の情報 (バージョンの古さはスルーしていただけると...) $ ansible --version ansible 2 . 7 . 10 補足②:なんでAnsible Vaultが必要だったの? Ansibleのroot ディレクト リをGit上の1PJと紐づけて、バージョン/履歴管理を行っており、 Ansible内で平文のまま扱われる=Git上で見えてしまう⇒公開される範囲が適切じゃない という課題が当時存在しました(PJの公開範囲が問題じゃないか、というのは置いておいて、、、)。 これが要因でパスワードの管理をAnsibleとは別のところでやっていたってのが、当時一番解消したいことでした。 これを解決するためAnsible Vault導入を始めました。 ここから本題:Ansible Vaultの紹介 Ansible Vaultでの暗号化は以下2つのパターンがあります。 1つのファイル丸ごと 1つの変数のみ それぞれについてコマンドの実行例などとともにご紹介していきます。 1. 1つのファイル丸ごと 1つのファイルの中身をまるっと暗号化する形式です。(そのまんまですねw) 暗号化されてる状態 $ANSIBLE_VAULT;1.2;AES256;test 64623535646465656235363633623431396365666237653663633962616437663239343762356463 3833333531663736656533643734303763393835383663650a386231626230373937666331306536 36613136303934643733346566346366653434643039626535646637353230663839353234363039 6364353566643030620a323733616636666131656263373336383233303636376339316634363061 3262 復号された状態 this_is_txt 上記のサンプルとして記載しているファイルを作成した際の過程をご紹介します。 # ファイルをvimで作成 $ vim encrypt_file $ cat encrypt_file this_is_txt # 暗号化するためのパスワードファイルを作成 $ vim .vault_password_file $ cat .vault_password_file test_password # ↑で作成していたtxtファイルを暗号化(--vault-idオプションにパスワードファイルを指定) $ ansible-vault encrypt --vault-id test @.vault_password_file encrypt_file $ cat encrypt_file $ANSIBLE_VAULT ; 1 . 2 ;AES256; test 64623535646465656235363633623431396365666237653663633962616437663239343762356463 3833333531663736656533643734303763393835383663650a386231626230373937666331306536 36613136303934643733346566346366653434643039626535646637353230663839353234363039 6364353566643030620a323733616636666131656263373336383233303636376339316634363061 3262 # 暗号化したファイルを復号して確認 $ ansible-vault view --vault-id test @.vault_password_file encrypt_file this_is_txt # 暗号化したファイルを復号して修正 $ ansible-vault edit --vault-id test @.vault_password_file encrypt_file ⇒viが起動するので修正 # ↑で修正したファイルの実体を確認 $ cat encrypt_file $ANSIBLE_VAULT ; 1 . 2 ;AES256; test 39643730306632346466383636663961306437333833303931343937376531316363656634393863 3864663636356461613434643036356234323034626234640a323635666362373936356465653534 37343132653463363732653635393037363436366663343262656433663232393338613036656233 6262373666656436340a343865393663323066353430663431653731333966643862653736646632 3334 # 暗号化したファイルを再度復号して確認 $ ansible-vault view --vault-id test @.vault_password_file encrypt_file this_is_txt2 ※各コマンドオプションなどの詳細については公式ドキュメントや私自身が参考にさせていただいた記事のご紹介だけにとどめておきます。 公式ドキュメント Ansible Vaultの使い方の調査 上記のような encrypt_file の作り方はいくつかやり方があるので、 こちら を参考に環境などに合わせて、適切な方法を選択していただければ。 この形式には、 暗号化した後に中身の確認や修正が簡単にできる って特徴があります(後述する 1つの変数のみ 形式との比較になりますが)。 コレが特徴?って思う方もいるかもしれませんが、 次にご紹介する 1つの変数のみ のご紹介を見てもらえると理解してもらえるかなぁ~と思います。 2. 1つの変数のみ 1つの変数のみを暗号化する形式です。(急募:ネーミングセンス) 変数名は暗号化せず、変数の中身だけ暗号化された状態を ansible-playbook コマンドなどで扱えます。 暗号化されてる状態 password_vars: !vault | $ANSIBLE_VAULT;1.2;AES256;test 36656531623938393464666363353630333865316435346533303035663965323862383666356437 3938613939663031343436663634623833396630373965370a306532643662353931313839393735 66623034373862626262333735363461386339373666663935383431383266656463323333393433 3331363264383963300a303039343335666134313862616633663634313035616165376437386436 32343966366636643061643439383235356236303533616566316432616566336330 復号された状態 password_vars: this_is_password 上記のサンプルとして記載している変数を作成した際の過程をご紹介します。 # 暗号化するためのパスワードファイルを作成 $ vim .vault_password_file $ cat .vault_password_file test_password # ↑で作成していたtxtファイルを暗号化(--vault-idオプションにパスワードファイルを指定) $ ansible-vault encrypt_string --vault-id test @~/.vault_password_file --stdin-name password_vars Reading plaintext input from stdin. ( ctrl-d to end input ) this_is_password ←ココでCtrl+D入力 ※ENTERを入力してしまうと末尾に改行が含まれてしまうので要注意 password_vars: !vault | $ANSIBLE_VAULT ; 1 . 2 ;AES256; test 36656531623938393464666363353630333865316435346533303035663965323862383666356437 3938613939663031343436663634623833396630373965370a306532643662353931313839393735 66623034373862626262333735363461386339373666663935383431383266656463323333393433 3331363264383963300a303039343335666134313862616633663634313035616165376437386436 32343966366636643061643439383235356236303533616566316432616566336330 # ↓のようにコマンドラインで暗号化したい文字列を指定するやり方もありますが、 # historyに残ってしまうのであまり好ましくありません(知られたくない文字列のハズなので) # ansible-vault encrypt_string this_is_password --vault-id test@~/.vault_password_file --name password_vars # ↑で生成した文字列をtxtに追記 $ vim encrypt_vars $ cat encrypt_vars password_vars: !vault | $ANSIBLE_VAULT ; 1 . 2 ;AES256; test 36656531623938393464666363353630333865316435346533303035663965323862383666356437 3938613939663031343436663634623833396630373965370a306532643662353931313839393735 66623034373862626262333735363461386339373666663935383431383266656463323333393433 3331363264383963300a303039343335666134313862616633663634313035616165376437386436 32343966366636643061643439383235356236303533616566316432616566336330 この方法には以下の注意点があります。 復号化した状態を修正/確認するのが手間(1つのファイル丸ごととは異なり) 変数名:(平文)+vaultで暗号化した文字列 という構造になってる関係で ansible-vault view や ansible-vault edit などの CLI 上で復号ができません 。 ansible-playbook コマンドなどでdebugモードで実行すれば中身の確認ができます。 (今回利用しているv2.7での挙動ですが、今後のバージョンアップで機能追加されそうな部分な気がします) memo)Ansible Vaultの歴史というか流れのようなもの Ansible Vaultリリース当初(v1.5)はファイル丸ごとの暗号化しかできなかった →その後、変数部分のみ暗号化が機能追加されてリリース(2.2か2.3~) v1.5当時?に書かれたもの: https://qiita.com/yteraoka/items/d18e3c353b6e15ca84a8 機能追加時の参考: https://qiita.com/yunano/items/86d3f9beb678adbff50d v1.5のタイミングはやはりファイル丸ごと暗号化するってのは不便な時があったようで、、、 当時は変数ファイルを分割する などで対処していたようです。 ※この対処法自体は今でも有効に活用できるので、頭の片隅にでも置いておくといいかもしれません。 まとめ 暗号化の単位(というか範囲)は2パターンあるよ! 1つのファイル丸ごと 1つの変数のみ それぞれメリットとデメリットがあるよ! 1つのファイル丸ごと メリット:暗号化したファイルの確認・修正が簡単にできる デメリット:変数名も一緒に暗号化されてるため変数の追跡がしづらい 1つの変数のみ メリット:変数の追跡が ラク にできる デメリット:暗号化した変数の確認・修正が手間 結論)暗号化する対象を見極めて、 1つのファイル丸ごと と 1つの変数のみ の形式をうまく使い分けようね! 具体例) 秘密鍵 ファイルを暗号化したい! ⇒ファイル丸ごとで暗号化! 環境変数 とか対象ホスト毎に変えているvarsも暗号化したい! ⇒変数のみ暗号化して既存varsファイルに追記! 今はべた書きになってるけど、ココの文字列も暗号化しときたいな~ ⇒とりあえず変数とtemplateに分けておく!暗号化するか否かはチーム内で議論して決めよう 何かめっちゃ暗号化してる変数増えてきてんな... 暗号化してる変数だけ別のvarsファイルに出してファイル丸ごと暗号化しちゃえ ⇒変数を追跡するのが難しくなり、チーム内の合意がないと混乱を招きます!(実体験) 変数ファイルの丸ごと暗号化は 慎重に やりましょう おわりに 今回Ansible Vaultについてご紹介しました。 この記事がどなたかの助けになれば幸いです。 ちょっとした小技集(蛇足あるいは自分用のメモ) ラベル差し替え ラベルってなんだろう?って方は こちら 。 ラベルって機能を利用してpasswordと暗号化する部分を紐づけていますが、間違えてラベルを付加してしまうことがあります。。。 その場合、ラベル部分のみは平文で記載& ansible-vault コマンド内部で処理されているようで、 vim などで直接編集することが可能です。 (v2.7ではラベルって機能自体が追加オプションを設定しないとあまり意味がない状態ではありますが...) $ cat encrypt_file $ANSIBLE_VAULT ; 1 . 2 ;AES256;hoge ←ココの末尾がtestになっていてほしかった 64623535646465656235363633623431396365666237653663633962616437663239343762356463 3833333531663736656533643734303763393835383663650a386231626230373937666331306536 36613136303934643733346566346366653434643039626535646637353230663839353234363039 6364353566643030620a323733616636666131656263373336383233303636376339316634363061 3262 $ ANSIBLE_VAULT_ID_MATCH =true ansible-vault view --vault-id test @.vault_password_file encrypt_file ERROR! Decryption failed ( no vault secrets were found that could decrypt ) on encrypt_file for encrypt_file ANSIBLE_VAULT_ID_MATCH=true を追加して ansible-vault view などを実行するとエラーになっちゃいますが、、、 vim で直接修正しちゃえば、、、 $ vim encrypt_file $ANSIBLE_VAULT ; 1 . 2 ;AES256; test ←ココをtestに修正 64623535646465656235363633623431396365666237653663633962616437663239343762356463 3833333531663736656533643734303763393835383663650a386231626230373937666331306536 36613136303934643733346566346366653434643039626535646637353230663839353234363039 6364353566643030620a323733616636666131656263373336383233303636376339316634363061 3262 $ ANSIBLE_VAULT_ID_MATCH =true ansible-vault view --vault-id test @.vault_password_file encrypt_file this_is_txt 同一のコマンドで確認できるようになりました! ( ANSIBLE_VAULT_ID_MATCH=true を外せばイイじゃないか、というのは置いておいて...) ・変数のみの暗号化している部分を修正する 変数名:(平文)+vaultで暗号化した文字列 という構造が ansible-vault view や ansible-vault edit で扱えない要因のため、 イイ感じに不要な部分を除去してやると、 ansible-vault view や ansible-vault edit で扱えるようになります。 $ cat encrypt_vars password_vars: !vault | $ANSIBLE_VAULT ; 1 . 2 ;AES256; test 36656531623938393464666363353630333865316435346533303035663965323862383666356437 3938613939663031343436663634623833396630373965370a306532643662353931313839393735 66623034373862626262333735363461386339373666663935383431383266656463323333393433 3331363264383963300a303039343335666134313862616633663634313035616165376437386436 32343966366636643061643439383235356236303533616566316432616566336330 $ vim encrypt_vars 1 ) 変数名の行を削除 2 ) インテンドを削除 ↓ $ANSIBLE_VAULT ; 1 . 2 ;AES256; test 36656531623938393464666363353630333865316435346533303035663965323862383666356437 3938613939663031343436663634623833396630373965370a306532643662353931313839393735 66623034373862626262333735363461386339373666663935383431383266656463323333393433 3331363264383963300a303039343335666134313862616633663634313035616165376437386436 32343966366636643061643439383235356236303533616566316432616566336330 $ ansible-vault view --vault-id test @.vault_password_file encrypt_vars this_is_password ⇒変数 password_vars の中身が確認できます!(editもできるよ!) ※↑の状態だと変数 password_vars と認識できないので、 ansible-playbook コマンドなどを実行する前にインテンドや変数名の行を再度追加する必要があるので注意 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
普段は楽楽精算開発に関わっているrsrksです。 前回投稿の JUnit ネタ tech-blog.rakus.co.jp に続き 今回はテストを簡易的な仕様書にする方法として JUnit の階層化を紹介します。 階層化 開発の中で 「このメソッドの仕様ってどうなってたっけ?」 と思ったら、まず実装を確認することが多いのではないかと思います。 しかし、実装から仕様を読み取るのはなかなか大変です。 「どのパターンでどのような挙動をするか」 を頭の中だけで網羅的に把握するのは困難です。把握するためにはパターンを表にまとめて仕様を整理する必要がありますね。 このパターンのまとめを JUnit では階層化によって実現できます。 最終的にはテストが簡易的な仕様書になるところまで持っていくことができます。 JUnit での階層化は @Nested アノテーション を付与するだけで実現できます。 ここでは例として「リク エス トで飛んできた名前をバリデートするメソッド」のテストを書きたいとします。 バリデートの仕様は簡単に以下のようにします。 エラーの場合 0文字の場合は必須エラーを返す 10文字以上の場合は文字数オーバーエラーを返す エラーにならない場合はnullを返す 実装例 (メソッドの中身は適当です) // 名前のバリデートを行うメソッドをテスト public class ValidateNameTest { String validateName(String name) { if (name.equals( "" )) { return "必須エラー" ; } if (name.length() >= 10 ) { return "文字数オーバーエラー" ; } return null ; } // 名前が無効な場合 @Nested class NameIsInValid { // 名前が0文字の場合は必須エラーを返す @Test public void nameLengthIs0_returnRequiredError() { assertEquals( "必須エラー" , validateName( "" )); } // 名前が10文字の場合は文字数オーバーエラーを返す @Test public void nameLengthIs10_returnRangeError() { assertEquals( "文字数オーバーエラー" , validateName( "1234567890" )); } } // 名前が有効な場合 @Nested class NameIsValid { // 名前が1文字の場合はnullを返す @Test public void nameLengthIs1_returnNull() { assertEquals( null , validateName( "1" )); } // 名前が9文字の場合はnullを返す @Test public void nameLengthIs9_returnNull() { assertEquals( null , validateName( "123456789" )); } } } 今回は階層が一つだけですが、 @Nested が付いたクラスの下にさらに @Nested をつければ階層はどんどん深くできます。 これだけでも見やすいですが、テストコードが長くなってくると全てのパターンを見るのは中々一苦労です。 そんな時はテストの実行レポートを出力するのがお勧めです。 IntelliJ だとテストを実行した時点で以下のような感じで表示され、一目で挙動を確認できます。 テストへの名前付け 上記でテストを実装しましたが、テストメソッドは全て英語で 命名 しました。 しかし、英語だと 命名 が難しい、、もっと分かりやすいテスト名にしたい、、という悩みが発生します。 そんな時は @DisplayName() という アノテーション があります。 @DisplayName ( "名前のバリデートを行うメソッドをテスト" ) public class ValidateNameTest { String validateName(String name) { if (name.equals( "" )) { return "必須エラー" ; } if (name.length() >= 10 ) { return "文字数オーバーエラー" ; } return null ; } @DisplayName ( "名前が無効な場合" ) @Nested class NameIsInValid { @DisplayName ( "名前が0文字の場合は必須エラーを返す" ) @Test public void nameLengthIs0_returnRequiredError() { assertEquals( "必須エラー" , validateName( "" )); } @DisplayName ( "名前が10文字の場合は文字数オーバーエラーを返す" ) @Test public void nameLengthIs10_returnRangeError() { assertEquals( "文字数オーバーエラー" , validateName( "1234567890" )); } } @DisplayName ( "名前が有効な場合" ) @Nested class NameIsValid { @DisplayName ( "名前が1文字の場合はnullを返す" ) @Test public void nameLengthIs1_returnNull() { assertEquals( null , validateName( "1" )); } @DisplayName ( "名前が9文字の場合はnullを返す" ) @Test public void nameLengthIs9_returnNull() { assertEquals( null , validateName( "123456789" )); } } } ↓実行結果も日本語で表示されます。 まとめ 今回はテストを簡易的な仕様書にする方法として JUnit の階層化を紹介しました。 テストの階層化自体は知っていたけど、何のために行うのかを意識してなかった方はぜひ「テストの仕様書化」を意識してもらえたらと思います。 参考 JUnit 5 User Guide エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは、あるいはこんばんは。すぱ..すぱらしいサーバサイドのエンジニアの( @taclose )です☆ 今回は 『 zsh を使って更に普段の業務の作業効率をあげよう!』 というテーマでお話をしたいと思います! これまで日常業務の作業効率やミスを減らすためにいくつかの記事を書いてきました( ssh ばかり)。 もうここまできたらさすがに ssh ネタはないでしょ?と思ってるそこのあなたっ これは ssh だけに限った話ではないですが、実は zsh を使うとコマンドの入力補完等により作業効率3倍、入力ミス90%減、身長3mmアップします!(筆者の体感) 具体的には ssh のログインやgitコマンドで TABキー を押すと補完がきいたり、ブランチ名が表示されたりコマンド履歴が便利に使えたり..etc この記事が読み終わった頃には以下のようなことができます! zsh に切り替えたコンソール zshとは? zshをインストールしよう! zshインストールされてないよね?確認! 最新バージョンを確認しよう インストール前の準備 zshのソースインストール zshを使いやすくカスタマイズしよう!(oh-my-zsh) oh-my-zshとは? oh-my-zshのインストール oh-my-zshに自分独自の設定を入れましょう! もしBashに戻したいのなら... 最後に 参考文献 zsh とは? 難しい話は置いといて、概要だけいうと、 zsh (ズィーシェル、もしくはそのまま zsh と呼ばれている)とは、簡単に言うと bash の上位互換 です。 ほかにもsh, ksh , bash , csh , tcsh とかありますが、普通にパソコン触ってれば shが初期、 bash が標準、 zsh は上位互換(機能拡張版) csh , tcsh は BSD UNIX とかで使われててるシェルだから関わる事はあまりない とだけわかっていれば十分です。ちなみに現在では MacOS では標準シェルとして zsh が採用されてます 。今回はそんな zsh を CentOS とかでも使いましょうという企画なわけですね! zsh をインストールしよう! zsh インストールされてないよね?確認! # 今自分が使っているシェルがzshではなくbashである事を確認 $ echo $SHELL /bin/bash # zshがインストールされていない事を確認 $ chsh -l /bin/sh /bin/bash /usr/bin/sh /usr/bin/bash はい、chshで zsh が一覧にないのでインストール必要そうですね!次! 最新バージョンを確認しよう 早速インストールなんですが、現在(2023年1月4日) zsh の最新は5.9となっています。 ZSH - Source download yum でインストールするとしたらversionいくつかな? $ yum info zsh で確認 ちょっと古いですね。今回はソースダウンロードして最新を入れましょう。 インストール前の準備 ncurses-develをインストールしましょう!これを入れないと zsh のインストール時に以下のようなエラー出ちゃいますよ! configure: error: in `/usr/local/src/zsh-5.9': configure: error: "No terminal handling library was found on your system. This is probably a library called 'curses' or 'ncurses'. You may need to install a package called 'curses-devel' or 'ncurses-devel' on your system." See `config.log' for more details ちなみに ncursesというのはTUI(TextUI)つまり Windows でいう ブルースクリーン 的なインタフェースを提供するための API 群の集まりです。 zsh はこれを使う事で入力補完とかを円滑に行えるようにしようとしてるわけですね!エレガント! 以下がncurses-develのインストール手順です。 # zshに必要なncurses-develをインストール $ yum install -y ncurses-devel zsh のソースインストール さ、お楽しみの zsh 早速入れていきましょう! # sourceダウンロードする前にディレクトリを移動 $ cd /usr/ local /src/ # ダウンロード $ wget https://sourceforge.net/projects/zsh/files/zsh/ 5.9 /zsh -5.9 .tar.xz/download -O zsh -5.9 .tar.xz # 解凍 $ tar xvf zsh -5.9 .tar.xz # 解凍したディレクトリに移動 $ cd zsh -5.9 / # マルチバイトを有効にしてビルドしてインストール $ ./configure --enable-multibyte $ make && make install # zshを使える状態にする $ echo /usr/ local /bin/zsh >> /etc/shells # ログイン時のシェルをzshに変更する $ chsh -s /usr/ local /bin/zsh # 一旦シェルを終了する $ exit これで zsh のインストールは終わり!ソースインストールとか慣れてない方にとっては英語の羅列が気持ち悪いだけですが、インストールがErrorやFatal!warning!!みたいになってないかぐらいは確認しておけばだいたい問題ないですよっ Self-signed certificate encountered. To connect to raw.githubusercontent.com insecurely, use `--no-check-certificate'. もし wget で上記のようなエラーが出た人は wget コマンド実行時に --no-check-certificate オプションを付与してくださいね 。 zsh を使いやすくカスタマイズしよう!(oh-my- zsh ) 早速シェルを立ち上げてみたが・・・これだけでは改善効果が感じられませんね。 せいぜい cd を入力途中で[Tabキー]連打で候補が次から次へと変わるぐらいです。かといって zsh の設定は多岐に渡るため 今回はoh-my- zsh というオススメ設定を紹介します。 oh-my- zsh とは? zsh の設定は .zshrc に記載するのですが、良い感じにその初期設定ファイルを生成したり、テーマ変更という形でプロンプトのデザインをさらっと変えられるようになります。 oh-my- zsh のインストール # oh-my-zshをインストール sh -c " $( wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh ) " これだけ!もしgit cloneでこけたよ!って人とかいたら、原因様々ですが普段git問題なく使えてる方ならば install.shが内部的に https の リポジトリ 指定してるのが癖ある ので、それかもしれません。 Cloning Oh My Zsh ... fatal: unable to access ' https://github.com/ohmyzsh/ohmyzsh.git/': Peer's certificate issuer has been marked as not trusted by the user. こんなエラーが出た方は一旦以下のコマンドを実行してしまえば問題なく次はインストール出来るかと思います。 $ git config --global http.sslVerify false oh-my- zsh に自分独自の設定を入れましょう! まずはこれまで .bashrc に記載した独自の設定を .zshrc の一番下に転記しましょう 次に .zshrc の11行目辺りに書かれている ZSH_THEME="xxxxx" という所に好きなテーマを指定しましょう。とりあえず eastwood にしてみてください。 $ source ~/.zshrc と入力して zsh の設定を反映させるとデザインが変わるはずです 他にもテーマはたくさんあります!以下から選んで下さいね! (oh-my- zsh Wiki : Themes) https://github.com/ohmyzsh/ohmyzsh/wiki/Themes もし Bash に戻したいのなら... 万が一、あまり zsh がお気に召さないという方は以下の手順で元通りです! # 使えるシェルを確認。 $ chsh -l /bin/sh /bin/bash /usr/bin/sh /usr/bin/bash # ログインシェルをBashに戻す $ chsh -s /usr/bin/bash # 不要なファイルを削除 $ rm -rf ~/.oh-my-zsh $ rm -rf ~/.zshrc 最後に お疲れ様でした!ここまでやると以下のような入力補完がバシバシ効いたUIになったんじゃないでしょうか。 今回はgitコマンドや ssh の入力補完を例にしていますが、 プラグイン 次第でもっともっと使いやすくできます。 うんうん、これで ssh コマンドの入力ミスも起きないですね! 尚、 ssh の後にタブキーで入力候補で __taclose.develop 等が出てくるのは、 以前sshの設定に関するまとめ記事 を読んでくださいね! . ssh /configの設定をすればOKです! また機会があれば続編やろうかな!お疲れ様でした!! 参考文献 tech-blog.rakus.co.jp 最新のzshをインストールする手順 - Qiita ohmyzsh - GitHub オレオレ証明書なGitへの新規リポジトリ作成 - Qiita エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは、uemura_rks です。 個人的な勉強目的で GitLab 上で AWS Lambda のデプロイを自動化 してみました。 GitLab での CICD や AWS SAM、あとは Docker に興味を持っている方に向けて、その構築履歴を紹介したいと思います。 基本的には各ツールのドキュメントを参照しつつ、追記した設定などを共有していきます。 作りたいもの 1.GitLab 構築 2.GitLab Runner 構築 GitLab Runner の登録 登録トークン取得 登録コマンドの実行 3.Lambda 環境構築 AWS SAM チュートリアル 4.デプロイ自動化 5.自動デプロイの確認 Lambda の変更 テンプレートの変更 終わりに 参考 作りたいもの ローカルに Docker で GitLab や GitLab Runner を立てつつ、プッシュしたら AWS 上に自動デプロイが走る構成を目指します。 GitLab に push したら GitLab Runner が走る。 GitLab Runner から ジョブ実行用コンテナを起動。 コンテナには AWS SAM CLI が使えるイメージを利用します。 AWS SAM CLI を使って Lambda 本体や AWS リソースを定義した Stack をデプロイ。 1.GitLab 構築 参考: GitLab Docker images | GitLab さっそく GitLab の構築からはじめていきます。 Docker 上に構築するので、参考ドキュメントの docker-compose.yml の部分をベースとして拝借していきます。 version : '3' services : gitlab : container_name : gitlab image : 'gitlab/gitlab-ce:latest' restart : always hostname : 'local-gitlab' environment : GITLAB_OMNIBUS_CONFIG : | external_url 'http://local-gitlab:8929' ports : - '80:8929' volumes : - ./gitlab/config:/etc/gitlab - ./gitlab/logs:/var/log/gitlab - ./gitlab/data:/var/opt/gitlab shm_size : '2gb' ドキュメントでは エンタープライズ 版の gitlab-ee イメージを指定していますが、 今回は個人利用なのでコミュニティ版の gitlab-ce を使います。 volumes: セクションに関して、フォルダ構成はこんな感じにしています $ tree ├── gitlab (GitLab の volume) ├── gitlab-runner (GitLab Runner の volume とする予定) ├── docker-compose.yml └── sampleapp (サンプルアプリを入れる予定) 一度立ち上げてみます docker-compose up -d 名前解決できるようにローカルマシンの hosts ファイルに 127.0.0.1 local-gitlab を追記すると GitLab( http://local-gitlab/ ) でアクセス出来るようになります。 初回は root ユーザでログインすることになりますが、 root のパスワードはこちらのコマンドで知ることが出来ます。 # Visit the GitLab URL, and sign in with the username root and the password from the following command: sudo docker exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password 2.GitLab Runner 構築 参考: Run GitLab Runner in a container こちらもドキュメントに載っている docker run コマンドを参考に、 docker-compose.yml に起こしていきます。 version : '3' networks : gitlab-network : name : gitlab-network driver : bridge services : gitlab : container_name : gitlab image : 'gitlab/gitlab-ce:latest' restart : always hostname : 'local-gitlab' environment : GITLAB_OMNIBUS_CONFIG : | external_url 'http://local-gitlab:8929' ports : - '80:8929' volumes : - ./gitlab/config:/etc/gitlab - ./gitlab/logs:/var/log/gitlab - ./gitlab/data:/var/opt/gitlab shm_size : '2gb' networks : - gitlab-network runner : container_name : gitlab-runner image : gitlab/gitlab-runner:latest restart : always volumes : - ./gitlab-runner/config:/etc/gitlab-runner - /var/run/docker.sock:/var/run/docker.sock networks : - gitlab-network ついでに内部ネットワークを作成して、二つのコンテナを同じネットワークに入れておきます。 GitLab Runner の登録 GitLab Runner コンテナも立ち上げたら、GitLab に Runner を登録していきます。 登録 トーク ン取得 参考: Registering runners (deprecated) | GitLab Runner の登録に入る前に GitLab の登録 トーク ンというものを取得しておきます。 GitLab に リポジトリ を作ると、 リポジトリ の Settings > CI/CD > Runners から GitLab Runner の登録時に必要な「GitLab の URL」と「登録 トーク ン」が取得できます。 注意点として、今紹介している登録 トーク ンを使った方法は、現在の GitLabバージョン15.6 では deprecated となっております。 新しく認証 トーク ンによる登録ができるようになる予定みたいですが、現在はまだ登録 トーク ンを使うしかないのでそのまま進めます。(絶妙にタイミングが悪かったです。) 登録コマンドの実行 参考: one-line-registration-command Runner の登録に必要な URL と登録 トーク ンが確認できたので、GitLab Runner コンテナに入って register コマンドを実行していきます。 gitlab-runner register \ --non-interactive \ --url "http://local-gitlab:8929/" \ --registration-token "PROJECT_REGISTRATION_TOKEN" \ --executor "docker" \ --docker-image alpine:latest \ --description "docker-runner" \ --docker-network-mode gitlab-network --url :docker ネットワーク内の通信になるので、コンテナポートを付け足しておきます。 --docker-image :後に作成する .gitlab-ci.yml にコンテナイメージを指定しなかった場合のデフォルトになるみたいです。今回は使う予定がないのでサンプルのまま登録しています。 --docker-network-mode :docker-compose.yml で用意したネットワークを指定しておきます。 登録に成功すると GitLab に「Available specific runners」が増えています。 3.Lambda 環境構築 GitLab Runner の構築までできたので、次は Lambda を用意していきます。 「Lambda のデプロイを自動化する」というゴールから考えると Lambda 単体で試してもよかったのですが、 前から興味があったという理由で今回は AWS SAM を使ってみます。 この投稿では触れませんが、 AWS SAM はローカル環境も手に入るのが良いです。 AWS SAM チュートリアル 参考: チュートリアル: Hello World アプリケーションのデプロイ - AWS Serverless Application Model チュートリアル の Hello World アプリケーションをデプロイしてみます。 API Gateway と Lambda ですね。 チュートリアル の #Step 1 をローカルで実行します。 #Step 1 - Download a sample application sam init すると、「 hello world 」と出力するだけの Lambda 関数とインフラ定義ファイルの template.yml が作成されます。 一度この状態でローカルからデプロイしてみます。 #Step 2 - Build your application cd sam-app sam build   #Step 3 - Deploy your application sam deploy --guided dry-run の結果を教えてくれます。 Waiting for changeset to be created.. CloudFormation stack changeset ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Operation LogicalResourceId ResourceType Replacement ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Add HelloWorldFunctionHelloWorldPermissionProd AWS::Lambda::Permission N/A + Add HelloWorldFunctionRole AWS::IAM::Role N/A + Add HelloWorldFunction AWS::Lambda::Function N/A + Add ServerlessRestApiDeploymentxxxxxxxxxx AWS::ApiGateway::Deployment N/A + Add ServerlessRestApiProdStage AWS::ApiGateway::Stage N/A + Add ServerlessRestApi AWS::ApiGateway::RestApi N/A ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:000000000000:changeSet/samcli-deploy0000000000/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Previewing CloudFormation changeset before deployment ====================================================== Deploy this changeset? [y/N]: AWS SAM は CloudFormation を拡張したサービスらしいので、メッセージに CloudFormation という単語がちょくちょく出てきますね。 問題ないので「Deploy this changeset? 」に y で答えてデプロイを実行します。 無事に CloudFormation に Stack ができています。 Lambda や API Gateway を個別に見に行ってもちゃんとできています。 デプロイできたので、 curl を叩いて hello world が返ってくることを確認しておきます。 $ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello {"message": "hello world"} 4.デプロイ自動化 参考: GitLab CI/CD を使用したデプロイ - AWS Serverless Application Model AWS SAM だけでも1コマンドでデプロイできるようになるので十分便利なのですが、極力人の操作を減らしたいです。 GitLab Runner を使って、プッシュしたら自動で SAM アプリがデプロイされるようにしていきます。 #Step 2 - Build your application cd sam-app sam build   #Step 3 - Deploy your application sam deploy --guided 先ほど手で実行した↑の部分が CI/CD 部分ということで、ここを .gitlab-ci.yml に切り出します。 image : public.ecr.aws/sam/build-python3.9 stages : - build - deploy job_build : stage : build script : - cd sam-app - sam build job_deploy : stage : deploy variables : GIT_CLEAN_FLAGS : none script : - cd sam-app - sam deploy --no-confirm-changeset --no-fail-on-empty-changeset GitLab Runner のジョブ実行用コンテナには AWS が提供しているイメージを利用します。 AWS SAM CLI が最初から入っているので助かります。 また、ジョブ実行時に利用される IAM のアクセスキーなどを GitLab の Variables に登録しておきます。 参考: Deploying AWS Lambda function using GitLab CI/CD | GitLab リポジトリ の Settings > CI/CD > Variables から登録できます。 あとは、 .gitlab-ci.yml をプッシュすれば GitLab Runner ジョブが立ちあがるようになります。 5.自動デプロイの確認 AWS SAM の Lambda と テンプレートをいじって push→deploy してみます。 Lambda の変更 Hello World アプリケーションで元々 コメントアウト されていた グローバルIP 取得の処理を アンコメントしてみます。 import json import requests def lambda_handler (event, context): ~~~ ~~~ try : ip = requests.get( "http://checkip.amazonaws.com/" ) except requests.RequestException as e: # Send some context about this error to Lambda Logs print (e) raise e return { "statusCode" : 200 , "body" : json.dumps({ "message" : "hello world" , "location" : ip.text.replace( " \n " , "" ) // ★ IP出力部分をアンコメント }), } テンプレートの変更 参考: FunctionUrlConfig - AWS Serverless Application Model API Gateway を破棄して、代わりに Lambda の 関数 URL をエンドポイントとして用意してみます。 Resources : HelloWorldFunction : Type : AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties : CodeUri : hello_world/ Handler : app.lambda_handler Runtime : python3.9 Architectures : - x86_64 FunctionUrlConfig : AuthType : NONE Outputs : HelloWorldFunctionUrlEndpoint : Description : "Hello World Lambda Function URL Endpoint" Value : !GetAtt HelloWorldFunctionUrl.FunctionUrl Properties:Events: セクションで定義されていた API Gateway を 関数 URL の設定に置き換えています。 変更が終わったので、プッシュして、GitLab の Pipeline が走ることを確認します。 Pipeline job の実行ログ 無事に Pipeline が走って、 Lambda Function → Modify Function URL → Add API Gateway → Delete されています。 最後に確認として変更後のエンドポイントに curl してみます。 $ curl https://xxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/ {"message": "hello world", "location": "3.115.88.55"} IPアドレス も返ってきました。 終わりに ひとまず GitLab + GitLab Runner + AWS SAM という構成で、プッシュしたものを AWS 上に自動でデプロイする土台は作ることができました。 昔、Lambda をマネジメントコンソールから毎回アップロードしていたことがあったので、今後はその手間から解放されそうです。 また、私は Docker についても最近触り始めたばかりなのですが、「GitLab + GitLab Runner」という構成は複数コンテナを組み合わせた構成ということもあって、Docker 初学者にとって良い練習教材になりました。 実業務では自動デプロイだけでなく UT や SAST など自動テストの考慮も必要になると思います。 それらと今回作ってみた構成の相性については、別途検証が必要かなと考えています。 参考 公式ドキュメント以外で参考にさせていただいたブログです。 Dockerコンテナでgitlabとgitlab-runnerを構築してCI/CD | SyachikuLOG エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは、sakekobaと申します。 今回は人気の高いGo言語の「メソッド」について記事を書きたいと思っております。 また、Go言語については、当ブログで先輩方が素敵な記事を複数記載されております。 こちらも併せてご覧頂けましたら幸いです。 Go言語 入門【環境構築とコーディング】 - RAKUS Developers Blog | ラクス エンジニアブログ Go言語の平行処理をやってみよう!【goroutine】 - RAKUS Developers Blog | ラクス エンジニアブログ 目次 目次 メソッドとは? Go言語のメソッドは何に紐づくの? メソッドの書き方、使い方 同一のメソッド名について インターフェースについて 最後に メソッドとは? Go言語だけでなく、メソッドを持つ言語は多いと思います。 「メソッド」と聞くと、まず オブジェクト指向 プログラミングを思い浮かべる方も多いのではないでしょうか。 メソッドとは、主に オブジェクト指向言語 で使われる言葉で、オブジェクトが持つ処理や手続きの方法を指す言葉です。 例えば、 「車」というオブジェクトであれば「走る」「ドアを開ける」「ドアを閉じる」等の処理(メソッド)が紐づいているイメージです。 Java 等では「クラス」という形でオブジェクトを定義して、そのクラスにメソッドも定義されていて、そこから インスタンス を生成して使う。。のような手順を踏みます。 ところが、Go言語はこの上記のような「クラス」は使用されません。 では、Go言語のメソッドはどこに定義されるのでしょうか。 Go言語のメソッドは何に紐づくの? Go言語の場合は「型」に直接紐づきます。 「型」といっても、Go言語の型はintやstringといった基本の型だけでなく、type という型を定義する 予約語 を用いて基本の型を拡張し、オリジナルの型を作ることが出来ます。 この type を使って拡張した型に、メソッドを追加することが出来ます。 たとえば、基本の型であるstring型を拡張(string型に様々な機能を追加出来るように)するMyStringという型を作る場合、Go言語では下記のように書いて定義することが出来ます。 type MyString string この新しく定義したMyStringに、下記のようにメソッドを追加することが出来ます。(例は敬称の「さん」を返すメソッド) func (s MyString) Keisyo() string { return "さん" } このように型に紐づけるのがGo言語におけるメソッドです。 メソッドの書き方、使い方 Go言語のメソッドは func (レシーバ 型) メソッド名(引数) 戻り値 { 処理 } という形で書きます。 (レシーバ 型)の部分は、メソッドが紐づく型(typeで指定した拡張型)と、その型からメソッドを呼び出す際に指定した変数が持つ値を受け取る変数名です。 先ほどの例では、 レシーバとして変数sを用意し、レシーバの型がtypeを使って定義したMyString、メソッド名がKeisyo()で、戻り値がstringです。 func (s MyString) Keisyo() string { return "さん" } 呼び出す際は下記のように、レシーバの型で変数を作成し、その変数にドットをつけてメソッド名を記載します。 func main() { namae := MyString( "たなか" ) fmt.Print(namae, namae.Keisyo()) } こうして呼び出したメソッドは、呼び出し元(ドットの前の型)と一致するレシーバの型を持つメソッドが呼び出されます。 型の定義、メソッドの定義から呼び出しまでを続けて書くと下記のような形になります。 package main import ( "fmt" ) type MyString string func (s MyString) Keisyo() string { return "さん" } func main() { namae := MyString( "たなか" ) fmt.Print(namae, namae.Keisyo()) } ⇒出力結果 たなかさん になります。 同一のメソッド名について メソッドは、「呼び出しに指定した型と一致するレシーバを持つメソッド」が呼び出されます。 つまり、レシーバの型さえ違えば、同一のメソッド名を作成することが可能です。 例えば、先ほどの例では Keisyo() を呼び出すと「さん」が返ってきていました。 ただ、呼び出し元の型によって敬称を使い分けたい場合、下記のようにtypeで型を複数定義し、typeで拡張した型ごとにメソッドを持たせることで、同じメソッド名で違う振る舞いをさせることが出来ます。 package main import ( "fmt" ) type MyString1 string type MyString2 string func (s MyString1) Keisyo() string { return "さん" } func (s MyString2) Keisyo() string { return "くん" } func main() { namae1 := MyString1( "たなか" ) namae2 := MyString2( "さとう" ) fmt.Println(namae1, namae1.Keisyo()) fmt.Println(namae2, namae2.Keisyo()) } ⇒出力結果 たなか さん さとう くん になります。 インターフェースについて 上記のように、同一のメソッド名を複数の型に持たせることが出来ます。 ただ、呼び出し元と同一のメソッド名しか呼び出せません。 Go言語は静的型付け言語です。変数がカッチリ定義されているので、1つの型として定義された変数には、たとえstringを基にしただけの似たような型であっても、別の型の変数を入れることは出来ません。 func main() { onamae := MyString1( "たなか" ) // ここでonamaeはMyString1の型として定義されてしまいます fmt.Println(onamae, onamae.Keisyo()) onamae = MyString2( "さとう" ) // onamaeはMyString1の型として使われたためエラーになります fmt.Println(onamae, onamae.Keisyo()) } つまり、Go言語で複数の型を使い分けようとすると、このままではif文等で分岐して型ごとの処理を書く必要があり、 ソースコード としてはとても煩雑になります。 どちらもKeisyo()というメソッド名を持っているので、まとめられたら楽そうです。 そのような時に使えるのが、まとめてメソッドを定義、実行できるインターフェースです。 Go言語のインターフェースの定義はシンプルで、下記のように記載すると、stringの戻り値を持つKeisyo()メソッドを持つ拡張型は全て、ONAMAEインターフェースの仲間入りをすることが出来ます。 type ONAMAE interface { Keisyo() string } こうしておくと、先ほどのMyString1型も、MyString2型も、Go言語ではONAMAEインターフェースで使うことが出来ます。 そのため下記のように、ONAMAEインターフェースに型を入れるだけで同じ呼び出し方法 fmt.Println(onamae,onamae.Keisyo()) で違う振る舞いをさせることが出来ます。 package main import ( "fmt" ) type ONAMAE interface { Keisyo() string } type MyString1 string type MyString2 string func (s MyString1) Keisyo() string { return "さん" } func (s MyString2) Keisyo() string { return "くん" } func main() { var onamae ONAMAE onamae = MyString1( "たなか" ) fmt.Println(onamae, onamae.Keisyo()) onamae = MyString2( "さとう" ) fmt.Println(onamae, onamae.Keisyo()) } ⇒出力結果 たなか さん さとう くん になります。 関数化しておくなどで使いまわす場合、おなじstringを戻り値としたKeisyo()さえ持っていれば、例えば「様」や「どの」を追加したくなった時など、色々な敬称の処理を追加しやすくなります。 また、インターフェースは下記のように、中に入っているタイプによって処理を変える事も出来る為、受け取った内容によって振る舞いを変えたいけど、処理をまとめたいという場合にはとても便利だと感じます。 switch onamae.( type ) { case MyString1: fmt.Println( "こんにちは" ) case MyString2: fmt.Println( "やっほー" ) } 今までのコードと繋げると、下記のようにonamaeに入った型によって、挨拶が変わります package main import ( "fmt" ) type ONAMAE interface { Keisyo() string } type MyString1 string type MyString2 string func (s MyString1) Keisyo() string { return "さん" } func (s MyString2) Keisyo() string { return "くん" } func main() { var onamae ONAMAE onamae = MyString1( "たなか" ) fmt.Println(onamae, onamae.Keisyo()) switch onamae.( type ) { case MyString1: fmt.Println( "こんにちは" ) case MyString2: fmt.Println( "やっほー" ) } onamae = MyString2( "さとう" ) fmt.Println(onamae, onamae.Keisyo()) switch onamae.( type ) { case MyString1: fmt.Println( "こんにちは" ) case MyString2: fmt.Println( "やっほー" ) } } ⇒出力結果 たなか さん こんにちは さとう くん やっほー になります。 最後に 今回はGo言語のメソッドについて、記事を書かせて頂きました。 ただ、私は普段Go言語を使って業務を行っているわけではありません。 むしろインフラ管理等を行っており、私はあまりプログラムに馴染みが無い状態です。 今、 ラク スではSRE課の方が先導して、Go言語の勉強会を開催してくださっています。 私のような初心者でも分かるよう、実例を交えてイメージしやすく教えてもらうことが出来、この記事を書こうと思えるまでに理解を深めることが出来ました。 直近の実務のことだけでなく、将来を見据えてGo言語のような需要がありそうな技術の勉強会を開催頂き、そこへの参加機会があるのは、 ラク スの良いところだと思います! エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
今年も早いものでもう年末です。 大掃除を意識した時に、「普段からこまめにやっておけば...」と毎年後悔しています。 そんな私とは違って、 PostgreSQL には普段からデータをこまめに掃除してくれる優秀な「VACUUM」という機能があります。 しかも ゴミがでやすい時は小まめに、そうでないときは手を抜いてゆっくりやる というように調整しながら掃除をしてくれます! ですがしっかりお世話をしないと、「あまりにも時間がかかりすぎる」「途中でやめてしまった」といったトラブルが発生します。 今回はそんな PostgreSQL のVACUUM機能を紹介したいと思います。 PostgreSQLのレコード更新/削除のしくみと問題点 1. PostgreSQLにおけるレコードの更新/削除のしくみ 2. ゴミデータが溜まることの問題点 PostgreSQLにおけるVACUUMの役割 1. 不要領域の回収/削除 2. トランザクション周回問題の防止 3. プランナ用の統計情報の更新 VACUUMの特徴 1. 実行手順 2. VACUUMの種類 AUTO VACUUM VACUUM FULL 実行タイミング AUTO VACUUM 設定ファイルの確認 1. VACUUMが実行されるタイミングを知る 実験 AUTO VACUUMの実行 ロングトランザクション 2. VACUUMのチューニングを実施する チューニング方法 バキュームの実行時間を短くしたい 任意のテーブルにバキュームの設定値をセットする CASE 1: 任意のテーブルのみ頻繁/緩やかにVACUUMが実行されるようにする CASE 2: 任意のテーブルを自動バキュームの対象外にする AUTO VACUUMの課題 VACUUM FULL 使い方 OSに空き領域を返してくれることを確認する VACUUM FULLの課題 終わりに 検証環境 vagrant + virtual box ubuntu20.24 + PostgreSQL 12 vagrant については vagrant+virtual box でDockerを動かす記事 もありますので是非ご一読ください。 PostgreSQL のレコード更新/削除のしくみと問題点 PostgreSQL は、実は裏でゴミデータを削除してくれています。 が、そもそもゴミデータとは何なのでしょうか? まずは、なぜそのゴミデータが出来上がるのか?と、それによる問題点を整理します。 1. PostgreSQL におけるレコードの更新/削除のしくみ PostgreSQL は、 追記型 アーキテクチャ を採用しています。 例えば、あるレコードの更新を行った場合は直接そのレコードを書き換えるのではなく、以下のような動作をします。 更新対象のレコードに削除フラグ(目印)を付ける 更新後のレコードを追加する 追記型の説明 この場合、グレーアウトされたレコードは使用していないにも関わらず残っています。 これが不要領域、いわゆる ゴミデータ です。 削除時も同様で、例えば100万件レコードを削除した場合はそれらのレコードに削除マークを付与するだけなので、100万件分のデータが残ったままになります。 またレコードだけでなく、インデックスも残ってしまいます。 2. ゴミデータが溜まることの問題点 このようなゴミデータが溜まり続けると、以下の問題が発生します。 ゴミデータが溜まり続けることでディスク使用量が増える メモリ上の共有バッファにゴミデータが増え、メモリアクセスではなくディスクアクセスが発生する ⇒ 性能劣化 上記の問題点を解決する方法として「VACUUM」という PostgreSQL の機能を使いましょう! PostgreSQL におけるVACUUMの役割 大きく3つの役割があります。 1. 不要領域の回収/削除 VACUUMは、ゴミデータと呼ばれる不要領域を再利用可能な領域にリサイクルしてくれたり、ディスクの空き容量を増やしてくれたりします。 VACUUM実行後 VACUUMを実行することで、2つのファイルが生成/更新されます。 空き領域マップ(FMS: Free Scan Map) 空き領域が登録されたマップ。 例えばレコードを追加する場合、空いている場所がどこにあるのかを探すときにこのマップがあれば便利です。 可視性マップ(VM: Visibility Map) どこにゴミデータがあるかを記録したマップ。 これにより、ゴミデータがある部分のみVACUUM処理を行えばよいという判断が可能なため、vacuum処理を速く完了させることができます。 可視性マップ その他の恩恵として、インデックスオンリースキャンが実行できるようになります。 2. トランザクション 周回問題の防止 PostgreSQL には、有名な トランザクション 周回問題という問題があります。 見えていてたはずのデータが突然見えなくなってしまうという現象が発生することを指します。 PostgreSQL では トランザクション にIDが割り振られています( トランザクション ID = XID)。 例えばレコードの追加を行った場合、追加したレコードのシステムカラムにXIDが登録されます。 XIDは32bit(約42億個)で管理されています。 XIDを全て使い切った場合は トランザクション を新規で発行できなくなるため、0から再利用する仕組みとなっています。 つまりXIDは循環するようにして割り当てられます。 PostgreSQL では MVCC を使用しデータ管理を行っています。 実行中の トランザクション は、自身が持っている トランザクション よりも古いXIDは見れますが、新しいXIDは見れないしくみになっています。 そこで、新しいXIDを使い続ければ自分より古いXIDが新しいXIDに変わってしまうため見えなくなります。 例えば以下の図のようにXIDが20億進むと、過去のXIDが未来のXIDに変わってしまうため、本来見れるはずのレコードが見れなくなってしまいます。 これが トランザクション 周回問題です。 下図参照 トランザクション 周回問題1 トランザクション 周回問題2 この問題は、FREEZE処理をVACUUMで行うことによって解決されます。 FREEZE処理とは、XIDに凍結されたことを示すマークを付けることです。 マークがあるXIDは全て過去のものとして扱うことができます。 具体的には、xminを「2(特殊なXID)」に書き換える処理を行っています。 また、設定値を変更することでどの程度XIDが消費されたらFREEZE処理を開始するといったコン トロール ができます。 その他にも、XIDが枯渇しないような仕組みがデフォルトで組み込まれています。 本記事では紹介しませんが、代わりに公式マニュアルを紹介させて頂きます。 www.postgresql.jp 3. プランナ用の統計情報の更新 詳細の説明は省きますが、効率良く SQL を実行するための「実行計画」を作成するプランナが利用する統計情報(データの分布などの情報)を更新してくれます。 実行計画についてはコチラの記事をぜひご参考ください。 soachr.hatenablog.com このようにVACUUMはゴミデータを削除してくれたり、XIDを凍結状態にすることで トランザクション 周回問題を防止します。 VACUUMの特徴 1. 実行手順 VACUUMは以下の 7つの手順 で実行されます。 手順 フェーズ 説明 1 initializing ヒープをスキャンし始める準備フェーズ。 2 scanning heap ヒープのスキャンを実行します。テーブルの先頭からゴミデータを探索します。発見したゴミデータのIDをメモリに乗せる。 maintenance_work_mem で設定可能。この値を越えた場合はスキャンを中断し、手順3に進みます。 3 vacuuming indexes インデックスのバキュームを実行します。フルスキャンするため最も時間がかかる処理。Postgres13で実装された パラレルスキャン により高速化を実現しています。 4 vacuuming heap テーブルのバキュームを実行します。 VM を使って必要箇所のみバキューム実行を行います。 heap_blks_scannedがheap_blks_totalより少ない場合、システムはこのフェーズの完了後に手順2のヒープのスキャン処理に戻ります。 5 cleaning up indexes インデックスの整理を行います。後始末フェーズです。 6 truncating heap テーブルの末尾を切り詰めるなどの後始末を行います。 7 performing final cleanup 空き容量マップをバキュームしたり、pg_classの統計処理を更新するなどの後始末を行います。 最も重要なフェーズは3番目のINDEX VACUUMです。 INDEX VACUUMはフルスキャンが発生するため実行時間が他のフェーズよりもかかります。 VACUUMに時間がかかっている場合は、ログを確認してINDEX VACUUMが複数回実行されてるかどうかを確認し、必要であればmaintenance_work_memを増やす対策が必要です。 ログの確認方法については後述の情報をご参照ください。 2. VACUUMの種類 VACUUMは、AUTO VACUUMとVACUUM FULLの2パターンがあります。 それぞれの特徴について軽く触れておきます。 AUTO VACUUM AUTO VACUUMは、設定値に従って自動的にVACUUMを実行してくれます。 基本的にデフォルトでONになっています(古いバージョンではOFFになっています!)。 VACUUM実行中であっても排他ロックを取らないため、レコードの参照等が可能です。 cronのようにVACUUMを定時で実行することができません。 デフォルト設定値もそれなりに適切なため、VACUUMは基本的にAUTO VACUUMを利用しておけば問題ないかと思います。 VACUUM FULL VACUUM FULLは自動ではなく明示的にVACUUMを実行します。 回収した不要領域をOSに戻してくれるためHDDなどの記憶領域が増えます。 その代わり、VACUUM実行中はテーブルに強力な排他ロックをかけるためSELECTすらできなくなるので気を付ける必要があります。 実行タイミング VACUUMが実行されるタイミングは大きく2つあります。 定時および設定値を越えた時 トランザクション 周回問題を防止する時 では、それぞれの特徴と実際の動作を確認してみましょう。 AUTO VACUUM AUTO VACUUMはデフォルトでONになっていますが、明示的に指定する場合は postgresql .confファイルから設定可能です。 設定ファイルの確認 設定ファイルは下記コマンドで探せます。 find / -name postgresql.conf 2> /dev/null 設定を確認してみましょう。 less /etc/postgresql/12/main/postgresql.conf #autovacuum = on ここの コメントアウト を外すと明示的に有効になります。 なお track_counts = on も必要ですが、こちらもデフォルトでONになっています。 #------------------------------------------------------------------------------ # AUTOVACUUM #------------------------------------------------------------------------------ #autovacuum = on # Enable autovacuum subprocess? 'on' # requires track_counts to also be on. #log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and # their durations, > 0 logs only # actions running at least this number # of milliseconds. #autovacuum_max_workers = 3 # max number of autovacuum subprocesses # (change requires restart) #autovacuum_naptime = 1min # time between autovacuum runs #autovacuum_vacuum_threshold = 50 # min number of row updates before # vacuum #autovacuum_analyze_threshold = 50 # min number of row updates before # analyze #autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum #autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze #autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum # (change requires restart) #autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age # before forced vacuum # (change requires restart) #autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for # autovacuum, in milliseconds; # -1 means use vacuum_cost_delay #autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for # autovacuum, -1 means use # vacuum_cost_limit これらの設定値を変更することによって、VACUUMの実行タイミングをコン トロール したりチューニングすることができます。 1. VACUUMが実行されるタイミングを知る 設定値でAUTO VACUUMの実行タイミングを調整することができます。 AUTO VACUUMは バキューム 閾値 を越えた場合実行されます。 バキューム 閾値 は以下の式で算出されます。 バキューム基礎閾値: #autovacuum_vacuum_threshold = 50 バキューム規模係数: #autovacuum_vacuum_scale_factor = 0.2 バキューム閾値 = バキューム基礎閾値 + バキューム規模係数 * タプル数 100万件のレコードを対象とした場合は バキューム 閾値 = 50 + 0.2 * 1000000 = 200050件 となります。 ※VACUUM実行のタイミングはautovacuum_naptimeで管理されます。 デフォルト設定値は60なので、60秒単位でAUTO VACUUMを実行するかどうかを監視しています。 実験 AUTO VACUUMの実行 実験前にVACUUMが実行されたかどうかの情報をチェックしておきましょう。 pg_stat_all_tables で確認することができます。 postgres=# SELECT * FROM pg_stat_all_tables WHERE relname = 'vacuum_test'; -[ RECORD 1 ]-------+------------ relid | 32812 schemaname | public relname | vacuum_test seq_scan | 0 seq_tup_read | 0 idx_scan | idx_tup_fetch | n_tup_ins | 0 n_tup_upd | 0 n_tup_del | 0 n_tup_hot_upd | 0 n_live_tup | 0 n_dead_tup | 0 n_mod_since_analyze | 0 last_vacuum | last_autovacuum | last_analyze | last_autoanalyze | vacuum_count | 0 autovacuum_count | 0 AUTO VACUUMが実行された回数 analyze_count | 0 autoanalyze_count | 0 autovacuum_count が0回になっているので、AUTO VACUUMは実行されていないことが分かります。 ついでに実ファイルも確認しておきましょう。 relidが32812とありますので、そちらが実ファイルの場所です。 実ファイルの場所は datid/relid です。以下の SQL でdatidを確認できます。 postgres=# SELECT datid, datname FROM pg_stat_database WHERE datname = 'postgres'; -[ RECORD 1 ]----- datid | 13461 datname | postgres postgres@ubuntu2004:~$ ls -lh /var/lib/postgresql/12/main/base/13461/32812* -rw------- 1 postgres postgres 0★ Dec 21 01:18 /var/lib/postgresql/12/main/base/13461/32812 実ファイルのサイズが0ですね。 続いて、レコード数がバキューム 閾値 を越えない場合はAUTO VACUUMが効かないことを確認します。 閾値 は20万50件なので、 閾値 を越えないよう20万件INSERTしてみます。 `INSERT INTO vacuum_test SELECT generate_series(1,200000),md5(clock_timestamp()::text);` -[ RECORD 1 ]-------+------------ relid | 32812 schemaname | public relname | vacuum_test seq_scan | 1 seq_tup_read | 200000 idx_scan | idx_tup_fetch | n_tup_ins | 200000 n_tup_upd | 0 n_tup_del | 0 n_tup_hot_upd | 0 n_live_tup | 200000 n_dead_tup | 0 n_mod_since_analyze | 0 last_vacuum | last_autovacuum | last_analyze | last_autoanalyze | 2022-12-23 05:06:47.194957+00 vacuum_count | 0 autovacuum_count | 0 ★カウントが増えていない analyze_count | 0 autoanalyze_count | 1  autovacuum_countが0のままですね。 実ファイルを確認してみましょう。 -rw------- 1 postgres postgres 14M★ Dec 21 01:20 /var/lib/postgresql/12/main/base/13461/32812 -rw------- 1 postgres postgres 24K Dec 21 01:20 /var/lib/postgresql/12/main/base/13461/32812_fsm 実ファイルが14Mに増加しています。 FSMが生成されていることも確認できますね。 続いてゴミデータを作るために1件を残して削除します。 DELETE FROM vacuum_test WHERE id <= 199999; `SELECT * FROM pg_stat_all_tables WHERE relname = 'vacuum_test';` -[ RECORD 1 ]-------+------------ relid | 32812 schemaname | public relname | vacuum_test seq_scan | 1 seq_tup_read | 200000 idx_scan | idx_tup_fetch | n_tup_ins | 200000 n_tup_upd | 0 n_tup_del | 199999 ★不要な行 n_tup_hot_upd | 0 n_live_tup | 1 n_dead_tup | 199999 n_mod_since_analyze | 199999 last_vacuum | last_autovacuum | last_analyze | last_autoanalyze | 2022-12-21 01:20:53.861522+00 vacuum_count | 0 autovacuum_count | 0 ★カウントが増えていない analyze_count | 0 autoanalyze_count | 1 n_tup_delが199999となっています。 こちらは不要な行を表していますので、先ほどDELETEしたレコードがゴミデータとなっています。 続いてバキューマの 閾値 (20万50件)を越えるレコードを追加すると、バキュームが実行されることを確認します。 あと50件以上レコードを作成すれば 閾値 を越えるためAUTO VACUUMが実行されるはずです。 ゆとりを持って100件追加してみましょう。 100件insert insert into vacuum_test select generate_series(1,100),md5(clock_timestamp()::text); auto_vacuumが実行されていることを確認 postgres=# SELECT * FROM pg_stat_all_tables WHERE relname = 'vacuum_test'; -[ RECORD 1 ]-------+------------------------------ relid | 32812 schemaname | public relname | vacuum_test seq_scan | 1 seq_tup_read | 200000 idx_scan | idx_tup_fetch | n_tup_ins | 200100 n_tup_upd | 0 n_tup_del | 199999 n_tup_hot_upd | 0 n_live_tup | 101 n_dead_tup | 0 n_mod_since_analyze | 0 last_vacuum | last_autovacuum | 2022-12-21 01:22:53.960128+00 last_analyze | last_autoanalyze | 2022-12-21 01:24:53.588466+00 vacuum_count | 0 autovacuum_count | 1 ★カウントが増えている analyze_count | 0 autoanalyze_count | 3 autovacuum_countが0から1に増加しています。 設定した 閾値 を越えたためAUTO VACUUMが実行されていますね。 実ファイルを確認してみます。 -rw------- 1 postgres postgres 14M★ Dec 21 01:27 /var/lib/postgresql/12/main/base/13461/32812 -rw------- 1 postgres postgres 24K Dec 21 01:20 /var/lib/postgresql/12/main/base/13461/32812_fsm -rw------- 1 postgres postgres 8.0K Dec 21 01:22 /var/lib/postgresql/12/main/base/13461/32812_vm VACUUMが実行されたため可視性マップも作成されていますね。 データファイルは圧縮されていないですが、不要領域が回収できており再利用できているかどうか確認します。 さらに20万レコードを追加 insert into vacuum_test select generate_series(1,200000),md5(clock_timestamp()::text); レコードを追加しても容量が14Mから変わっていません。 不要領域を回収して空き容量にリサイクルし、再利用出来ていることが確認できます。 -rw------- 1 postgres postgres 14M★ Dec 21 01:27 /var/lib/postgresql/12/main/base/13461/32812 -rw------- 1 postgres postgres 24K Dec 21 01:27 /var/lib/postgresql/12/main/base/13461/32812_fsm -rw------- 1 postgres postgres 8.0K Dec 21 01:27 /var/lib/postgresql/12/main/base/13461/32812_vm さらに20万レコードを追加 postgres=# insert into vacuum_test select generate_series(1,200000),md5(clock_timestamp()::text); INSERT 0 200000 postgres=# select count(*) from vacuum_test; -[ RECORD 1 ]- count | 400101 容量を確認します。 postgres@ubuntu2004:~$ ls -lh /var/lib/postgresql/12/main/base/13461/32812* -rw------- 1 postgres postgres 27M★ Dec 21 01:34 /var/lib/postgresql/12/main/base/13461/32812 -rw------- 1 postgres postgres 24K Dec 21 01:27 /var/lib/postgresql/12/main/base/13461/32812_fsm -rw------- 1 postgres postgres 8.0K Dec 21 01:27 /var/lib/postgresql/12/main/base/13461/32812_vm 再利用可能な空き容量が無くなったため追加したレコード分実ファイルが増加しました。 今回はVACUUMが実行されましたが、ある条件では実行されないケースがあります。 ロング トランザクション VACUUMが機能しない時、ロング トランザクション が原因になっているケースがあります。 ロング トランザクション とは、 トランザクション が開始されてから、コミット/ ロールバック が行われず長時間生存している トランザクション の事です。 ロング トランザクション よりも後の トランザクション 更新/削除されたレコードはバキューム処理の対象外となってしまいます。 ロング トランザクション バキュームが実行されたにも関わらずゴミデータが回収されていない場合は、ロング トランザクション があることを疑ってみましょう。 下記 SQL を実行すると、ゴミデータがいくら残っているかを確認できます。 SELECT relname, n_live_tup, n_dead_tup, round(n_dead_tup*100/(n_dead_tup+n_live_tup), 2) AS dead_ratio, pg_size_pretty(pg_relation_size(relid)) FROM pg_stat_user_tables WHERE n_live_tup > 0 ORDER BY dead_ratio DESC; -[ RECORD 1 ]--+------------ relname | vacuum_test n_live_tup | 1 n_dead_tup | 1100000 ★不要行。ゴミデータ dead_ratio | 99.00   ゴミデータが占める割合 pg_size_pretty | 72 MB ゴミデータが多く、VACUUM処理で不要領域が回収できていないことが考えられます。 ロング トランザクション が存在しているかどうかは下記 SQL で確認可能です。 SELECT pid, query, xact_start, state FROM pg_stat_activity WHERE state = 'idle in transaction'; pid | 1478 query | SELECT pid, query, xact_start, state FROM pg_stat_activity; xact_start | 2022-12-22 06:39:26.865784+00 state | idle in transaction★ stateが idle in transaction の トランザクション は、 トランザクション がスタートしているにも関わらずCOMMITもROLLBACKも行われていない状況です。 この トランザクション のxact_startの日時が古い場合はロング トランザクション と考えられるため、 pg_terminate_backend でプロセスの終了を実施します。 SELECT pg_terminate_backend(pid); 2. VACUUMのチューニングを実施する 頻繁にレコードが更新されるテーブルはメンテナンス負荷がかかるため、チューニングを実施するか自動バキュームの対象外にすることが望ましいです。 チューニング方法 auto_vacuumのログを確認してチューニングを行います。 auto_vacuum実行ログはデフォルトでOFFになっているため、下記 SQL でログ出力を行います。 VACUUMが設定したms以上かかった場合はログを出力します。 0に設定した場合はAUTO VACUUMの結果が出力され、-1では出力が無効になります。 log_autovacuum_min_duration = 60000ms ログは以下のように出力されます 2022-12-23 04:13:46.320 UTC [11751] LOG: automatic vacuum of table "postgres.public.vacuum_test": index scans: 1★ pages: 0 removed, 8334 remain, 0 skipped due to pins, 0 skipped frozen tuples: 999999 removed, 4 remain, 0 are dead but not yet removable, oldest xmin: 568 buffer usage: 44216 hits, 0 misses, 864 dirtied avg read rate: 0.000 MB/s, avg write rate: 4.742 MB/s system usage: CPU: user: 0.35 s, system: 0.03 s, elapsed: 1.42 s 2022-12-23 04:13:46.443 UTC [11751] LOG: automatic analyze of table "postgres.public.vacuum_test" system usage: CPU: user: 0.01 s, system: 0.00 s, elapsed: 0.12 s 特筆すべき点は、index scansです。 これはVACUUMの手順3(vacuuming indexes)が何回行われたかを指します。 今回は値が1なので1回行われたことになり、SCAN⇒VACUUMのループが発生しなかったと考えられますのでOKです。 これが1回以上であればループが発生していますので、vacuuming indexesを早く終わらせるための施策を実施する必要があります(maintenance_work_memを増やすなど)。 AUTO VACUUMが終わらない場合はまずココを疑ってみてチューニングをしてみましょう。 バキュームの実行時間を短くしたい システムのメンテナンス時間を短くしたいなど、できるだけ短時間でバキュームの実行を完了させたいケースになります。 また、VACUUMが実行されているにも関わらずテーブルが肥大化し続ける場合も有効です。 以下の方法がありますの状況に応じて組わせると良いかと思います。 バキュームの 閾値 を下げる autovacuum_vacuum_scale_factorを下げることでバキューム 閾値 が下がります。 例えばautovacuum_vacuum_scale_factorを0.2から0.01に下げると、 バキューム基礎閾値: #autovacuum_vacuum_threshold = 50 バキューム規模係数: #autovacuum_vacuum_scale_factor = 0.01 (100万行の場合) バキューム閾値 = 50 + 0.02 * 1000000 = 10,050件 となります。 デフォルトでは200,050件を越えなければVACUUMが実行されませんでしたが、ココを調整することで改善が望めます。 なお、 閾値 を下げすぎた場合は不必要なVACUUMによりオーバーヘッドが発生しますので注意が必要です。 バキュームの頻度を上げる autovacuum_vacuum_cost_delay を下げる(手動vacuum実行時はデフォルトで無効) 自動バキュームは autovacuum_cost_limitの設定値を処理すると、autovacuum_vacuum_cost_delayの設定値だけ一時停止します。 つまりautovacuum_vacuum_cost_delayを下げることで、vacuumの間隔を短くすることができ、早くVACUUMを終わらせることが可能になります。 なお、こちらを0にすることで全く遅延させることなく次のvacuum実行が可能になります。 maintenance_work_mem を増やす(最大1GB) maintenance_work_memを増やすことでVACUUM実行フェーズの内最も時間がかかる インデックスのバキューム を高速化します。 autovacuum_max_workers を増やす autovacuum_max_workersは、同時に実行することができるautovacuumプロセスの最大数です。 巨大なテーブルが存在する場合は増やすと改善が望めます。 任意のテーブルにバキュームの設定値をセットする postgresql .confの値を修正すると、全テーブルに対して設定を反映してしまいます。 そのため、各テーブルに必要なパラメータを設定するようにしましょう。 CASE 1: 任意のテーブルのみ頻繁/緩やかにVACUUMが実行されるようにする 任意のテーブルにパラメータをセットし、バキューム 閾値 をコン トロール できます。 ALTER TABLE vacuum_test SET (autovacuum_vacuum_threshold=50,autovacuum_vacuum_scale_factor=0.01); CASE 2: 任意のテーブルを自動バキュームの対象外にする 頻繁に更新が走るような巨大なテーブルは、システムが利用されていない夜間に手動でバキュームを行う運用方法も考えられます。 その場合は、下記の SQL で自動バキュームの対象外とすることができます。 ALTER TABLE vacuum_test SET (autovacuum_enabled = off); ALTER TABLE -[ RECORD 1 ]-------+------------------------------ relid | 32812 schemaname | public relname | vacuum_test seq_scan | 6 seq_tup_read | 800103 idx_scan | idx_tup_fetch | n_tup_ins | 600100 n_tup_upd | 0 n_tup_del | 200099 n_tup_hot_upd | 0 n_live_tup | 400001 n_dead_tup | 0 n_mod_since_analyze | 0 last_vacuum | last_autovacuum | 2022-12-21 01:25:53.554516+00 last_analyze | last_autoanalyze | 2022-12-21 01:34:54.471287+00 vacuum_count | 0 autovacuum_count | 2 analyze_count | 0 autoanalyze_count | 6 postgres=# DELETE FROM vacuum_test WHERE id <= 400000; -[ RECORD 1 ]-------+------------------------------ relid | 32812 schemaname | public relname | vacuum_test seq_scan | 6 seq_tup_read | 800103 idx_scan | idx_tup_fetch | n_tup_ins | 600100 n_tup_upd | 0 n_tup_del | 200099 n_tup_hot_upd | 0 n_live_tup | 400001 n_dead_tup | 0 n_mod_since_analyze | 0 last_vacuum | last_autovacuum | 2022-12-21 01:43:54.840929+00 last_analyze | last_autoanalyze | 2022-12-21 01:44:54.584522+00 vacuum_count | 0 autovacuum_count | 2 analyze_count | 0 autoanalyze_count | 6 autovacuum_countに変化がありません。 AUTO VACUUMの課題 定期実行が不可能 設定値を越えた場合にバキューム処理が実行されるため、システムが動いていない夜間にバキュームを実行させたいといったケースには対応できません。 空き領域をOSに返さない auto_vacuumでは不要領域を回収していることを確認しましたが、空いた領域をOSに返していません。 この場合、例えば、HDDの使用量によって課金請求が発生するため不要なデータを削除してHDD使用量を下げたいといったケースには対応できません。 こういったケースについては、VACUUM FULLを利用することで空き領域をOSに返すことができます。 VACUUM FULL 基本的な動きは以下の通りです。 テーブルのレコードを取得し新しいテーブルに詰め込む 新しいテーブルにインデックスを作成 テーブルを入れ替える 主な特徴は 不要領域を回収する 空き領域をOSに返す 実行時はテーブルロックがかかる テーブルの作り替えとインデックスの再作成を実施するため時間がかかる VACUUM実行時に古いテーブルと新しいテーブルが同時に作成されるため一時的に容量が辛くなる 利用シーンとしては、たとえばHDDの使用量によって課金請求するシステムであれば、ユーザーがシステム画面上で任意の時間にVACUUM FULLを使ったメンテナンスを実行するケースが思いつきます。 使い方 下記 SQL で実行可能です。 VACUUM FULL tablename; VACUUM FULLを行った場合は空き領域をOSに戻してくれるかどうかを確認してみましょう。 OSに空き領域を返してくれることを確認する その前に、実験用のテーブルはAUTO VACUUMの対象外にしておく必要があります。 ALTER TABLE vacuum_test SET (autovacuum_enabled = off); 実ファイルを確認しておきます。 postgres=# select relname, relfilenode from pg_class where relname = 'vacuum_test'; relname | relfilenode -------------+------------- vacuum_test | 32777 (1 row) postgres@ubuntu2004:~$ ls -lh /var/lib/postgresql/12/main/base/13461/32777* -rw------- 1 postgres postgres 0★ Dec 19 04:35 /var/lib/postgresql/12/main/base/13461/32777 ファイル容量は0ですね。 続いて100万件レコードを追加してみます。 postgres=# insert into vacuum_test select generate_series(1,1000000),md5(clock_timestamp()::text); INSERT 0 1000000 postgres@ubuntu2004:~$ ls -lh /var/lib/postgresql/12/main/base/13461/32777* -rw------- 1 postgres postgres 66M★ Dec 19 04:36 /var/lib/postgresql/12/main/base/13461/32777 -rw------- 1 postgres postgres 40K Dec 19 04:36 /var/lib/postgresql/12/main/base/13461/32777_fsm ファイル容量が66Mに増加しています。 大量にDELETEしてゴミデータを作成します。 postgres=# delete from vacuum_test where id < 999999; DELETE 999998 postgres@ubuntu2004:~$ ls -lh /var/lib/postgresql/12/main/base/13461/32777* -rw------- 1 postgres postgres 66M★ Dec 19 04:40 /var/lib/postgresql/12/main/base/13461/32777 -rw------- 1 postgres postgres 40K Dec 19 04:36 /var/lib/postgresql/12/main/base/13461/32777_fsm -rw------- 1 postgres postgres 8.0K Dec 19 04:40 /var/lib/postgresql/12/main/base/13461/32777_vm ファイル容量が減っていません。 VACUUM FULLを実行してみましょう。 VACUUM FULL vacuum_test; テーブルの再作成が行われるため、古いテーブルの実ファイルは空になっています。 postgres@ubuntu2004:~$ ls -lh /var/lib/postgresql/12/main/base/13461/32777* -rw------- 1 postgres postgres 0 Dec 19 04:46 /var/lib/postgresql/12/main/base/13461/32777 新しく作成されたテーブルの実ファイルを確認します。 postgres=# select relname, relfilenode from pg_class where relname = 'vacuum_test'; relname | relfilenode -------------+------------- vacuum_test | 32785 (1 row) postgres@ubuntu2004:~$ ls -lh /var/lib/postgresql/12/main/base/13461/32785* -rw------- 1 postgres postgres 8.0K★ Dec 19 04:46 /var/lib/postgresql/12/main/base/13461/32785 ファイル容量が減っていることが確認できます。 VACUUM FULLの課題 処理の重さ VACUUM FULLはテーブルとインデックスの再作成を行うため処理が非常に重くなります。 そのため、AUTO VACUUMでは本当に達成できないケースなのかを検討したうえでVACUUM FULLをせ択するようにしましょう。 排他ロック VACUUM実行中は強力な排他ロックがかかるため、システムが稼働していない時間帯にVACUUMの実行を終える必要があります。 排他ロック問題に関しては、 pg_repack と呼ばれる拡張があります。 こちらは、排他ロックを取らずにVACUUM FULL相当の処理を行ってくれる拡張となっていますので導入を検討してみるという手もあります。 終わりに VACUUMの機能と特徴を紹介させて頂きました。 デフォルト設定でも運用可能ですが、「DBの寿命はアプリケーションよりも長い」と言われるように、長期間運用を続けていくうちにレコードやゴミデータが溜まることで様々な問題が発生します。 PostgreSQL とは切っても切り離せないVACUUMの深淵を少し覗くことで、普段からこまめに掃除をしてくれているその仕組みと恩恵を知ることができ、さらに興味が湧いてきました! エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
先日、JUnit4からJUnit5への移行作業を実施しました。 移行する際に色々とコードの変更が必要なのですが、作業しながら「パッケージと名前の変更点が一括でまとまってる記事があったらな~」と思ったのでこの記事でまとめることにしました。 アノテーション 変更されたもの 削除されたもの アサート 変更されたもの 削除されたもの おわりに 執筆 & 移行の際の参考サイト アノテーション 変更されたもの JUnit4 JUnit5 org. junit .Before org. junit .jupiter. api .BeforeEach org. junit .After org. junit .jupiter. api .AfterEach org. junit .BeforeClass org. junit .jupiter. api .BeforeAll org. junit .AfterClass org. junit .jupiter. api .AfterAll org. junit .Test org. junit .jupiter. api .Test 削除されたもの JUnit4 JUnit5 org. junit .runners.Enclosed org. junit .jupiter. api .Nested org. junit .Ignore org. junit .jupiter. api .Disabled org. junit .experimental.categories.Category org. junit .jupiter. api .Tag org. junit .runner.Runwith org. junit .jupiter. api .extension.ExtendWith org. junit .Rule org. junit .jupiter. api .extension.ExtendWith org. junit .ClassRule org. junit .jupiter. api .extension.ExtendWith ↓パラメータ化テスト (少し特殊なので別に記載) JUnit4 JUnit5 アノテーション (Runwithの引数に渡す) アノテーション org. junit .runners.Parameterized org. junit .jupiter.params.ParameterizedTest パラメータ指定 import org. junit .runners.Parameterized.Parameters org. junit .jupiter.params.provider.ValueSource org. junit .jupiter.params.provider.CsvSource etc... アサート 変更されたもの JUnit4 JUnit5 org. junit .Assert.assertTrue org. junit .jupiter. api .Assertions.assertTrue org. junit .Assert.assertFalse org. junit .jupiter. api .Assertions.assertFalse org. junit .Assert.assertEquals org. junit .jupiter. api .Assertions.assertEquals org. junit .Assert.assertArrayEquals org. junit .jupiter. api .Assertions.assertArrayEquals org. junit .Assert.assertNull org. junit .jupiter. api .Assertions.assertNull org. junit .Assert.fail org. junit .jupiter. api .Assertions.fail 削除されたもの JUnit4 JUnit5 org. junit .Assert.assertThat サードパーティー 製のライブラリを使う assertJ static org.assertj.core. api .Assertions.assertThat hamcrest org.hamcrest.MatcherAssert.assertThat Truth com. google .common.truth.Truth.assertThat おわりに 変更点盛りだくさんでした。 本記事がこれから移行する方の助けになれば幸いです。 執筆 & 移行の際の参考サイト https://speakerdeck.com/akkie76/koredeshi-bai-sinai-junit-5-hefalsemaiguresiyonfang-fa https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
ラク スでメールディーラーの運用サポートチームのリーダをやっています、@neroblubrosです。 この記事は ラク ス Advent Calendar 2022 の25日目のトリです。最近購入したキーボードの話をします。 手帳が置けないという問題 そうだセパレートタイプのキーボードにしてみよう! 欠点は矢印のキーが独自配列であること 手帳が置けないという問題 Todoやスケジュールは私は手帳に書いて記録しています。なので、パソコンで作業をするときも手帳を見ながら行います。 マイクロソフト のエルゴミックキーボードを使っていて、キーボードはすごく良いのですが、困ったことがあります。 それは、キーボードが長方形じゃないので意外と机上の場所を取るというのと、キーボードの前に手帳を置いて作業をしているのですが、それが使いづらいのです。 っていうか、そもそも手帳が机からはみ出しているし。 キーボードを使うときに手帳の上に自分のひじが乗っかって、打鍵がしにくかったり、気がつくとページがくしゃくしゃになったりして、なんとかしたいなーと思っていました。 そうだセパレートタイプのキーボードにしてみよう! ある日、同じチームの同僚がセパレートタイプのキーボードを使っていたのを見てひらめきました! セパレートタイプのキーボードにして、左右のキーボードの間に手帳を置けばええやん!! どうせなら自作して自分の気に入ったキーボードを作ろうと思いましたが、お値段がアレなので Mistel社のBAROCCO にしました。 軸は赤軸の静音です。キーボードに「高さ」があったので、12mmの パームレスト も買いました。 もちろん、 パームレスト もセパレートです。 これで手帳が置けなかったら本末転倒ですが、こんな感じ! 最高。 欠点は矢印のキーが独自配列であること 私にとって最高のキーボードなのですが、ひとつだけ欠点があります。それは矢印キーが独自の配列であることです。 矢印キーが一列に並んでいます。一般的にはこう↓ですよね。 このせいで打ち間違い多発。特に下を押しているつもりが上押してて、上と下をかなりの頻度で押し間違える。 なんとかならんかったんかな、これ。 矢印キーに少し難があるものの、机が広く使えるようになって快適なエンジニアライフを過ごしています。 皆さんがお使いのキーボードで良いキーボードがあれば教えてください。 あれはいいキーボードだ。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは、あるいはこんばんは。すぱ..すぱらしいサーバサイドのエンジニアの( @taclose )です☆ みなさん、 sshでパスワード不要にする記事 は読んでくださいましたか? 今回はその続編とも言える記事になります。 ネットで調べてるとどうも GitHub にログインする ssh の設定で苦労されてる内容をよく見かけます。 今一度何が正しくて、どんなエラーが出た時はどうしたら良いのか?を整理しました 。 誤った理解でそれっぽい設定しちゃってる方もおられるようなので、今一度振り返ってみてもらえればと思います。 まずはSSHの設定の基礎的な事(自信ある人は飛ばす!) GitHubのサーバ側の設定をしよう! まずは公開鍵の作成 GitHubに公開鍵を設定しよう! GitHub用に記載すべきssh/configはこれで決まりだ!! (1)どこに接続する時もこの設定 (2)Host __github.com* (3)個人用/会社用のGitHubへの接続設定 GitHubに接続できない!エラー別対応法 ssh: connect to host github.com port 22: Resource temporarily unavailable Permission denied (publickey). 秘密鍵や設定ファイルのパーミッションが良くない例 よくわからずUser名をgitじゃなくした例 いざgit cloneしたら出来ないんですけど! 最後に 参考文献 まずは SSH の設定の基礎的な事(自信ある人は飛ばす!) 2021年8月13日から GitHub でパスワードによる認証が廃止されました。 一応別口も用意されていますが、安全性や利便性を考えれば ssh でのログインに切り替える良いタイミングです! 以下の記事を参考にしながら、まずは ssh/config の基本的な記載方法と便利さを実感してください。 なんとなく理解出来てるよって人は、不明点があれば参考にしてくださいね。 tech-blog.rakus.co.jp では次へいきましょうっ GitHub のサーバ側の設定をしよう! まずは公開鍵の作成 今回はあえて個人用のアカウント(taclose)と会社用のアカウント(officer)がある前提で記載します。既に公開鍵作ってるよ!という方は公開鍵は使いまわしても問題ないのですが、さすがに個人用と仕事用は使い分けるので、まずは鍵の作成から入りましょう! 鍵の作成時に鍵の名前指定を忘れずに!id_ rsa ファイルを上書きしちゃうの!?って ssh -keygenに心配されちゃいますよ! # 個人開発用の鍵の作成 名前指定忘れずにね!(今回はid_rsa_tacloseとしました。自由に!) $ ssh-keygen -t rsa Generating public/ private rsa key pair. Enter file in which to save the key (/Users/(username)/.ssh/id_rsa): id_rsa_taclose # ここ! Enter passphrase (empty for no passphrase): # 何も書かずにEnter Enter same passphrase again: # 何も書かずにEnter # 鍵の移動(秘密鍵と公開鍵両方移動させましょう) $ cp -pr id_rsa_taclose* ~/.ssh/ # 権限を変更 秘密鍵のアクセス権は600に変更しましょう。 $ chmod 600 ~/.ssh/id_rsa_taclose # GitHubに公開鍵を持ってく必要があるのでクリップボードにコピーする。 $ pbcopy < ~/.ssh/id_rsa.pub # (Mac)ならこうっ $ clip.exe < ~/.ssh/id_rsa.pub # (Windows)ならこうっ $ cat ~/.ssh/id_rsa.pub # こんなのでも良い!(表示された内容を選択してCtrl+C) もし会社用の公開鍵も用意するなら、同じ要領で id_rsa_officer とか作って下さいね! GitHub に公開鍵を設定しよう! GitHub にアクセスしてログインしてください。 右上のユーザアイコンをクリックし、Settingsを選択 GitHub ユーザアイコンを選択してSettingsを選ぶ 左メニューの SSH and GPG keys を選択 SSH and GPG keysを選びましょう New SSH key ボタンを選択 New SSH keyを選択 タイトルはusername等適当な値でOKです。Key Type: Authentication Keyとし、先ほどコピーした公開鍵を張り付けて Add SSH key Add SSH key これでサーバ側の設定は完了です☆ GitHub 用に記載すべき ssh /configはこれで決まりだ!! まず答えを書きます! [root@A8630-LT keisuke.maeda]# cat ~/.ssh/config Host * // (1) ServerAliveInterval 60 IdentitiesOnly yes TCPKeepAlive yes Host __github.com.* // (2) Hostname ssh.github.com Port 443 User git Host __github.com.taclose // (3) IdentityFile ~/.ssh/id_rsa_taclose Host __github.com.officer // (4) IdentityFile ~/.ssh/id_rsa_officer (「あ〜なるほどね、うんうん」という人は次にいきましょうっ) 解説します。 (1)どこに接続する時もこの設定 Hostって部分が俗にいう エイリアス (別名)をつける部分ですが、* や ?とかが指定可能です。 Host * みたいに 正規表現 で書いておけば、どのHost設定にも共通の設定として記載できます。 なので、 「 SSH の接続がぷつぷつ切れて困ってるんです」 とか 「 秘密鍵 を使い分けてるんですが、どうもうまくいかない」 そういう人は共通設定にこれ入れておけば安心ですね! (2)Host __ github .com* (1)の説明と重複しますが、こんな風に定義しておけば 『__ github .com~みたいな設定は共通でこういう定義ね!』と出来ます。 私の作業するNWでは github .comに対してport 22番での通信が出来ませんので、 Hostname ssh.github.com Port 443 という定義を GitHub に接続する共通の設定にしていますが、ここは以下のようにする人も多いかもしれません。 Hostname github.com Port 22 後半によくあるエラーの対策方法の説明があるので、そこで詳細に触れましょう!今は 「 GitHub への ssh 接続で22ポート使える人とそうじゃない人がいるのか。 GitHub は443ポートでもできるんだ。」 ぐらいで思っておいてもらえればOKです。 (3)個人用/会社用の GitHub への接続設定 よく見かける ssh の設定方法の説明だと1つのHostに対して何度もHostname, port, User等を定義しているのを見かけますが、共 通化 して書く事でここまで綺麗にかけるんですね! でも 秘密鍵 を使い分けてるだけで本当にアカウント使い分けできてるの?と思われるでしょう。 以下がコマンドの実行例です。 [root@A8630-LT keisuke.maeda]# ssh -T __github.com.taclose Hi taclose! You've successfully authenticated, but GitHub does not provide shell access. [root@A8630-LT keisuke.maeda]# ssh -T __github.com.officer Hi kmaeda-rakus! You've successfully authenticated, but GitHub does not provide shell access. おお! ssh での接続テストをしてみたところ、 Hi taclose! とか Hi kmaeda-rakus! と呼び分けてくれてますね! 秘密鍵 に紐づいたアカウントを自動で選択してくれているわけですね! 実際 __ github .com* の設定をみても、 User git としており、tacloseなんてうたってないわけですが、こんな結果になるわけです。 すぱ..すぱらしい!!エレガント! GitHub に接続できない!エラー別対応法 さて、では接続出来なかった人のためによくあるケースを元に対策を記載まとめていこうと思います。 と、その前に接続テストの方法は以下になります。 ssh /configの記載は各自の内容に読み替えて実施してくださいね! 私の場合は以下になります。 # ssh/configでUserやHostname設定してあるのでこうなる # 意味としては「ssh -T git@ssh.github.com -i ~/.ssh/id_rsa_taclose -p443」これと同じ意味になります。 $ ssh -T __github.com.taclose # 接続うまくいかない時は -vTにすると原因がわかりやすい事もありますよ!エラー例も載せておきます。 # これをみたらport何番使おうとしてるのか?とかがよくわかりますね!設定ミスにも気づけます。(わざとport 22使ってエラー出してます) $ ssh -vT __github.com.taclose OpenSSH_7.4p1, OpenSSL 1.0 .2k-fips 26 Jan 2017 debug1: Reading configuration data /root/.ssh/config debug1: /root/.ssh/config line 1 : Applying options for * debug1: Reading configuration data /etc/ssh/ssh_config debug1: /etc/ssh/ssh_config line 58 : Applying options for * debug1: Connecting to github.com [ 20.27 . 177.113 ] port 22 . debug1: connect to address 20.27 . 177.113 port 22 : Resource temporarily unavailable ssh: connect to host github.com port 22 : Resource temporarily unavailable ssh : connect to host github .com port 22: Resource temporarily unavailable このエラーが出た人は今いるNW環境が GitHub に対するport 22が許可されていない事が考えられます。 私もこれに該当していたのでそんな人は私が先ほども記述していた以下のような設定を試して下さい。 以下の設定は GitHub に443ポートでアクセスする設定です。 Host __github.com.* Hostname ssh.github.com # ここがgithub.comじゃないよ! Port 443 # ここが22じゃないよ! User git Permission denied (publickey). これは一番よくみるエラーかもしれません。 秘密鍵 や設定ファイルの パーミッション が良くない例 # 正しいパーミッション (userやgroupの列は気にしないで下さいね!私はWSLでさくっと環境準備したのでrootになってます。) $ ls -al ~/.ssh/ drwx------ 1 root root 512 12 月 19 19 : 06 . -rw------- 1 root root 1904 12 月 19 19 : 06 config ... -rw------- 1 root root 1675 3 月 3 2022 id_rsa_taclose ... # 設定ずれてる方は以下のコマンドを実行しましょう $ chmod 700 ~/.ssh # .sshフォルダは700 $ chmod 600 ~/.ssh/config # 設定ファイルは600 $ chmod 600 ~/.ssh/id_rsa_taclose # 秘密鍵は600 よくわからずUser名をgitじゃなくした例 たまにあるのが、Userという設定項目だから無条件にgitという値を使わずにtacloseとかにしちゃう例です。例えば以下。 # taclose@じゃないよ!git@だよ!エラーになるよ! $ ssh -T taclose@ssh.github.com -i ~/.ssh/id_rsa_taclose -p443 Permission denied (publickey). ssh の接続では常にユーザはgitです!お忘れなく もしここまできてもこのエラーが治らない人は -vT で詳細な理由を確認してみてください。 実はconfigファイルに記載した 秘密鍵 のファイルパスが間違ってるだけでも以下のようになります。 $ ssh -T __github.com.taclose no such identity: /root/.ssh/id_rsa_taclose2: No such file or directory Permission denied (publickey). 悩んだらまず -vT お忘れなく! いざgit cloneしたら出来ないんですけど! GitHub にアクセスして、 リポジトリ を確認したらこんな風になってるはずです。 GitHub の リポジトリ ( SSH ) じゃ、Cloneするかな!って実行すると・・・? # git clone git@github.com:taclose/mkmotd.git Cloning into 'mkmotd' ... ssh: connect to host github.com port 22 : Resource temporarily unavailable fatal: Could not read from remote repository. はい、こけちゃいます。 ssh /configに設定したのはあくまで __github.com.taclose なので以下のように書けばいけます 。 # git clone __github.com.taclose:taclose/mkmotd.git Cloning into 'mkmotd' ... remote: Enumerating objects: 13 , done . remote: Counting objects: 100 % ( 13 / 13 ), done . remote: Compressing objects: 100 % ( 8 / 8 ), done . remote: Total 13 (delta 2 ), reused 8 (delta 2 ), pack-reused 0 Receiving objects: 100 % ( 13 / 13 ), 4.07 KiB | 0 bytes/s, done . Resolving deltas: 100 % ( 2 / 2 ), done . 『でも、デフォルトでいきたいんですけど!』 って人は ssh /configの設定を以下のように修正するといけますよ。 github .comのデフォルトを追加 Hostの所には半角スペース区切りで複数定義できます。なので、 github.com と言われれば __github.com.taclose の設定を使ってね ってしたわけですね! これでさっきのコマンドを打つと・・・? [root@A8630-LT tmp]# git clone git@github.com:taclose/mkmotd.git Cloning into 'mkmotd' ... remote: Enumerating objects: 13 , done . remote: Counting objects: 100 % ( 13 / 13 ), done . remote: Compressing objects: 100 % ( 8 / 8 ), done . Receiving objects: 100 % ( 13 / 13 ), 4.07 KiB | 0 bytes/s, done . Resolving deltas: 100 % ( 2 / 2 ), done . remote: Total 13 (delta 2 ), reused 8 (delta 2 ), pack-reused 0 成功! 最後に ssh 接続失敗系のエラーはたくさんありますが、 GitHub の場合はサーバ側は GitHub がよしなにやってくれてます。だから、案外コピペミスとかNW設定周りぐらいしか原因はないです。 ただ、 Mac ユーザにとっては当たり前ですが、 ssh -addが必要とかはあるので、そういった点は参考文献にも記載しましたが GitHub Docs: SSH のトラブルシューティング ここでも解説されてるものがあります。公式の トラブルシューティング なので見てみると良いかもしれませんね! エラーに屈せず、勉強の機会にしちゃいましょう!お疲れ様でした! 参考文献 実践sshコマンド:基本からオススメの設定 / ノウハウをまとめたよ! - RAKUS Developers Blog | ラクス エンジニアブログ GitHub Docs: SSH のトラブルシューティング エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
はじめに 皆さんこんにちは。インフラエンジニアやってますmoja_chiroです。 今回は Python と Selenium を使用してちょっとした自動化を行い、業務効率UPを目指しました。 日々の運用業務の中で月次集計のレポート作成で手間と時間が取られていたため、データの集計作業(コピペ)を自動化させて楽になりました。帰宅前に Python プログラムを実行しておくと、翌日出社したタイミングではエクセルに結果が保存されているという内容です。 一部 Selenium ではなく、PyAutoGUIも使用しました。 一部のコードを記載しておりますが、あくまで個人の業務効率UPを目的としているため、コードの不備はあらかじめご了承ください。 はじめに Pythonとは Seleniumとは Pythonをインストール Seleniumをインストール WebDriver(ChromeDriver)のインストール Seleniumの基本的な使い方(Googleでの検索結果の取得と保存) 要素の取得 要素の操作 ブラウザの要素確認 ログインページのサンプル XPathによる検索 今後の発展 さいごに Python とは Python とは、組み込み開発、WEBアプリケーション、デスクトップアプリケーションなどで利用されるプログラム言語です。 インフラ畑の人間からすると、プログラム言語っていうと敷居が高いイメージです。 Python を検索すると、 人工知能 や 機械学習 の分野やWeb アプリ開発 でよく利用されており人気の言語らしいです。 言語としての特徴は、文法がとてもシンプルで、標準ライブラリや外部ライブラリが多く、初心者でも扱いやすい言語です。 ▼公式ドキュメント www.python.org ▼ Wiki ペディア ja.wikipedia.org Selenium とは Selenium とは ブラウザー の自動化を可能にし、ブラウザを自動的に操作するツールとライブラリーです。 ▼ Selenium の公式ページ www.selenium.dev Python をインストール まず始めに Python の環境を構築する必要があります。 ▼公式ページ www.python.org Python 公式サイトから、 Python パッケージをダウンロードします。 ダウンロードしたパッケージをインストールします。 PowerShell で Python を実行するときに必要となる、 スクリプト の実行許可を設定します。 ダウンロードからインストール手順はこちらのサイトが分かりやすいと思います。 ▼ Windows 版 Python のインストール www.python.jp ウィザードの中で出てくるパスを通しておく必要があります。   ※ Add python.exe to PATH のチェックを入れる また、開発するにあたっては Python 実行環境を構築する必要があります。 次のコマンドを入力して、仮想環境を構築します。 C:\Users\user1\sample1>python -m venv virtual_environment ・仮想環境への切替 Windows の PowerShell を使用する PS C:\Users\user1\sapmle1> PS C:\Users\user1\sapmle1> virtual_environment\Scripts\activate.ps\Scripts\activate.ps 仮想環境を終了する場合は、 deactivate コマンドを実行します。 Selenium をインストール ・pipを使用して Selenium をインストールする pipとは、The Python Package Indexで公開されている Python パッケージのインストールなどを行うユーティリティで、 Python 3.4以降では標準で付属されています。 パッケージのインストールは、pipのinstallコマンドで行います。 Selenium パッケージをインストールするときは、次のように実行します。 C:\Users\> python -m pip install selenium インストール時に SSL のエラーが発生する場合は、trusted-hostのオプションを付けるか、pip.iniファイルを作成して実行してください。 C:\ProgramData\pip\pip.ini pip.iniの中身 [global] trusted-host = pypi.python.org pypi.org files.pythonhosted.org WebDriver(ChromeDriver)のインストール Selenium では、WebDriverを使用してブラウザを操作します。そのため Selenium を使用するためにはWebDriverのインストールが必要です。 WebDriverは、使用するブラウザの種類によって使い分けが必要で、今回は使い慣れている「 Google Chrome 」のドライバー(ChromeDriver)を使用します。 ChromeDriverのインストールについては、以下のリンクからDriverをダウンロードしてください。 インストールするバージョンは、現在使用中の「 Google Chrome 」のバージョンに合わせます。 ▼ ダウンロードサイト sites.google.com chrome ドライバーは作業用フォルダーと同じ場所に保存する方が管理しやすいです。 保存先の ディレクト リを指定する場合は、 executable_path="" で任意のパスを指定する必要があります。 指定する場合は以下のように記述します。 from selenium import webdriver from selenium.webdriver.chrome.service import Service driver_path = r"\C:\python\selenium\web_driver\chromedriver.exe" service = Service(executable_path=driver_path) driver = webdriver.Chrome(service=service) Selenium の基本的な使い方( Google での検索結果の取得と保存) ではまず手始めに、 Google のページを3秒間表示してブラウザを閉じてみます。 表示させたいURLを driver.get("URL") に記述します。 import time from selenium import webdriver from selenium.webdriver.chrome.service import Service driver_path = r"\C:\python\selenium\chromedriver.exe" service = Service(executable_path=driver_path) driver = webdriver.Chrome(service=service) driver.get( "https://www.google.co.jp/" ) time.sleep( 3 ) driver.quit() 「 chrome は自動テストソフトウェアによって制御されています。」というメッセージが表示されています。 自動的にブラウザが閉じれば成功です。 要素の取得 Selenium で Webブラウザ の自動化をするためには、そのブラウザ内で使用されている要素を取得する必要があります。 WebDriverには標準のロケータが8種類あります。 以下の8種類の要素に対して実行したい操作を記述していくという流れで自動化を進めます。 ロケータ 詳細 class name class名に値を含む要素を探す (複合クラス名は使えない) css selector CSS セレクタ が一致する要素を探す id id属性が一致する要素を探す name name属性が一致する要素を探す link text a要素のテキストが一致する要素を探す partial link text a要素のテキストが部分一致する要素を探す tag name タグ名が一致する要素を探す xpath XPath と一致する要素を探す ページ内の要素が複数ある場合は、複数系の「s」がついたメソッドを利用します。 取得した内容はリストとして扱われます。 要素の操作 要素に対して実行できるコマンドは次の5つです。 click (どの要素にも実行可能) send keys (テキスト フィールドとコンテンツの編集可能な要素にのみ実行可能) clear (テキスト フィールドとコンテンツの編集可能な要素にのみ実行可能) submit (フォーム要素にのみ実行可能) select (リスト要素の選択で実行可能) ※ ※selectは要素のタイプによって動作が異なります。詳しくは、 Selenium のドキュメントを確認ください。 www.selenium.dev ブラウザの要素確認 実際にWEBページの取得したいデータの場所を特定し、elementを取得する必要があります。 ここが大変ですが、「 Google Chrome 」の デベロッパ ーツールで確認できます。 「 Google Chrome の縦 三点リーダ ー」→「その他のツール」→「 デベロッパ ー ツール」を押下、またはショートカットキーの「F12」もしくは「Ctrl+Shift+I」で起動します。 左上の要素選択モードのアイコンをクリックします。ショートカットは、「Ctrl+Shift+C」です。 ログインページのサンプル 実際に Python と Selenium を使用して自動化を行ったログインページです。 ユーザー名の要素確認結果 パスワードの要素確認結果 ユーザー名とパスワードの値を入力して自動ログイン # アイテムの取得 login = driver.find_element_by_name( 'userName-inputEl' ) password = driver.find_element_by_name( 'password-inputEl' ) # テキスト送信 login.send_keys( "admin" ) password.send_keys( "password" ) # ボタンのクリック driver.find_element_by_id( "loginBtn-btnEl" ).click() XPath による検索 要素の特定が難しい場合は、直接パスを指定することも可能です。 要素を選択した状態で、右クリック「copy」を選択することで XPath をコピーできます。 ▼ XPath の取得 //*[@id="loginBtn-btnInnerEl"] ▼full XPath の取得 /html/body/div/form/div[3]/a/span/span/span[2] 今後の発展 現在、ブラウザの表示とデータの収集までできるようになったので、取得したデータの加工も自動化できるように勉強中です。 Python の Selenium 以外のツールを利用して、他の定型業務も自動化していきたいと考えております。 また、機会があればその内容をまとめてみたいと思います。 さいごに Python と Selenium を使用したWeb スクレイピング の話を紹介しました。 みなさんも自動化で業務の効率化をされてはいかがでしょうか。 ただし、 データ取得先のサーバーへの過度な負荷をかけないこと 利用規約 を守ること 著作権法 を守ること について、くれぐれもご注意ください。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
はじめに こんにちは、 id:FM_Harmony です。 今回は iOS アプリの開発で実践した Xcode でのlldbを使った デバッグ 事例 について、 3件ほど紹介したいと思います。 lldbを使った デバッグ は ブレークポイントで処理を止めて、変数を読み書きする 位かと思っていましたが、 他にもいろいろなことが出来ると知ったので、 iOS アプリ開発 のTIPS(ノウハウ/テクニック)として紹介します。 はじめに lldbとは lldbを使ったデバッグ事例 アプリアイコンのバッジを付与する デバッグ方法 活用例 UserDefaultsを読み書きする デバッグ方法 活用例 アプリに保存したCookieを扱う デバッグ方法 活用例 余談:ブレークポイントで止まらない事例 終わりに lldbとは 前回私が投稿した記事でも紹介しましたが、 Xcode にはlldbというデバッガが入っています。 tech-blog.rakus.co.jp 普通は Xcode で ブレークポイント を貼ることで処理を止めて、lldbを使って変数の確認や操作ができますが、 ビルドの設定等によってはそれが出来ないことがあります。 一時的に設定を変えて ブレークポイント で処理が止まるようにもできますが、 動作確認したいことによっては、設定を変えずにlldbを使って デバッグ できる事があります。 lldbを使った デバッグ 事例 アプリアイコンのバッジを付与する デバッグ 方法 プッシュ通知機能がアプリに実装されている場合、 受信した通知に合わせて、アプリアイコンにバッジを付与することができます。 アプリアイコンバッジの例 iOS の場合、通知内のパラメータでバッジを付与することが出来ますが、 アプリからもコードでバッジを付与することが出来ます。 そのため、 ブレークポイント で処理を止めなくても、 Xcode から処理を止めた上で、 以下のコードをlldbで実行することで、任意のタイミングでバッジを付与することができます。 (lldb) expression - l swift -- import UIKit // アプリアイコンのバッジ件数を変更する (lldb) expression - l swift -- UIApplication.shared.applicationIconBadgeNumber = /**【アプリアイコンのバッジ件数(数値)】**/ ; expr は式を評価するコマンドで、1行目でUIKitの フレームワーク を読み込み、 2行目でアプリアイコンのバッジを付与します。 注意点としては、処理を止めた箇所によってはSwiftのコードが実行できないため、 expr -l swift で実行する言語にSwiftを指定する必要があります。 但し、 Objective-C のコードであれば、言語を指定することなく、 そのまま実行することが可能です。 また、 ブレークポイント で処理を止めた箇所によっては、 UIKitの フレームワーク は読み込まれていない可能性があります。 その場合はバッジを付与する前に読みこむ必要があります。 活用例 例えば、アプリアイコンのバッジ件数が期待値となる動作を行いたい場合、 バッジ件数を確認前の状態に戻すことができるので、とても便利です。 楽楽精算が提供しているiOSアプリ は、プッシュ通知機能を提供しており、 アプリからログアウトすることで、それまで付与されていたアプリアイコンのバッジの件数が0件になります。 実際の動作確認では、都度ログアウトする必要があったため、 コマンド1回実行することでアプリアイコンのバッジ件数を戻すことができ、楽に動作確認をすることができました。 UserDefaultsを読み書きする デバッグ 方法 アプリアイコンのバッジのように、UserDefaultsもlldbからコマンドを実行して、 値を読み書きすることが出来ます。 (lldb) expression - l swift -- import Foundation // キー:fugaに対して値:hogeをUserDefaultsに書き込む (lldb) expression - l swift -- UserDefaults.standard. set ( "hoge" , forKey : "fuga" ) // キー:fugaに対する値をUserDefaultsから読み取る (lldb) expression - l swift -- UserDefaults.standard.string(forKey : "fuga" ) 1行目で必要な フレームワーク を読み込み、2行目以降で操作する流れは、 アプリアイコンのバッジの例と同じです。 活用例 UserDefaultsに保存した情報によっては、アプリの操作で値が変更できないことがあります。 例えば「新バージョンでは利用しなくなった過去バージョンの設定値が残っていた時に何か処理を行う」 といった場合です。 具体的に、旧バージョンではUserDefaultsにある機能の設定値を格納していたが、 新バージョンでその機能は廃止し、設定値が残っていればお知らせを表示するといった場合を考えます。 お知らせが表示されるかを確認する場合、廃止した設定値を画面から操作することはできないため、 旧バージョンで設定値を変更して新バージョンへアップデートする方法があります。 しかし、アップデート前後のアプリを ipa などで用意して、都度アップデートするのは手間が掛るため、 上記の方法でUserDefaultsの値を直接書き換えた方が楽でした。 アプリに保存した Cookie を扱う デバッグ 方法 lldbから HTTPCookieStorage を利用することで、 アプリに保存した Cookie を デバッグ 中に扱うことができます。 (lldb) expression - l swift -- import Foundation (lldb) expression - l swift -- let $storage = HTTPCookieStorage.shared // 名前:hogeに合致する最初のCookieを取得 ※例示のため強制アンラップしている (lldb) expression - l swift -- var $cookie = $storage .cookies ! .first{ $0 .name == "hoge" } ! // 取得したCookieの値を確認 (lldb) expression - l swift -- print( $cookie ) // 取得したCookieをアプリから削除 (lldb) expression - l swift -- $storage .removeCookie( $cookie ) 1行目で必要な フレームワーク を読み込んで、以降の行で処理を行う流れはこれまでと同じです。 上記で紹介したコマンドと異なる点は、2行目、4行目でコマンドの結果を変数に格納しているところです。 lldbでは先頭に $ マークを付けると、以降の デバッグ で変数として利用可能になるので、 HTTPCookieStorageや Cookie を変数に格納して、 Cookie の確認や削除を行っています。 活用例 アプリに正しく Cookie が保存されたか、 Cookie の有無で変わる挙動を確認するのに役立ちました。 WebViewを利用した画面では、 Cookie を利用することがありますが、 その Cookie はアプリが持つ Cookie とは別に保存されます。 そのため、WebViewへのアクセスで Cookie が更新されたとき、 更新された Cookie をアプリへ同期する必要があります。 例えば、 safari のWebインスペクタを利用すればWebViewが利用している Cookie は確認できますが、 実際にアプリが利用している Cookie は確認することができないため、 上記の デバッグ 方法により正しい Cookie が保存されたか確認できる点で非常に有効です。 余談: ブレークポイント で止まらない事例 今回紹介した事例はいずれも ブレークポイント ではなく、 Xcode の Pause program execution の機能を使って、 処理を止めた際の デバッグ 方法です。 ブレークポイント で処理を止めた際は、実装されているコードを操作できるので、 フレームワーク の読み込みやUserDefaults、HTTPCookieStorageの操作は楽です。 では、どんな時に ブレークポイント を使えないのかというと、 例えば、releaseビルドで作成した ipa では ブレークポイント が破線になってしまい、 利用することができませんでした。 終わりに いかがでしたでしょうか。 個人的な感想ですが、これまで po コマンドで変数を確認するくらいしかlldbを利用していなかったので、 実行する言語の指定や変数の宣言、値の格納といったことまでlldbで行えることに驚きました。 今回の記事が、 iOS アプリ開発 に携わる皆様のお役に立ちましたら幸いです。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは、あるいはこんばんは。すぱ..すぱらしいサーバサイドのエンジニアの( @taclose )です☆ みなさん前回の記事で ssh のログインは楽に出来るようになりました! 前回の記事:  実践sshコマンド:基本からオススメの設定 / ノウハウをまとめたよ! - RAKUS Developers Blog | ラクス エンジニアブログ しかし!ここで問題になるのが安易にログインできるが故に間違ってログインしちゃう問題です。 そこで今回は間違ってログインしないように ログインしたら好きなキャ ラク ターにおはやいます! してもらいましょう! これで ログインしたサーバが自分用の開発サーバなのか、真面目にやってる検証環境なのか一目瞭然 ですね! ログインした時の画像を準備しよう! 画像をテキストデータに変換しよう! サーバに設定しよう! ログインしてみよう! 付録 画像をもっと加工したい! コマンドのオプション 終わりに 参考文献 ログインした時の画像を準備しよう! 手順は追って説明しますが、準備さえ整えば以下のようにコマンド1個で画像が作れて、アップロードもコマンド1つだけです! ssh でログインした時に表示される画像 著作権 上アニメのキャ ラク ターとかはこのブログでは扱えませんので、今回は以下のサイトから画像を準備しました。 無料ドット絵アイコン素材のピクセルガロー|ドット絵アイコンが使い放題 皆さまも子供の写真だったり「DEV」って書かれただけの画像でも構いませんのでご準備をっ 画像をテキストデータに変換しよう! コンソールには当然ながら画像を表示する機能なんてありませんので、一旦用意した画像を変換する必要があります。本当は「画像サイズを小さくして、背景色を黒にして。。。」と手順が大変なのですがコマンドを用意しましたので、以下の手順に沿って作業を進めてください! # ImageMagickをインストールする $ yum install -y ImageMagick 下記のコマンドでmkmotdコマンドをダウンロードしてください。 $ wget --no-check-certificate "https://raw.githubusercontent.com/taclose/mkmotd/main/mkmotd" -O mkmotd ※ wget コマンドを使えないよ!という方は以下の スクリプト を mkmotd とか、好きな名前で保存してください! #!/usr/bin/bash # ImageMagickが使えるか? if !( type "convert" > /dev/null 2 > & 1 ); then # 対象のコマンドをインストールするような処理 echo "please install ImageMagick!!" echo "$ yum install -y ImageMagick" exit 1 ; fi #定数 tmp= "tmp/temp_image" ; dot_path= "tmp/dot_image" # DEFAULT path= "" size= "32" trans_position= "0,0" background= "0;0;0" # 環境準備 mkdir -p tmp # OPTION GET while getopts p:s:t:b: - : opt; do optarg= " $OPTARG " [[ " $opt " = - ]] && opt= " $OPTARG " && optarg= " ${!OPTIND} " && shift case $opt in p | path ) path= " $optarg " ; ;; s | size ) size= " $optarg " ; ;; t | trans-position ) trans_position= " $optarg " ; ;; b | background ) background= " $optarg " ; ;; * ) echo " $0 : illegal option -- ${opt##-} " > & 2 ; exit 1 ;; esac done # CONFIRM echo "path= $path " echo "size= $size " echo "trans_position= $trans_position " echo "background= $background " trans_position=( ${trans_position//,/ } ) transparent_after= " $background " # URLなら画像を取得する if [[ $path =~ ^http ]] ; then echo "TASK: Download start." tmp_name= `basename " $path " ` wget --no-check-certificate " $path " -O " $tmp_name " > /dev/null 2 > & 1 path= " $tmp_name " echo "new path= $path " fi # 透過したい色を取得する echo "TASK: Transparent color get." rgb= `convert " $path " -crop 1x1+${trans_position[0]}+${trans_position[1]} txt: - | tail -n1 | awk '{print $3}' ` echo $rgb trans_r= `bc <<< "obase=10; ibase=16; ${rgb:1:2} " ` trans_g= `bc <<< "obase=10; ibase=16; ${rgb:3:2} " ` trans_b= `bc <<< "obase=10; ibase=16; ${rgb:5:2} " ` transparent_before= " ${trans_r} ; ${trans_g} ; ${trans_b} " echo "get trans_color: $transparent_before " # リサイズ処理をする echo "TASK: Resize image." convert " $path " -interpolative-resize ${size} x ${size} -compress none " $tmp " ppm= $( convert " $tmp " -interpolative-resize ${size} x ${size} -compress none ppm: - 2 > /dev/null ) # get cols, then discard ppm parameters set ${ppm} cols= ${2} shift 4 { # get three each values (rgb) from ppm for i in $( eval echo { 1 .. ${#} .. 3 } ) do j= $(( i +1 )) k= $(( i +2 )) rgb= " ${!i} ; ${!j} ; ${!k} " # replace transparent color if [ ${rgb} == ${transparent_before} ] ; then rgb= ${transparent_after} fi # start a new line when reach cols if [ $(( ( ${i} + 2) / 3 % ${cols } )) -eq 0 ] ; then echo -e " \0 33[48;2; ${rgb} m \0 33[m" else echo -en " \0 33[48;2; ${rgb} m \0 33[m" fi done } > $dot_path cat $dot_path echo "COMPLEATE!! Let's Setting!!" echo -e " \e [34;1m$ scp $dot_path {your-name}@{your-server}:/etc/motd \e [m" exit 0 いざ、変換処理を実行! # 実行権限ないなら $ chmod +x mkmotd を忘れずに! $ ./mkmotd -p tmp/military_totsugeki.gif 最後にscpコマンドが出たら成功です!ローカルに tmp/dot_image というファイルが出来上がってるはずです。 サーバに設定しよう! 最後に出てきたscpコマンドのようにしてもらえれば完成です! # your-name, your-serverは各自のユーザ名やドメイン名にしてくださいね $ scp tmp/dot_image your-name@your-server:/etc/motd # 前回の記事でログインを簡易化した方は以下のようなコマンドにしてもOKです $ scp tmp/dot_image __taclose.develop:/etc/motd ログインしてみよう! よし!ログインしてみましょう! ログイン成功! 感動ですね! 付録 画像をもっと加工したい! 今回用意した スクリプト では画像サイズや背景色とかを選べるように作ってありますので、以下のオプション指定の例を参考に試してみてください。 // 一番ミニマムな例 画像パスだけ指定 $ mkmotd.sh -p "logo.png" // 一番ミニマムな例 画像URLだけ指定 $ mkmotd.sh -p "https://www.rakus.co.jp/images/common/favicon.png" // フルで指定(デフォルト値) $ mkmotd.sh --path "logo.png" --size 32 --trans-position 0 , 0 --background "0;0;0" // サイズは48px、左上から 5 , 5 の座標の色が背景色、この背景色を赤にする $ mkmotd.sh --path "logo.png" --size 48 --trans-position 5 , 5 --background "255;0;0" 各種オプション指定の意味は以下のようになっています。 コマンドのオプション option default 説明 -p, --path 無し(必須指定) ファイルパスかhttpから始まるURLを指定してください。http指定の場合は wget コマンドも必要です。 -s, --size 32 画像サイズを変更します。コンソール表示を考えると16~64辺りが妥当です。 -t, --trans-position 0,0 左上からの座標を指定。指定された座標にある色を背景色として変更対象とします。 -b, --background 0;0;0 背景色と指定された色をRGBで指定された色に変換します。0~255でRGBを指定してください。ex:赤なら"255;0;0"です。 終わりに 画像が可愛いものを使っているのでちょっとゆるい感じになりましたが、 ログインしているサーバに間違いがないかを意識せずともクリアできるというのは案外大事 な事です。 人間は文字を読まずにアイコンだけを見て操作している事って非常に多いので、皆さんも仕事のモチベーションアップのためにも「好きなキャ ラク ターにおはやいます!」してもらってくださいね! よし、私も好きなキャ ラク ターを選び直そう! 参考文献 GitHub - taclose/mkmotd: sshでログイン時に表示するmotdのドット絵作成ツール 実践sshコマンド:基本からオススメの設定 / ノウハウをまとめたよ! - RAKUS Developers Blog | ラクス エンジニアブログ sshログインした時に任意の画像を出現させるようにするのをDockerでしてみた - Qiita 無料ドット絵アイコン素材のピクセルガロー|ドット絵アイコンが使い放題 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター