TECH PLAY

フォルシア

フォルシア の技術ブログ

241

FORCIAアドベントカレンダー2020 4日目の記事です。 事業開発部の田中です。業務では webコネクト の開発・保守に携わっています。 Node.jsを基盤とし、フロントはReact+Redux+Next.js、サーバーサイドはExpress.jsを利用し、TypeScriptで開発を進めています。 OSS(Deno)を触りたくなった動機 2020年5月にDeno 1.0.0がリリースされたのを受け、Twitterや様々な技術ブログでそれが注目されているのを知りました。 初学者(webエンジニア歴約1年)の私には、何か難しいことが議論されているな、立ち入るのはハードルが高そうだなと感想を持つのみでした。しかし、ふと 公式Deno のプロジェクトにアクセスしてみると・・・。  そう、ロゴがとても可愛いのです。 出典: Deno公式ArtWork そこで興味が湧き、少し調べていると、業務で使用しているNode.jsと深く関連があること、やはりロゴが可愛いことから、触ってみようと思いました。 (ロゴの創作コミュニティも活発であり、ArtWorkが 公式サイト にいくつも掲載されています。) Denoとは 多くの記事やブログで取り上げられているため、詳しく記載することは省きますが、Denoは、Node.jsの制作者であるRyan Dahl氏がNodeでの反省点に基づき立ち上げた、新しいJavaScript / TypeScriptランタイムのプロジェクトです。そのため、公式Documentや解説記事ではNodeと比較し、Denoが特徴づけられていることが多いです。 Nodeとの大きな違いとして、以下がよく挙げられています。 npmを使用しないこと Denoはpackage.jsonを使用しないこと URLまたはファイルパスとしてモジュールを参照すること すべての非同期処理はPromiseで表現されること 明示的に許可しないかぎり、ネットワーク接続やファイル読み取りができないこと ES modulesを使用し、require()が使えないこと 実際に触ってみて楽しかったこと マニュアルや基本的解説で手を動かす Denoを学ぶにあたり、 公式マニュアル や、日本語で解説されているDenoBookを読み解きました。マニュアルということもあり、かなり基本的なところから、また多くがNodeと比較しつつ丁寧に解説されています。 しかし初学者の私は、比較対象のそもそものNode機能の名称や処理を理解できておらず(NodeのStreamやhttp-server等)、知らない単語をひとつひとつ調べながら手を動かしました。 手を動かしている内に、業務で使用しているNodeで何が処理されていたのか、Denoを学びながら理解を深めることができました。 手探り感が楽しい 検索してヒットする記事では、1年前の記事の通りに実装してみても動作しなかったり、解説されているコマンドが使用できなかったりしました。 例えば、2020年11月時点最新のdeno.1.5 環境において、過去の記事をもとに以下を実行すると・・・ (Denoでは、TypeScriptをそのまま実行できるのです。) console.log("hello"); deno hello.ts 実行結果 error: Found argument 'hello.ts' which wasn't expected, or isn't valid in this context USAGE: deno [OPTIONS] [SUBCOMMAND] For more information try --help エラーとなってしまいます。正しく動作させるには、 deno run hello.ts としなければなりません。そう、使用できるコマンドが変わってしまっているため、新しいものを使用しなければならないのです。 コマンドのヘルプを見ればすぐに正しいコマンドがわかるのですが、Denoでは今現在も破壊的な変更が加えられていっており、日々のキャッチアップが必須です。 昨年リリースされた記事の内容がそのまま使える、また昨日使えていた機能がそのまま使える保証はないようです。 一方、それだけの速度で開発が進められているので、開発速度を一端を感じられるのはとても新鮮な体験でした。 さらに、Deno特有のURL参照でモジュールを使用する関係上、以下のことも起きます。 先述のDeno 1.5.2環境にて、以下のようなソース(httpサーバの立ち上げ)を実行するとします。ポイントはDenoの標準モジュールであるserveをimportする際、URL指定になっている点です。 import { serve } from "https://deno.land/std@0.55.0/http/server.ts"; const s = serve({ port: 8000 }); console.log("http://localhost:8000/"); for await (const req of s) { req.respond({ body: "Hello World\n" }); } 上記ソースを以下のとおり実行してみると・・・ (Denoではネットワークとの通信、またファイルの読込が全てセキュアであり、deno runだけを実行するとモジュールの取り込みでエラーになってしまいます。モジュールの取り込みを明示的に許可する必要があるので --allow-netを付与しています。) $ deno run --allow-net httpServer.ts error: TS2345 [ERROR]: Argument of type 'string | URL' is not assignable to parameter of type 'string'. Type 'URL' is not assignable to type 'string'. return new URL(url).pathname ~~~ at https://deno.land/std@0.55.0/path/win32.ts:917:18 TS2345 [ERROR]: Argument of type 'string | URL' is not assignable to parameter of type 'string'. Type 'URL' is not assignable to type 'string'. return new URL(url).pathname; ~~~ at https://deno.land/std@0.55.0/path/posix.ts:438:18 Found 2 errors. インターネットで検索して出てくるコードをそのままコピペして実行するだけでは、TypeScriptの型チェックで怒られてしまいました(またDeno1.0.0リリース当時の公式マニュアルにも記載があったものでもあります)。 正しく動作させるには、標準モジュールのserveのバージョンを、インストールされているDeno環境に対応するものにする必要があります(もちろんDeno 1.0.0環境に戻すことで先述のコードは動作します)。 import { serve } from "https://deno.land/std@0.79.0/http/server.ts"; const s = serve({ port: 8000 }); console.log("http://localhost:8000/"); for await (const req of s) { req.respond({ body: "Hello World\n" }); } ちなみに、修正前の std@0.55.0はDeno1.0.0がリリースされた2020年5月当時のバージョンです。 わずか半年前のコードが動かない、そんなスピード感で開発が進められています。Deno関連のソースコードを追ってみる際も、新しいバージョンとなると、ディレクトリ構造から変わっている場合があり、GitHubレポジトリを探検することになります。 そんな開発の速さをリリースノートを追って体感しつつ、探検しながらコードを書くのは、一種のゲーム要素すら感じられ、とても楽しいものでした。 まとめ 初学者である私が、ロゴが可愛いという理由でOSSに触れてみましたが、体験したことのない文化に触れることができると共に、普段開発業務で用いている技術への根本的な理解を深めることができ、とても有意義な体験でした。 注目されているDenoですが、まだまだ実用化には機能追加が必要であり、また自身の技術と理解を深めるべく、 ぜひコントリビュートしていきたいと思います。
アバター
競技プログラミング Advent Calendar 2020 3日目の記事です。 旅行プラットフォーム部エンジニアの谷井です。 普段の業務では主にTypeScript + PostgreSQLで開発を行っています。今回は「個人的な課題をJavaScriptで解決してみたら、競プロの世界に足を踏み入れていた」話を書きます。 日常生活のちょっとした困りごとを自分のコードで解決できるのはエンジニアの役得ですね! 今回はアプリの構成やUIはスコープ外とし、ロジックの部分だけを取り出して扱うので、「JavaScriptの書き方は一通り学んだが、複雑なアプリは作ったことがない」という方も、ぜひパズル
アバター
競技プログラミング Advent Calendar 2020 3日目の記事です。 旅行プラットフォーム部エンジニアの谷井です。 普段の業務では主にTypeScript + PostgreSQLで開発を行っています。今回は「個人的な課題をJavaScriptで解決してみたら、競プロの世界に足を踏み入れていた」話を書きます。 日常生活のちょっとした困りごとを自分のコードで解決できるのはエンジニアの役得ですね! 今回はアプリの構成やUIはスコープ外とし、ロジックの部分だけを取り出して扱うので、「JavaScriptの書き方は一通り学んだが、複雑なアプリは作ったことがない」という方も、ぜひパズルのつもりで考えながら読んでみてください! 解決したい課題 「連戦の少ない総当たりの対戦順を決めたい」 この記事を読んでいるみなさんも、「連戦の少ない総当たりの対戦順を楽に求めたい!」と思ったことはきっと一度や二度ではないですよね。 私は大学から躰道という武道をやっており、地区の選考会運営などで対戦順を決める機会がありました(躰道についてはこの記事では到底語り尽くせないのでぜひ動画を検索してみてください)。 選考会では対象選手の総当たり戦を順番に行うのですが、連続して試合に出ることは選手にとっても負荷が大きく、連戦を極力減らした組み合わせが求められます。 また、事前の大会で実施した対戦カードはその結果を流用するため、 一部の組み合わせを対戦順から除外する 必要がありました。 当然、試合順を事前に考えておければ楽なのですが、対象選手や人数が当日確定することもあり、その場で急いで対戦順を考えなければいけません。 今回はこれまでは紙とペンでやっていた地味に大変なこの作業を、自動化していきたいと思います。 実現したい内容は下記の図のようなイメージです。 「選手を登録し、すでに実施済みの試合を選択すると、総当たりに必要な残りの試合を(連戦の少ない形で)自動で提示してくれる」という流れです。 この記事では、(ii)→(iii)の組み合わせ最適化について考えます。 n人(~10程度)の総当たり戦の対戦順を決める 同時に行う試合の数は1試合とする 一部の組み合わせを除外した上で、連戦数を最小にする ここでの「連戦数」とは「前の試合に出た選手と同じ選手が出る試合の数」のこと たとえば"A-B", "B-C", "A-C", "D-E"の順で実施した場合、1,2試合目のBと2,3試合目のCが連戦となるため、連戦数は2 方針 選手のリストを作る リストから組み合わせを列挙する 列挙した組み合わせを並べ替え、評価関数を通してコスト(連戦数)が最小のものを取り出す パフォーマンスや実装の手軽さを考慮すると他の言語に軍配が上がりそうですが、今回はTypeScriptで作っているWebアプリに乗せることを想定しているため、一旦JSで実装してみたいと思います。 本文中のサンプルコードはNode.js 12.19.0で動作確認しています。 実装 はじめに、選手の組み合わせの表現方法を考えます。 A, B, C...と選手が与えられたとき、A対Bの試合を "AB" のように文字列で与えても良いのですが、連戦判定をよりシンプルに行うために、各選手にビットを割り当てて表現してみたいと思います。 すなわち、選手A, B, C, D...に対して 1, 2, 4, 8... と数値を割り当てていき、選手同士の対戦組み合わせはその和によって表現することにします。 たとえば"A-D"の試合は 9 として一意に表現できますね。 表1: n=5の場合の各試合の表現 続けて、実際にコードを書いていきましょう。 1. 選手を表すリストを作る 参加人数nが与えられたとき、各選手に割り当てられたビットに1を立てた数値の配列を作っていきます。 n個の要素の配列を作り、map関数で各要素をindex分だけシフトさせた数値に変換します。 なお、 Array(n) では空配列が生成されるので、一度スプレッド演算子で展開しています。 const createList = n => [...Array(n)].map((_, i) => 1 2. 組み合わせを全て列挙する 二重のループを通して二選手の数値の和を配列に加えていきます。 "A-B"と"B-A"は区別する必要がないため、内側のループのカウンタが外側のそれを超えない範囲であることに注意します。 引数には先ほどのcreateListで作った選手を表す配列を渡します。 const combination = list => { let combinationList = []; for (let i = 0; i 一つ目の例は ["A-B", "A-C", "B-C"] を表す配列が得られたことになります。 3. すでに結果がある試合を除外する 今回は、実施済みの試合を表す数値の配列 excludeList が与えられているものとします。 たとえば、"A-C", "C-D"の試合が実施済みの場合は excludeList は [5, 12] となります。 Array.filter() を使って、これらを除外した「これから行う試合のリスト」を作成します。 const combinationList = combination(createList(5)); const excludeList = [5, 12]; const filteredList = combinationList.filter(elm => !excludeList.includes(elm)); 4. 連戦数を計算する関数(評価関数)を作る さて、並べ替えて対戦順の探索をする前に、連戦数を求める関数を作っておきます。 各要素から順に、「1つ前の要素と比較して同じ選手が含まれる場合はコストに1加算する」操作を行います。 「同じ選手が含まれるかどうか」の判定は、ビット論理積によって判定することができます。 同じ選手が含まれている場合は同じ位置に1が立っているため、論理積を取ると0になりません。 表2: 連戦となる場合、ならない場合の2試合の論理積の結果 これを順繰りに判定し、連戦の場合はコストを加算していきます。 for文で書いても良いのですが、配列を畳み込んでいってある値を得たいときは、その意図を明示するためにも Array.reduce() 関数をよく使います。 const evaluationFunc = list => list.reduce((cost, _, idx, src) => src[idx] & src[idx - 1] ? ++cost : cost, 0); // example const listA = [3, 5, 6, 9, 10, 12]; const listB = [3, 12, 5, 10, 6, 9]; evaluationFunc(listA); // 4 evaluationFunc(listB); // 2 idxが0のとき、 src[idx - 1] = undefined となりますが、論理積を取ると0になるので分岐は省略します。 src[idx] は第二引数で表せますが、こちらの方が操作を直感的に理解しやすそうなためこのように書いています。 5. 並べ替えて評価する 順番を入れ替えるため、順列を求める関数を実装します。 const permutation = (list, k) => { let ans = []; if (list.length [i]); } else { for (let i = 0; i 全部並べ替えてから評価しても良いのですが、連戦なしの解が見つかった時点で打ち切りたいため、再帰の一番浅い階層で評価しながらfor文を回します。 const search = (filteredList) => { let ans = []; let cost = undefined; for (let i = 0; i これで、連戦数 cost の対戦順 ans を得ることができます。 試しに参加人数を6名、実施済みの試合を"A-C", "C-D", "B-E", "B-F"として対戦順を求めてみます。 const list = createList(6); const excludeList = [5, 12, 18, 34]; const filteredList = combination(list).filter(elm => !excludeList.includes(elm)); search(filteredList); // { // ans: [ 3, 20, 9, 6, 40, 17, 36, 24, 33, 10, 48 ], // cost: 0 // } これを復元すると、 となり、確かに指定した試合を除いた、連戦のない対戦順を求めることができました。 上記の例では試しに手元で10回計測したところ、実行時間は平均10.8秒でした。 改善 さて、一応答えを求めることはできましたが、どうにも愚直にやりすぎている気がしてなりません。 かの老子も「千里の道も全探索から」とは言いましたが、もう少し効率よく探すことはできないでしょうか。 うすうす勘付いていましたが、いかにも競プロチックな問題ですね。 競プロど素人の私では調べようにも効率が悪いと思い、社内の競プロ歴戦の猛者達にレビューをお願いしたところ、以下のような啓示を賜ることができました。 連戦にならない試合同士をコスト0の辺、連戦になる試合同士をコスト1の辺でつないだ無向グラフを考えると、連戦数を最小化する問題は、「このグラフのすべての頂点を1度ずつ通るもっとも合計コストの小さい経路はどれか?」という問題に帰着し、これは 巡回セールスマン問題 と呼ばれる これは計算量 O(2^n n^2) (n: 頂点数=試合数)で決定的に求めるアルゴリズムが知られている ただ、巡回セールスマン問題については近似的によい解を求める方法も考案されており(2-optなど)、今回のケースでは十分な解が得られる可能性が高い アルゴリズムや問題の名称を知ることで「検索する」という手段を手に入れたので、調べながら改良してみたいと思います。 2-opt法 次のゴールである「効率よく探す」方法のひとつとして、局所探索法があります。 これは「現在の組み合わせに少しだけ変化を加え、コストが下がれば採用する」という操作を、コストが下がらなくなるまで繰り返すものです。 その中でも巡回セールスマン問題によく使われる2-opt法は、グラフ任意の2つの辺を選びそれらをつなぎ変えることで、組み合わせを変化させていきます。 つまり、 ...-a-b-...-c-d-... のようなグラフに対して、 ...-a-c-...-b-d-... のように b-...-c のブロックを反転させてつなぎ替えるような操作を試していくことになります。 再実装 1から3までの手順については既に作成した関数を流用し、近傍探索部分を追加で実装していきます。 まず辺を入れ替えた際の連戦数の変化について考えます。 最初の実装同様に全体の連戦数を数える評価関数を通すこともできますが、入れ替え前後でコストが変わり得るのは交換した辺の部分のみのため、差分だけを計算することで計算量を減らします。 const getSwapCost = (list, i, j) => { const getLocalCost = (x, y) => list[x] & list[y] ? 1 : 0; const costBefore = getLocalCost(i, i + 1) + getLocalCost(j, j + 1); const costAfter = getLocalCost(i, j) + getLocalCost(i + 1, j + 1); return costAfter - costBefore; } (今回も、配列長を超えて参照した場合コストは0と計算されるので、 i , j が配列末尾だった場合も例外処理は不要です。) 続いて、コストが下がることがわかった場合に、2つの辺をつなぎ替える関数を作成します。 const swapEdges = (list, i, j) => { const head = list.slice(0, i + 1); const reverseTarget = list.slice(i + 1, j + 1); const tail = list.slice(j + 1); return [...head, ...reverseTarget.reverse(), ...tail]; } // example const list = [0, 1, 2, 3, 4, 5, 6]; swapEdges(list, 2, 5); // [0, 1, 2, 5, 4, 3, 6] さらに、入れ替えた際にコストが最も下がる辺の組み合わせを探し、新しい対戦順を返す関数を作ります。 任意の2辺について試しますが、 i と j が連続していると入れ替え操作をしても配列が変わらない(2つの辺が同じ1つの頂点につながっていてつなぎ替えようがない)ため、内側のカウンタ j は i+2 から始まるようにします。 また返り値は、コストが下がった場合には採用された新しい並び順を、コストが変わらなかった場合は null を返すようにしておきます。 const improve = list => { let iBest, jBest; let diffBest = 0; for (let i = 0; i 最後に、 improve の結果が null になるまで繰り返し探索するメインの関数を実装します。 const localSearch = list => { const totalCost = evaluationFunc(list); if (totalCost !== 0) { while (true) { let improvedList = improve(list); if (!improvedList) break; list = improvedList; } } return { ans: list, cost: evaluationFunc(list) } }; 実行する際は、同様の手順で filter した配列を渡します。 localSearch(filteredList); // { // ans: [ 3, 36, 9, 6, 24, 33, 20, 40, 17, 10, 48 ], // cost: 0 // } 最初の実装と同様の配列を渡すと、別の解ですが連戦0の並び順を得ることができました。 しかし、実行時間は全探索の平均10.8sに比べて平均1.2msと、大幅に短縮することができました! 初期解について 2-opt法の探索では、与えられた解から変化させて探索するため、初期解の良さが最終的な解の良さに影響を与えます。 特に、今回は組み合わせを作成するロジック上、連戦が相当数続く並びが初期解となるため、 filteredList をランダムに並べ替えてから実行する方が良いかもしれません。 さらにいえば、複数のランダム初期解からそれぞれ2-optで探索すると、最適解が得られる確度が上がりそうですね。 ランダムに並べ替える関数も実装して試したところ、100セットの探索中、9回は連戦数1の解、それ以外の91回は連戦0の解が導出されていました。 const shuffle = list => { for (let i = list.length - 1; i >= 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [list[i], list[j]] = [list[j], list[i]]; } return list; } おわりに 最後まで読んでいただき、ありがとうございました! 今回は身近な(とても個人的な)課題をJavaScriptを使って解決しました。 また、2-opt法を用いることで、実行時間を劇的に短縮することができました。 さらなる拡張も現実的になったので、今後の展望としては 同一選手が3連戦する場合を評価に組み込む 選考試合を2コート同時並行で行う 選手数がより増えるケース なども対応・実証していきたいと思います。 これまで競プロをやってみたいとは思いながらも手を出せていませんでしたが、期せずしてその奥深さの一端に触れることができました。 与えられた問題ありきでなく現実で必要な問題設定を自ら考えることで、より興味と実感を持って学ぶことができたように感じます。 現実的な課題をいかに既知の問題に落とし込むか、また落とし込んだ問題に対して効率よく解ける引き出しをどれだけ持っているか、という部分はセンスや経験が問われることを痛感したので、これからも普段の業務で使う技術領域にとらわれず、幅広く学んで技術を磨いていきたいと改めて感じました。
アバター
これは、 Kubernetes3 Advent Calendar 2020 の2日目の記事です。 フォルシアでは複数のアプリにおいてKubernetesが用いられています。 参考: https://www.forcia.com/blog/001519.html しかしながら、デプロイ周りについてはまだまだ仕組み化がされておらず、いい感じにデプロイできる仕組みはないかと調べていると「GitOps」というワードが出てきました。 勉強がてら(結構こすられたネタだとは思うのですが)GitOpsを実際に構築してみた学習記録を記したいと思います(筆者は1ヶ月前まではKubernetes何それ状態でした)。 GitOpsとは Weave社が提唱した概念です。 https://www.weave.works/technologies/gitops/ GitOps can be summarized as these two things:An operating model for Kubernetes and other cloud native technologies, providing a set of best practices that unify deployment, management and monitoring for containerized clusters and applications.A path towards a developer experience for managing applications; where end-to-end CICD pipelines and Git workflows are applied to both operations, and development. 要するに 全てのリソースの変更や運用に対してコマンドラインを用いずにgit経由で行うことでコードとして履歴管理しようぜという思想 といった感じです。 よりイメージを深めるために、GitOpsを実現した結果期待される状態を述べると、以下のようになります。 開発者はデプロイを全く意識しなくていい(Git/GitHub/GitLabの操作だけでなんかデプロイされる) k8sで言うと手作業でkubectlとかしなくていい アプリケーション部分(テスト/ビルド)とインフラ部分(デプロイ)を疎に繋げられる Gitが信頼できる唯一の情報源(SSOT:Single Source of Truth)(差分検知/自動反映でGitのコードがインフラにある) すごい!!GitOps最高!! これが実現されれば、デプロイ作業から人々が開放されます。 特にGitがSSOTになるというのは素晴らしいと個人的に感じます。本番環境やステージング環境の状態がGitレポジトリを見れば一発でわかるのです。 さて、またk8sのGitOpsには主に2つの派閥があります。 Push型 CIのPipelineで kubectl してデプロイする Pull型 CDツールがSSOT(manifestレポジトリ)の更新を検知してデプロイする Push型は以下のような問題があり推奨されていません。 参考: https://www.weave.works/blog/why-is-a-pull-vs-a-push-pipeline-important サービスの世代管理が困難 意図したデプロイ結果になっているか確認が困難 パワフルな権限を持つCI etc... なので今回はPull型のGitOpsを構築することにしてみました。 GitLab CIとArgoCDでk8sのGitOpsを実現する GitOpsを試すために今回はCIツールとして慣れ親しんだGitLab CI/CD(フォルシアではGitLabを用いてコード管理を行っています。 参考 )を、CDツールはGUIが用意されているArgoCDを選択しました(ただGUI画面眺めてニヤニヤしたかっただけです)。 他に有名なCDツールとしては Flux (最近 Flux v2 がリリースされました)や Jenkins X などがあります。 以下構築した全体像です。 詳しくは今から述べていきます。 ポイントとして、アプリのレポジトリとマニフェストのレポジトリを分けているところがあります。これは ArgoCDのベストプラクティス に則っています。運用の手間は増えますが、アプリの差分とmanifestの差分がはっきり分かれるのでわかりやすく僕も好みです。 ArgoCDのインストール 事前にアプリを動かすk8s clusterにArgoCDをdeployしておきます。全体像の絵で示したようにArgoCDはk8sのcluster上で動くからです。 https://argoproj.github.io/argo-cd/ の手順をそのままやります。 > kubectl create namespace argocd > kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml # しばらく待ってPodが作成されていることを確認(まあまあの時間がかかります)* ❯ kubectl get pod -n argocd NAME READY STATUS RESTARTS AGE argocd-application-controller-5785f6b79-s2cvr 1/1 Running 0 2m39s argocd-dex-server-7f5d7d6645-z46hr 1/1 Running 0 2m39s argocd-redis-cccbb8f7-dfbjk 1/1 Running 0 2m39s argocd-repo-server-67ddb49495-nxkw4 1/1 Running 0 2m39s argocd-server-6bcbf7997d-cj5bg 1/1 Running 0 2m39s podが全て立ち上がったことを確認したのち、 > kubectl port-forward svc/argocd-server -n argocd 8080:443 でport-forwardさせてあげると、 http://localhost:8080 でGUI画面にアクセスできるはず。簡単。初期のログインアカウントはadmin, パスワードは以下のコマンドの実行結果(argocd-serverのPod名)です。 kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2 CLIツールもインストールしておきます。 # ArgoCD CLIのインストール > VERSION**=**$(curl --silent "https://api.github.com/repos/argoproj/argo-cd/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') > curl -sSL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/$VERSION/argocd-linux-amd64 > chmod +x /usr/local/bin/argocd # login; 上記のport-forwardを行っている場合***>** argocd login localhost:8080 CIパイプライン 以下の .gitlab-ci.yml はアプリのレポジトリに配置しています。 stages: - build - update_manifest - open_MR ############################################################################## ## Variables ## ############################################################################## variables: APP_NAME: gitops-demo-app # アプリレポジトリ名 CI_REGISTRY_IMAGE: /$APP_NAME # Docker push先のレジストリ名 CD_PROJECT_ID: # manifestレポジトリID(GitLabのプロジェクトID) CD_CHART_REPO: gitops-demo-chart # manifestレポジトリ名 CD_GIT_REPOSITORY: # manifestレポジトリのsshパス CD_MANIFEST_FILE: Chart.yaml # image tag書き換え対象のmanifestファイル名 TAG: $CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA # 書き換えのtag名 ############################################################################## ## Build Image ## ############################################################################## build_image: image: name: mgit/base:kaniko-executor-debug-stable entrypoint: [""] stage: build before_script: - echo $CI_REGISTRY_IMAGE:$TAG $PWD # login - echo "{\"auths\":{\"https://index.docker.io/v2/\":{\"auth\":\"${DOCKERHUB_TOKEN}\"}}}" > /kaniko/.docker/config.json script: # Docker Build && Push image - cat Dockerfile - > /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$TAG --build-arg COMMIT_HASH=$CI_COMMIT_SHORT_SHA ############################################################################## ## Deployments ## ############################################################################## update_manifest: image: mikefarah/yq:3.3.4 stage: update_manifest variables: GIT_STRATEGY: none retry: 2 script: # Add SSH key to root - mkdir -p /root/.ssh - echo "$SSH_PRIVATE_KEY" > /root/.ssh/id_rsa - apk add --no-ceche openssh - ssh-keyscan -H gitlab.fdev > /root/.ssh/known_hosts - chmod 600 /root/.ssh/id_rsa # Git - apk add --no-cache git - git config --global user.name $APP_NAME - git config --global user.email $APP_NAME"@gitlab.com" - git clone --single-branch --branch master $CD_GIT_REPOSITORY - cd $CD_CHART_REPO - git checkout -b update-image-tag-$TAG # Update Helm image tag - > yq write --inplace --verbose $CD_MANIFEST_FILE appVersion $TAG - cat $CD_MANIFEST_FILE - git commit -am "update image tag" && git push origin update-image-tag-$TAG only: - master open_merge_request: image: registry.gitlab.com/gitlab-automation-toolkit/gitlab-auto-mr stage: open_MR variables: GIT_STRATEGY: none script: # Create merge request - > gitlab_auto_mr --source-branch update-image-tag-$TAG --project-id $CD_PROJECT_ID -t master -c WIP -r only: - master これでアプリのレポジトリのmasterブランチにpushされると <branch>-<commit hash> とtag付けしたimageがbuildされ、DockerHubにpushされ、manifest repoのimage tagの値を更新したMRを自動生成してくれるところまでやってくれます。このパイプラインで直接(manifest repoのmasterブランチの)manifestのimage tagを更新してしまうところまでできるのですが、k8sにdeployする前に一旦人間のチェックが必要かと思い、MRを作成することにしました。 以下でステージごとにやっていることを説明していきます。 環境変数の設定 buildしたdocker image のpush先はDocker Hub, また異なるレポジトリ間で操作をしたいため、以下の環境変数を設定しました。( .gitlab-ci.yml にベタガキは危ないためプレビルドインしておく) DOCKERHUB_TOKEN : DockerHubにloginするために必要なtoken ( echo -n USER:PASSWORD | base64 で作成) GITLAB_PRIVATE_TOKEN : CLIでMRを作るために必要 SSH_PRIVATE_KEY : CI上でmanifest repoにアクセスするための秘密鍵 build stage build_image: image: name: mgit/base:kaniko-executor-debug-stable entrypoint: [""] stage: build before_script: - echo $CI_REGISTRY_IMAGE:$TAG $PWD # login - echo "{\"auths\":{\"https://index.docker.io/v2/\":{\"auth\":\"${DOCKERHUB_TOKEN}\"}}}" > /kaniko/.docker/config.json script: # Docker Build && Push image - cat Dockerfile - > /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$TAG --build-arg COMMIT_HASH=$CI_COMMIT_SHORT_SHA CI パイプラインは Docker コンテナ Runner で実行することが一般的なので、パイプラインの中で docker build するには privileged モードで Runner のコンテナを実行する必要があります。いわゆる DinD (Docker in Docker) です。DinDはセキュリティ的に危ないことが知られています。なのでDinDせずにコンテナ内でdokcer buildできる kaniko を使うこととします。 -destination $CI_REGISTRY_IMAGE:$TAG で <branch>-<commit hash> でtag付けしてDockerHubにpushしています。 update_manifest stage update_manifest: image: mikefarah/yq:3.3.4 stage: update_manifest variables: GIT_STRATEGY: none retry: 2 script: # Add SSH key to root - mkdir -p /root/.ssh - echo "$SSH_PRIVATE_KEY" > /root/.ssh/id_rsa - apk add --no-ceche openssh - ssh-keyscan -H gitlab.fdev > /root/.ssh/known_hosts - chmod 600 /root/.ssh/id_rsa # Git - apk add --no-cache git - git config --global user.name $APP_NAME - git config --global user.email $APP_NAME"@gitlab.com" - git clone --single-branch --branch master $CD_GIT_REPOSITORY - cd $CD_CHART_REPO - git checkout -b update-image-tag-$TAG # Update Helm image tag - > yq write --inplace --verbose $CD_MANIFEST_FILE appVersion $TAG - cat $CD_MANIFEST_FILE - git commit -am "update image tag" && git push origin update-image-tag-$TAG only: - master CIで一番ややこしいところ。違うレポジトリ(manifest repo)をcloneしてきてtagの部分のみを上書きしてcommit, pushする作業を行っています。 manifest repoにアクセスするための秘密鍵を登録してレポジトリをclone, tagを更新したのちcommitして update-image-tag-$TAG ブランチにpushしています。 tagの更新はyamlのラッパーである yq を用いて行っています。 yq w <yaml_file> <path_expression> <new value> で値の更新ができます。 https://mikefarah.gitbook.io/yq/commands/write-update open_MR stage open_merge_request: image: registry.gitlab.com/gitlab-automation-toolkit/gitlab-auto-mr stage: open_MR variables: GIT_STRATEGY: none script: # Create merge request - > gitlab_auto_mr --source-branch update-image-tag-$TAG --project-id $CD_PROJECT_ID -t master -c WIP -r only: - master 最後にmanifest repoでMRを自動でopenします。 いい感じのものが作られていたので使わせてもらっています。 https://gitlab.com/gitlab-automation-toolkit/gitlab-auto-mr 中身は GitLabのMR API を叩いているのですが、この時にprivate_tokenが必要なため、環境変数として GITLAB_PRIVATE_TOKEN を設定しておかなければいけないのがミソかも。 ArgoCD to Kubernetes 以上まででアプリの更新が行われれば、自動でmanifestのimage tagの更新(のMR)が行われるまでできました。 あとはmanifestの更新を検知して自動でk8sにdeployするところをArgoCDでやってもらいます。 kubectl create namespace gitops-demo # アプリ用のNamespaceを作成 # 今回はCLIで設定したがGUIでも同様の設定が可能 argocd app create webapp \ --repo <manifestrepoのurl> \ --path . \ --dest-server https://kubernetes.default.svc \ --dest-namespace gitops-demo \ --sync-policy automated \ # GitRepoを監視して変更があったら自動更新する設定 --auto-prune \ --self-heal こんな感じでGUIで確認できました。 あとはアプリ用に適当にport-forwardさせてあげるとアプリの画面を見ることができました〜! また、アプリのレポジトリの更新を行うとパイプラインがまわり、マニフェストのレポジトリにMRが作成されます。そしてmergeを行うと、それをArgoCDが検知してdeployが勝手に走ります。 そしてしばらく待ち(Argo CDは 3分おき(調整可能) にリポジトリの変更をみてデプロイする)、deployが完了するとアプリの更新が行えていることが確認できました。簡単! ArgoCDのその他機能 ArgoCD(≒k8sがデフォルトで提供する)のdeploy strategyはRollingUpdateなのですが、 Argo Rollouts を使用するとBlue-Green updateやCanary updateなども選択できます。 また、deploy状況の通知関係も Argo CD Notifications を使えば実現できます。例えばdeployが完了すればSlackに通知するみたいなことも簡単にできます。 以上のようなArgoCDのカスタマイズをしたものをArgoCDでdeployすることもできるのでArgoCDの設定もGit管理できるのも便利だったりします。 最初はGUIがあるのでArgoCDを選択したというのが大きかったのですが、シンプルながらかゆいところに手が届く機能が充実しており、完成度の高いCDツールであると使いながら感じました。 まとめ CDの部分よりはCIのところで時間を割いたのでCD部分の検証は不十分ですが、初期設定を除いてアプリレポジトリを更新すれば自動でk8sのデプロイが実現するところまで確認できました。これはとても便利。 Gitの管理を行っているので再現などもかなりやりやすくなると思われます。 k8s化することだけでdeploy作業はしやすくなったと社内のエンジニアから聞いていましたが、GitOpsを導入することでより簡潔にできそうです。温かみのある作業を自動化してより生産性のある作業に没頭できる時間を増やしていきたいですね。 参考 数時間で完全理解!わりとゴツいKubernetesハンズオン!! - Qiita ArgoCD公式ドキュメント GitOps in Kubernetes: How to do it with GitLab CI and Argo CD GitLabCI+ArgoCDを使って、「マージしたら5分でKubernetesへデプロイ」を実現する - エニグモ開発者ブログ GitOps in Kubernetes with GitLab CI and ArgoCD CodeBuild で Docker イメージに Git のコミットIDをタグ付けてバージョン管理する | Developers.IO gitops-using-flux-and-gitlab
アバター
FORCIAアドベントカレンダー2020 1日目の記事です。 こんにちは、新卒エンジニア2年目の高橋です。 アドベントカレンダーのネタ探しに迷走し続け、自分が使っている技術の説明をしても中途半端な内容になりそうだなーと思い、自分にしか書けないことってなんだろうと考えた結果、プログラミングを始めてからこれまでに作成したコードやアプリについて振り返ってみようと思い立ちました。半分日記のような形式になりそうですが、ある技術についての記事が「点」での解説だとすると、エンジニアの成長という「線」の記事も需要あるかなと思ったのと、自分も書いていて楽しいだろうと思ったのでこの内容で行きます! 会社での業務については同期入社の中曽が書いた下記の記事で触れられているので、この記事では自分が業務外で作ってみたアプリなどを例に挙げながら、エンジニアを1年半続けたことによるスキルの変化や、そのとき考えていたことなどを中心にまとめたいと思います。 19新卒入社からの軌跡 未経験エンジニアは1年でどこまで成長できるのか また面談の場や、友人と話していると以下の様な質問をよくされます。 入社するまでに何を勉強したらいいですか? まず何の言語を学習するのがいいですか? プログラミングができると、結局何が作れるようになるのでしょうか? 自分のこれまでの振り返りを通して、最後に上記質問に対しての自分なりの回答ができればと思います。興味があればお付き合いいただけると嬉しいです! 余談ですが、過去のFORCIA CUBEの記事をみると自分の同期が書いた「振り返ってみた系」の記事がたくさん出てきて、みんな振り返るの大好きだなと(笑)。 入社前 〜Hello, プログラミング〜 まずは自分がエンジニアを志望するきっかけとなった、大学院での出来事についてです。私は大学院までは化学専攻で分子の物性について研究していました。研究のプロセスは以下のような物です。 実験器具を用いて測定。取得したデータはCSV形式で出力 データを取り込み、グラフや図に落とし込み人間が分析できるようにする 過去のデータや他の実験とデータを比較し、分析する 私がいた研究室ではデータの取り込みやグラフの作成は「 Igor 」というアプリケーションを利用していたのですが、このアプリでは独自のプログラミング言語によりマクロを作成して処理を自動化したり、GUI(ボタンなどで挙動を制御できる)を作成してグラフ作成や分析を簡易にしたりすることができるものでした。VBAでExcelのマクロを作るようなイメージですね。 それまでほとんどプログラミングに触れたことはありませんでしたが、測定では大量のデータを処理する必要があり、手作業で行うのは大変手間がかかっていたため、処理を自動化するためのマクロを作成してみることにしました。例えば、 result.20201201_001.csv result.20201201_002.csv result.20201201_003.csv ・・・ result.20201201_100.csv という名前の測定データに対して、一連のデータを順に取り込み、必要な処理を施して、全てのデータを一つのグラフにプロットする、みたいな感じです。手作業でデータを処理していたときは100個のデータを全て処理しようと思うと途方もない時間がかかってしまいますが、一度プログラムを書いてしまえば以降はワンクリックで全て処理できてしまい、「プログラミングってすげー!」と感動したのを覚えています。単純ですね。またちょっとここをこうしたいんだけどな、と思ったときも自分でプログラムを書き換えて処理を変えることができるというのも、プログラムを自分で書けることは大きな強みになると実感しました。 このプログラムの作成では、 forによる繰り返し ifによる分岐、例外処理 というまさにプログラミングの基礎となる概念を理解でき、また何よりもプログラムによって課題を解決できるという大きな成功体験を得ることができました。プログラミングを始めるきっかけは人それぞれかと思いますが、私の場合は必要に駆られて始めた結果、その便利さに惹かれていきました。 使用した技術 Igor 入社1ヶ月 〜API、恐ろしい子〜 近年の(と言ってもかなり前からですが)Web開発において重要な要素に「API」があります。雑に解説すると「外部からサービスを利用できるような仕組み」のことです。例えばLINEは通常アプリから操作してメッセージを送りますが、LINEが提供するAPIを利用すると、アプリの外からプログラムによりメッセージを送る操作をすることができます。 入社後の研修でプログラムの勉強をしていく中でAPIという概念を知り、自分で試しに使ってみたい!と思い立って作成したのが、Slackの同期チャンネルで現在も稼働中の「お誕生日bot」です。ネーミングセンスのなさ(笑)。 このプログラムはGoogleが提供している、無料で外部サーバー上でプログラムを動かすことができるGoogle Apps Scriptで動いており、Slack APIを用いることで自分の同期の誕生日にチャンネルにメッセージを通知するbotです。 実装は非常に単純なものですが、APIを用いて簡単に外部サービスと連携できるというのはカルチャーショックでした。こんな便利な機能をなんで無料で使えるんだ・・・と不思議に思った記憶があります。 今時だとプログラムを動かすサーバーも無料で借りれたりして、何かを試しに作ってみるハードルはかなり低いですね。 使用した技術 Slack API Google App Script 入社8ヶ月 〜アプリがないなら、自分で作ればいいじゃない〜 時は2019年年末。私が企画・運営を担当していたフォルシア忘年会の出し物で、社内タイピング競争を行うことになったのですが、メモ帳などに文章を入力してもらうだけだと誤字の判定も難しいし見る側も味気ないため、タイピングゲームのように文字の入力を判定できる形式が好ましいと考えていました。またタイピングの課題の文章を自分たちの選んだ文章にしたいという要望もあったのですが、課題の文章を作れるようなタイピングゲームはざっとみた限り見当たりませんでした。「じゃあ自分で作ったらええやん!」と思い立ち作成してみました。 この頃には一年目の研修もほぼ終わり、すでに実際の業務でも保守や開発を通してWeb開発の流れも一通りざっくりとは理解できていたつもりだったので、Webを構成するための最も基本的なプログラミング言語であるHTML, JavaScript,CSSを用いて0から簡易的なタイピングアプリを作成しました。 普段の業務ではすでにあるプログラムに対して改修や追加を行うので、0から自分でアプリを作り上げるのは良い腕試しになりました。企画の趣旨としては「誰が一番タイピングが早いのか」を競うもので主役はあくまでも人ですが、それを引き立てるために十分活躍してくれたと思うので個人的には満足できる出来でした。 このタイピングアプリの作成を通して、プログラミングの面白さは、 自分で考えた物を、自分で形にすることができる 目に見える物や動きのあるものも作れる だれかの役に立つものもアイデア次第で作れる ということかなと考え始めます。フォルシアのエンジニアと話していると、競技プログラミングのようなアルゴリズムやデータ構造に興味がある人、サービスを安定的に運用するためのインフラに興味がある人など様々なタイプのエンジニアがいるなと思うのですが、この辺りから自分は人の役に立つ便利でいけてるものを作る、というプロダクトに対して興味があることに気が付きます。 使用した技術 JavaScript + jQuery HTML CSS 入社1年半 〜Never Ending Catching Up〜 この頃は業務でReact, TypeScriptを使い始めていました。先ほどWebの基本的なプログラミング言語はHTML、JavaScript、CSSという話をしましたが、これらの言語をそのまま使うとコードが煩雑になったりメンテナンス性が低くなるため、大規模開発になるとなかなか辛いところがあります。そこで上に挙げたようなライブラリを使うことで、開発体験や保守フェーズでのメンテナンス性を向上させることができます。 簡単に説明すると、ReactはJavaScriptとHTMLを合わせたようなライブラリで、コンポーネントという単位でWebページに表示するパーツを分けることで部品の使い回しがしやすかったり、動的に変化する要素の描画が簡単にできたりします。TypeScriptはいわゆるAltJSと呼ばれるJavaScriptを拡張したようなライブラリで、JavaScriptに静的に型をつけることができます。型を付けることで予期せぬ値が関数に渡されることを防ぐことができるため、コードの信頼性が向上します。 素のJavaScriptと比較すると、これらのライブラリは初めは少しとっつきにくさがあります。もともと簡単に書けていたのにまどろっこしい書き方になることもありますが、それぞれのライブラリの思想が分かってくると合理的だと思えるようになりました。 世の中で流行っている技術というのはそれなりに流行る理由があるからで、それらをキャッチアップしていくことはエンジニアとして今後活躍していくためには重要です。業務中にそのような技術に触れる機会も多い一方で、そうでないものについては自分で時間を作って触ってみる、勉強する必要がありますが、これがなかなか億劫だったりします。 今年の秋のシルバーウィークに社内でリモート開発合宿が開催されたので、これを好機と思い、社内ではまだ利用されていなかったGraphQLに触れてみようと、ECサイトをイメージしたアプリを作ってみました。 GraphQLはAPIのインターフェイスを定義するクエリ言語で、これまでメジャーだったREST形式と違いクライアント側で必要なデータを柔軟に指定してリクエストできるのが特徴で、また型付けにも強い言語です。 タイピングゲームを作っていた頃から比べると、比較的新しい技術を使っており「今時のエンジニア感」を勝手に感じていました(笑)。 Webの技術の進歩は本当に早いと実感しており、1年前に使っていた技術が新しい物に取って代わることはざらにあります。絶えずキャッチアップし続ける必要があるのは大変ですがそれは逆にチャンスでもあって、経験が少ないエンジニアでも新しい技術にキャッチアップすることで最前線に立つことができるようになります。 何でもかんでも新しいものが優れているというわけではもちろんないですが、これまでの技術が含んでいた欠点を補う形で作られるライブラリも多く、それらを身に付けることが「イケてるサービス」を作るためには欠かせないと考えています。 使用した技術 React Redux TypeScript GraphQL まとめ ここまで自分の振り返りにお付き合いいただきありがとうございます。一年の終わりにこれまでを振り返るのはとても有意義だと記事を書きながら感じていました。自分がこれまで何を考え、何をしてきたのかを振り返ることで、初心を思い出しこれからの方向性も見えてきたような気がします。 特に私はアルゴリズムやコンピューターサイエンスよりも「何をつくるか」「それがどんな価値をもたらすか」に興味があることに改めて気付きました。エンジニアとして今後のキャリアを考える際には、それらの軸を忘れないようにしたいです。 皆さんもぜひ時間を作って今年一年を振り返ってみてください。 さて、最後にはなりますが記事の冒頭で触れた以下のよくある質問に対して、簡単にではありますが回答します。 入社までに何を勉強すればいいですか? 入社後に研修があるのでプログラミング言語の書き方などはそこで身に付けることができますし、最新の技術は仕事を通して触れることができるので、その方が効率が良いのかなと思います。 個人的に勉強しておくとよかったなと思う内容はコンピューターサイエンスですね。興味があるないにかかわらずエンジニアをしていると避けては通れない道だと思うので。コンピューターがなぜ動くのかとか、ネットワークがなぜつながるのかとか、雰囲気だけでも知っておくとその後の内容が理解しやすくなるかと思います。もちろん、興味があるものがあるならまずそこから手を付けるのが一番だと思います! まず何の言語を学習するのがいいですか? 言語ごとに特徴や思想があって、本当はやりたいことに応じて言語を選択する必要がありますが、初めに触れてみるにはどれでもいいかなーと思います(一部例外あり)。私の場合は前述のIgorというソフトに組み込まれているという利用が超限定的な言語から始まりましたが、プログラミングの概念自体は言語を超えてある程度共通しているので、まあ問題ないかなと思います。とっつきやすいものだと文法がすっきりしていて書きやすいPythonがおすすめでしょうか。 プログラミングができると、結局何が作れるようになるのでしょうか? 上に挙げたようなものです(笑)。文中でも書きましたが無料で提供されているAPIを活用すると、比較的簡単に便利なものが作れるので楽しいです。
アバター
今年もやります!FORCIAアドベントカレンダー2020 こんにちは。旅行プラットフォーム部 エンジニアの高橋です。 気付けばもう12月、毎年思うことではありますが1年が経つのは本当に早いですね。 今年はコロナウイルスの影響もあり、年初に思い描いていた一年とは大きく異なるものになりました。 外出自粛要請解除後にひさしぶりにオフィスに出社した際は、普段人であふれている新宿からは考えられないほど閑散としていて、影響の大きさを身に染みて感じました。 しかしそんな状況だったからこそ、新しい働き方を模索するなど前向きな姿勢を感じる場面も多かったです。リモートワークの改善、各種イベントのオンライン開催など、新しいことに挑戦したからこそ得ることのできた知見も多かったように思います。 そんな今年一年間で各社員が得た知見や新たな取り組みなどを皆さんと共有できたら、と思い、今年もアドベントカレンダーを行います! 過去のアドベントカレンダーはこちらから! 2019年: https://www.forcia.com/blog/advent-calendar2019/ 2018年: https://www.forcia.com/blog/advent-calendar2018/ アドベントカレンダーとは 元々はキリスト教において、待降節(advent)にクリスマスまでカウントダウンするためのカレンダーで、毎日一つずつ日付の窓を開き、中に入っているお菓子や小さな贈り物を楽しむものが一般的です。 この風習になぞらえて、インターネット上では12月1日からクリスマスまでの25日間、特定のテーマや団体に関するブログ記事を毎日1件ずつ、持ち回りで投稿するお祭りが様々なサイトで開催されています。 FORCIA アドベントカレンダー 2020 明日からはじまります 明日12月1日より、フォルシア社員 計25人が記事を投稿します。エンジニア以外にも、営業やカスタマーサクセスの社員も参加し、幅広いテーマの記事を公開していきます! 記事のテーマを少しだけお見せすると、以下の様なものがあります。 kubernetes導入してみた Next.js 新バージョンについて 機械学習 キーボードへのこだわり これ以外にも様々なジャンルの記事が公開される予定ですので、ぜひお楽しみに! 新しい記事が公開されたら、下記の特集ページに追加していきます。 FORCIA アドベントカレンダー2020 また、更新情報はフォルシアのSNSでも告知しますので、ぜひフォロー&チェックしてくださいね。 Twitter: @forcia_pr Facebook: @forciapr
アバター
フォルシア技術研究所(技研)の原です。 技研では、新しいサービスの創出、および既存のサービスの拡張や効率化に資するべく、今までのフォルシアでは使われていなかった技術の開発、導入を進めています。 その一つが、商用アプリへの社内初の Kubernetes の導入です。この記事では、フォルシアでの Kubernetes の利用、工夫、苦労したところなどを紹介したいと思います。 (その他、技研ではRust によるインメモリDBの開発なども行っており、Rust については、 Software Design 6月号(技術評論社) に「入門! Rust」という特集記事に私と技研の松本が執筆させていただいたり、 実践Rustプログラミング入門(秀和システム) をフォルシアで監修させていただいたり、執筆に松本が参加させていただいたりしております) Kubernetes とは? Kubernetes はコンテナオーケストレーションシステムと呼ばれるものです。オーケストレーションって何ぞや?と言いたくなるかもしれませんが、たくさんのコンテナを効率よく管理するためのツールであり、Kubernetes を使うと以下のようなメリットがあります。 同じコンテナを複数実行することで、簡単にレプリカを作って、レプリカ間でロードバランスをすることができる 複数のワークノード(物理マシンやVM)でクラスタを構成し、ワークノードの存在をほとんど意識せずに、インフラを利用することができる 必要なリソース(CPU、メモリ)に応じて、自動的にワークノードを増やしたり減らしたりできる(自動なので、これもユーザーがその増減を意識することはない) サービスの規模に応じてインフラを柔軟に増減させることができるので、フォルシアでもサービス展開が進んでいる SaaS 型のサービスと非常に相性がよく、SaaS型サービスを展開していく上で、基盤となり得るものです。 Kubernetes + ecflow でワークフローを実行する Kubernetes は宣言的 フォルシアのアプリを Kubernetes 上で稼働させる上で必要なことの一つに、「バッチ処理が終わったあとにバッチで加工されたデータを使ってアプリ(Pod)を起動(deploy)する」というフロー処理があります。 Pod というのは、Kubernetes でのワークロードリソースの最小単位で、各Pod では1つまたは複数のコンテナが実行され、Pod 内のコンテナではネットワークやボリュームを共有しています。Pod にアプリのコンテナなどを搭載して、サービスを提供します。 Kubernetes のアーキテクチャは「宣言的である」とよく言われます。ユーザーは、Kubernetes に「希望する状態」(たとえば、レプリカを3つ作ってほしい、docker イメージを更新したアプリ(Pod)に取り替えてほしい、など)を宣言します。その「希望」はYAML または JSON で記述された「マニフェスト」と呼ばれるもので表現して、 kubectl apply というコマンドでKubernetes に入力します( kubectl は Kubernetes の API サーバーとやりとりをしており、 kubectl を使わずに直接APIサーバーと通信することも可能 )。 その宣言を受けて、Kubernetes は現状のKubernetes クラスタの状態と入力された宣言との違いを察知し、その差をなくすようなアクションを行います(たとえば、レプリカ数が足りなければ、希望するレプリカ数になるように Pod を追加で作成する、など)。 宣言した状態になった? このようにして、Kubernetes はKubernetes クラスタの状態をユーザーが宣言した状態に近づけ、最終的には宣言した状態と差がないようにしてくれますが、フロー処理を実行するためには「宣言した状態になった」ということを検知して、それをトリガーに後続の処理を実行するということが必要になります。 たとえば、Kubernetes の Jobリソースと呼ばれるものを用いてバッチジョブを起動して、そのバッチジョブの終了をトリガーにして新しい Deployment リソースを作成してPod を deploy する場合などです(図1)。 「指定したジョブを実行して完了させる」という状態を宣言した Job リソースを作成すると、バッチジョブのPodが作成されてジョブの実行を開始し「ジョブを完了させる」という「宣言した状態」に近づけます。この操作の中で、 「Jobリソースの作成」( kubectl apply で実行)は宣言を入力しているだけで、 kubectl apply はその宣言が受け付けれられると終了しますので、その後、宣言した状態が実現したのか、またはエラーが発生したのか、などは別途ユーザーが検知をする必要があります。 図1: Kubernetes のリソースの作成と、宣言された状態になるまで待つフローの一例 宣言した状態になったことを検知 Pod のリソースの状態を問い合わせる kubectl get pod には --wait というオプションがあり、ある状態になるまで kubectl が終了するのを待つというオプションが実装されてはいますが、まだ実験的(experimental)な オプションであり、エラーの検知やエラーハンドリングを柔軟に行えるようにするため、 --wait オプションを用いず、リソースの状態をAPIサーバーにポーリングしています。ただし、「ポーリング」といっても一定時間間隔でリクエストを送信するのではなく、 watch オプションを用いて、リソースの状態の変化があったときにそのリソースの内容を受け取ることができるようにして、APIサーバーへのリクエストの負荷を小さくしています(AWS の SQS のロングポーリングに似ています)。 API サーバーを通じてPodの状態を取得していますが( kubectl get pod -o yaml --wait に対応)、リソースの状態にはエラーの場合を含め、いろいろなパターンがあることがわかりました。そのパターンを網羅し、読み取ったPodのリソース状況をトリガーに、エラーハンドリングも含めて適切な処理ができるようなモジュールを開発しました。このモジュールによって、バッチ処理が終了したり、Pod がすべて正常に起動したなどの"イベント"を検知して、次の Kubernetes リソースの適用などのフロー処理をしています。 (これらの処理は、Kuberenetes Operator を実装できれば、Kuberenetes に閉じた世界で実現出来そうですが、そのハードルは高いので、今回はリソースの状態問い合わせを行う外部モジュールを開発して対応しました。) フロー処理には ecflow を活用 世の中には様々なワークフローエンジンがあります。その中で、Apache airflow が有名なものの一つです。私も使ってみたのですが、airflow は時間がかかるETL 処理を扱うことが前提になっており、前のタスクが完了して次のタスクが投入されるまでに数十秒以上の時間がかかります。それぞれのタスクの実行時間が短い場合には、タスクとタスクの間の数十秒という時間が大きなオーバーヘッドになってしまいます。 そこで、欧州中期予報センター(ECMWF)が自らのスーパーコンピュータでのプログラム実行のために開発した ecflow というワークフローエンジンを用いています。ECMWF が開発している天気予報のためのスパコン用のプログラム(数値予報モデル)は世界一の精度を誇り、ecflow は複雑な依存関係を持った大量のタスクから天気予報のためのプログラム実行の運用を担っています。ecflow は apache 2.0 ライセンスで配布されており、天気予報以外のワークフローにも用いることができます(ecflow については、 FORCIA Meetup #1 〜DevOpsやっていかnight〜 でも紹介しました)。 kubectl apply で新規のリソースを作成したり、既存のリソースを更新して、ポーリングによって「宣言した状態になった」(またはその仮定でエラーが生じた)ということを検知する一連の処理を一つのタスクとして、前のタスクが「宣言した状態になった」のを検知して次のタスクを投入する、という処理を ecflow でやっています。 マニフェストのテンプレート化 「宣言した状態」を記述したものをマニフェストといい、JSON または YAML で記述しています。ecflow の中で kubectl apply で適用するマニフェストは Mako Templates for python によってテンプレート化して、テンプレートに与える変数を ecflow から与えています。Kubernetes マニフェストのテンプレート化には、Helm や kuscomize などのツールがありますが、他の社内ツールでも使い慣れたもので直感的にテンプレート化したいと考え、社内でも利用実績がある Mako によるテンプレートを選択しました。 Kubernetes は SaaS 型のサービスとの相性がよいですが、マニフェストをテンプレート化をすることで、複数のサービス提供先にも同じテンプレートで対応することができて、実装がすばやくできるようになり、また保守性が非常によくなりました。 フォルシアのプラットフォームへのKubernetes の導入 これまでも、大量のログを扱うログ基盤(HDFSやSpark)に Kubernetes をオンプレミスで導入していましたが、上で紹介した技術を用いてGoogle Hotel Ads のサービスを提供するアプリに Kubernetes を導入しました。そして、そのノウハウをほぼそのまま、 フォルシア web コネクト にも適用して、9月1日よりサービス提供をしています。 AWS EKS の利用 Kuberenetes を利用するにあたり、クラウドのマネージドシステムを利用して、Kubernetes で実行するアプリなどの開発に集中できるようにしました。 クラウドのマネージドシステムとして提供されている Kubernetes には、AWS の EKS、GCP の GKE、Azure の AKS などがあります。GKE なども試用してみましたが、フォルシアではAWSの利用が最近活発になっており、これまでの経験による"土地勘"があることを重視して、AWS EKS を選択しました。 これまで、ログ基盤でオンプレのKubernetes を構築した経験がありましたが、マネージドシステムの Kuberenetes は簡単にクラスタを構築でき、オンプレのこれまで利用してきた Kubernetes とほぼ同じように使えており、マネージドシステム特有の制約は特に問題になることなく利用できています。 ingress を実現するALBの target-typeに注意 EKS の ingress は ALB で実現 EKS では ingress のリソースを deploy すると、Application Load Balancer (ALB) が構築され、ingress の機能を実現します。ALB を ingress として使う場合には、リクエストを受けるポートを NodePort として公開するService リソースを適用することが必要です。 その結果、たとえば、ワークノードが2つあり(AとB)、NodePort が 30001 の場合、ワークノードAまたはBの 30001 ポートにクラスタ外部から接続すると、サービスを提供しているPodのいずれかにリクエストが転送されます。ワークノードAでPodがサービスを提供していて、ワークノードBの 30001 ポートに接続すると、その接続はワークノードBからワークノードAに転送されることになります。 デフォルトは target-type=instance EC2でwebアプリを構築している場合、ALB ではリクエストを振り分ける先であるターゲットグループをインスタンス(EC2のインスタンスID)で指定すること(target-type=instance)が一般的かと思います。 EKS の場合もこれがデフォルトになっており、Kubernetes に deploy されている ALB コントローラが、クラスタのワークノードをALBのターゲットグループに登録してくれます。ALB は登録されたワークノードのいずれかのNodePortにリクエストを振り分け、その NodePort からさらにService のエンドポイントになっているいずれかのPodにリクエストが割り振られることになります。 時々、ALBから504エラーが この状態でサービスにデッドタイムが生じないかを調べるテストをしていたところ、時々、ALBが 504 (Gateway Timeout) のエラーを出しました。エラーとなるタイミングを調べてみると、クラスタオートスケーリングによってワークノードが削除される(スケールイン)ときにエラーになる場合があることがわかりました。 ALB はNodePortを通じて登録されているワークノードのいずれかにリクエストを振り分けますが(図2)、その登録はワークノードが削除されるタイミングで解除されます。 しかし、スケールインの場合には、ワークノードが削除される前にそのノードで実行されていた Pod は削除されてリクエストを受け付けなくなります(削除されたPodは別のワークノードに deploy されます)。 つまり、リクエストを受ける Pod はないが、ワークノードはまだ存在しているという場合が生じます(図3)。その結果、ワークノードが存在しているので ALB がリクエストをそのノードに振り分けるものの、リクエストを受け付けるポートがないという状況が発生して、そのためタイムアウトになっていた、と推定しています。 図2: ALBからのリクエストの流れ(target-type=instance の場合) 図3: ワークノードの削除プロセス中の状態(target-type=instance の場合) target-type=ip にすることで解決 この状況に困っていたのですが、ALB のtarget-type のもう一つのモードである ip に切り替えること(ingress の annotation で指定する)で解決しました。 target-type = ip の場合、ALBコントローラによってPod の IPアドレスをターゲットグループに登録されます(図4)。そして、Pod が削除されると即座にALBのターゲットグループからそのIPアドレスを削除され、その Pod にはリクエストがALBから振り分けられなくなります(図5)。ワークノードがスケールインで削除される場合には、ノードが削除される前にPodが削除され、ALB のターゲットグループからもそのPod の IPアドレスが削除されるため、ALBがリクエストを振り分けたけどリクエストを受け付けてくれるPodがない、ということがほぼなくなります。 図4: ALBからのリクエストの流れ(target-type=ip の場合) 図5: ワークノードの削除プロセス中の状態(target-type=ip の場合) さらに、target-type = ip の場合はALBとPodが直接つながっているため、Pod が削除されるプロセスの中で、Pod からALBのヘルスチェックに対して unhealthy のシグナルを送り、Pod が削除される前にALBからリクエストが割り振られないようにすることができます。 それによって、Pod が削除されるタイミングとターゲットグループからの削除のタイミングの微妙なタイムラグによって、ターゲットグループに存在している間にALBがリクエストを振り分けたけどPodはすでにない、という場合が起こらないようにして、504 エラーが発生することを回避しています。 また、target-type = instance の場合には、ALB から NodePort を経由して Pod にリクエストが送信されていましたが、target-type = ip の場合は、ALB から直接 Pod にリクエストが送信されるので、オーバーヘッドが少なくなると思われます。 EKS で ingress を使う場合には、target-type = ip にすることを忘れないようにしましょう。 CKA, CKADに認定 Linux Foundation では、Kubernetes管理者の責任を果たすためのスキル、知識、および能力をが備わっていること認定するCertified Kubernetes Administrator (CKA) 試験、Kubernetes用のクラウドネイティブアプリケーションを設計、構築、構成、公開できる能力が備わっていることを認定するCertified Kubernetes Application Developer(CKAD)試験を実施しています(詳しくは Linux Foundationのページ )。 いずれも、ハンズオン形式での試験で、実際のKubernetes クラスタを操作して、クラスタを試験問題が要求する状態にします(Kubernetes が宣言的であるからこそ、可能な試験形態と言えます)。 私は、Kubernetes の社内初の商用化の後に、自分のスキルの確認や知識の整理のためにCKA および CKAD を受験し、ともに 90%以上の得点で合格できました。実務経験で培ったものは大きく、多くは経験で対応できたものの、あやふやだった知識を再確認したり整理するためのよい機会にもなりました。 その他、AWS 認定のソリューションアーキテクトアソシエイト、ディベロッペーアソシエイト、SysOps アドミニストレータアソシエートにも合格しましたが(いわゆるアソシエイト三冠達成)、その学習の中でAWSの様々なサービスやその背景にある思想を知ることができて、Kubernetes とともに「クラウドネイティブ」に対する理解が深まりました。 まとめ 新たなものを導入するのは、技術の習得や、求められるサービスレベルに達していることを確認することなど、いろいろな障壁があります。 また、導入することへの強い必要性がないと、優先度が上がらず、いつまでもダラダラと取り組み、結局、モノにならないということも多々あります。 本件では、いつまでにKubernetes の導入の可否を判断するということを設定し、Kubernetes の導入実績を作ることは今後の技術開発やビジネス展開に大きな意義があるという信念を持って、Kubernetes や周辺技術の習得、それを踏まえたシステムの開発、ロングランテストや負荷テストを含む徹底的なテストを行い、その結果、ゴールに達することができました。 この技術をSaaSビジネスの基盤として今後も展開していくとともに、さらなる技術の研究開発に取り組んでいく所存です。
アバター
FORCIAアドベントカレンダー2019 25日目の記事です。 FORCIAアドベントカレンダー最終回を担当します、エンジニアの武田です。 WebAssemblyについて、今まで触ったことがなかったのでこの機会に学んでみました。 業務でRustを書く機会があるためwasm-bindgenを利用してみましたが、こちらのドキュメントのexamplesが非常に良かったためそのご紹介をします。 WebAssemblyとは 高速、安全で効率良く動作することを目指して提案されたWebの標準規格です。詳しくはW3CのSpecificationのDesign Goalsを参照してください。 2019
アバター
FORCIAアドベントカレンダー2019  25日目の記事です。 FORCIAアドベントカレンダー最終回を担当します、エンジニアの武田です。 WebAssemblyについて、今まで触ったことがなかったのでこの機会に学んでみました。 業務でRustを書く機会があるためwasm-bindgenを利用してみましたが、こちらのドキュメントのexamplesが非常に良かったためそのご紹介をします。 WebAssemblyとは 高速、安全で効率良く動作することを目指して提案されたWebの標準規格です。詳しくは W3CのSpecification のDesign Goalsを参照してください。 2019年12月5日に W3Cの勧告 となり、HTML、CSS、JavaScriptに次いで4番目のブラウザ上で動作する標準の言語として認められました。 基本的に直接WebAssemblyのコードを書くことはなく、他言語からコンパイルして作成されます。 CやC++、Rust、Go、KotlinやTypeScript(AssemblyScript)などから生成でき、今後さらにサポートする言語は増えていくと考えられます。 高速に実行できる、というメリットは非常に大きいですが、バイナリフォーマットで軽量なため、構造解析とコンパイルが高速という点も魅力的です。 実際のユースケースについては こちら にまとめられています。やはり画像/動画処理やゲーム、科学シミュレーションなど計算量を必要とされるところがメインの使いどころになりそうです。 wasm-bindgenについて WebAssemblyとJavaScriptの間のデータの受け渡しをwrapしてくれるツール/ライブラリです。 js-sys (JavaScriptのAPIが利用できる)や web-sys (documentオブジェクトやwindowオブジェクトなどが利用できる)といったクレートが含まれています。現時点でも非常に多くのAPIが利用可能となっており、フロントの実装すべてをRustで書く、ということも不可能ではなさそうです。 wasm-bindgenのexamplesについて 今回紹介したかったのは wasm-bindgenのドキュメント です。examplesから始まっており、実際に動かして試してみることができます。 wasm-bindgen-cliのインストール rustはインストール済みの前提です(rustupというツールから簡単にインストールできます)。 $ cargo install wasm-bindgen-cli # こちらも既にインストール済みの場合は不要です 以降のexamplesで npm run build 、もしくは npm run serve をした場合に wasm-packの有無をチェックしてなければインストール コンパイルターゲットにwasm32-unknown-unknownがなければ追加 を 自動 で実行してくれるようです。事前準備はrustとwasm-bindgen-cliのインストールのみでWebAssemblyが動かせます! hello-worldを動かす $ git clone https://github.com/rustwasm/wasm-bindgen.git $ cd wasm-bindgen/examples/hello_world $ npm install $ npm run serve この状態で http://localhost:8080 にアクセスしたときに Hello, World!! のalertが画面に表示されれば成功です。WebAssemblyでは文字列を扱うのにも工夫が必要ですが、この辺りはwasm-bindgenがWebAssembly、JavaScript間のデータのやり取りをwrapしてくれています。 canvasを触ってみる おまけでweb-sysクレートを利用しているexamplesであるcanvasを触ってみます。実行するとニコちゃんマークが表示されます。 今回はcanvasのコードを少しいじって別の絵を表示するようにしました。 <html> <head> <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/> </head> <body> <canvas id="canvas" height="300" width="300" /> </body> </html> use std::f64; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; fn write_some_object(ctx: &web_sys::CanvasRenderingContext2d, x: f64, y: f64) { ctx.begin_path(); let mut rot = f64::consts::PI / 2.0 * 3.0; let step = f64::consts::PI / 5.0; let outer = 20.0; let inner = 10.0; ctx.move_to(x, y - outer); for _i in 0..5 { ctx.line_to(x + rot.cos() * outer, y + rot.sin() * outer); rot = rot + step; ctx.line_to(x + rot.cos() * inner, y + rot.sin() * inner); rot = rot + step; } ctx.line_to(x, y - outer); ctx.close_path(); ctx.set_line_width(5.0); ctx.set_stroke_style(&JsValue::from("gold")); ctx.stroke(); ctx.set_fill_style(&JsValue::from("yellow")); ctx.fill(); } #[wasm_bindgen(start)] pub fn start() { let document = web_sys::window().unwrap().document().unwrap(); let canvas = document.get_element_by_id("canvas").unwrap(); let canvas: web_sys::HtmlCanvasElement = canvas .dyn_into:: () .map_err(|_| ()) .unwrap(); let context = canvas .get_context("2d") .unwrap() .unwrap() .dyn_into:: () .unwrap(); context.begin_path(); context.move_to(80.0, 130.0); context.line_to(150.0, 70.0); context.line_to(220.0, 130.0); context.close_path(); context.set_fill_style(&JsValue::from("green")); context.fill(); context.begin_path(); context.move_to(60.0, 170.0); context.line_to(150.0, 90.0); context.line_to(240.0, 170.0); context.close_path(); context.set_fill_style(&JsValue::from("green")); context.fill(); context.begin_path(); context.move_to(50.0, 210.0); context.line_to(150.0, 130.0); context.line_to(250.0, 210.0); context.close_path(); context.set_fill_style(&JsValue::from("green")); context.fill(); context.begin_path(); context.move_to(130.0, 210.0); context.line_to(170.0, 210.0); context.line_to(170.0, 240.0); context.line_to(130.0, 240.0); context.close_path(); context.set_fill_style(&JsValue::from("brown")); context.fill(); context.begin_path(); context.move_to(130.0, 210.0); context.line_to(170.0, 210.0); context.line_to(170.0, 240.0); context.line_to(130.0, 240.0); context.close_path(); context.set_fill_style(&JsValue::from("brown")); context.fill(); context.begin_path(); context.move_to(130.0, 210.0); context.line_to(170.0, 210.0); context.line_to(170.0, 240.0); context.line_to(130.0, 240.0); context.close_path(); context.set_fill_style(&JsValue::from("brown")); context.fill(); write_some_object(&context, 150.0, 80.0); } 今までcanvasは利用したことがなかったのですが、位置を移動する、線を引く、線を閉じる、など面白いAPIですね。 JavaScriptでも書けるコードではありますが、ぜひ上のコードをwasm-bindgenを利用して動かしてみて、どんな絵が表示されるか確認してみてください。 さいごに wasm-bindgenを利用することで比較的簡単にRustのコードをブラウザ上でWebAssemblyとして動作させることができます。 今ある別言語のソースコードからWebAssemblyにコンパイルして、Webで再利用できるようになります。 また、今までWebで使えなかったソースコードをWebで再活用することができます。これからさらにエコシステムも発展していき、WebAssemblyを利用しやすい環境が整ってくるでしょう。 フォルシアではWebAssemblyをプロダクション環境で採用した事例はまだありませんが、高速で快適なWebを目指して、パフォーマンスの問題が起きたときなどに取れる強力な選択肢の一つとしてWebAssemblyの利用も検討していければと考えています。 FORCIAアドベントカレンダー2019は本日で終了となります。皆さん、メリークリスマス&よいお年を!
アバター
FORCIA アドベントカレンダー2019  24日目の記事です。 こんにちは。24日目のアドベントカレンダー記事を書かせて頂きます、20卒エンジニア採用内定者の照沼です。 この記事が公開されているであろう12/24のクリスマスイブですが、皆様いかがお過ごしでしょうか。 私はおそらく精神と時の部屋にて絶賛修士論文の執筆最中だと思います。・・・早く卒業したい! さて、技術記事が中心のこの場で何を書こうか非常に迷ったのですが、フォルシアにとって初の試みであったというサマーインターンを経由して内定を頂いた視点から、私がどのようなモチベーションでこの会社に決めたのかを、自分の備忘録兼これから就活する学生の方向けに記していければよいなと思います。 フォルシアを知ったきっかけ きっかけは2018年のサマーインターンが始まりでした。 卒論を契機にプログラミングに触れ始めたものの、主に自然言語処理・データ整形・基礎分析のそれぞれ初等レベルのスキルセットしか持ち合わせていなかった私は、学外に出てエンジニアとしてインターンシップに参加することを恐れながらも、「日給2万×5日間」の広告に目を奪われ即決でエントリーしました。 学生はとにかくお金が無い・・・。 体験記などの詳細は私の同期がブログ記事をあげているのでこちらをご覧下さい。 FORCIA Summer Internship 2018 参加しました - NoiminのNoise FORCIA Summer Internship 2018 参加記 - てんぷらのぷらはC++のぷら 個人的な感想としては、社内の膨大な顧客データに触れるなど、なかなか学内では経験できない量と質のデータを扱うことができ、とても貴重な体験をすることができた思います。また、作業過程は結構ハードであったものの、普段ゆったりとしたペースで研究している私にとっては現場でのスピード感を意識して作業できたことはとても新鮮でした。 さらにご縁があってこうして今につながっているので、人生わからないものだなあと思います。 内定承諾までの軌跡 その後早期選考に呼んで頂き、ありがたいことに内定を頂くことになりました。しかし、承諾するまでにはいろいろな葛藤がありました。おそらく承諾をするまでの間、これほどまでに自分と向き合った時間はないと思います。 1. 正解がわからない どのような企業からであっても、早めに内定を頂けるのはとても幸せな状況だと思います。 なぜならばその企業を軸に、自分にはどういう環境が適しているのかが消去法的にわかるからです。しかし私は極度の心配性なので、当時は正直漠然と、大手企業・メガベンチャー等を志向していました。 ・・・いわゆる、入ってしまえば安定かもってやつです。 しかし、その後いくつかの企業から内定を頂いたものの、しばらくの間は承諾に踏み切ることができませんでした。理由としては上記の通り、自分がどういう環境に適しているかがわからずになんとなくの選り好みで考えてしまっていたからです。 2. 何で食べていくのかをある程度絞り込まないといけない そこでまず考えるべきは、自分が今後どういうスキルで食べていきたいのかをある程度はっきりさせておくことだと思います。 正直ここは一番考えるのがしんどいフェーズですが、ここでしっかり自分と向き合えるかが鍵になってきます。 いろいろなロールモデルを人に尋ねたりインターネットで探しながら、自分はどういう職種で食べていきたいのかを考えてみてください。ここでいうスキルはエンジニアでなくてもマーケティングであったり人事であったり様々ですし、一つに絞り込む必要はないと思います。 私の場合は自分の専門分野からの発展性を考えて、データ分析・データベース設計・アプリ設計の3つに絞りました。 判断軸としては、極度の心配性であるが故、普遍的に必要とされることに重点を置きました。これを自分に置き換えて、「自力でITサービスを設計できるような人になる」としました。 極論、会社という後ろ盾がなくても自力で価値を発揮できる人間になろうという考えのものです。ここで、本当の意味での心配性・安定志向の自分が強く表れたと思います。 3. 自分がどういう環境で活きるのかを考える 次に考えるべきなのは、仮に先述のスキルを磨くことができる環境が見つかったとして、果たして本当に自分がそこで目論見通り成長していけるのか、についてです。 いわゆる社風との相性というものなのですが、いまいちぼんやりしていて学生には現実味のないものに聞こえますよね。 なので、ここで必要になってくるのは、自分が何に起因して成長しているのかを過去の経験に基づいてきちんと考えることです。 私の場合は、一番楽しかったサークル活動での経験や、反対に一番辛かった研究室生活の経験を中心にエッセンスを抽出して考えました。 ポジティブな状態になれるのはどういうときか、逆に気持ち的にはネガティブだけどなんとか頑張れたあのときはどういう状態だったのかをきちんと内的状況・外的状況共に言語化しておくことで、初めて社風の話が現実味のあるものになるのではないかと思います。 ここでいよいよ社風についての情報をインプットするフェーズに入ります。 これについてはひたすら自分が興味関心のある企業の社員の方に会うしかありません。人間関係・評価基準・「ぶっちゃけた話、ここがまだまだだよ」な話、など失礼を承知でガンガン質問しましょう(ただし、お会いする企業・社員の方に対するリスペクトは忘れずに!)。 いろいろな方と会う過程でどんどん比較を重ねていくことによって、自然とこういう風な人になりたい、というロールモデルができ上がってくると思います。ある種これは今の自分と社風をつなぐ自分の理想像だと思います。 私自身、本当に迷惑なのではないかというくらいに、いろいろな企業の方に何回も会いに行ってお話をしました。ただそのおかげで、求めるスキルセット・自分に向いていると思われる職場環境・なりたい人物像を見つけることができたと思っています。 就活中にお時間を割いて頂いた皆様にはこの場を借りて厚く御礼を申し上げます。本当にありがとうございました。 結論 こうして書き連ねると1〜3はとても首尾良く進んだように見えますが、実際は人に会いながらグルグルと何回も行ったり来たりを繰り返しました。 この泥臭さが大事です。頑張ってください。 フォルシアを選んだ理由 結論を書くと、内定承諾理由としてはここまで私が述べた、自分の求める条件がここフォルシアによく当てはまっていたからというとてもシンプルなものです。 具体的には、まずエンジニアリングに関する点が挙げられます。データ分析を勉強してきた身としては分析だけでなく、データの前処理の技術やデータを取得する技術にも興味があります。 その点フォルシアでは、検索プラットフォームSpookを基盤にして他にはない幅広い種類のデータを扱っているため、そこから同上の技術を磨くことが可能で、なおかつその結果をフロントエンドに反映させられる点がよいなと思いました。 他には、社風に関する点として、自分が見てきた同規模の企業ではあまり感じることができなかった温かさや、実直に努力している人を大切にする職場の雰囲気の良さ。 また、自分の理想とする人物像に関する点として、目先にとらわれず本質的に課題に取り組むことで全体的な解決を目指す聡明な方々に溢れている、ということが挙げられます。 五感をフル活用して自分の居場所を探す 上述した3つの理由(エンジニアリング・社風・人物像)が、私がフォルシアの内定を承諾した主な理由です。 このように一つひとつの要因を精査して自分の行きたい企業を選んでください。しかし、しっかりと精査して企業を絞り切った上でまだ迷うようであれば、残る考案材料として、私は「ご自身の直感」がよいと思います。直感は自分がなんとなく感じているけれど言語化し切れていない要因の塊から形成されるものなので、そこから納得のいく選択への手助けができるのではないかと思います。 直感を形成するものには、音楽の趣味が合う人が多い・話しやすい人がいる・技術的に優れた人がいる等、様々な要因があると思います。 正直、私自身まだ言語化し切れていないけれどもとりあえず何かがよいな、と思ってしまっている部分も多少はあります。 ですが、大事なのは自分の新しい居場所を探す感覚です。例えるなら最初は嫌だった学校だけど、卒業するときには名残惜しくなるくらい自分にとって愛おしい組織になっているあの感じです。 五感をフル活用して見定めてください、きっと自分にぴったりの組織が見つかるはずです。 承諾してから現在、結び フォルシアの先輩社員方には、承諾する前はもちろんのこと、承諾した後も密に関わってくださり、変わらずよくして頂いています。 また、現在は長期インターンとして、担当ウェブサイトのアクセスログを解析することにより同サイトのUX/UI改善していくというプロジェクトに参加しており、微力ながら少しずつ会社の技術に触れさせて頂いている段階です。 最後になりましたが、就活には100%完璧な答えはありません。大切なのはいかに自分が選んだ道を正解(自分の理想の人生)に近づけられるかで、そのための最も効率の良い戦い場所を探すことが就活、ひいては人生のゴールだと私は思っています。 ・・・とまあ非常に長くなってしまいましたがまだまだ書き切れないことはたくさんあるので、もし何か聞きたいことがある人は直接お話ししましょう(笑)! 私はもともと就活に対してあまりやる気はない方で、どうすればよいかわからず途方に暮れていた人間でしたが、自分の人生の舵を自分で切るために努力するのだという考え方をもってから、自然と足が動くようになりました。 この記事を読んでくださった方の人生が少しでも今より良い方に向くことを心から願っております。メリークリスマス! 募集要項:新卒の方は こちら      キャリアの方は こちら エントリーをご希望の方: 採用応募フォーム 採用に関するご質問・面談をご希望の方: 採用お問い合わせフォーム
アバター
この記事はCompetitive Programming (1) Advent Calendar 2019 23日目の記事です。 旅行プラットフォーム事業部の大沢です。 競技プログラミングを2年前に始めて以来、週末のAtCoderコンテストにはほとんど欠かさず出ています。 私は昨年末に青色コーダーになり、実力をどうにかキープしています。まだ時間はかかってでも強くなりたい気持ちがあります。 この記事の気持ち 二分探索についての教材は世の中に多くあり、良質な記事も多い反面、「半開区間」などの考え方が難しく混乱するという意見も耳にしています。また、実際に書いてみると意外とバグりやすいことで
アバター
この記事は Competitive Programming (1) Advent Calendar 2019 23日目の記事です。 旅行プラットフォーム事業部の大沢です。 競技プログラミングを2年前に始めて以来、週末のAtCoderコンテストにはほとんど欠かさず出ています。 私は昨年末に青色コーダーになり、実力をどうにかキープしています。まだ時間はかかってでも強くなりたい気持ちがあります。 この記事の気持ち 二分探索についての教材は世の中に多くあり、良質な記事も多い反面、「半開区間」などの考え方が難しく混乱するという意見も耳にしています。また、実際に書いてみると意外とバグりやすいことでも有名で、私もよくハマってしまうことがありました。「半開区間」という言葉を使わず、私なりにわかりやすいと思う理解と、バグりにくい書き方を記事にしてみました。 メインターゲットの読者は、以下のいずれかを想定しています。 二分探索って何だろう?という方 二分探索の概要をなんとなく知っている方 二分探索を学んだことがあるが、理解がちょっと怪しい方 二分探索を実際に書いたことがあるけれど、よく細部をバグらせてしまう方 二分探索をあらためて直感的に理解したい方 特に、細部をバグらせないような、直感的でおすすめな理解の仕方を紹介したいと思います。 二分探索とは 二分探索は一言で言うと「 境目を見つける 」アルゴリズムです。 探索範囲の1か所に境目があって、「 境目の左側が全てある条件を満たし、右側が全てその条件を満たさない 」ことがわかっているときに、その境目を高速に見つけることができます。 もちろん条件を満たす満たさないは 左右逆でも使えます 。 境目というのは何でもよいです。読みかけの本のここまで読んだ/読んでないのページの境目とか、背の順に並んだ児童の中で身長が100cm未満/以上の境目とか、納期に間に合う/間に合わないのタスク量の境目とか、 全社員を満足させるために足りる/足りないのピノの箱数の境目 、とか・・・。 とにかく「 1か所の境目 」の両側で判定結果が二分されていることが重要です。判定結果が「1か所の境目」で二分されない条件では基本使えないと思ってください(使えないことは無いですがこの記事では扱いません)。 実装は後ほど解説しますが、まずは長さ10の配列を使って、二分探索の動き方を見ていきましょう! 以下の例では左側が条件を満たす側だとします(逆の場合は後述します)。 図1 図1で、黒い枠線は要素数10の配列だとします。上の緑文字がindexです。 青い領域は条件を満たすことが確定した領域、赤い領域は条件を満たさないことが確定した領域です。 ok , ng の2つの変数を用意し、探索範囲の外になるような値を設定する ※1 ok=-1 , ng=10 と置く ok と ng の平均を求める。 4 となる is_ok(4) == True となり条件を満たすので ok = 4 とする ※2 ok と ng の平均を求める。 7 となる is_ok(7) == False となり条件を満たさないので ng = 7 とする ok と ng の平均を求める。 5 となる is_ok(5) == True となり条件を満たすので ok = 5 とする ok と ng の平均を求める。 6 となる is_ok(6) == False となり条件を満たさないので ng = 6 とする ok と ng の差が1になったので処理を終了する ※1 ok,ngの初期値について 探索範囲の思いっきり外側でもよいし、探索範囲の内側でもよい。 大事なのは ok は確実に条件を満たすゾーン、 ng は確実に条件を満たさないゾーンに含まれていること。これが間違っていると正常に動作しません。 ※2 判定関数 is_ok(i) はindexが i のときに条件を満たすなら True 、そうでなければ False を返します。配列外の i が引数で来たときにも、満たす側なら True 、満たさない側なら False と返すものとします。 今回の例では単純に、 def is_ok(i): return i のような実装がされていると思ってください(ここでは配列の中身すら無視されていますが・・・)。 理解のポイントとしては、 ok は常に条件を満たすことが確定したゾーンの一番右側にいる ng は常に条件を満たさないことが確定したゾーンの一番左側にいる 最終的に ok と ng は密着する(差が1になる)ことがわかれば完璧です! この挙動をするコードをPythonで書くと次のようになります。 ok = -1 ng = 10 while ng-ok > 1: mid = (ok+ng) // 2 # 平均(小数切り捨て) if is_ok(mid): ok = mid else: ng = mid print(ok,ng) # "5 6" が出力される それでは、5で最終結果として得られた ok , ng の値は何を示すでしょうか? ok・・・条件を満たすなかで最大のindex ng・・・条件を満たさないなかで最小のindex この理解でほとんど問題ありません(注意すべき点は後述します)。 実際 ok として得られた 5 は、 is_ok(i) を満たす i のうち最大の整数です。 左側がngの場合 また、左側がngとして実装した場合、コードは例えばこのようになります。 def is_ok(i): return i > 5 #大きい側がTrue ok,ng = 10,-1 # さっきと逆なので注意 while ok-ng > 1: # さっきと逆なので注意。abs(ok-ng)のように汎用的に書く流派もある mid = (ok+ng) // 2 # 平均(小数切り捨て) if is_ok(mid): ok = mid else: ng = mid print(ok,ng) # "6 5" が出力される そして、 ng・・・条件を満たさないなかで最大のindex ok・・・条件を満たすなかで最小のindex となります。 ここでよく混乱しがちなのが、二分探索で得られた2つのポインタのうち、最終的にどちらを使えばよいのか?という問題です。 大体の場合、 ok を使えばOK ところで、このような問題文をよく見ませんか? 条件を満たすなかで最大の〇〇を求めよ 条件を満たすなかで最小の〇〇を求めよ このような問題文が出てきたときは、二分探索を使えるケースが少なくないです。 そして、 条件を満たすなかで最大の〇〇 → ok を左側として実装し、最終的に ok を使う 条件を満たすなかで最小の〇〇 → ok を右側として実装し、最終的に ok を使う なんと、 ok として得られた値をそのまま使えばよいのです! 「条件を満たす側」「条件を満たさない側」と分けてきたのは、このためです。 一般的な二分探索では2つのポインタを high , low みたいな名前で管理することが多いと思うのですが、 ok , ng とすることで、何を扱っているのかがわかりやすくなり、何かと嬉しいことが多いです。 この ok , ng で管理する方式は私が考えたのではなく、いわゆる「 めぐる式二分探索 」として知られています。 配列外参照には注意 配列の要素すべてが条件を満たさない場合、 ok = -1 となり、 ok が配列外を指します。 同様に、要素すべてが条件を満たす場合、 ng = 10 となり、 ng が配列外を指します。 左を ng とするケースではこれの逆で、 ng = -1 や ok = 10 の状態が生じます。 私がよくやるのは、下のような関数を作って判定します。 def is_ok(i): if i = N: return False return 有効なiに対する判定 引数の i が配列外など、有効な範囲にないときの処理を忘れないようにしましょう。 また、向きにも注意で、 ok を返す側の異常値のときに True 、 ng を返す側の異常値のときに False を返してください。 「個数を求める」場合などもちょっと注意 条件を満たすものがいくつあるか? などという問題に対してはちょっとだけ注意が必要です。 結論を書くと、 条件を満たす側が左側なら → ng が答え 条件を満たす側が右側なら → N - ok が答え になります(が、これは覚える必要はありません)。 あくまで ok が指すのはギリギリ条件を満たすボーダーの入力(ここではindex)です。 indexから個数を求める必要があるということは頭の片隅に置いてください(これが頭から抜けていると、二分探索の実装がバグっているのか?と錯覚して焦りがちです)。 なぜこのように求まるかはぜひ考えてみてください。次節のような図を描けば、感覚的にも理解できるかと思います。 最終的なok,ngについて視覚的な理解 ところで、要素数 10 の配列を二分探索した場合、得られる結果は 11 通りあります。 要素数 N なら N+1 通りです。 図2 上の図に書かれた 0 ~ 10 の青い数字は、要素の境目に番号を振ったものです。 このように 0 始まりで番号を振った場合、二分探索の結果のうち、 ok , ng の大きい方に一致します。 たとえば ok=5, ng=6 ならば 6 という境目 ok=3, ng=2 ならば 3 という境目 が求められたことを示します。 これがイメージできていれば、最終的に二分探索によって何が得られているのかが確実に理解できているはずです!図で捉えればもう怖いものはないですね! 二分探索の強力さ 探索範囲が N 要素の場合、 log_2(N) 回程度の比較回数でよいです。 例であげたような 10 要素ぐらいの場合では効果は薄いのですが(むしろ二分探索を使わない方が実装が単純な分よい)、 探索範囲の規模が大きくなるほど効果が強い です! 例えば 100000 (=10^5)要素 → 17回程度 5000兆 (=5*10^15)要素 → 53回程度 の比較回数で求まりかなり強力です! 図1(再掲) 図1を改めて見ていただくと、未確定のゾーン(白い部分)が 1回の処理でおよそ半分 にしぼり込まれていく様子がわかると思います。 先ほど5000兆要素と書きましたが、実際にこれだけの長さの配列がメモリに乗ることは現実にはないと思います。 実は、 探索対象は配列でなくてもよい のです。 is_ok(x) 関数の結果の、 True / False が切り替わる 境目が1か所以下 (=単調性がある)ならば、二分探索が使えます。 探索対象が浮動小数をとる場合 お気づきの方もいるかと思いますが、探索対象が配列でなくてもよいということは、 is_ok(x) の引数 x が 整数以外を取ってもよい ということです。 実際、境目となる浮動小数の値を二分探索で調べたいケースもあります。 while ng-ok > 1: でループさせるような先ほどの実装では期待した動きになりません。 このようなときは、何も考えずに 100 回程度ループするのが定石となっているようです。 (一般的なdouble型の精度より遥かに余裕がある回数なので、お好みで調整してください。) for i in range(100): mid = (ok+ng) / 2 # 平均(浮動小数) if is_ok(mid): ok = mid else: ng = mid このケースでは ok と ng は同じ値に収束してくるので、どちらを使う?のように考える必要はないですね。 bisectモジュールの使えるところと使えないところ ※これはPythonista向けの話題です。 Pythonには、ソートされた配列の中に、ある値が入るべき境界を二分探索で見つけたいとき、標準モジュールに bisect.bisect_left() や bisect.bisect_right() といった関数があります(C++ だと std::lower_bound() や std::upper_bound() に相当する気がします)。 非常に便利ですので、これらについて最後に簡単に紹介します。 まず使用例です。 from bisect import bisect_left, bisect_right arr = [1,3,5,5,5,6,7] # 昇順にソートされている必要がある l = bisect_left(arr, 5) # 5が入るべき境目のうち最も左側の境目を返す r = bisect_right(arr, 5) # 5が入るべき境目のうち最も右側の境目を返す print(l,r) # "2 5" が出力される このように、昇順ソートされた配列の境目を見つけるタイプの問題であれば、 bisect モジュールを呼び出せば自分で実装する必要はありません。私も時々これのお世話になっています。 ちなみにこれの返り値の正体は、 図2で書かれている青い数字 に相当するものが返ってきます。 これがわかっているだけでもライバルに差がつきます! また、配列の中身が数値でなくても bisect は利用できます。文字列やタプルの場合も辞書順比較をしてくれます。Comparableな要素の配列で、正しく昇順ソートされていればよしなにやってくれます。 では逆に使えないケースはどんなときかというと、 探索範囲が配列ではないとき です。 関数に入力される x のうち、条件を満たす/満たさない x のボーダーを見つけたい場合は、自前実装するしかありません。 x について条件を満たすかどうかの判定が 配列の値以外 によるのであればこれに該当します。 (配列でなく関数の結果を探索する場合、二分法と呼ぶのが正しい気がしていますが、競プロの文脈では区別されないことが多いです。この記事でも「二分探索」に統一して呼んでいます。) 降順ソートされた配列に対しては、自前実装で対応してもよいのですが、私は反転した配列に bisect を使い、それの結果を反転することが多いです。 配列の反転には O(N) かかりますが、入力の時点で O(N) かかっているはずなので問題になることはおそらくありません。 まとめ okは条件を満たすことが確定したゾーン、ngは満たさないことが確定したゾーン どちら側が条件を満たすのか、には要注意 okとngの2変数で未確定のゾーンをはさみながら絞り込んでいく 最終的には ok を使おう ただしそのまま使えないケースもあるので、よく見極めて ok と ng の境目を求めているのだということを理解しよう 図でイメージできれば、もう間違えない! 水色になってぜひフォルシアへ! フォルシアでは 2021年度新卒採用 を行っています。 近年 AtCoderJobs からの応募・入社が増えてきています。 強い技術を持ちながらビジネスに活かしたいWebエンジニアの方、水色以上になってAtCoderJobs経由で応募いただくと書類選考が免除されます! 仕様を実装に落とし込むのが早く、計算量の感覚も身についていると、とても素敵です! 競プロerの皆さん、ぜひ一緒に働きましょう!
アバター
FORCIA アドベントカレンダー2019  21日目の記事です。 エンジニアの島本です。私は入浴・朝晩のストレッチなど日常的に体をほぐしているのですが、日々腰痛に悩まされていました。 しかし、骨盤を後傾させて「反り腰」を改善するとよい、という後輩から聞いたアドバイスを実践したところ、腰痛・モモ裏の張り・肩こりのすべてがやわらいでびっくりしています(後輩は整体師から教わったことを共有してくれました)! 体の仕組みから原因を特定し、原因に合わせた対応をすることで改善する。これはアルゴリズムや仕様を理解して、最適な設計・実装をするというプログラマの日常と全く一緒ですね。 さて、オライリーからも ヘルシープログラマ~プログラミングを楽しく続けるための健康Hack~ という本が出ているように、プログラマにとって重要な健康をHackする様々な方法が世の中で紹介されていますが、それらを実践できている人は少ないのではないでしょうか。 しかし、仕事でハイパフォーマンスを維持するのに健康は欠かせませんよね。 私は現在、社内最大の売上を担うプロジェクトのエンジニアリーダーという重要な役割を担う傍ら、週末にはtoC向けのサービス作りをしており、常に高いパフォーマンスを維持できるよう日ごろから健康に気を使っています。今回はそんな私が、様々な文献から仕入れ、手を抜きつつ実践してきた健康Hackをご紹介します。 眠気に打ち勝つ 睡眠の質向上 眠気をなくす最善の方法は「眠くなくなるまで寝る」ですが、これを実践するのはなかなか難しいですよね。 睡眠時間確保の次のアプローチは睡眠の質向上です。寝具にこだわることなども大事ですが、私が実践しているのは「入眠の1時間半前にお風呂に入る」、これだけです。 皮膚体温と深部体温(体の内部の体温)の関係上、このタイミングで入眠することで睡眠の質が向上します。 (参考: スタンフォード式 最高の睡眠 ) これを実践したところ、明らかに寝つきが良くなったと実感できました。元々入浴の習慣があったため導入へのハードルはほぼ0でした。 シャワー派の方は熱いシャワーを浴びた1時間後を目安にベットに入るのがよいそうです。 NO MORE !目覚めのコーヒー 世界一美味しい飲み物は「コーヒー」ですよね。カフェインによる覚醒作用は高いパフォーマンスを発揮するのにも役立つため、毎日何杯も飲みたくなってしまいます。 ただし、コーヒーの覚醒作用に頼ってしまうと人間が本来持っている覚醒力が減少してしまうため、朝のコーヒーは完全に脳が覚醒してから飲むのがよいそうです。 また、夕方以降のコーヒーも睡眠の質を下げるので控えた方がよいです。私も寝起きのコーヒーをやめて、昼食後から夕方にかけて1〜3杯程度飲むようにしたところ、午前中に頭がぼーっとすることが減りました。 胃を酷使しない 健康やダイエットに興味がある方は、ファスティング(断食)という言葉を耳にしたことがあるかと思います。 私は職場の先輩のファスティング体験を聞き興味を持ったのですが、ファスティング中の激しいスポーツは危険ということもあって断念しました(新宿1部リーグのサッカーチームに所属しており週1,2回プレーしています)。 ファスティングの目的は、「人間の体の真の機能を取り戻す」ことです。 胃腸は約7~8時間でものを消化するので、もし3食のスパンが8時間より短ければ、胃腸は不眠不休ということになります。 胃を休ませることによって体の機能を取り戻し、感覚を鋭敏にすることがファスティングの目的なので、要は胃を休めればよいということです。 私は、 朝食を抜く 夜は固形物を減らしスムージーやプロテインでお腹を満たす という生活に変えたところ、体が軽くなるのを実感できました。また便通も改善しました。 しかし、朝食を抜くと空腹との戦いが始まります。空腹を紛らわすには水を飲み塩をなめるのがよいそうです。 それでも空腹に勝てないときは、我慢せずに早めに昼食をとるなりナッツを食べるなりしています。 完璧を目指してストレスを感じるより、ズボラにでも続けることを優先しています。 炭水化物の支配から抜け出そう 現代人は炭水化物に支配されていると言っても過言でありません。コンビニやお店のどこでも食べられるし美味しいので、食事の中心が炭水化物になっている人がほとんどかと思います。 ですが、炭水化物の摂取しすぎは体に悪影響です。農業が誕生したことにより、人類は大量のデンプンの摂取が可能になり(現代人の主要栄養素は米、小麦、トウモロコシ、じゃがいも)、糖になるこれらデンプンの大量摂取は現代のあらゆる病気の原因となっています。 (参考: GO WILD 野生の体を取り戻せ! 科学が教えるトレイルラン、低炭水化物食、マインドフルネス ) ただし、炭水化物そのものが悪いのではなく、炭水化物の取りすぎや多様性のない食事が問題なのです。 そのため、小腹が空いたときはパンやおにぎりを食べるのでなくナッツを食べるようにするなど、炭水化物を惰性で食べないようにするのが重要です。 私は、 朝食:抜き 昼食:好きなもの 夕食:炭水化物少なめ+スムージーやプロテイン というくらいズボラに実践し、サッカーがあるときには炭水化物中心の食事でカーボローディングしています。 食べる楽しみを忘れないのも大切です。 戦略をもって散歩しよう 健康に大切なのは睡眠と運動ですよね。私は日常的に運動する時間を作るために、通勤時間に散歩を組み込むようにしています。 日常に取り入れやすい「1駅手前で降りて歩く」方法は非常にシンプルであると理解しつつも、朝の時間がもったいなどの理由で実践に移せない人も多いのではないでしょうか。そこで、散歩の間の時間をもったいと思わないような戦略を持ち込むのがオススメです。 私はこちらを実践しています。 思考するかネタを用意しておく 歩きながら本を読む(聞く) Appleの共同創業者、故スティーブ・ジョブズは何か重要な話をするときや考えをまとめる際にはとにかく公園や道路など、あちこちをよく散歩していたというエピソードがあります。このように、なかなか考えがまとまらないときに、ふらっと外を歩いていると良いアイデアが突然浮かんでくることがあるのは万国共通の体験ではないでしょうか。 朝の散歩の前に思考すべきネタを用意することで、散歩しながら仕事を進めることができます。 また、 Audible などのオーディオブックは、非常に便利なサービスですが利用するにはお金がかかります。しかし、スマホのKindleの読み上げ機能を使うことで無料でオーディオブック化することができます。 1駅歩く分少し早く起きるだけで、通勤時間が散歩と思考と読書の時間に変わり、人生がより豊かになるのでぜひお試し下さい。 Don't think! train. トレーニングは思考停止状態で始めよう 日々の散歩だけでなく強度のあるトレーニングもしたいですよね。ただ、ジムに通ったり家の周辺を走ったりするのはなかなか続かないのではないでしょうか。 そこでおすすめなのが Nike Training Club です。 パーソナルトレーニングアプリで、メニューは自宅で5分程度でできる簡単ものから、屋外やジムで器具を使って行う本格的なものまで幅広く用意されています。 何よりよいのが音声サポートです。「それでは始めます」と、どんどんメニューをこなさなければいけない厳しさと「もう少しです。頑張りましょう!」と励ましてくれる優しさがあります。 一人で黙々とトレーニングを続けるには意思が必要ですが、自宅で言われるがままに体を動かすだけでよいので「スタートボタンを押す」ことさえできれば思考停止状態で続けることができます。 番外編 〜オフィスで飲む1杯のコーヒーにこだわろう〜 最近はコンビニで美味しくてコスパの良いコーヒーが飲めますが、さすがに毎日だと飽きてしまいますよね。 オフィスで自分好みの豆で淹れた美味しいコーヒーが飲みたいと思っている人は多いのではないでしょうか? 私もその一人であり、今は家で挽いた豆を kintoのカフェプレスマグ を使ってフレンチプレスで飲んでいます。 これまでにオフィスでドリップすることや、家で淹れたコーヒーを魔法瓶に入れ持参するという方法を試してきましたが、最終的には淹れたてのコーヒーの香りが楽しめることと手軽さのバランスが最もよいこの方法に落ち着きました。 私がこれまで試した中で、良かった道具を紹介します。 一人用コーヒーメーカー ハンドドリップよりもお手軽で、 コンパクトで広いスペースを必要としません メタルのフィルターのためペーパーフィルターの購入が不要であり、オイルがカットされずコーヒー本来の持つ味と香りが楽しめます ナポリ式コーヒー コンロにセットするだけで蒸らしから抽出までできるため、お手軽かつ本格的なコーヒーが飲めます タイガーの夢重力 コンパクトでびっくりするくらい軽いです しっかり保温もできるため家で淹れたコーヒーをオフィスに持参するのに最適です 個人的にはハンドドリップコーヒーが一番好きで、家でドリップしたコーヒーをオフィスに持参していた時期もありましたが、朝の10分を節約したいという気持ちや淹れたてのコーヒーをその場で飲めない切なさから、オフィスで淹れるようになりました。 美味しいコーヒーをオフィスで飲みたいと思っている方の参考になれば幸いです。 さいごに 以上、私がズボラに実践してきた健康Hackを紹介しました。健康に気を付けて、ハイパフォーマンスで仕事していきましょう!
アバター
FORCIAアドベントカレンダー2019  20日目の記事です。 こんにちは。アドベントカレンダー20日目を担当します、旅行プラットフォーム事業部の高橋です。19新卒として今年度フォルシアに入社し、現在はwebアプリの保守・運用・開発を行っています。 また、今年の10月に発足した 技術広報チーム にもjoinし、このアドベントカレンダーの企画と運営もしています。 さて、技術広報としてフォルシアの技術をどの切り口からアピールしていこうかと思案している折、このような記事を見かけました。 Slackはただのコミュニケーションツールじゃない、企業の技術を映す鏡だ ふむふむなるほど、確かにフォルシアでもSlackはもはや欠かせないツールとなっていますし、その機能の拡張性はエンジニアの心をくすぐるものがあります。 この記事を読みながら、 弊社のSlack活用も悪くない線をいっているのでは・・・? と手前味噌ながらに思いましたので、私からフォルシアのSlack活用術についてご紹介します。 活用のまとめ 体系的なチャンネル名 外部サービスとの連携 社内の交流を加速させる仕組み チャンネルのプレフィックス フォルシアのSlackの社内チャンネルには数百もの数があり、私が参加しているチャンネルも相当の数があります。 フォルシアではチャンネル名の前に次のようなプレフィックスを付けることで、チャンネルを分類・整理しています。 プレフィックス 対象 例 00 全社共通 00_all 01 組織 01_sales 02 顧客 02_forcia 03 社内プロジェクト 03_appring 10 情報共有 10_forcia_cube 11 サークル 11_tennis xx その他 xx_fresh2019 zzz 個人 zzz_takahashi フォルシアにSlackが導入されたのは2016/01頃から2016/03にかけてです。この期間に社内のIT管理部門を中心にプレフィクスの整理をし、Slackの使用感に応じて、柔軟に拡張・整備がされていきました。 このルールにのっとれば、社員それぞれがチャンネルを新しく作成することができます。 プレフィックスのルールを決めておくことで検索がしやすい、並び順が整然として見やすい、などのメリットがあります。 この中で特徴的なのは、 zzz から始まる個人チャンネル(通称 「ずずず」チャンネル )でしょうか。社員個人の考えていること、最近あったこと、おいしかったランチのお店など、個性あふれるつぶやきがされます。 また新人研修の際には、新入社員の個人チャンネルに先輩社員がjoinして、新入社員が作業の様子などをそのチャンネルにつぶやくことで 進捗の管理に利用 されることもあります。入社間もない頃は、多くの社員が参加しているチャンネルだとなんとなく発言するのに緊張してしまいますが、 zzz は自分のためのチャンネルなので 比較的低いハードルで発言できる のがよいですね。 システム監視 フォルシアは旅行業界を中心として、様々な企業のwebアプリケーションを開発・運用しています。 それらのシステムが問題なく稼働できるよう、細心の注意を払って運用を行ってはいるものの、時として種々の要因によりシステムに問題が生じる場合があります。 システムの状況監視は社内サーバーから常時行っていますが、システムに問題が生じた場合の 通知手段の一つとしてSlackが用いられます 。 システム監視関連の通知は #00_system_report というチャンネルに通知されます。 システムに異常を検知した場合は、該当のサーバー、対応手順書のリンク、エラーの内容などがまとめられた通知がされ、対応者はこの投稿に対してスレッド形式で現在の状況をコメントしていく運用がなされています。こうすることで対応者以外も現在の状況を知ることができ、 連携や協力が行いやすく なっています。 さらに対応が完了した際は、このスレッドに「 対応完了 」と書き込むことで、障害報告レポートが自動的に起票されるようにもなっています。 外部サービスとの連携 Slackの大きな魅力として、 APPの導入による機能の拡張 や、 豊富なAPIを活用した外部サービスとの連携 があるかと思います。 フォルシアでもいくつかのサービスと連携して、開発が円滑になるような仕組みが整備されています。 GitLabとの連携 こちらの記事でも紹介した通り、フォルシアではソースコードの管理に GitLab を利用しています。 GitLab CI/CD 導入の手引き GitLabのリポジトリはチーム単位、プロジェクト単位などで作られますが、リポジトリに新たに動きがある(push、コメント、マージリクエストの提出など)と、対応する #_チーム名_dev などと名付けられたSlackチャンネルに通知が送られるような運用が行われています。いちいちGitLabのサイトに行かなくてもSlack上で確認できるので、開発やレビューの手間を削減することができています。 そのほかにも、現在提出されているマージリクエストを 毎朝Slackに投稿してくれるbot なども社内で自作されました。 私が所属するチームではこのbotの導入により、気づかれずにレビューされないまま放置されるマージリクエストを減らすことができました。 GitLab CIによるテストの成功/失敗も、もちろんSlackに通知されます。 esaとの連携 フォルシアでは社内の文書を共有するツールとして、 esa というサービスを利用しています。 esaに新しく投稿があると、 # xx_doc チャンネルに記事のタイトルや内容の一部が通知されます。このチャンネルを流し見しておくことで、他のチームの状況だったり誰かの投稿した技術記事などを見逃すことなく知ることができます。 社内交流の促進 ここまで紹介してきたものは業務改善、開発速度の向上などに役立つ機能の話でしたが、次に Slackを用いた社内交流の促進 という観点でいくつか紹介したいと思います。 シャッフルランチ 今年から始まり、社内では毎月の恒例行事として定着してきたシャッフルランチ。この開催にもSlackが用いられています。詳しくは以下の記事をご覧ください。 普段関わりのない社員同士をマッチング シャッフルランチはじめました~企画編~ GoogleカレンダーとSlackからの情報で「グループ分け」 シャッフルランチはじめました~テクノロジー編~ シャッフルランチのコンセプトは、「 普段関わりの薄い人とランチに行く 」ことです。ではどのようにして関わりの薄さを判定するのか、そこで目を付けたのがSlackのチャンネルです。 プレフィックスの説明でも述べたように、Slackのチャンネルは様々な単位で作成されます。AさんとBさんが 共通して参加しているチャンネルが多ければ 、その二人は 関わりが強く 、反対に 共通のチャンネルが少なければ関わりが薄い と考えられるだろう、と仮定してシャッフルランチbotの実装が行われました。 私も毎回参加していますが、確かに業務で関わりの薄い社員と同じグループになることが多く、よくできた仕組みだなと驚いています。 最近では強化学習によりさらにアルゴリズムが強化されたようなので、興味がある方はぜひこちらもご覧ください。 AIで解く最適化問題 ~今日から使える深層強化学習~ イケメンスタンプ #zzz_ikemen チャンネルは、 イケメン というスタンプが押された投稿が共有されるチャンネルです。 このチャンネルに参加していると、自分が参加していないチャンネルでの「イケメン」な投稿も知ることができます(例:新しく案件受注しました!、この日のオンコール当番代わりますよー、など)。 さらに毎月一回、その月のイケメンスタンプを押された数が集計され、 獲得数が上位の人には表彰 が行われます。 こちらの仕組みには「 ホメルくん 」を使わせていただいています。 スタンプ一つで気軽に称えあえる、良い文化だなと思います。 イケメンスタンプ以外にも、用途に合わせて数多くのスタンプが自作され、現在ではスタンプの総数が 1483種類 にもなりました! 複雑な感情もスタンプを使うと表現できることが多いので、他のSNSなどを利用している際に「今Slackのあのスタンプが使えたら・・・!」ともどかしくなることが結構あります(笑)。 さいごに フォルシアのSlack活用術はいかがでしたでしょうか。 「こんな機能が欲しい!」と思った時、発想力と実装力次第で機能を拡張できるのがSlackの良いところかと思います。 開発環境の向上、社内の交流の促進など、Slackは大きな役割を担う存在となっているので、今後も継続して改善していきたいと思います。
アバター
これは、 AWS Advent Calendar 2019 の19日目の記事です。 旅行プラットフォーム事業部 WEBエンジニアの西山です。 フォルシアでは少数精鋭でプロジェクトに取り組むことが多く、フルスタックエンジニアとしてフロントからサーバサイドまではもちろん、手を挙げればインフラチームと協力してインフラ関連の業務に当たることができます。 また、昨今では社内外のプロジェクトでAWSを始めとしたクラウドサービスを利用することが増えてきました。 この記事では、WEBエンジニアの私がスキマ時間を利用した AWS認定ソリューションアーキテクト-アソシエイト の勉強法、および学習過程で感じたことを紹介します。 始める前の状態 インフラ関連知識 サーバ構築自動化や、アプリケーションデプロイパイプライン作成などの業務経験あり AWS関連: EC2, S3など基本的なサービスに触れた経験はあり 勉強方法 1. サンプル問題に取り組み、学習を始める前の自分の理解度を知る AWS公式サンプル問題 に触れ、実際にどのような問題が出題されるのかの感覚と、学習開始前の自分の理解度を測りました。 2. 学習 スキマ時間で学習できること、インプットとアウトプットをバランス良くできること、辞書的に使える読み物で網羅的に把握することを念頭に下記を利用しました。 Udemy | これだけでOK! AWS 認定ソリューションアーキテクト - アソシエイト試験突破講座(初心者向け21時間完全コース) 多くのブログなどで紹介されていますが、とてもおすすめです AWSハンズオンメインの動画です。スマホアプリでは動画をダウンロードしてオフライン環境での再生も可能なので通勤や散歩、寝る前などスキマ時間で学習できます セールで購入すればかなり安価で購入することができます 講義、ハンズオン、小テスト、模擬テストとバランス良くコンテンツが用意されており、ハンズオン以外はすべてスマホで完結できます 書籍 動画で学んだことを書き込むノート代わりとして利用しました 基本的には上記動画で一通り押さえているので、ざっと流し読みする程度でも内容は入ってきます 書店で中身を確認して自分に合いそうなものをえらびました 3. AWS公式模擬試験 有料ですが、本試験と同じ形式なので、受験前に慣れておくために受験しました。 感じたこと 関連サービスに触れるなかでソフトウェアを学び直す機会になった AWSには多くのソフトウェアがサービスとして組み込まれています。 そのため資格勉強を通して、ソフトウェアについて知識の整理ができました。 例えば下記などが挙げられます。 ECS, EKSを通してdockerやk8sなどのコンテナ周りの基本的な知識や、コンテナオーケストレーションツールの利点を把握 RDS, DynamoDBなどを通してデータベースの種類ごとの特徴、利点の把握 他にも、AWSと直接の関係はないもののBatchを通してLinuxが用意しているcrontabの仕組みや、S3のバージョン管理からgitのバージョン管理の仕組みを学び直す事ができました。 顧客サービスをシステム構成観点でより深く考えられるようになった 今回学習したソリューションアーキテクトアソシエイトでは 顧客の要件に基づき、アーキテクチャ設計原則に沿ってソリューションを定義できること が求められます。 はじめはインフラ/ネットワーク設計に関する知識のみが求められるのかと思っていましたが、実際には AWSが提唱するアーキテクトのベストプラクティス を下記の観点で学ぶものでした。 運用上の優秀性 セキュリティ 信頼性 パフォーマンス効率 コスト最適化 例えば、運用上の優秀性においては、保守・運用フェイズでの登場人物を想定し、各登場人物に適切な権限を付与することでスムーズな運用を実現するにはどうしたらよいかを検討します。 通常、WEBエンジニアはどのようにサービスに機能を追加していくかにフォーカスを当てて考える事が多いですが、AWS学習を通して上記の観点を学ぶ上で、サービス開発を一歩引いた広い観点で捉えることができ、よりお客様のサービスに寄り添えるエンジニアになれると感じました。 さいごに 社内インフラエンジニアに聞いたところ、数年前までは学習コンテンツも充実しておらず、AWSの公式ドキュメントをひたすら読み込んで資格対策をしていたそうです。 今は学習教材も充実しており、かなり少ないコストで学習および資格取得ができるようになっていると実感しました。 インフラエンジニアではないけどAWSの資格とってみようかな、と思っている方の背中を少しでも押すことができたら幸いです!
アバター
FORCIAアドベントカレンダー2019 18日目の記事です。 こんにちは。アドベントカレンダー18日目の記事を担当させて頂きます、エンジニアの澤田です。 普段の業務ではJavaScript やPython などでプログラムを書くことが多いですが、今回はあえて、普段使用していない関数型プログラミング言語Haskell に触れてみつつ、以前から興味があったメタプログラミングを実際にやってみようと思います。 Haskell にはメタプログラミングを行うためのTemplate Haskellという言語拡張があり、これを使えば簡単にメタプログラミングができるのではないか?という期待を胸に手を出し
アバター
FORCIAアドベントカレンダー2019  18日目の記事です。 こんにちは。アドベントカレンダー18日目の記事を担当させて頂きます、エンジニアの澤田です。 普段の業務ではJavaScript やPython などでプログラムを書くことが多いですが、今回はあえて、普段使用していない関数型プログラミング言語Haskell に触れてみつつ、以前から興味があったメタプログラミングを実際にやってみようと思います。 Haskell にはメタプログラミングを行うための Template Haskell という言語拡張があり、これを使えば簡単にメタプログラミングができるのではないか?という期待を胸に手を出してみました。 では、早速始めていきましょう! なお、GHC はバージョン8.0.2を、Template Haskell はバージョン 2.11.1.0を使用しています。 メタプログラミングとは メタプログラミングとは、プログラムを生成するプログラムを書くことで、マクロやテンプレートメタプログラミングによって行われることが多いようです。 このプログラム生成は、プログラムが処理されるどの過程で行われるのでしょうか?少し見てみましょう。 プログラムのコードは言語処理系によって、基本的に以下の段階を経て実行プログラムに変換される(コンパイル型)か直接実行されます(インタプリタ型)。 字句解析: 文字列を字句(トークン)列に変換する 構文解析: 字句列の文法を解析して抽象構文木(AST: Abstract Syntax Tree)に変換する 意味解析: 抽象構文木に対して意味的な解析を行い中間コードに変換する 最適化: 中間コードを計算量・メモリ使用量などの観点から効率化する コード生成: オブジェクトプログラム(アセンブリ言語、機械語)を生成する 上記のどの段階に対してメタプログラミングを行えるかは、言語ごとの拡張機能によって異なっていて、C のプリプロセッサマクロは「字句解析」の段階で変換され、Lisp マクロは「構文解析」で、D テンプレートは「意味解析」で変換されます。 Template Haskell は、上記の「構文解析」で変換されるマクロで、抽象構文木を組み替えたり合成したりすることができます。 抽象構文木を直接操作できるなんてワクワクしませんか? では、やっていきましょう! Template Haskell で抽象構文木を眺めてみる Template Haskell には、クォート式(Quotation)という特別な括弧で囲われた式などを、抽象構文木に変換して出力する機能があります。 この式の抽象構文木はどうなっているのかな?と思ったら簡単に確認できるわけです。すごいですね。 見てみましょう。 まず、 -XTemplateHaskell オプションを付けて ghci を起動します。 そして Language.Haskell.TH モジュールを読み込みます。 $ ghci -XTemplateHaskell Prelude> :module + Language.Haskell.TH Prelude Language.Haskell.TH> 1 + 2 の抽象構文木を見てみます。 Prelude Language.Haskell.TH> runQ [e| 1 + 2 |] InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) (Just (LitE (IntegerL 2))) おお、出ましたね!図にするとこんな感じです。 変数への束縛の場合はどうなるでしょうか。 Prelude Language.Haskell.TH> :{ Prelude Language.Haskell.TH| runQ [d| Prelude Language.Haskell.TH| x = 1 Prelude Language.Haskell.TH| y = 2 Prelude Language.Haskell.TH| |] Prelude Language.Haskell.TH| :} [ValD (VarP x_1) (NormalB (LitE (IntegerL 1))) [],ValD (VarP y_0) (NormalB (LitE (IntegerL 2))) []] こちらも図にしてみます。 面白いですね! 今度はこの抽象構文木を元の式などに戻してみましょう。 抽象構文木を元の式などに戻してみる 抽象構文木を元の式などに戻すには、基本的にppr関数に抽象構文木を渡すだけでOKですが、 GHC.Num.+ などは、そのままでは名前として解釈してくれないので、先頭にシングルクォートを付けて、 '(GHC.Num.+) などのように書き直す必要があります。 それでは先の 1つ目の例を出力してみましょう。 Prelude Language . Haskell . TH > ppr ( InfixE ( Just ( LitE ( IntegerL 1 ))) ( VarE ' ( GHC . Num .+)) ( Just ( LitE ( IntegerL 2 )))) 1 GHC . Num .+ 2 おお、元に戻りました! 続いて先の2つ目の例です。2つ目の例では、新しい変数 x と y を導入していますので、名前として解釈させてもそんな変数は無い、と怒られてしまいます。 そんなときは mkName "x" などのように記述して名前を作る必要があります。 それでは先の2つ目の例も出力してみましょう。 Prelude Language.Haskell.TH> ppr [ValD (VarP (mkName "x")) (NormalB (LitE (IntegerL 1))) [],ValD (VarP (mkName "y")) (NormalB (LitE (IntegerL 2))) []] x = 1 y = 2 できました! 次はいよいよ抽象構文木を書き換えてみましょう。 抽象構文木を書き換えてみる これまで、式などを抽象構文木にしたり、抽象構文木から式などを復元する方法を見てきました。 この相互の行き来ができるなら、抽象構文木を直接書き換えることもできなくはなさそうです。 しかし 1 + 2 のような単純な式でも、抽象構文木は、 InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) (Just (LitE (IntegerL 2))) のように複雑な記述になってしまいます。 これを間違えずに書き換えるのはかなり大変そうですよね・・・。 そこで、先ほど出てきたクォート式(Quotation)を使います。このクォート式には全部で 4種類あります。 (各クォートの型のところに書かれている Q は Quotation Monad の略です ) 式クォート(Expression quotations) 構文: [| ... |] または [e| ... |] 型: Q Exp 宣言クォート(Declaration quotations) 構文: [d| ... |] 型: Q [Dec] 型クォート(Type quotations) 構文: [t| ... |] 型: Q Type パタンクォート(Pattern quotations) 構文: [p| ... |] 型: Q Pat そして、上記のクォート式を接合(Splice)する、 $( ... ) という特別な括弧があります。 この接合用の括弧を使うと、 Q Exp 型などの抽象構文木を通常のHaskellのコードに埋め込むことができます。 1 + 2 の 2 の部分だけ抽象構文木で記述した 3 に書き換えて接合してみます。 Prelude Language.Haskell.TH> runQ [| 1 + $(return (LitE (IntegerL 3))) |] InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) (Just (LitE (IntegerL 3))) 接合して抽象構文木を書き換えることができました!! そしてクォート式を使わない場合、以下のように書かないといけません。 Prelude Language . Haskell . TH > runQ ( return ( InfixE ( Just ( LitE ( IntegerL 1 ))) ( VarE ' ( GHC . Num .+)) ( Just ( LitE ( IntegerL 3 ))))) InfixE ( Just ( LitE ( IntegerL 1 ))) ( VarE GHC . Num .+) ( Just ( LitE ( IntegerL 3 ))) これはしんどいですね・・・。 さいごに この記事を書くにあたって、メタプログラミングについて勉強して、実際の業務に役に立つ何かを見つけ出そうと意気込んでいましたが、抽象構文木を実際に見るだけでも面白くなってしまい、通常のコードと抽象構文木を行ったり来たりするのに終始してしまいました・・・。 「これが具体的に何の役に立つのか?」と聞かれると困ってしまいますが、抽象構文木を操作するという新鮮な体験をすることができました :) 皆さんも新しいプログラミング言語に興味を持った際には、新しいプログラミング手法も試してみてはいかがでしょうか。
アバター
FORCIAアドベントカレンダー2019 17日目の記事です。 検索プラットフォーム事業部エンジニアの相澤です。 普段はPostgreSQLで複数の旅行会社のデータをまとめるような処理を取り扱っています。 弊社の得意な分野はまさに旅行系の「複雑かつ膨大な」在庫・料金などのデータ処理なのですが、これを高速に扱えるのであれば、他の部分に目が行くのがエンジニアのサガ。 そこで、様々な会社から入稿される施設データの中で特に厄介なものである、「フリーテキスト入力」をなんとか綺麗にできないかと考えました。 前がたり 旅行会社が持つ情報というのは、「電話番号」「緯度経度」「郵便番号」「住所」「禁煙
アバター
FORCIAアドベントカレンダー2019  17日目の記事です。 検索プラットフォーム事業部エンジニアの相澤です。 普段はPostgreSQLで複数の旅行会社のデータをまとめるような処理を取り扱っています。 弊社の得意な分野はまさに旅行系の「複雑かつ膨大な」在庫・料金などのデータ処理なのですが、これを高速に扱えるのであれば、他の部分に目が行くのがエンジニアのサガ。 そこで、様々な会社から入稿される施設データの中で特に厄介なものである、「フリーテキスト入力」をなんとか綺麗にできないかと考えました。 前がたり 旅行会社が持つ情報というのは、「電話番号」「緯度経度」「郵便番号」「住所」「禁煙・喫煙/露天風呂/インターネット環境/WiFi etcの有無」「バリアフリー/幼児/ペットetcの対応状況」というものになっているのですが、電話番号・郵便番号・緯度経度は数字の全角半角の表記ゆれがある程度でデータ管理がしやすいのに対し、施設名・住所は大抵の場合、入力する人が入力欄に書き込んだ通りにデータ入稿されるため、表記ゆれや意図しないデータが入るなど非常に管理しにくいのです。 フリーテキスト入力なものでも「説明」「補足」「口コミ」といったデータは、 最低限の処理を施したらwebサイトに掲載できるのですが(なお、HTML制御文字が混ざっていたり、HTMLが入力されていたりする場合があります。口コミや宣伝文句に<strong>とか<big>とか混ぜないでください・・・。そういったものは取り除く必要があります)、住所に関しては違います。 別々の会社から入稿されたこのデータとあのデータは、同じ施設のデータなのか、はたまた同名の異なる施設のデータなのか。住所は施設(建物)の同一性を保証する重要なデータとなります(なお建物名は表記ゆれが大きく、A棟B棟,離れ別館などがあるので案外当てになりません)。 そこで今回は フリーテキストで入力された住所を、人が同じ住所か違う住所か判断できるレベルまでPostgreSQLの文字列置換を使って正規化することに取り組んでみたいと思います。 今回は住所に関する専門的な知識は使わず、一般的な技術の範囲内で取り組んでいきます。 やっていくぞ! 目標 与えられたデータを、同じ住所のものは同じ形に正規化する関数を作成します! 準備 テーブル locationList を下記のように定義します。 論理名 物理名 型 フリーテキスト入力住所カラム address_original test 空の正規化住所カラム address_normalized text 以下のようにして置換を行い、正規化します。 CREATE OR REPLACE FUNCTION normalize(original text) RETURNS text AS $funcbody$ DECLARE result text; BEGIN result := original; -- この辺に置換処理を書く RETURN result; END; $funcbody$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE; UPDATE locationList SET address_normalized = normalize(address_original); step1 :注意文言や制御文字の削除 (regexp_replace を用いた削除) 住所欄には、住所以外の文言が入力されている場合が多々あります。 例: 『(駐車場は裏にあります)』『※〇〇駅徒歩5分 』『〇〇市(旧:☓☓町)』  そのほか電話番号、セールストークetc・・・ こういうもののほとんどはカッコや記号の後ろに書いてあるので、これを目印に消しましょう。 使う関数は正規表現を扱える regexp_replace ですね!以下のようになります。 丁寧にやるならカッコの対応をきちんと種類ごとにしてもいいですが、 現実的に入力されるカッコの種類は一定ではなく、シンタックスも必ずしも一致しません。 カッコは最短マッチで消した後、残っているものも消してしまいます。 -- カッコの中身の削除 result := regexp_replace( result ,E'(\\(|(|\\[|「|【|『|〈|《).*?(\\)|)|\\]|」|】|』|〉|》)' ,E'' ,'g' ); result := regexp_replace( result ,E'(\\(|(|\\[|「|【|『|〈|《|\\)|)|\\]|」|】|』|〉|》)' ,E'' ,'g' ); -- 注意文言の削除 result := regexp_replace( result ,E'(※|*|◎|~|~|★|■|◆|●|☆|□|◇|○|●|(TEL)|℡|〒).*$' ,E'' ,'g' ); HTML制御文字は & から始まり、 ; で終わりますが、入力システム側で大文字に変換されたりして、無効化されている場合もあります。大文字小文字両方に対応し、制御文字を取り除きます。 -- 制御文字の削除 result := regexp_replace( result ,E'(&|&).+?(;|;)' ,E'' ,'g' ); step2:かな・アルファベット・数字表記ゆれ対応 (translateを用いた置換) 住所にはたくさんの"かな"が含まれますが、ひらがな・カタカナ・濁点・半濁点は意外と一致しませんので正規化します。translateという関数は、一対一対応で文字を置換してくれますので、これを使いましょう。 ちなみに濁点や半濁点は単体でも入力できますし、半角カナでは独立するので、気をつけて置換する必要があります。 -- 制御文字の削除 result := regexp_replace( result ,E'(&|&).+?(;|;)' ,E'' ,'g' ); -- 濁点半濁点対応 result := translate( result ,'ゔヴがぎぐげごガギグゲゴざじずぜぞザジズゼゾだぢづでどダヂヅデドばびぶべぼバビブベボぱぴぷぺぽパピプペポ゚゙゛゜' ,'ううかきくけこかきくけこさしすせそさしすせそたちつてとたちつてとはひふへほはひふへほはひふへほはひふへほ' ); -- 歴史的仮名遣い対応 result := translate( result ,'ゐゑヰヱ' ,'いえいえ' ); -- 小文字を大文字に result := translate( result ,'ぁぃぅぇぉァィゥェォァィゥェォヵヶっッッゃゅょャュョャュョ' ,'あいうえおあいうえおあいうえおかけつつつやゆよやゆよやゆよ' ); -- 半角カナを全角カナに result := translate( result ,'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン' ,'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわおん' ); -- 全角カナをひらがなに result := translate( result ,'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン' ,'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわおん' ); step3:スペース・ハイフンの置換(unicodeを用いた置換) さて、基本的な正規化を終えるにはあと一歩ですがこれが面倒なのです。 スペース・ハイフンには実はものすごい種類があります(今回は対応しませんがチルダも厄介です)。エンジニアでもなければ全角ハイフン・半角ハイフンなどは区別しないのかもしれません。 基本的な記号は置換してしまいましょう。 全角スペース・半角スペース・タブ → 今回は 消します 全角ハイフン・半角ハイフンetc.. → 半角ハイフンに統一しましょう(全角ハイフン系の記号にすると漢数字のイチと見分けが付きにくので) ここで E'(全角スペース|半角スペース|タブ)' のようなパターンの書き方もできますが、これだと一目で見て何をやっているのかよくわからなくなってしまいますね。 スペースはまだマシですが、ハイフンなどは何が対応できていて何に対応できていないのかがわからなくなってしまいます。 ここはUnicodeを使用して置換をコントロールします。具体的には以下のようにします。 --スペースの削除 result := regexp_replace( result ,E'(\\u0020|\\u00A0|\\u0009)' ,E'' ,'g' ); -- ハイフンの統一 result := regexp_replace( result ,E'(\\u002D|\\uFF0D|\\u2212|\\u2015|\\u2010|\\u2011|\\u2012|\\u2013|\\u2014|\\uFF70|\\u4E00)' ,E'\u002D' ,'g' ); ハイフンとして長音記号を扱うかどうかは、かなり難しいところです。 というのも、 ディズニーランド のように何故かハイフンと長音を混同した入力がみられるためです(制御文字などに対しても思うのですが、素朴な疑問としてどうやって打っているのでしょうか・・・)。私は長音記号はハイフンとしませんでした。 ちなみに、「一丁目3一7」のようにハイフンの代わりに漢数字のイチが入力される場合もあり、そちらも正規化を諦めました(普通に不正データでは・・・)。 step4:住所ゆれの対応(捕捉変数を使う) 住所にはいくつかの"どちらもあっている"パターンがあります。 1. 〇〇県(〇〇郡)〇〇町 の 郡はあってもなくても良いものです。 〇〇県 + (1文字以上) + 郡 + (1文字以上)市町村 というパターンに関して"郡"を抜いてしまえば良さそうです。 100%の精度ではないですが、ある程度はカバーできそうです。 PostgreSQLの正規表現には(先読み|後読み)(肯定|否定)はありませんのでキャプチャー(捕捉変数)を用いて置換します。 -- 都道府県 + 郡 の無視 result := regexp_replace( result ,E'^(北海道|青森県|岩手県|宮城県|秋田県|山形県|福島県|茨城県|栃木県|群馬県|埼玉県|千葉県|東京都|神奈川県|新潟県|富山県|石川県|福井県|山梨県|長野県|岐阜県|静岡県|愛知県|三重県|滋賀県|京都府|大阪府|兵庫県|奈良県|和歌山県|鳥取県|島根県|岡山県|広島県|山口県|徳島県|香川県|愛媛県|高知県|福岡県|佐賀県|長崎県|熊本県|大分県|宮崎県|鹿児島県|沖縄県)(.+?郡)(.+(市|町|村))' ,E'\\1\\3\\4' ); 行政区画の中に"字"(あざ)、大字(おおあざ)が入ってくる場合があります。私の祖父の家もそういった行政区画だったのですが、住んでいる本人たちも正式にはどう書くべきか知らないようでした。 市町村名などに"字"が含まれる場合に誤って消してしまうかもしれませんが、それで混同してしまうような市町村はなさそうだったので、私はこれを無視します。 -- 字・大字は判断に使えないので無視する result := regexp_replace( result ,E'大?字' ,E'' ,'g' ); また、京都には「〇〇通り」「〇〇上ル」「下る」「入る」といった地名があるようですが、この送り仮名はあったりなかったりひらがなだったりカタカナだったりします。送り仮名は消してしまいましょう。 -- 入る・上る・下るの[る]はあったりなかったりカタカナだったりするので消す result := regexp_replace( result ,E'(入|上|下)る' ,E'\\1' ,'g' ); -- 通りの[り]はあったりなかったりカタカナだったりするので消す result := regexp_replace( result ,E'(通)り' ,E'\\1' ,'g' ); step5:番地以下の正規化(以下は新しい技術性はありません) 番地以下のカテゴリーには区切るハイフンだけではなく、丁・丁目・番・番地・番街・番町・号・ハイフンなどのバリエーションがあるので、これらをすべてハイフンで統一してしまいましょう。 余計なことはしないように、数字の後ろの場合にのみ置換をかけます。 result := regexp_replace( result ,E'(0|1|2|3|4|5|6|7|8|9|〇|一|二|三|四|五|六|七|八|九|十|百|千)(丁目?(の|\\u002D)?|番(地|町|街)?(の|\\u002D)?|の|号)' ,E'\\1\u002D' ,'g' ); この処理には問題があります。「四ノ宮(→変換されて「四の宮」)」のような地名は、巻き込まれて「四-宮」になってしまいます。 予め、漢数字 + ノ + 数字以外("四ノ宮"など) は特別に扱うためにカタカナのノに戻します。 また、北海道では「条」が「丁目」のように使われています。 京都やその他の地域でこのようなことはないので、北海道の「条」だけ変換をかけると良さそうですね。 -- 番地の正規化 -- 漢数字 + ノ + 数字以外("四ノ宮"など) は特別に扱うためにカタカナに戻す result := regexp_replace( result ,E'(一|二|三|四|五|六|七|八|九|十|百|千)(の)([^0-9])' ,E'\\1ノ\\3' ,'g' ); result := regexp_replace( result ,E'(0|1|2|3|4|5|6|7|8|9|〇|一|二|三|四|五|六|七|八|九|十|百|千)(丁目?(の|\\u002D)?|番(地|町|街)?(の|\\u002D)?|の|号)' ,E'\\1\u002D' ,'g' ); -- 北海道は「条」をハイフンとして扱う result := regexp_replace( result ,E'^(北海道.*)(0|1|2|3|4|5|6|7|8|9|〇|一|二|三|四|五|六|七|八|九|十|百|千)(条)' ,E'\\1\\2\u002D' ,'g' ); step6:説明や建物名を除く 住所に混入された説明や建物名を完全に除くのは難しいです。できることがあるとすれば、「最初に登場する算用数字・ハイフン群の後ろは、説明か何かと判断する」という方法です。 ただし現実には「2条河原」のように地名も算用数字で入稿される場合があるので、それは少々大雑把すぎるやり方です。 -- 不要な説明や建物名を除く result := regexp_replace( result ,E'^(.*?)((0|1|2|3|4|5|6|7|8|9|\\u002D)+)(.*)$' ,E'\\1\\2' ); result := regexp_replace( result ,E'\\u002D$' ,E'' ,'g' ); 結果 これらの処理を行うことで、私の扱っているデータからは句読点や記号は綺麗サッパリ消すことができました。 しかし、今回作った関数ではうまく扱えていないパターンもありますのでご紹介します。 建物の名前の扱い 記号やそのほか様々なものが入ってきており、住所に建物の名前が入っているパターンでは、建物の名前をきれいにすることができませんでした。 今回は建物自体が同じかどうかを判断するものだったので、無視しましたが、 テナント等を判別する場合難しい課題になりそうです。 千葉県浦安なのに住所が「東京都浦安~」となっているもの 千葉県の「東京」ディズニーラントだけではなく、千葉・埼玉にこのパターンが結構ありました(データ提供サイトのご都合かもしれません)。 郵便番号などその他のデータをもとに上書きしてしまったほうが良いかもしれません。 旧字の統一 旧字体が正式な住所の場合、新字と旧字が混ざってしまうパターンがあります。 そういったパターンすべてを洗い出してtranslateすれば良いのですが、洗い出しができませんでした。 漢数字・ローマ数字 漢数字やローマ数字を算用数字に置換することができませんでした。 うまいやり方があるのでしょうか・・・ 京都の住所の一部 「 〇〇番地〇〇通り〇〇丁目 」のように、丁目以下にも細かい情報が入ってきて対応しきれていません。 ハイフンと漢数字のイチと長音記号 「星のリゾート」のような記載や「三丁目一2(ハイフンではなく漢数字のイチ)」のようなパターンへの対応ができませんでした。 住所の真ん中に説明がガンガン入ってくるパターン 顔文字 取り除ききれない部分がありました。 上記に関しては私では解消しきれませんでしたが、住所や日本語の知識をつければ対応できるものもありそうです! 完成品 CREATE OR REPLACE FUNCTION normalize(original text) RETURNS text AS $funcbody$ DECLARE result text; BEGIN result := original; --スペースの削除 result := regexp_replace( result ,E'(\\u0020|\\u00A0|\\u0009)' ,E'' ,'g' ); -- ハイフンの統一 result := regexp_replace( result ,E'(\\u002D|\\uFF0D|\\u2212|\\u2015|\\u2010|\\u2011|\\u2012|\\u2013|\\u2014|\\uFF70|\\u4E00)' ,E'\u002D' ,'g' ); -- カッコの中身の削除 result := regexp_replace( result ,E'(\\(|(|\\[|「|【|『|〈|《).*?(\\)|)|\\]|」|】|』|〉|》)' ,E'' ,'g' ); result := regexp_replace( result ,E'(\\(|(|\\[|「|【|『|〈|《|\\)|)|\\]|」|】|』|〉|》)' ,E'' ,'g' ); -- 注意文言の削除 result := regexp_replace( result ,E'(※|*|◎|~|~|★|■|◆|●|☆|□|◇|○|●|(TEL)|℡|〒).*$' ,E'' ,'g' ); -- 制御文字の削除 result := regexp_replace( result ,E'(&|&).+?(;|;)' ,E'' ,'g' ); -- 濁点半濁点対応 result := translate( result ,'ゔヴがぎぐげごガギグゲゴざじずぜぞザジズゼゾだぢづでどダヂヅデドばびぶべぼバビブベボぱぴぷぺぽパピプペポ゚゙゛゜' ,'ううかきくけこかきくけこさしすせそさしすせそたちつてとたちつてとはひふへほはひふへほはひふへほはひふへほ' ); -- 歴史的仮名遣い対応 result := translate( result ,'ゐゑヰヱ' ,'いえいえ' ); -- 小文字を大文字に result := translate( result ,'ぁぃぅぇぉァィゥェォァィゥェォヵヶっッッゃゅょャュョャュョ' ,'あいうえおあいうえおあいうえおかけつつつやゆよやゆよやゆよ' ); -- 半角カナを全角カナに result := translate( result ,'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン' ,'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわおん' ); -- 全角カナをひらがなに result := translate( result ,'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン' ,'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわおん' ); -- アルファベットの正規化 result := translate( result ,'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ,'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ); result := translate( result ,'abcdefghijklmnopqrstuvwxyz' ,'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ); result := translate( result ,'abcdefghijklmnopqrstuvwxyz' ,'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ); result := translate(result ,'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ' ,'ABGDEZHQIKLMNXOPRSTUFCYW' ); result := translate(result ,'αβγδεζηθικλμνξοπρστυφχψω' ,'ABGDEZHQIKLMNXOPRSTUFCYW' ); -- 数字の正規化 result := translate(result ,'0123456789ⅰⅠⅱⅡⅲⅢ' ,'0123456789121233' ); -- 都道府県 + 郡 の無視 result := regexp_replace( result ,E'^(北海道|青森県|岩手県|宮城県|秋田県|山形県|福島県|茨城県|栃木県|群馬県|埼玉県|千葉県|東京都|神奈川県|新潟県|富山県|石川県|福井県|山梨県|長野県|岐阜県|静岡県|愛知県|三重県|滋賀県|京都府|大阪府|兵庫県|奈良県|和歌山県|鳥取県|島根県|岡山県|広島県|山口県|徳島県|香川県|愛媛県|高知県|福岡県|佐賀県|長崎県|熊本県|大分県|宮崎県|鹿児島県|沖縄県)(.+?郡)(.+(市|町|村))' ,E'\\1\\3\\4' ); -- 字・大字は判断に使えないので無視する result := regexp_replace( result ,E'大?字' ,E'' ,'g' ); -- 入る・上る・下るの[る]はあったりなかったりカタカナだったりするので消す result := regexp_replace( result ,E'(入|上|下)る' ,E'\\1' ,'g' ); -- 通りの[り]はあったりなかったりカタカナだったりするので消す result := regexp_replace( result ,E'(通)り' ,E'\\1' ,'g' ); -- 番地の正規化 -- 漢数字 + ノ + 数字以外("四ノ宮"など) は特別に扱うためにカタカナに戻す result := regexp_replace( result ,E'(一|二|三|四|五|六|七|八|九|十|百|千)(の)([^0-9])' ,E'\\1ノ\\3' ,'g' ); result := regexp_replace( result ,E'(0|1|2|3|4|5|6|7|8|9|〇|一|二|三|四|五|六|七|八|九|十|百|千)(丁目?(の|\\u002D)?|番(地|町|街)?(の|\\u002D)?|の|号)' ,E'\\1\u002D' ,'g' ); -- 北海道は「条」をハイフンとして扱う result := regexp_replace( result ,E'^(北海道.*)(0|1|2|3|4|5|6|7|8|9|〇|一|二|三|四|五|六|七|八|九|十|百|千)(条)' ,E'\\1\\2\u002D' ,'g' ); -- 不要な説明や建物名を除く result := regexp_replace( result ,E'^(.*?)((0|1|2|3|4|5|6|7|8|9|\\u002D)+)(.*)$' ,E'\\1\\2' ); result := regexp_replace( result ,E'\\u002D$' ,E'' ,'g' ); RETURN result; END; $funcbody$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE; 最後に フォルシアではデータクレンジングを専門にしているエンジニアもいますが、今回は私の勉強も兼ねていたので、自力で試行錯誤しました。 正規表現はパズルみたいな面白さもあり、仕様には知識だけではなく経験も必要なので、適切な課題を設定して取り組むのは面白いですね。
アバター
FORCIAアドベントカレンダー2019  16日目の記事です。 初めまして!今年の4月にキャリア入社しました、営業の佐塚と申します。 去年の3月までは、福井県の放送局でアナウンサーとして働いていました。 なぜまったく違うITの世界に飛び込んだのかは、涙なしには語れない壮大な物語があるのですが(嘘です)、 話すと半日はかかってしまうので(嘘です)、こちらでは簡単なご紹介だけとさせていただきます。 さて、みなさんは「四角」ってお好きですか? 私は中でも平行四辺形が大好きで、辺ABを1秒に2cmずつ移動する点Pとは懇意にしていました。 間違えました。「四角」ではなく「死角」ですね。 相手のに入れたときはうれしいですが、相手に入られたときはヒヤッとしますよね。 今日は「四角」でも「死角」でもなく、「資格」について、IT分野の資格「ITパスポート」 通称iパスを受けてきましたので、その時の体験をご紹介できればと思います。 ITパスポートとは ITパスポートを主催する情報処理推進機構のwebサイトでは、このように紹介されています。 iパスは、ITを利活用するすべての社会人・これから社会人となる学生が備えておくべきITに関する基礎的な知識が証明できる国家試験です。 そうです、泣く子も黙る国家資格なのです。エッヘン! 建築士や公認会計士、調理師、美容師といったその道のプロフェッショナルともいえる国家資格が、実はITの分野にもあるんですね。 と言っても、難しく考えたり、構えたりする必要は全くありません。上の説明にもある通り、iパスは「ITに関する基礎的な知識が証明できる国家試験」ですから、IT分野の「基本のキ」とも言える問題が出題されます。初心者でも大丈夫。むしろ、初心者こそ、文系こそ、非エンジニアこそウェルカムな試験なのです。 私の受験理由 そんなITパスポートを、私が受けようと思った理由は「ITの分野に関する知識を少しでもつけたかったから」です。 全く違う業界からITの業界に飛び込んだので、少しでも早く、ITに関する知識をつけたいと思っていました。資格取得だけがその手段ではありませんが、目標を設定して、そこに向けて勉強していくというのは、私の性分には合っていたかなと思います。知識を習得したという客観的な証明にもなりますよね。 それと、フォルシアに「資格取得支援制度」があったことも大きかったです。 フォルシアでは、業務に関わる資格を取得できた場合、受験料と、勉強に使用した書籍代を補助してもらえる制度があります。個人のスキルアップを会社として後押ししてもらえるというのは、とても励みになります。金銭的にも助かりました。 申し込んでから試験当日まで ITパスポートは、全国の会場で、比較的頻繁に試験が行われているので、試験日程は選びやすかったです。私は1か月ほど前から勉強を始めて、2週間程度で参考書を1冊一通り読んだら、残りの2週間は、ひたすら過去問題集を解くという勉強方法を取りました。 入門的な試験なので、難しい計算をしたり、プログラミング言語を深く理解していないと解けないような問題はありませんが、例えばハードウェアに関することからデータベースに関すること、マネジメント、法務、経営戦略など、広い分野から問題が出ます。 特定の分野の得点が低いと合格できないシステムのため、全分野覚えるべきことをきちんと覚える必要があります。私にとってはこれが大変だったので、とにかく過去問を解いて、間違えた問題を2日後ぐらいにまた解いて......と繰り返すことで覚えていきました。 試験当日 iパスは、IT系の資格らしく、CBT形式と呼ばれる、試験会場のコンピュータ上で問題に解答する形式です。ペーパーテストに慣れていると少し違和感があるかもしれませんが、無駄が少なく、また終了時に、即座に自分の点数がコンピュータ上に表示されるため、どのくらいできたのかをすぐに知ることもできます。 学生の頃のテストでは、シャーペンの音が響いていましたが、iパスではマウスの「カチカチ」という音が会場に響いていたのが面白く心地よかったです(会場にはヘッドホンも置いてあったので、気になる方はそれを付けることもできます)。 受験してみて やはり「受けてよかった」という気持ちが強いです(無事合格しました!)。 というのも、IT分野の国家試験には、他にも様々な種類があり、私は現在、「基本情報技術者」の取得を目指して、次の勉強を始めているのですが、iパスで学んだことが基礎となって、より詳しい内容を学ぶことができていると感じます。 基本情報技術者は、ぐっと学ぶ内容が多くなり、名前についている「基本」の文字に「本当か!?」と言いたいぐらい、非エンジニアの私にとっては難しく感じています。それでも、この勉強を「面白い」とも感じられているのは、iパス受験時に、基本的なことをきちんと学習できたからに他なりません。 例えば、コンピュータが「2進数」を用いて、実際どのようにして四則演算を行っているかや、そのほかの処理にどのように応用しているかということは、iパスで基本を学び、基本情報技術者の勉強で「なるほどそういうことだったのか!」とさらに理解を深めることに繋がっていて、楽しさが広がっていくように感じています。 これからIT系の企業への就職を目指している学生のみなさん、転職でIT分野に飛び込もうとしている社会人のみなさん、そして、私と同じようにIT分野初心者の駆け出し営業の方に、少しでも参考になれば嬉しいです。
アバター