TECH PLAY

ニフティ株式会社

ニフティ株式会社 の技術ブログ

487

Notionを日々便利に使わせてもらっていますが、たまに感覚的に合わない挙動が行われることがあります。個人的によく遭遇するパターンについて回避策を書いていこうと思います テキストをコピペしたら1つのブロックになってしまった 回避策:Markdownで書いてから貼り付ける 回避策:Notion AIに頼む テキストをコピペしたら1つのデータベースアイテムになってしまった 回避策:Spredsheetなど表計算ソフトを中継してから貼り付ける 回避策:ブロックにしてからD&Dする 回避策:CSVとしてインポートする テキストをコピペしたら1つのブロックになってしまった 改行ごとにブロックにして欲しかったのに1つのブロックに書かれた文章になってしまうケース。 原因は貼り付けた内容がMarkdownとして認識されているからだと思います。 Markdownでは行末にスペース2つか空行を追加しないと改行だと認識されません。 通常であればスペースとして変換されるところを、ブロック内改行(Shift+Enter)にしているところを見ると、完全にMarkdownを再現しているわけではなく、Notionに合った形で変換しているのでしょう。 原因が推察できれば、おのずと回避策もわかってきます。 回避策:Markdownで書いてから貼り付ける ワンステップでできないのが歯痒いのですが、現状これが早い解決策だと思います。 エディタで行末にスペース2つ付けるだけですし(正規表現で $ に対して処理)、下書きファイルの拡張子をmdにしておけば拡張機能の設定によっては自動で付けてくれます。 貼り付ける際に内容によってはコードブロックにされてしまうので、ペーストしてスタイルを合わせる(Ctrl+Shift+V)を利用しましょう。 ちなみにSpredsheetを中継する技は今回は使えません、行ごとに水平線が差し込まれます。 回避策:Notion AIに頼む Notion AIに頼めばブロックに分けて貼り付けてくれます。 コードブロックにしたい内容も改行されてしまうのではないか、と思われるかもしれませんがちゃんと認識してコードブロックに変換してくれました。 テキストをコピペしたら1つのデータベースアイテムになってしまった 改行ごとに別アイテムにして欲しかったのに1つのデータベースアイテムのタイトルになってしまうケース。 リッチテキストエディタを採用しているツールは多々ありますが、テーブル表示のコピペ挙動はそれぞれ違っていることが多く、Notionデータベースも同じ。これに関しては挙動から回避策を探っていくしかないです。 こちらの回避策は複数あるものの、残念ながらどれもワンステップでできません。 回避策:Spredsheetなど表計算ソフトを中継してから貼り付ける 貼り付ける数が多くなければ体感一番早いのはこの方法です。 複数プロパティでもそのまま貼り付けられるので汎用性が高いです。 貼り付けたい順番とプロパティの順番が異なっていたら、一時的ビューを作るなどして貼り付けたい並びに調整しましょう。 回避策:ブロックにしてからD&Dする データベースアイテムもページ、もっと言えばブロックなので、ブロックをD&Dすればいいという理屈です。 上に書いた方法でテキストを改行ごとのブロックにした後に、データベースへD&Dすればデータベースアイテムとして扱われます。 回避策:CSVとしてインポートする 貼り付ける数も多くて、プロパティもしっかり入れたい時はCSVインポートが適切です。 データのインポート – Notion (ノーション)ヘルプセンター 1行目に書くプロパティ名を間違えると、新規プロパティとして作成されてしまうので注意が必要。 重要なデータベースにインポートしたい場合は、一度新規データベースとしてインポートしたあとに移動させるといいでしょう。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
こんにちは。会員システムグループでエンジニアをしている山田です。 今回はterraformの注意点について紹介します。 概要 terraform import はterraformを使っていなかった頃のリソースを取り込むためのものであり、定常運用で使うものではないという話です。 terraformで運用するならterraformだけで完結させてください。importは常用するための機能ではありません。 importの限界 importを前提とした運用には以下の問題があります import漏れ コンソールで作成したものを後からimportする、という運用では、作られたリソース全てがterraform管理下にある、という保証ができません。 コンソール上でよしなに作成されたIAM RoleやS3 Policy、Route53レコードなどを見逃してしまい、terraform管理外だから不要だと思って消したら実は必要だった、というような事態が発生します。 再現性がない 1から実行されたことのないterraformコードには再現性がありません。 例えば暗黙的な依存関係にあるInternet GWとNAT GWは resource "aws_internet_gateway" "igw" { ... } resource "aws_nat_gateway" "ngw" { ... depends_on = [aws_internet_gateway.igw] } のように depens_on を明示しないと実行に失敗しますが、importではこれに気づくことができません。 importブロックが使えないことがある terraform 1.5よりimportブロックが使えるようになりました。 これはimportの内容をtfファイルに記述できるものであり、コマンド実行前にplanができることでterraform importコマンドを使うより安全な操作が行えます。 しかしながら、AWS APIが非対称なケース、つまりPOSTで設定した内容と、GETで帰って来る内容が異なるようなリソースでは実行に失敗します。 ex) aws_lb_listener の default_action resource "aws_lb_listener" "https" { ... default_action { type = "forward" target_group_arn = aws_lb_target_group.a.arn } ... } Planning failed. Terraform encountered an error while generating this plan. ╷ │ Error: Invalid Attribute Combination │ │ Only one of "default_action[0].target_group_arn" or "default_action[0].forward" can be specified. │ default_action[0] │ │ with module.ecs_front.aws_lb_listener.https, │ on ../../modules/ecs_front/lb.tf line 41, in resource "aws_lb_listener" "https": │ 41: resource "aws_lb_listener" "https" { │ 上記例では default_action[0].target_group_arn のみを指定しようとしていますが、AWS APIのGET結果は default_action[0].target_group_arn と default_action[0].forward が両方指定された状態で返ってきてしまいます。両方指定された状態をterraform planは許容しないのでエラーとなります。 terraform importコマンドを使えばplanを実行することはないため、失敗することはなくなります。ただしimport後にchangedが出てしまうことがあります。 不要なchanged terraform importしたリソースは、 再度plan・applyすると必ずchangedが出る ものがあります。 これは主にAWS上に存在せず、tfstate上にのみ存在する値があることによって発生します。 ex) aws_ecs_service の wait_for_steady_state など、terraform自体の実行を制御するパラメータ ex) sensitive = true の設定などにより、terraform上でsensitiveとして扱うべきパラメータ これらがあるため、「importした後にchangedが出なくなるようにコードを編集すればOK」とはいきません。特に前者は tfstateを直接編集しないとapplyすらできない というケースも存在します。 import不可リソース 稀ですがimportが不可能なリソースも存在します。AWS Providerの場合はドキュメントにimportできない旨の記載があります。 ex) aws_lb_target_group_attachment これはimportができないため、applyしてみるしかありません。 (上記の場合、全く同じ設定でapplyすればエラーなく成功します) どうすればよいのか コンソール作業をやめて terraformですべて作るようにしましょう 。 一度コンソール作業を行わないとどのようなリソースを作ればよいのかわからない、という場合は以下のような手順にします。 コンソール上でお試しリソースを作り、動く状態にする このとき、リソース名は本番用とは別の名称にする 同等の構成を再現できるようにterraformコードを書き、applyする こちらのリソース名は本番相当とし、お試し用とは被らないようにする お試しリソースを手動で全削除する 動作検証を行う Terraformで0から作成できる状態を担保し、コードが信頼できる状態を保つようにしましょう。importは最後の手段です。 今回はTerraformの適切な使用方法と、Terraform inportの限界について紹介しました。 Terraformを利用する際はぜひこれらの注意点を念頭に置いて実践してみてください。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに こんにちは! ニフティ株式会社、エンジニア定例運営の小林、柴田です。 ニフティでは毎年エンジニアの新人研修を先輩エンジニアが 内製 で行う文化があります(通称、 エンジニア定例 と呼ばれています)。 開催期間としては短期集中的に6月(一部5月)に実施し、準備は2か月前後で行います。 前年より資料の一般公開に取り組んでおり、今年も同様に一般公開いたします。 – 前年: ニフティ株式会社 エンジニア新人研修の内容を公開します | 2023年度版 新人研修の狙い ニフティ全体の技術力向上 以下を経験することで社内全体の技術力向上を目的とし、エンジニア定例の目指すところとしています。 体系立った基礎学習 チームでのサービス開発 短期的なサービス運用 生徒から講師による継続的運用 内製力の強化 育成強化 Webサービスを開発する上で必要なスキルを体系立てて学んでもらう スキルアップ 業務では学べない技術を積極的に取り組んでいく 今回の公開講義資料について 研修の到達目標として、「Webアプリケーションを独力で開発、クラウド上でリリースできるレベルの知識、技術力を身に付ける」を掲げており、それに伴う講義を行っています。 今年は新しく「サーバー運用入門」が追加され、「機械学習」については近年のトレンドを加味し「生成AI」に着目した内容に刷新しております。 それでは、以下公開可能な講義資料を掲示しますので、学習にお役立てください。 講義資料一覧 Git2024 AWS2024 [NEW!] サーバ運用入門2024 コンテナ2024 データベース2024 セキュリティ2024 オブジェクト指向2024 機械学習2024 Webアプリ2024 モバイルアプリ2024 ※ニフティ社内の開催日順で記載しています。 終わりに ニフティでは、長年にわたり自社開発の研修プログラムを実施しています。 このプログラムは、継続的な改善と進化を遂げており、教材の更新や新しい講座の導入、さらには前年度の新入社員が翌年には講師を務めるなど、独自の取り組みを行っています。 この制度の大きな利点は、 受講者としての経験を講師の立場で活かせること 、と感じています。 私自身、過去の受講で気づいた改善点を教材に反映できるだけでなく、講義後のフィードバックを通じて自己成長の機会を得られています。 この循環型の学習システムは、個人のスキル向上と同時に、研修プログラム全体の質の向上にも貢献しています。  弊社のエンジニア育成への取り組みや、最新の技術動向について詳しく知りたい方は、ニフティエンジニアブログをぜひご覧ください。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
チームの今後について 現在のチームの課題と、それに対する取り組みについてお聞かせください R.Fさん 私たちのチームが持っているシステムの数が膨大な為、知識の吸収やメンバーの作業リソースが追い付いていないのが課題ですね。さらに、レガシーなシステムも扱う中で勤続年数が5年目以下の社員しかいないのが現状です。 なので、そもそもチームでの扱う必要があるシステムなのか、維持する必要があるのか、といった前提から考えていくような提言を現場からしていけるようになっていきたいです。 お金を使ってメンバーやリソースを増やして解決したとしても、それって課題をすり替えてるだけで、後々より大きなツケを払わないといけなくなるんですよね。なので現場の課題を組織全体の課題に昇華させる必要があります。そういった現場から組織に働きかけるような取り組みをしていきたいです。 M.Mさん やっぱりレガシーシステムの刷新や取り扱いは課題として上がりますね。 R.Fさんが言っていたようにサービス数が膨大なので管理が大変ですし、言語や扱う分野もバラバラなので難しいです。全部をPythonに当てはめることは難しいので、リプレイスをする際には適切な技術選定をしていく必要もあります。 また、お客様のお金を扱うサービスでは強固なセキュリティの担保が必要なのでそういった点についても考えながら刷新に取り組んでいます。 S.Oさん M.M君と似た話ですが、チーム内にはレガシーなシステムが複数存在し、ものによってはブラックボックスになっているものもあるので、拡張や運用する際の障害になるという課題があります。 何も考えずに最新技術を取り入れるだけだと将来の負債にもなりえるため、そのシステムにあった最適な技術を取り入れていきたいです。 課題は山積みですが、今できる工数削減などを実践してチームの時間を増やす取り組みをしています。具体的には、追加機能が作られた際には都度テストの工数がかかっていたのですが、一部を自動化することで他のやりたいことに時間をさけるようにしました。 見えるところからコスト削減をしていって、課題に向き合う時間を増やしていきたいです。 休日の過ごし方について お休みの日はどんなことをされていますか?趣味やマイブームについて教えてください S.Oさん 私は旅行や音楽鑑賞ですね。 先週は金沢で海鮮料理を楽しんだり、21世紀美術館で作品に触れたりしていました。朝の散歩がてら兼六園に行って落ち着きながら回ることが出来たのも良かったです。 また、R.Fさんに影響されてアーキテクチャやクラウドに関する勉強をしています。最近はSQSやLambdaの並列化などを勉強してチームに還元できたので達成感がありました。 M.Mさん YouTube見ることがなんだかんだで多くなっています。スポーツ全般見るのが好きで、特にサッカーは一番です。最近は国立競技場でのJリーグを試合を観戦しに行きました。 自主学習では幅広く勉強しており、特にセキュリティに関する勉強をしています。ニュースを追ったり、効率的なソフトウェア管理方法を模索していたりします。 R.Fさん スクラム系であったり、組織に関する本を読んでいます。 今はニフティのN1!制度の中でアジャイルコーチを任命されているのですが、スクラムに触れた当初はスクラムのことを信じ切れていませんでした。 スクラムに限った話ではないかもしれないですが『導入すればうまくいく』みたいなことに懐疑的で、今までと違うやり方を導入していこうとしたらどこかにしわ寄せは出てしまうはずなのに、いろんな事例とかを見てもなかなかそういう話って書いてなくて、違和感を感じていました。 組織で仕事をしているので一部の人だけ楽しんでる、みたいなことは避けたくて、スクラムに関する書籍を読んだりやイベントに参加してきて学びを深めています。そのおかげで最近はスクラムへの考え方が変わってきました。 最近読んだ本の中では「ワインバーグのシステム思考法」「学習する組織」「大規模スクラム Large-Scale Scrum(LeSS) アジャイルとスクラムを大規模に実装する方法」が印象に残っています。 ニフティのN1!制度について このインタビューに関する求人情報 /ブログ記事 課金システムチームの求人情報 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する .is-style-rounded + .has-text-color{ font-size:95%; }
アバター
自己紹介 R.Fさん ■入社年度 2020年4月 ■主担当サービス チームの業務全般 ■趣味 スクラム関連の本を読む S.Oさん ■入社年度 2021年4月 ■主担当サービス 課金系/書面発送/サービス申込サービスの開発・運用 ■趣味 旅行/音楽フェス M.Mさん ■入社年度 2021年4月 ■主担当サービス 課金系/書面発送/サービス申込サービスの開発・運用 ■趣味 動画鑑賞/サッカー観戦大河ドラマ鑑賞/囲碁 課金システムチームとは 課金システムチームとはどういったシステムを管理しているチームですか R.Fさん ニフティの回線をお客様に契約を申し込んで頂く所から、ご利用料金の請求まで様々なシステムを取扱っています。 一例ですが、回線サービスの申し込み管理システムであったり、申込サービス内容や取り扱い説明書の書面データを発送するシステム、ご利用料金の決済システムなど様々なシステムを取扱っています。 PPPoE 固定IPオプション https://csoption.nifty.com/staticip/pppoe/ @nifty MAX光 https://setsuzoku.nifty.com/max/ その中でも、皆さんはどのようなシステムを担当されていますか R.Fさん メンバー全員でチームが抱えているシステムの面倒を見れるようにしてます。 というのも、うちのチームでは古くから扱っているシステムがあってそれぞれ担当者がいたんですけど、その担当者が離れた途端にそのシステムについて理解している人がいなくなってしまうといった過去があったんです。 そういった経験から、属人化を防ぐためにチームの方針として新旧のシステム問わず知見をメンバーに共有することを徹底して、全員の経験値が平等になるようにしています。 メンバーへの知見共有はどのように行われているのですか S.Oさん 自分たちはスクラム開発を取り入れていて、全員に対して課題報告や知見共有のタイミングがあるので、そういった時間で話をしています。 加えて、一つのタスクに対してもチーム全体で関わるようにしたいのでモブプログラミングを取り入れて、複数人で作業することを意識したりしています。 M.Mさん 成果物がある程度できたところで共有会を欠かさず実施したり、議論が必要な部分では全員で話し合うことを意識しています。 タスクのチケット管理をしていて、そのチケットの中に残すべき証跡はしっかり残すようにして貰ったりしていますね。形にすべきものはしっかりドキュメント化するように各メンバーが意識するようにしています。 なんにせよ、担当している人しかわからない。属人化を防がないといけないので何かは残してね。という状況にしていますね。 R.Fさん 長く使われているシステムをアップデートするためにレポジトリを確認してみたら、コメントが何もなくてコミットメッセージが少しあるだけで意図が分からなかったりして手がかかってしまったことがあったんですよ。 目の前のタスクを早くこなすことも勿論大事ですが、5年後10年後そのサービスを扱う人を考えてものづくりをする必要があると考えています。 業務内容について 業務で扱っている技術について教えてください M.Mさん 基本的にはバックエンド系の開発ではPython,Docker、クラウドではAWSを主に使っております。 既存システムの調査ではGo, PL/SQL, Java, C, JavaScript, Perlなどを見ますね。ちょっとした確認程度も含めて色々な技術に触れる機会があります。 S.Oさん 新しく触ったりする技術や機能は基本的にメンバー間で知識が偏らないように進めています。 最近では監視設定を導入する際に、メンバーが新機能を見つけては共有会などを開いたりしていました。 業務時間内で調査する時間を意識的に取って、メリットデメリットを共有したりなどを話し合って技術選定をしています。 R.Fさん S.O君やM.M君がよく便利な機能を見つけて提案してくれていてとても助かっています。 今まで取り扱っていたシステムは社内向けのAPIだったため、厳密な性能を求められていませんでした。そこからお客様が見るデータを取扱うように切り替わったので、監視の徹底やAPIの安定稼働をシステム的に達成するためにAWSのApplication Signalsを導入しました。 S.Oさん 新しい技術の導入は、以前の開発案件で上がった課題を基に考えることが多いですね。 M.Mさん 雑談から「ここの部分まだいけてないよね」とか共有したり、話をしていく中で「その手があったか!」みたいな気付きもあって、そこから課題の共有や発見、解決に繋がったりしています。 業務で苦労した、頑張った案件や経験はどういったものがございますか R.Fさん 私がサブチームリーダーになる前のチームは、個人の開発力でタスクをこなしていく風潮が強かったんです。なので「チームで開発を行っていく」という意識を定着させることに取り組みました。 開発に関する課題を開発力のある人が解決してくことで、仕事が回る場面も勿論ありました。そうすることで新しい技術をチームに取り入れる機会もあったんですが、全員がその技術を扱えるわけではなかったり、長い目で見た時のパフォーマンスやチームの再現性に繋がらない状況でした。 このままでは将来を見越した開発という観点で安心できないと考え、チームビルディングを考えるようになりました。 タスクの大小に関わらず『チームとしてどうすべきか』という観点で話し合い、チーム全員で仕事の進め方を決めるようにしています。個人ではなくチームとして活動する意識を日頃から持つことを意識しています。普段の声掛けやコミュニケーションや打ち合わせなどでこういったイメージを持ってもらうようにしています。 S.Oさん 私は他チームが持っていた請求系システムを刷新したことが一番記憶に残っています。レガシーなシステムだったので構造を理解するのに苦労はしましたが、請求系のドメイン知識や、PL/SQLに関する知識が付きました。 また、リプレイス先の技術選定については、前プロジェクトで似たような構成でシステム刷新したことから、同様にAWSを採用しました。 以前開発したシステムに関する課題の解決策を考えて適応することで、アーキテクチャや監視、テスト周りをより良いものに作ることができ、結果としてシステム全般に関する面でチームとして成長できたという実感があります。 M.Mさん 細かい機能追加ももちろん頑張っていますが、なんだかんだで毎年一件ぐらい新規開発案件があって、そこでは特に気合を入れて頑張っています。先ほどのS.Oさんの話にもあったように前回の開発の経験から反省を生かしてより良いシステムを作る雰囲気がありますね。 同じ様な構成のシステムであったとしても、クラウドが持つ便利な機能を新たに取り入れたり、システムごとのやりたいことを考えて構築しています。より良くできたら達成感がありますね。 最近はこういった経験から、ただ作るだけでなく運用の負担を軽くするために何が出来るかを考えられるようになってきたのでチームとして良い傾向だと感じています。 後編に続きます! 今回はニフティの課金システムチームのインタビューの様子をお届けしました。続きは後編の記事をご覧ください。 【インタビュー】ニフティの課金系システムチームに聞く!チームの課題と余暇の過ごし方【課金システム後編】 このインタビューに関する求人情報 /ブログ記事 課金システムチームの求人情報 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する .is-style-rounded + .has-text-color{ font-size:95%; }
アバター
はじめに こんにちは。ニフティ株式会社の添野 隼矢です。 Vitestの Browser Modeが興味深いと聞き、実際に試してみようと思い、 本ブログ記事を書くことにしました。 以下、Vitestの公式ガイドの「Getting Started」に従って、Browser Modeを試していきます。 https://vitest.dev/guide/browser/ 今回は、以下の項目でテスト環境を作成します。 テスト言語:TypeScript ブラウザプロバイダ:Playwright ブラウザ:Chromium フレームワーク:Svelte $ pnpx vitest init browser This utility will help you set up a browser testing environment. ? Choose a language for your tests › - Use arrow-keys. Return to submit. ❯ TypeScript - Use TypeScript. JavaScript Choose a language for your tests › TypeScript ? Choose a browser provider. Vitest will use its API to control the testing environment › - Use arrow-keys. Return to submit. ❯ playwright - Playwright relies on Chrome DevTools protocol. Read more: https://playwright.dev webdriverio preview Choose a browser › chromium Choose your framework › svelte Install Playwright browsers (can be done manually via 'pnpm exec playwright install')? … yes Installing packages... @vitest/browser, @testing-library/svelte, playwright, @sveltejs/vite-plugin-svelte Packages: +111 -3 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- Progress: resolved 208, reused 164, downloaded 7, added 111, done node_modules/.pnpm/msw@2.4.1/node_modules/msw: Running postinstall script, done in 1.5s devDependencies: + @sveltejs/vite-plugin-svelte 3.1.2 + @testing-library/svelte 5.2.1 + @vitest/browser 2.0.5 + playwright 1.46.1 Done in 5s Created a config file for browser tests vitest.config.ts Added "test:browser" script to your package.json. Installing Playwright dependencies with `pnpx playwright install --with-deps`... Packages: +3 +++ Progress: resolved 3, reused 3, downloaded 0, added 3, done Downloading Chromium 128.0.6613.18 (playwright build v1129) from https://playwright.azureedge.net/builds/chromium/1129/chromium-mac-arm64.zip 138.1 MiB [====================] 100% 0.0s Chromium 128.0.6613.18 (playwright build v1129) downloaded to /Users/Library/Caches/ms-playwright/chromium-1129 Downloading FFMPEG playwright build v1009 from https://playwright.azureedge.net/builds/ffmpeg/1009/ffmpeg-mac-arm64.zip 1 MiB [====================] 100% 0.0s FFMPEG playwright build v1009 downloaded to /Users/Library/Caches/ms-playwright/ffmpeg-1009 Downloading Firefox 128.0 (playwright build v1458) from https://playwright.azureedge.net/builds/firefox/1458/firefox-mac-arm64.zip 79.6 MiB [====================] 100% 0.0s Firefox 128.0 (playwright build v1458) downloaded to /Users/Library/Caches/ms-playwright/firefox-1458 Downloading Webkit 18.0 (playwright build v2051) from https://playwright.azureedge.net/builds/webkit/2051/webkit-mac-13-arm64.zip 68.3 MiB [====================] 100% 0.0s Webkit 18.0 (playwright build v2051) downloaded to /Users/Library/Caches/ms-playwright/webkit-2051 Add "@vitest/browser/providers/playwright" to your tsconfig.json "compilerOptions.types" field to have better intellisense support. Created example test file in vitest-example/HelloWorld.test.ts You can safely delete this file once you have written your own tests. All done! Run your tests with pnpm test:browser pnpm run test:browser を実行すると、 @sveltejs/vite-plugin-svelte パッケージがES module (ESM)であるのに対し、 CommonJSのコンテキストでインポートしようとしているとエラーが出ます。 そのため、package.jsonに以下を追加します。 "type": "module" package.jsonの修正後、再度実行すると新たなエラーが発生します。 表示されたエラーは、Svelteコンポーネント内でTypeScriptを使用しているにも関わらず、 TypeScriptのプリプロセッサが設定されていないというものになります。 そのため、以下の内容のsvelte.config.jsを追加します。 import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; const config = { preprocess: vitePreprocess(), }; export default config; svelte.config.jsを追加後、再度 pnpm run test:browser を実行したところ、新たなエラーが発生しました。 表示されたエラーは、プロジェクトにSvelteパッケージが導入されていないことを示しています。 そのため、Svelteを追加します。 $ pnpm add svelte 再度実行すると、ブラウザが正常に立ち上がり、テストが正常に実行されていることを確認できました。 $ pnpm run test:browser RERUN x1 ✓ vitest-example/HelloWorld.test.ts (1) ✓ renders name Test Files 1 passed (1) Tests 1 passed (1) Start at 17:20:25 Duration 13ms PASS Waiting for file changes... press h to show help, press q to quit 次に、Vitestの動作をより深く理解するために、initで自動生成されたコードを以下のように修正してみます。 最初は何も表示されていない画面を表示し、1秒後に “Hello Vitest!” というテキストが表示されるような処理に変更 HelloWorld.svelte <script lang="ts"> export let name: string </script> <h1>Hello {name}!</h1> HelloWorld.test.ts import { expect, test } from 'vitest' import { render } from '@testing-library/svelte' import HelloWorld from './HelloWorld.svelte' test('renders name', () => { const { getByText } = render(HelloWorld, { props: { name: 'Vitest' }, }) const element = getByText('Hello Vitest!') expect(element).toBeInTheDocument() }) 修正後のコード HelloWorld.svelte <script lang="ts"> import { fade } from 'svelte/transition'; export let name: string; let visible = false; setTimeout(() => { visible = true; }, 1000); </script> {#if visible} <h1 transition:fade>Hello {name}!</h1> {/if} HelloWorld.test.ts import { expect, test } from 'vitest' import { render } from '@testing-library/svelte' import HelloWorld from './HelloWorld.svelte' test('shows greeting after delay', async () => { const { getByText } = render(HelloWorld, { props: { name: 'Vitest' } }) // 最初はHello Vitest!が表示されていないことを確認 expect(() => getByText('Hello Vitest!')).toThrow() // 遅延後にHello Vitest!が表示されることを確認 await new Promise(resolve => setTimeout(resolve, 1100)) expect(getByText('Hello Vitest!')).toBeVisible() }) テストを実行した結果、以下の動作が確認できました。 1. テスト開始直後:画面上に何も表示されない。 2. 1秒経過後:画面に “Hello Vitest!” というテキストが表示される。 上記より、以下の点が確認できました。 1. Vitestが1秒の遅延を正確に処理し、その後の状態変化を適切にテストできていること。 2. 実際のブラウザに近い環境でコンポーネントが正しくレンダリングされ、DOMの更新が適切に行われていること。 3. 意図した動作(最初は何も表示せず、1秒後に特定のテキストを表示する)が正確にテストされ、期待通りの結果が得られること。 この結果から、Vitestの Browser Modeが非同期処理を含むUIコンポーネントのテストに効果的であることが分かりました。 特に、時間に依存する動作や状態変化を伴うコンポーネントのテストにおいて、Vitestが優れた能力を発揮することが確認できました。 VitestのBrowser Modeの機能を試していく過程で、Playwrightでよく使用していたスクリーンショット機能を試してみました。 await page.screenshot({ path: 'screenshot.png' }) しかし、上記をテストコードに追加して実行したところ、 page オブジェクトでエラーが発生しました。 上記のエラーより、Vitestの環境ではPlaywrightの page オブジェクトが直接利用可能ではないことが分かりました。 まとめ VitestのBrowser Modeは、公式には執筆時点ではExperimentalの機能になります。 上記のスクリーンショットの件などもあり、Playwrightの直接的な代わりにはならなさそうですが、単純なコンポーネントテストであれば、Browser Modeも考慮にいれてよいかもしれません。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに N1! 制度 でスクラムエバンジェリストを担当している西野です。 8月にCTIでCo-Avtiveコーチングの研修をうけてきたのですが、スクラムマスターにかなりフィットするスキルだなと思ったのでレポートしてみたいとおもいます。 コーチングに興味を持ったきっかけ 数年前、スクラムマスターについてより深く学ぶ「アドバンスドスクラムマスター」の研修を受けた際、コーチングをやってみるワークがありました。 スクラムチームの成長を助ける方法として良さそうだと思ったので、その研修以来1on1の場を通じてメンバーにコーチングをやっています。 スクラムチームに対しコーチングを始めたころは手応えを感じていたのですが、時間が経つにつれて 「恣意的に会話誘導しているかも…」 「アドバイスをしてしまっていないか?」 「1on1を終えたメンバーに活力を与えられているのだろうか?」 といった自身のスキルの限界を感じるシーンが増えてきて、一度きちんと学びたいという気持ちが膨らんできました。 その時ふと、「コーチング研修を受けて良かった」と言っていた人がいたことを思い出し、コーチング研修に興味を持つようになりました。 なお、ニフティのN1! ではこういった研修などに使える活動予算が年間100万円(!)もあるので、その予算を使わせてもらっています。 スクラムマスターはコーチング研修をうけるべき? これはかなり食い気味に「YES」です。 スクラムガイドにも ⾃⼰管理型で機能横断型のチームメンバーをコーチする。 組織へのスクラムの導⼊を指導・トレーニング・コーチする。 https://scrumguides.org/docs/scrumguide/v2020/2020-Scrum-Guide-Japanese.pdf とコーチングへの言及があります。 ティーチング(教える)ことは学校や会社で経験していますが、自分の場合きちんとしたコーチングを受けた経験はありませんでした。 スクラムマスターに必要なスキルである「コーチング」を、頭ではなく体で覚える経験は、なかなか他の手段で代替が効かないものだと思います。 そもそもコーチングってなに? 一般的にコーチというと、スポーツのコーチというイメージが強いと思います。 自分の場合コーチングについて学び始める前は、コーチは「プレイヤーになにかアドバイスをする人」または「練習をさせる指導者」というイメージを持っていました。 スクラムガイドにある「コーチ」や、ここでいう「コーチング」は上記とは異なります。 「アドバイスをする」「練習などの行動を指示する」のはコーチングではなくティーチングと呼ばれるスキルです。 コーチングは「気持ちを上向かせる」「相手の成長をサポートする」、要は「他者の在り方を支援」をすることに特化したスキルだと今は思っています。 (念の為、近年のスポーツのコーチは、いわゆる自己成長を促すコーチング要素が多くなっていると聞いたことを付け加えておきます) コーチング研修を受けてみてどう変わったか 研修の詳細を書くことはできないのですが、もし書けたとしても「体験しないと伝わらないな」という内容が多かったので、研修を受けて気付いたことを中心にまとめます。 話題にフォーカスして人を見ていなかったと気づけた 研修をうけて一番衝撃が大きかったことは、これまでやってたつもりの「傾聴」が全然できていなかったことでした。 傾聴のつもりで自分がやっていたのは、「相手の話の内容をちゃんと理解して、次に何の質問をしようか一生懸命考えている」という状態でした。 相手の会話にどう切り込もうか考え込んでいるうちに、相手が何の話をしているかわからなくなってしまうことは普段の経験でもあるのではないでしょうか。 話の内容を深く理解することと、相手を知ることは必ずしもイコールではありません。 ほかに、メモをとりながら1on1をしていたことも、話の内容ばかりに目を向けてしまって相手を見ないことに繋がってしまっていたかもと思うようになりました。 相手の表情や声のトーン、体の動きをよく観察することも含めて傾聴です。 今までの1on1の場で、相手に意識がむいていない瞬間がどれほど多かったか気付かされました。 コーチングは異空間 「傾聴」をはじめとして、普段の会話とコーチングの対話には大きな差があります。日常会話ではまずしないな、と思うような質問をあえてすることが相手の新たな気づきに繋がります。 コーチングを日常の会話と同じ流れでやってしまうと、相談会や愚痴の発散で終わってしまいがちです。 「ここはコーチングの場だ」という違いを、コーチだけでなくコーチをうける側(クライアント)にも強く意識してもらう必要があります。 実際、研修の理解を深めるためにコーチングをやってみたときに、コーチングの場であることをクライアントが意識できず失敗してしまったことがあります。 練習として家族にコーチングをしてみたのですが、「コーチングの練習をしたい」としか伝えておらず、コーチングとは何か、この場をどうしていきたいかという前提の会話をしなかったために「何回も同じような質問をされて嫌な気持ちになった」というフィードバックをもらってしまいました。 家族間なので正直にフィードバックがもらえてまだよかったのですが、今までチームメンバーに対してこういうコーチングをしていたのかもしれないと思うと怖くなります。 効果的にコーチングを行うための聞き方や質問の仕方は日常会話とはかなりかけ離れています。研修中、飛躍した提案や、考えたこともないようなことを聞かれてクライアントとしてドキッとするシーンもありました。 研修を受けてみると、その「ドキッとする」気持ちは自分が思い悩むだけでは出ない答えに繋がることが体験できます。 一人で考えていると、どうしても思考の範囲が狭くなってしまいます。一見突拍子もなく感じる質問が、それを打破するきっかけになるのです。 このように「相手をドキッとさせるかもしれない」ような質問をする場でもあるので、受ける側とどんな場をつくっていくのかを擦り合わせないと、場が成立しないばかりか、ストレスを感じる方向に働いてしまう可能性があることにも気づけました。 スクラムマスターとしてどうコーチングを行うか 最後に、コーチングの研修をうけてみて自分がスクラムマスターとしてチームに接する上で変えようと思ったことを挙げていきます。 自律には「自分の価値観」が必要 スクラムマスター同士で集まると「どうやったらチームの自律を促せるのか」という話題があがることが過去何回かありました。 自律を養うためには「自分で決定、行動すること」を繰り返す必要があると思っていますが、そもそも決定することが苦手という人もいるでしょう。 決定は得意という人でも、決めたことを必ずやり切れる人は滅多にいません。 人が意志決定する重要なファクターとして、自分の「価値観」があります。 どれだけ世の中的に良いとされていても、自分にとって価値があると思えないことを無理に決定してしまうと、モチベーション持てずにやりきることができません。 仕事における「自分の価値観」が見つかるようチームをサポートする 仕事のフレームワークはさまざまにありますが、スクラムは「価値観」に言及していることが特徴的だと思います。 スクラムがうまく機能していないときは、スクラムにおける価値基準と、その人の仕事の価値観を照らし合わせることをしていないのではないか、と気づきました。 しかし、働くうえで何を大事にしているかをぱっと答えられる人はそれほど多くはないでしょう。自分も改めて考えると言語化が難しいです。 スクラムの価値基準は「確約」「集中」「公開」「尊敬」「勇気」ですが、その価値はわかりつつも、チームメンバーの価値観がスクラムの価値基準とどの程度マッチしているのか、あまり向き合ってこなかったと思います。 スクラムマスターとして、まずチームメンバーそれぞれが「自分の価値観」が見つけられるようサポートする必要があるのではないか?と思い至りました。その手段としてコーチングがあります。 「自分自身」に向き合うことは、多くの人が無意識に避けている部分です。 コーチングの時間を持つことで、意図的に「自分」や「自分の価値観」に向き合ってもらうことができます。 スクラムマスターは、そのときコーチとして「自分」に向き合うことの抵抗感をやわらげ、自分自身と向き合うことから逃げないように導くことで、指示以外の方法でその人の行動を変容させることができるのではないかと思えるようになりました。 過去ではなく未来に目を向けさせる 研修前にコーチングを意識して指示ではなく質問をするよう心がけていた時期もありますが、「なんでこのアクションになったの?」「なんでこの決定になったの?」という「WHY」の問いかけをしてしまうことがありました。 これは質問される方はもちろん、質問をする側も息苦しく、これを繰り返しても安心できるチームになりそうなイメージがわきませんでした。 今なら、この問いが効果的でない理由がわかります。未来に焦点を当てた問いかけではないからです。 もし同じことを問うなら「このアクションをすることで、何を得たい?」「この決定をすることはこの先どんな影響がある?」というような質問に変えると思います。 スクラムマスターとして、変えられない過去に焦点を向けるより、変えられる未来の行動に焦点をあわせてもらうほうが、よりスクラムチームの成長をサポートしやすいからです。 自分に向き合い、変化し、成長するために 私はコーチングの研修をうけてみて「スクラムチームが無意識のうちに逃げていることに対面させ、行動の変容を促し、成長を導く」ということにもっとチャレンジしたいと思うようになりました。 コーチングを通じてチームメンバーが自身の価値観を見つけ、自律的に行動できるようサポートすることは、スクラムチーム全体の成長に繋がります。 もうひとつ重要な気づきがあります。スクラムマスター自身が肉体的・精神的に元気で前を向ける状態にないとコーチは難しいということです。 自分のことでいっぱいいっぱいのときは、他人に目を向けられません。他人をみれないときは、スクラムマスターとして十全には振る舞えません。 チームメンバーが価値観に向き合うよう促すと同時に、スクラムマスターである自分自身の価値観にも向き合っていく必要性を感じています。 この自分自身と向き合う手段として、外部のコーチをつけてみるという選択肢があることを知れたのもよかったです。 さいごに コーチングの基礎研修を受けた後のアクションとして、コーチングの学習・実践をより深めていく方向もあったのですが、今はまず自分の価値観を知る方向でアプローチしていこうと考えています。 誰の人生においても、自分とどう向き合って生きていくかは大きな課題として立ち塞がります。 スクラムマスターは、スクラムチームや組織など外側に目を向けている時間が長いです。そんな中で、スクラムチームや組織の中に自分がいることを忘れていた気がします。 コーチングの学習を通じて、他者の成長を促すだけでなく、スクラムマスターである自分自身と正しく向き合おうという視点が持てたことは大きな変化でした。 記事の最初に「スクラムマスターはコーチングの研修を受けるべき」と書きましたが、これはチームのためだけでなく、スクラムマスターの自己管理のためでもあります。 スクラムマスターが成長する一つの手段として、コーチングの研修を受けてみるのはかなり効果的だと思いました。 今は、社内にコーチ制度を導入するために、コーチングに興味がある人を集めてコーチングの価値を広められるよう頑張っています。なにか実を結んだら、この取り組みも記事にしたいと思います。お楽しみに! ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
イベント概要 NIFTY Tech Talkは、ニフティ株式会社の社員が主催するトークイベントです。 本イベントでは、ニフティグループの社員が業務を通じて学んだことを発信しています! テーマ 天気予報があなたの手元に届けられるまで   〜今日傘を持って出掛けることができるための仕組みの裏側〜 2008年に開始した@nifty天気予報がついにリニューアル。その開発のウラ側をお話ししていきます。 また、今回はコラボ企画となっており、ウェザーニューズ様とコラボします! リニューアルで使用したNext.js、Go、GraphQLでどのように開発していったのか、気象データを提供する側の仕組みなどをウェザーニューズとニフティの技術者が紹介していきますので、是非ご参加ください。 今回はハイブリッド開催となっております。 お時間やご興味のある方は是非新宿まで足をお運びください! 開催概要 日程:10月2日(水)19:00〜20:30 ハイブリッド開催 現地参加: ニフティ 新宿本社 来場予定で19:30以降に来られる方は「 @NIFTYDevelopers 」からご連絡お願いします オンライン参加: YouTube Live 登録はこちらから https://nifty.connpass.com/event/328943/ こんな方におすすめ 以下について興味があるかたにおすすめです Next.js、Go、GraphQLについて興味がある方 サービスリニューアル時の技術選定に興味がある方 他社とのデータ連携を行うシステムに興味のある方 デザインシステムを用いたフロントエンド開発に興味がある方 タイムテーブル 時間 コンテンツ 19:00 – 19:10 オープニング 19:10 – 19:25 天気予報があなたの手元に届けられるまで(仮) 19:25 – 19:40 @nifty天気予報:フルリニューアルへの挑戦 19:55 – 20:10 @nifty天気予報のフロントエンドを実装するまで 20:10 – 20:25 AWS AppSyncを用いたGraphQL APIの開発について 20:25 – 20:30 クロージング 登壇者プロフィール 登壇者未定(ウェザーニューズ) ウェザーニューズ様からの登壇となります。 登壇者決まりましたら反映いたします 渡邊 大介(ニフティ) 自社WEBサービスの開発・運用担当をしています。 @nifty天気予報ではPLをしています。 佐々木 優(ニフティ) @nifty天気予報で開発を担当しています。 フロントエンドの他、最近はAWSのやサービスのインフラコストを削減できないかなどを勉強中です。 川端 航平(ニフティ) SREとして社内横断でプロジェクトサポートをしています。 @nifty天気予報のフルリニューアルプロジェクトにもサポートとして参加。 ニフティグループでは一緒に働く仲間を募集中です 新卒採用、キャリア採用を実施しています。ぜひ リクルートサイト をご覧ください。 ニフティエンジニアが業務で学んだことやイベント情報を エンジニアブログ にて発信しています! ニフティエンジニアのX(旧Twitter)アカウント NIFTY Tech Talkのことや、ニフティのエンジニアの活動を発信していきます。 @NIFTYDevelopers アンチハラスメントポリシー 私たちは下記のような事柄に関わらずすべての参加者にとって安全で歓迎されるような場を作ることに努めます。 社会的あるいは法的な性、性自認、性表現(外見の性)、性指向 年齢、障がい、容姿、体格 人種、民族、宗教(無宗教を含む) 技術の選択 そして下記のようなハラスメント行為をいかなる形であっても決して許容しません。 不適切な画像、動画、録音の再生(性的な画像など) 発表や他のイベントに対する妨害行為 これらに限らない性的嫌がらせ 登壇者、主催スタッフもこのポリシーの対象となります。 ハラスメント行為をやめるように指示された場合、直ちに従うことが求められます。ルールを守らない参加者は、主催者の判断により、退場処分や今後のイベントに聴講者、登壇者、スタッフとして関わることを禁止します。 もしハラスメントを受けていると感じたり、他の誰かがハラスメントされていることに気がついた場合、または他に何かお困りのことがあれば、すぐにご連絡ください。 ※本文章はKotlinFest Code of Conductとして公開された文章( https://github.com/KotlinFest/KotlinFest2018/blob/master/CODE-OF-CONDUCT.md )を元に派生しています。 ※本文章はCreative Commons Zero ライセンス( https://creativecommons.org/publicdomain/zero/1.0/ ) で公開されています。
アバター
はじめに みなさん、こんにちは!「爆速でシリーズ」って聞いたことありますよね?でも、そのまま真似してみても、環境の違いやバージョンアップのせいで動かないことってありますよね。でも大丈夫!今回は、そんな壁に負けずに挑戦してみましょう! お題概要 今回のミッションは、S3のバケットを確認して、ファイルのダウンロードとアップロードができるようにすることです。わくわくしますね! お題詳細 ある日、企画部門から「システムで生成されるCSVデータを確認したい」という依頼が飛び込んできました。時間がない中で、どうしよう?どうしよう?と悩んでいたら、「S3を閲覧するツールを作ってみては?」というアイデアが浮かびました。他にも方法はあるかもしれませんが、今はこのアイデアで突き進むしかありません! そこで、SvelteKitを使ってS3バケットの一覧参照、ダウンロード、アップロードができるツールを作ることにしました。さあ、一緒に挑戦してみましょう! 作業概要 ここからが本番です!以下の順番で作業を進めていきます SvelteKitの初期設定 .envファイルの作成 ディレクトリ、ファイルを作る 追加でいろいろインストールする ダウンロード機能を実装する アップロード機能を実装する 一覧表示機能を作成する 表示用ページを作る 起動する 動かしてみた 準備はいいですか?それでは、一つずつ見ていきましょう! 1. SvelteKitの初期設定 まずは、SvelteKitのプロジェクトを立ち上げます。今回は、Node.jsのコンテナ内で作業していますが、コンテナの設定は省略させていただきます。ごめんなさいね! 公式ページ を参考に、以下のコマンドを実行しました $ npm create svelte@latest ./ > npx > create-svelte ./ create-svelte version 6.3.10 ┌ Welcome to SvelteKit! │ ◇ Which Svelte app template? │ SvelteKit demo app │ ◇ Add type checking with TypeScript? │ Yes, using TypeScript syntax │ ◇ Select additional options (use arrow keys/space bar) │ Add ESLint for code linting, Add Prettier for code formatting, Add Playwright for browser testing, Add Vitest for unit testing │ └ Your project is ready! Install more integrations with: npx svelte-add Next steps: 1: npm install 2: git init && git add -A && git commit -m "Initial commit" (optional) 3: npm run dev -- --open To close the dev server, hit Ctrl-C Stuck? Visit us at https://svelte.dev/chat 選択肢はこんな感じで進めました SvelteKit demo appを選択 TypeScriptを使用 ESLint、Prettier、Playwright、Vitestを選択 最後に、npm installを実行して準備完了です! npm install 2. .envファイルの作成 S3にアクセスするための大切な情報を.envファイルに保存します。セキュリティ面で気になる方もいるかもしれませんが、今回は爆速重視で進めちゃいます! touch .env .envファイルの中身はこんな感じです # 機密 AWS_ACCESS_KEY_ID= # IAMで発行したキーを入れてください AWS_SECRET_ACCESS_KEY= # IAMで発行したシークレットキーを入れてください AWS_REGION=ap-northeast-1 # 使用するリージョンを指定してください AWS_ENDPOINT_URL=https://s3.amazonaws.com # 必要に応じて変更してください S3_BUCKET_NAME= # 使用するバケット名を入れてください エンドポイントは、業務用に別のものを使う可能性もあるので、変更できるようにしています。臨機応変に対応できるって素晴らしいですよね! 3. ディレクトリ、ファイルを作る さて、ここからが本格的な作業の始まりです!各機能のコードを置くためのディレクトリとファイルを作成しましょう。 mkdir ./src/routes/api mkdir ./src/routes/api/download mkdir ./src/routes/api/upload mkdir ./src/routes/api/list-files touch ./src/routes/api/download/+server.ts touch ./src/routes/api/upload/+server.ts touch ./src/routes/api/list-files/+server.ts 正直に言うと、私もSvelteKitをまだ完全には理解していません。でも大丈夫! 社内でよくSvelteKitの話を耳にする 最新技術に乗り遅れたくない GPTがあるから何とかなる! という3つの理由でSvelteKitを選びました。ファイル構成が正攻法かどうかはわかりませんが、とにかくチャレンジです! 4. 追加でいろいろインストールする S3の操作には@aws-sdk/client-s3を使います。これはGPTさんが教えてくれました。ありがとう、GPTさん! さらに、@types/nodeもインストールしておくと良さそうです。準備万端ですね! npm install @aws-sdk/client-s3 npm install --save-dev @types/node 5. ダウンロード機能を実装する GPTさんに「S3のバケットのダウンロード機能をSvelteKitで作成したい」とお願いして、こんなコードができました src/routes/api/download/+server.ts import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; import type { RequestHandler } from '@sveltejs/kit'; import { error } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; const s3Client = new S3Client({ region: env.AWS_REGION, credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY }, endpoint: env.AWS_ENDPOINT_URL, }); export const GET: RequestHandler = async ({ url }) => { const fileName = url.searchParams.get('file'); if (!fileName) { throw error(400, 'File name is required'); } const bucketName = env.S3_BUCKET_NAME; const s3Key = fileName; try { const command = new GetObjectCommand({ Bucket: bucketName, Key: s3Key, }); const response = await s3Client.send(command); if (!response.Body) { throw error(404, 'File not found'); } // ストリームをArrayBufferに変換 const chunks: Uint8Array[] = []; for await (const chunk of response.Body as AsyncIterable<Uint8Array>) { chunks.push(chunk); } const fileBuffer = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)); let offset = 0; for (const chunk of chunks) { fileBuffer.set(chunk, offset); offset += chunk.length; } return new Response(fileBuffer, { headers: { 'Content-Type': response.ContentType || 'application/octet-stream', 'Content-Disposition': `attachment; filename="${fileName}"`, }, }); } catch (err) { console.error("Error", err); throw error(500, 'Failed to download file from S3'); } }; 詳細は省きますが、なんとなくバイナリデータをレスポンスに入れている感じがわかりますね。面白いです! 6. アップロード機能を実装する アップロード機能もGPTさんにお願いしました。結果はこちら src/routes/api/upload/+server.ts import { error, json } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit'; import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; import { env } from '$env/dynamic/private'; const s3Client = new S3Client({ region: env.AWS_REGION, credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY }, endpoint: env.AWS_ENDPOINT_URL, }); export const POST: RequestHandler = async ({ request }: { request: Request }) => { try { const formData = await request.formData(); const file = formData.get('file') as File; if (!file) { throw error(400, 'No file uploaded'); } const bucketName = env.S3_BUCKET_NAME; const key = `in/${Date.now()}-${file.name}`; const fileBuffer = await file.arrayBuffer(); const putObjectParams = { Bucket: bucketName, Key: key, Body: Buffer.from(fileBuffer), ContentType: file.type, }; const command = new PutObjectCommand(putObjectParams); await s3Client.send(command); return json({ message: 'File uploaded successfully', key }); } catch (err) { console.error('Error uploading file:', err); throw error(500, 'Internal server error'); } }; const key = の部分に「in/」があるのは、今回はアップロード先を固定してみたかったからです。必要なければ削除してOKです!(直に書くのはよくないですね!) 7. 一覧表示機能を作成する 一覧表示機能も、もちろんGPTさんにお願いしました import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3"; import type { RequestHandler } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; const s3Client = new S3Client({ region: env.AWS_REGION, credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY }, endpoint: env.AWS_ENDPOINT_URL, }); export const GET: RequestHandler = async () => { const bucketName = env.S3_BUCKET_NAME; try { const command = new ListObjectsV2Command({ Bucket: bucketName, Delimiter: '/' }); const response = await s3Client.send(command); const allFiles = []; if (response.Contents) { const files = response.Contents .filter(item => item.Size !== 0) // サイズが0のファイルを除外 .map(item => ({ key: item.Key, size: item.Size, lastModified: item.LastModified })); allFiles.push(...files); } // バケット直下のディレクトリの処理 if (response.CommonPrefixes) { const dirs = response.CommonPrefixes.map(prefix => ({ key: prefix.Prefix, isDirectory: true })); allFiles.push(...dirs); } return json({ files: allFiles }); } catch (err) { console.error("Error listing files:", err); throw error(500, 'Failed to list files from S3'); } }; ListObjectsV2Commandで取得したファイル情報をallFilesに詰め込んでいるんですね。わかりやすい! 8. 表示用ページを作る 最後に表示用ページを作ります。既存のファイルを上書きしちゃいましょう! src/routes/+page.svelte <script lang="ts"> class HttpError extends Error { constructor(public status: number, message?: string) { super(message || `HTTP error! status: ${status}`); this.name = 'HttpError'; } } import { onMount } from 'svelte'; interface S3File { key: string; size: number; lastModified: string; } let files: S3File[] = []; let loading = true; let error = ''; let fileInput: HTMLInputElement; onMount(async () => { await fetchFiles(); }); async function fetchFiles() { try { loading = true; const response = await fetch('/api/list-files'); if (!response.ok) { throw new HttpError(response.status); } const data = await response.json(); files = data.files; } catch (e) { if (e instanceof HttpError) { error = `Failed to fetch files: ${e.message}`; } else { error = 'An unexpected error occurred while fetching files'; } console.error(e); } finally { loading = false; } } function formatBytes(bytes: number, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } async function downloadFile(fileName: string) { try { const response = await fetch(`/api/download?file=${encodeURIComponent(fileName)}`); if (!response.ok) throw new Error('Download failed'); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); } catch (error) { console.error('Error downloading file:', error); alert('Failed to download file'); } } async function uploadFile() { if (!fileInput.files || fileInput.files.length === 0) { alert('Please select a file to upload'); return; } const file = fileInput.files[0]; const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/upload', { method: 'POST', body: formData }); if (!response.ok) { throw new HttpError(response.status, 'Upload failed'); } alert('File uploaded successfully'); await fetchFiles(); // Refresh the file list } catch (error) { if (error instanceof HttpError) { console.error('Error uploading file:', error.message); alert(`Failed to upload file: ${error.message}`); } else { console.error('Unexpected error during upload:', error); alert('An unexpected error occurred during upload'); } } } </script> <h1>S3 Bucket Files</h1> <div class="upload-container"> <input type="file" bind:this={fileInput}> <button on:click={uploadFile}>Upload File</button> </div> {#if loading} <p>Loading...</p> {:else if error} <p class="error">{error}</p> {:else if files.length === 0} <p>No files found in the bucket.</p> {:else} <table> <thead> <tr> <th>File Name</th> <th>Size</th> <th>Last Modified</th> </tr> </thead> <tbody> {#each files as file} <tr> <td> <button on:click={() => downloadFile(file.key)} class="download-link"> {file.key} </button> </td> <td>{formatBytes(file.size)}</td> <td>{new Date(file.lastModified).toLocaleString()}</td> </tr> {/each} </tbody> </table> {/if} <style> .upload-container { margin-bottom: 20px; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } .error { color: red; } button { margin-left: 10px; padding: 5px 10px; background-color: #007bff; color: white; border: none; cursor: pointer; } button:hover { background-color: #0056b3; } .download-link { background: none; border: none; color: #007bff; text-decoration: none; cursor: pointer; padding: 0; font: inherit; } .download-link:hover { text-decoration: underline; } </style> styleもそれっぽく作ってくれていますね!tableタグしか使えない私には夢のようです! 9. 起動する いよいよ起動です!以下のコマンドで立ち上げましょう npm run dev -- --host 0.0.0.0 毎回、「 — –host 0.0.0.0 」を入力するのが面倒な方は、vite.config.tsをこんな感じに変更しておくと便利ですよ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()], server: { host: '0.0.0.0' } }); これで npm run dev だけで起動できます。簡単ですね! 10.動かしてみた 単に開いてみた アップロードしてみた OK押してみた あれ? inとoutのSizeとLast Modifiedの表示、なんだか不思議ですね!まるで宝探しゲームみたい。これは次のアップデートで解明する楽しい謎になりそうです! おっと、「ファイルを選択」ボタンの横のファイル名がクリアされていないみたい。でも大丈夫!これはユーザーに「あなたが最後に選んだファイルだよ〜」って親切に教えてくれているんですね。ちょっとしたサプライズ機能かも?(笑) 終わりに(後日談というか今回のオチ) なんと驚くことに、私たちが苦労して作ったものと似たようなものを公式が作っていたんです! https://github.com/aws-amplify/amplify-ui/issues/5731 「時代が私に追いついた!」なんて言えたら格好いいんですけどね(笑)。でも、これって逆に考えると、私たちが本当に必要とされているものを作っていたってことですよね。すごくない? この経験を通して、私はとても大切なことを学びました。それは、「SvelteKitがよくわからなくても、GPTの力を借りればここまでできる」ということです。正直、最初は不安でしたが、やってみたらどんどん形になっていって、すごくワクワクしました! みなさんも、「やったことないからわからない」って思っても、まずは一歩踏み出してみてください。きっと新しい発見があるはずです。失敗を恐れずに、どんどんチャレンジしていきましょう! そうそう、最後にちょっとした宣伝です。ニフティでは、このような面白い挑戦を常に募集しています。一緒に新しいことにチャレンジしてみませんか?きっと楽しい経験になりますよ! さあ、次はどんな冒険が待っているでしょうか。楽しみですね!それでは、また次回の「爆速シリーズ」でお会いしましょう。バイバーイ!  終わりの終わりに いかがでしたでしょうか。何とも言えないテンションだったかと思います。今回はある程度、原稿を書いた状態で、「明るい口調で書き直して!」とGPTさんにお願いしてみたところこのような文章になりました。中の人はこんなキャラではありません。お許しください。ただ、最後の「時代が私に追いついた」の部分については、「本当に必要とされているものを作っていた」なんてポジティブな考えはまったく思っていませんでした。こういうものの考え方もあるのだなあと感心しました。コード生成だけではなく考え方の参考にもなると気づかされまして今後、活用していきたいと思います。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
こんにちは。ニフティ株式会社のyamanakaです。 所属しているチームでPlaywrightのフィクスチャを導入したため、備忘録も兼ねて調べた内容やフィクスチャ概要から実際の使用例( @nifty紹介特典 から紹介コード払い出し)とともに紹介します。 はじめに ウェブアプリケーションの自動テストは、品質保証プロセスにおいて不可欠な要素となっています。その中で、Microsoftが開発したPlaywrightは、強力で使いやすいクロスブラウザテスト自動化ツールとして注目を集めています。 Playwrightは、Chromium(Chrome、Edge)、Firefox、WebKitベースのブラウザに対応し、JavaScriptやTypeScriptを使用して直感的なAPIを提供します。これにより、高速で信頼性の高いE2Eテストを簡単に作成できます。 しかし、効率的なテスト環境を構築するには、単にテストケースを書くだけでは不十分です。ここで重要な役割を果たすのが「フィクスチャ」です。 フィクスチャとは フィクスチャとは、テストの実行に必要な前提条件や環境を設定するためのコードのことです。Playwrightにおいて、フィクスチャは以下のような役割を果たします。 1. テスト環境の一貫性を保証 2. コードの重複を削減 3. テストの可読性と保守性を向上 4. テスト実行の効率化 例えば、ブラウザの起動、ページの読み込み、ユーザーログインなど、多くのテストケースで共通して必要な操作をフィクスチャとして定義することで、テストコードをクリーンに保ち、効率的に管理できます。 フィクスチャの定義 Playwrightでは、 test.beforeEach() や test.beforeAll() などのフックを使用してフィクスチャを定義します。一般的な形式は以下のとおりです。 javascript import { test as base } from '@playwright/test'; export const test = base.extend({ myFixture: async ({ }, use) => { // フィクスチャのセットアップ const value = 'フィクスチャの値'; // フィクスチャの使用 await use(value); }, }); 別ファイルでブラウザ、コンテキスト、ページフィクスチャをそれぞれ作成します、そしてこのフィクスチャを使用するテストファイルでは、以下のようにインポートして使用します。 javascript // myFixtures.js import { test as base, chromium } from '@playwright/test'; export const test = base.extend({ page: async ({ }, use) => { const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); await use(page); await context.close(); await browser.close(); }, }); javascript // myTest.spec.js import { test } from './myFixtures'; test('フィクスチャを使用するテスト', async ({ page }) => { await page.goto('https://example.com'); // テストロジック }); 上記により、基本的なブラウザ、コンテキスト、ページフィクスチャを使いこなすことで、テストの基盤を強固にできます。 カスタムフィクスチャの作成 基本的なフィクスチャに加えて、プロジェクト固有のニーズに合わせてカスタムフィクスチャを作成することができます。 これにより、テストコードの再利用性と管理性をさらに向上させることができます。 ユーザー認証とAPIレスポンスのモック化を例にあげます。 javascript import { test as base } from '@playwright/test'; export const test = base.extend({ loggedInPage: async ({ page }, use) => { // ログイン処理 await page.goto('https://example.com/login'); await page.fill('#username', 'testuser'); await page.fill('#password', 'testpass'); await page.click('#login-button'); await use(page); }, // APIモックを設定するフィクスチャ mockApi: async ({ page }, use) => { // APIリクエストのモックを設定 await page.route('**/api/dashboard', route => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ username: 'testuser', lastLogin: '2023-04-01' }) }); }); // モック設定済みのページをテストに提供 await use(page); }, // ログイン済みかつAPIモック適用済みの環境を提供するフィクスチャ preparedTestEnvironment: async ({ loginPage, mockApi }, use) => { // loginPage に mockApi の設定を適用 await mockApi(loginPage); // 準備完了した環境をテストに提供 await use(loginPage); }, }); この組み合わせたフィクスチャを使用するテストは以下のようになります。 javascript import { test } from './myFixtures'; // テスト test('ダッシュボードテスト', async ({ preparedTestEnvironment }) => { // 実際のダッシュボードページに遷移 await preparedTestEnvironment.goto('https://example.com/dashboard'); // ここで 'https://example.com/api/dashboard' にリクエストを送る // テストアサーションなど }); 遷移する処理とAPIリクエストのモックを別々の概念として考えながら組み合わせてユニットテストを構築すると良いのかなと記事にしながら思いました。 フィクスチャのスコープ Playwrightでは、フィクスチャのスコープを適切に設定することで、テストの効率と信頼性を向上させることができます。 テストファイルスコープ テストファイル内でのみ使用するフィクスチャは、そのファイル内で定義します。 javascript import { test as base } from '@playwright/test'; const test = base.extend({ localFixture: async ({}, use) => { // このファイル内のテストでのみ使用可能なフィクスチャ await use('local value'); }, }); test('ローカルフィクスチャを使用', async ({ localFixture }) => { console.log(localFixture); // 'local value' }); グローバルスコープ プロジェクト全体で使用するフィクスチャは、専用のファイル(例: fixtures.ts )で定義し、インポートして使用します。 javascript // fixtures.ts import { test as base } from '@playwright/test'; export const test = base.extend({ globalFixture: async ({}, use) => { await use('global value'); }, }); // test.spec.ts import { test } from './fixtures'; test('グローバルフィクスチャを使用', async ({ globalFixture }) => { console.log(globalFixture); // 'global value' }); 非同期フィクスチャの扱い方 Playwrightのフィクスチャは非同期操作を効果的にサポートしており、async/awaitを使用して簡単に非同期処理を組み込むことができます。 基本的な非同期フィクスチャ javascript import { test as base } from '@playwright/test'; export const test = base.extend({ asyncData: async ({}, use) => { const data = await fetchDataFromApi(); await use(data); }, }); test('非同期データを使用', async ({ asyncData }) => { console.log(asyncData); // テストロジック }); エラーハンドリング エラーが発生した場合のハンドリング処理も忘れず作成しましょう。 javascript import { test as base } from '@playwright/test'; export const test = base.extend({ errorProneFixture: async ({}, use) => { try { const result = await fetchDataFromApi(); await use(result); } catch (error) { console.error('フィクスチャの初期化中にエラーが発生しました:', error); throw error; // テストを失敗させる } }, }); フィクスチャの依存関係 フィクスチャは他のフィクスチャに依存することができ、これによって複雑なテスト環境を構築できます。 javascript import { test as base } from '@playwright/test'; export const test = base.extend({ config: async ({}, use) => { // 設定をロード const config = await loadConfig(); await use(config); }, apiClient: async ({ config }, use) => { // configフィクスチャを使用してAPIクライアントを初期化 const client = new ApiClient(config.apiUrl); await use(client); }, loggedInUser: async ({ page, apiClient }, use) => { // apiClientフィクスチャを使用してユーザーを作成 const user = await apiClient.createUser(); // ページにログイン await page.goto(config.loginUrl); await page.fill('#username', user.username); await page.fill('#password', user.password); await page.click('#login-button'); await use(user); }, userWithData: async ({ loggedInUser, apiClient }, use) => { // ログイン済みユーザーにデータを追加 const userData = await apiClient.addUserData(loggedInUser.id, { posts: ['Test Post'] }); await use({ ...loggedInUser, ...userData }); }, }); test('ユーザーデータのテスト', async ({ userWithData, page }) => { await page.goto('https://example.com/profile'); const postTitle = await page.textContent('.post-title'); // 検証 expect(postTitle).toBe('Test Post'); expect(userWithData.posts).toContain('Test Post'); }); この例では、複数のフィクスチャが互いに依存関係を持っています。 1.  apiClient  は  config  に依存 2.  loggedInUser  は  page  と  apiClient  に依存 3.  userWithData  は  loggedInUser  と  apiClient  に依存 実践的な例:払い出したURLに遷移するテスト ニフティでは光回線サービスをお友達に紹介すると、紹介した方&紹介された方それぞれにニフティポイントをプレゼントするサービスがあります。( @nifty紹介特典 ) 今回は上記サービスから紹介コードを払い出し、紹介ページから申込画面に遷移する内容を例にあげます。 import { test as base } from '@playwright/test'; import { expect } from "@playwright/test"; export const test = base.extend({ mainPage: async ({ page }, use) => { await page.goto('https://setsuzoku.nifty.com/syokai/index.htm'); await use(page); }, searchUrl: async ({ mainPage }, use) => { await mainPage.getByRole('link', { name: '紹介する' }).click(); // ページにログイン await mainPage.fill('#id_user_id', 'xxxxxxxx'); await mainPage.click('#next'); await mainPage.fill('#id_pw', 'xxxxxxxx'); await mainPage.click('#login'); const url = await mainPage.evaluate(() => { const element = document.querySelector('.shokaiCode_signUp a'); return element ? element.href : null; }); await mainPage.goto(url); // url を返すように変更 await use({ page: mainPage, url: url }); }, }); test('払い出されたURLに遷移', async ({ mainPage, searchUrl }) => { const { url } = searchUrl; // URLを確認する await expect(mainPage).toHaveURL(url); }); おわりに Playwrightのフィクスチャを導入することで、コードの再利用や共通化することができるためチームとしても一貫したテストを採用しやすくなります。 APIレスポンスのモック化について調べながらコードを書きましたが、ユニットテストに対して柔軟で安定したテスト環境が構築できると思いました。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
この記事は、リレーブログ企画「24卒リレーブログ」の記事です。 はじめに 初めまして、新卒1年目の緑川です。 現在、OJTジョブローテの1期目として、 会員システムグループの第二開発チーム に所属しています。主な業務はオプションサービスの開発と運用であり、フロントエンド・バックエンド問わず様々な技術を日々学んでいます。 開発にあたって Svelte というフレームワークを使用する場面があり、そのために Svelteチュートリアル をやってみたので、この記事ではそこで重要だと思った点や難しかった点、知らなかった用語などを自分なりの理解でまとめていきたいと思います。あくまでチュートリアルの説明の補足のようなものなので、そちらを読んでいる前提で執筆します。また、各項目の解説の密度は、自分の理解度に依拠しているために偏りがあり、一部の項は一纏めにしています。 なお、チュートリアルはSvelteとSvelteKit(Svelteの拡張版のようなもの。今回は割愛)それぞれの基本と応用とで4つのPartに分かれていますが、今回は Part1:Basic Svelte をやりました。 Svelteとは まずSvelteについて説明します。Svelteとは、前述のチュートリアルによれば、 web アプリケーションを構築するためのツールです。他のユーザーインターフェースフレームワークと同様、マークアップ(markup)、スタイル(styles)、振る舞い(behaviours) を組み合わせたコンポーネントでアプリを  宣言的(declaratively)  に構築することができます。 Svelte を理解するには、HTML、CSS、JavaScript の基本的な知識が必要です。 https://learn.svelte.jp/tutorial/welcome-to-svelte だそうです。少し使ってみた感じではHTMLとJS(JavaScript)をシームレスに融合させ、その上簡単に書けるようにしたものといった印象を受けました。自分はJSについても詳しくはないので、本記事で解説する単語の中にはJSのものも含まれています。 Introduction Svelteにようこそ ここから解説に入っていきます。 Svelteチュートリアルは以下のような画面で進行します。 左上がファイル構造を表しています。右上がファイルの中身で、ここを変更するとリアルタイムで下の表示に反映されます。 フットプリント…プログラムが動作する際のメモリ使用量の多さ コンポーネント…次項で解説される オーバーヘッド…プログラムが行う作業のために間接的に生じる余計な負荷 Your first component, Dynamic attributes ここでは、変数やコードを中括弧で埋め込むというSvelteの基本的な動作が解説されています。変数の中身にはテキストだけでなく画像も使用することができます。 Styling これは要するにCSSです。 Nested components この項目はSvelteを学ぶにあたって重要です。この後もほぼ全ての項でコンポーネントのインポートが行われます。 HTML tags @html によってHTMLタグを使うことができます。これは後述の Bindings/Textarea inputs でも出てきます。 DOM…Document Object Modelの略。Webページの要素やコンテンツなどをツリー構造で表現したもの brob…Binary Large Objectの略。単にバイナリデータの塊を表現したもの サニタイズ…脆弱性を突こうとする入力を別の表記に置き換え無害化することを指す。 英語では「消毒する」などの意味がある Reactivity Assignments カウントを増やす関数と、カウントを表示するテキストを作り、「クリックした時にこの関数を実行する」とボタンに設定することでそれらを結びつけています。 Declarations, Statements, Updating arrays and objects 他の変数に依存する変数は、 $: をつければ自動的に連動計算してくれます。また、 $: は変数だけでなく文やブロックなどにも付与できます。ただし、連動計算は代入によって起こるものであり、リストに追加する場合はもう一工夫必要だそうです。 このリアクティブ宣言という概念は開発でよく使う印象があります。 statements…プログラミングにおける文のこと Props Declaring props, Default values, Spread props Introduction/Styling の項などでコンポーネント同士の独立性についての話がありましたが、もちろんデータの受け渡しも可能です。受け渡しにあたっては import と export をセットで使用します。これも開発には欠かせないものです。 プロパティはデフォルト値も設定することができ、しない場合はundefinedになります。また、渡す変数が複数あり、かつその数と名前が一致していれば、一気に代入できます。 Logic If blocks, Else blocks, Else-if blocks 他のあらゆる言語、フレームワークと同様、if、else、else-ifも実装されています。これらのブロックは # にはじまり / に終わります。elseのような間に挟むものには : がつきます。 HTMLでこのような処理を実装しようとするとJSを組み込む必要がありますが、Svelteであれば地続きの感覚で記述できます。 Each blocks # にはじまり / に終わるのはifと同じです。配列やそれに類するものをあらかじめ宣言しておけば、eachブロックを利用して一気に代入できます。 Keyed each blocks ここは少し理解に時間がかかった部分です。 まず初期化時に、 Thing コンポーネントに things の中身が渡され、 emoji は things の name に対応したものがセットされます。 things が決めているのは name とコンポーネントの数なので、 things = things.slice(1); によって配列の先頭を消すと、コンポーネントは banana 〜 egg の4つになります。しかし、 emoji を決めているのは Thing であり初期化時点から動かないこと、そしてコンポーネントは末尾から消える法則があることから、ズレが生じるようです。 // Thing.svelte <script> const emojis = { apple: ' ', banana: ' ', carrot: ' ', doughnut: ' ', egg: ' ' }; export let name; const emoji = emojis[name]; </script> // App.svelte <script> import Thing from './Thing.svelte'; let things = [ { id: 1, name: 'apple' }, { id: 2, name: 'banana' }, { id: 3, name: 'carrot' }, { id: 4, name: 'doughnut' }, { id: 5, name: 'egg' } ]; function handleClick() { things = things.slice(1); } </script> <button on:click={handleClick}> Remove first thing </button> {#each things as thing (thing.id)} <Thing name={thing.name} /> {/each} shift メソッドを使わず slice メソッドを用いているのは、 shiftがsliceと違って配列を直接変更するもの だからです。 Reactivity/Updating arrays and objects で述べられたように、リアクティビティは代入によってトリガーされるため、 shift メソッドを使う場合は以下のように記述する必要があり、冗長になります。 things.shift(); things = things; Await blocks utils.jsの getRandomNumber() は、まず /random-number  というAPIエンドポイントにリクエストを送信して結果が帰るまで待ち、レスポンスが成功した場合レスポンスの本文をテキストとして取得するのを待ち、それをApp.svelteに渡します。 res.ok は通常、 HTTPステータスコードが200-299の範囲内にある場合にtrue だそうです。また、waitingメッセージを表示するため、APIにはあえて遅延が組み込まれているようです。エンドポイントの実装はサーバサイド(バックエンド)で行われており、ここからは見えません。 // utils.js export async function getRandomNumber() { const res = await fetch('/random-number'); if (res.ok) { return await res.text(); } else { throw new Error('Request failed'); } } App.svelteは let promise = getRandomNumber(); と #await promise で、一連の処理が終わるのを待ちます。 {number} があるのに let number などがないのは不思議に思えますが、 {:then number} が getRandomNumber() の返した値を number として割り振っているのだと思われます。 // App.svelte <script> import { getRandomNumber } from './utils.js'; let promise = getRandomNumber(); function handleClick() { promise = getRandomNumber(); } </script> {#await promise} <p>...waiting</p> {:then number} <p>The number is {number}</p> {:catch error} <p style="color: red">{error.message}</p> {/await} Events DOM events, Inline handlers, Event modifiers Reactivity/Assignments では button にイベントハンドラを付与していましたが、 div に付与することもできるそうです。インラインで宣言することも、修飾子をつけることで条件・属性を追加することもできます。 Component events Inner.svelteの1行目に import { createEventDispatcher } from 'svelte'; とありますが、これは今までのように他のファイルからではなく svelteパッケージから関数をインストール しているということです。 ここでは、ボタンを押すと sayHello 関数が発火し(実行され)、 sayHello 関数は message イベントを発生させています。App.svelteの方では、 message イベントが発生したのを受けて handleMessage 関数が発火しています。 // Inner.svelte <script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); function sayHello() { dispatch('message', { text: 'Hello!' }); } </script> <button on:click={sayHello}> Click to say hello </button> イベントディスパッチャ…イベントを送信するもの。イベントを受信するものはイベントリスナー Event forwarding, DOM event forwarding コンポーネントのイベント(前項 Component events で作ったもの)はバブルしないため、 Component events では2層だった構造が3、4層と多層化する場合、中間層はフォワードする(イベントを受け渡す)必要があります。 また、今まで button にイベントハンドラを設定する機会が何度かありましたが、もう一つファイルを用意する必要があるような凝った button の場合は、ファイルをまたぐためにイベントフォワーディングを使用するようです。 バブルする(バブリング)…イベントが発生した要素から、親要素を通って最上位の要素まで順番に伝わっていくこと Bindings Text inputs Svelteでは、基本的に変数の更新に応じてその変数を使用するコンポーネントも更新されるという処理がされます。 Reactivity/Assignments の時も、 button コンポーネントに足し算関数を発火させるインベントハンドラを定義することで遠回りに表示を更新していました。これも似たようなもので、 bind を使って input から name を変更し、 name の変更が h1 に反映されるという処理が行われています。 <script> let name = 'world'; </script> <input bind:value={name} /> <h1>Hello {name}!</h1> Numeric inputs, Checkbox inputs number が数値ボックスで range が数値バーです。「 input.value を使わなければならない」という話は、前項の説明の「 on:input イベントハンドラを追加し〜」という箇所と同じ話です。 チェックボックスもほとんど同じ感覚で bind を使用できます。 numeric…数値という意味 <label>…htmlの要素の一つ。主にフォーム部品を関連づけるために使う Select bindings on:change={() => (answer = '') は、セレクトボックスで新しい選択肢を選んだ(=質問を切り替えた)時に、答えの部分を空にするという意味です。 {selected ? selected.id: '[waiting...]'} は、 selected が真値である(なんらかの質問を選択している)場合 selected.id が表示され、そうでない場合 [waiting...] が表示されるということを示しています。ここでは、 bind をつけないと selected が真値になりません。 <form on:submit|preventDefault={handleSubmit}> <select bind:value={selected} on:change={() => (answer = '')} > {#each questions as question} <option value={question}> {question.text} </option> {/each} </select> <input bind:value={answer} /> <button disabled={!answer} type="submit"> Submit </button> </form> <p> selected question {selected ? selected.id : '[waiting...]'} </p> Group inputs, Select multiple Group inputs で使用したチェックボックスは、 <select multiple> に置き換えることもできるそうです。こうすると記述が短く分かりやすくなりますが、その代わり、NOTEに書かれているように複数選択する際に少し工夫が必要になります。 Intl.ListFormat…リストを整形するオブジェクト。オプションを指定することで最終的に表示するものをA, B and Cのような形式にしている Textarea inputs import { marked } from 'marked' でマークダウンをHTMLに変換するためのライブラリ marked をインポートし、 @html marked(value) でHTMLに変換した文字列 value をHTMLとして解釈しています。 <script> import { marked } from 'marked'; let value = `Some words are *italic*, some are **bold**\n\n- lists\n- are\n- cool`; </script> <div class="grid"> input <textarea bind:value></textarea> output <div>{@html marked(value)}</div> </div> Lifecycle onMount document.querySelector('canvas') では、ドキュメント内の最初の <canvas> 要素を選択しています。この方法で HTML 内の <canvas> 要素を操作できるようになります。 canvas.getContext('2d') では、選択した <canvas> 要素の描画コンテキストを取得しています( '2d' は 2D 描画コンテキスト)。このコンテキストを通じて、 <canvas> 上に図形や線を描いたり、色を塗ったりすることができます。「コンポーネントが破棄されてもloopが動く」とは、 <canvas> を消すと画像が見た目の上では消えるものの、 cancelAnimationFrame がないとバックで処理が継続してしまうということです。 // App.svelte <script> import { onMount } from 'svelte'; import { paint } from './gradient.js'; onMount(() => { const canvas = document.querySelector('canvas'); const context = canvas.getContext('2d'); let frame = requestAnimationFrame(function loop(t) { frame = requestAnimationFrame(loop); paint(context, t); }); return () => { cancelAnimationFrame(frame); }; }); </script> svg…画像フォーマットの一種。大きさを自由に変えられる beforeUpdate and afterUpdate scrollableDistance = div.scrollHeight - div.offsetHeight でスクロール可能な総距離を計算し、 div.scrollTop > scrollableDistance - 20 で現在のスクロール位置が下端から20ピクセル以内にあるかチェックしています。 div.scrollTo(0, div.scrollHeight) は要素を最下部にスクロールするという意味です。 beforeUpdate(() => { if (div) { const scrollableDistance = div.scrollHeight - div.offsetHeight; autoscroll = div.scrollTop > scrollableDistance - 20; } }); afterUpdate(() => { if (autoscroll) { div.scrollTo(0, div.scrollHeight); } }); 状態駆動…アプリケーションの状態(データ)に基づいてUIを自動的に更新する方法。Svelteは基本的に状態駆動の考え方に基づいているが、スクロール位置の取得やスクロールの実行はDOMの直接操作を必要とするため実現が難しい tick テキストが新しい値に変更されると、古いテキストを選択している情報は失われます。 <script> の末尾の this.selectionStart = selectionStart; と this.selectionEnd = selectionEnd; の2行は、現在の選択範囲を過去の選択範囲と同じにするというもので、テキスト変更後も選択範囲を維持しようとしています。しかし、そのままだとテキストの変更がこの2行より後になってしまうので意味がなく、したがってtickが必要となります。 <script> let text = `Select some text and hit the tab key to toggle uppercase`; async function handleKeydown(event) { if (event.key !== 'Tab') return; event.preventDefault(); const { selectionStart, selectionEnd, value } = this; const selection = value.slice(selectionStart, selectionEnd); const replacement = /[a-z]/.test(selection) ? selection.toUpperCase() : selection.toLowerCase(); text = value.slice(0, selectionStart) + replacement + value.slice(selectionEnd); this.selectionStart = selectionStart; this.selectionEnd = selectionEnd; } </script> Stores Writable stores, Auto-subscriptions, Readable stores Storeにおいて明示的に subscribe するならば unsubscribe も必要です。 $ 記法(自動サブスクリプション)を使えば自動的にリアクティブな更新をしてくれて、そしてコンポーネントが破棄されるときに自動的に unsubscribe も行ってくれます。 Reactivity/Declarations などで使用したリアクティブ宣言と自動サブスクリプションは似て非なるものです。 また、Storeは用途によって書き込み可能にしたり読み取りのみ可能にしたりできます。 subscribe…データの変更を監視し始めること。unsubscribeはその逆で、不要になった監視を停止すること リアクティブ宣言… $: を使用。依存する値が変更されるたびに自動的に再計算される。コンポーネント内のローカルな状態・計算に使う 自動サブスクリプション… $ を使用。Storeの値が変更されるたびに自動的に更新される。グローバルな状態(Store)にアクセスするために使う Derived stores 他のStoreを参照するStoreです。 export const 【exportする変数名】 = derived(【参照するStore】,($【参照するStore】) =>【計算ロジック】); という形式で記述します。参照するStoreを2回書いていますが、1回目はどのStoreから派生するかを指定していて、2回目では実際のStoreの値にアクセスしています。 //stores.js export const elapsed = derived( time, ($time) => Math.round(($time - start) / 1000) ); Custom stores countという値自体に増減などのロジックを組み込んでいます。 increment: () => update((n) => n + 1) は JSのオブジェクトリテラル記法におけるメソッド定義の短縮構文 で、 increment: function() { return update((n) => n + 1); } と同じ意味です。 // stores.js function createCount() { const { subscribe, set, update } = writable(0); return { subscribe, increment: () => update((n) => n + 1), decrement: () => update((n) => n - 1), reset: () => set(0) }; } Store bindings 書き込み可能なStoreは bind したりコンポーネント内で直接代入することも可能という話です。 ${$name} は、 $ が二つ続いていてわかりづらいですが、中の $name がこの Stores の項で学んだことで、外の ${} はテンプレートリテラル(JSの文字列を作成するための機能)です。 "Hello " + $name + "!” と同じです。 // stores.js export const greeting = derived(name, ($name) => `Hello ${$name}!`); まとめ 今回はSvelteチュートリアルのPart1を体験して学んだことをまとめてみました。評判通り、学習コストが低めで書きやすいという印象でした。まだまだ学習中の身であるため、誤った理解をしている部分もあるかもしれませんが、Svelteの基本を習得しスキルアップした実感を持っています。 Svelteは人気上昇中のフレームワークなので、フロントエンド系の開発に携わっている方は今の内に学んでおくと将来様々な場面で役に立つと思います。自分も学習を続けたいと考えており、次はSvelteKitの基礎であるPart3に挑戦する予定です。 リレーブログ企画「24卒リレーブログ」は、この記事で終了となります。執筆に協力していただいた皆さん、見てくださった皆さん、ありがとうございました。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
この記事は、リレーブログ企画「24卒リレーブログ」の記事です。 はじめに こんにちは! 始めまして、24卒新入社員の滝川です 現在、ジョブローテ期間中で入会システムチームに所属しており、@nifty光や@nifty withドコモ光の開発や運用、Toil削減の業務を日々行っています。その中で既存のAWSリソースをTerraform化する業務がありました。その際、トレーナーの方に特に意識してほしいと言われたことが「 ディレクトリ構成をしっかり考えること 」でした。この経験を通じて得た知見はとても有益だと考えています。そこで、この記事では、Terraformのディレクトリ構成とモジュール化について、私が得た知見を共有していきたいと思います。 ディレクトリ構成を考えなければいけない理由 Terraformは、インフラストラクチャをコードとして管理する(IaC)ためのツールです。その主な利点は、インフラの構築や運用を簡素化し、一貫性を保ちながら効率的に管理できることです。しかし、ディレクトリ構成を考慮せず記述してしまうとこの本来の利点が損なわれてしまいます。 具体的には 可読性の低下 :どのリソースがどこで定義されているのか把握しづらくなる 重複コードの増加 :似たような設定が複数の場所に散らばり、DRY原則に反する 変更の難しさ :一つの変更が複数の場所に影響を及ぼし、予期せぬエラーの原因となる チーム協業の障害 :ほかのメンバーがコードを理解し、修正することが困難になる スケーラビリティの制限 :プロジェクトの成長に伴い、管理が難しくなる 結果として、Terraformを使用する利点が大幅に減少してしまいます。これらの課題を解決し、Terraformの利点を最大限に活かすには 「モジュール化」 が必須です。 モジュール化とは? Terraformにおけるモジュール化とは、インフラストラクチャのコードを論理的に分割し、 再利用可能なコンポーネント として組織化する手法です。と言われてもイメージがつきにくいと思います。 実際にディレクトリ構造やコードを見た方がイメージがつきやすいと思いますので、具体的なモジュールの例を見ながら説明していきます。 全体のディレクトリ構造 project-root/ ├── modules/:再利用可能なインフラストラクチャコンポーネントを管理 │ ├── vpc/ │ │ ├── main.tf   :リソースの主要な定義を含む │ │ ├── variables.tf :モジュールの入力変数を定義 │ │ └── outputs.tf  :モジュールの出力値を定義 │ └── ecs/ │ ├── main.tf │ ├── variables.tf │ └── outputs.tf ├── environments/      :環境固有の設定を管理(dev,staging,prod等) │ └── dev/ │ └── main.tf    :開発環境固有のリソース構成を定義し、モジュールを呼び出す │ └── prod/ │ └── main.tf               └── README.md module配下にリソース単位でモジュール化(vpc,ecs) 再利用性を高める 可読性を高める 環境分離をしている 他の環境に影響を与えることなく作業ができるため リスク軽減 が図れる 環境ごとの差分を簡単に管理することができる 実際にECSを作成してみた enviroments/dev/main.tf ECSモジュールの呼び出しと環境固有の設定を行っています。 # プロバイダーの設定 provider "aws" { region = "ap-northeast-1" } # モジュール呼び出しとdev環境固有の変数の定義 module "ecs" { source = "../../modules/ecs" # ecs/main.tfで定義された変数に値を入れている project_name = "my-ecs-project" task_cpu = 256 task_memory = 512 container_name = "my-app" container_image = "nginx:latest" container_port = 80 desired_count = 1 # module/vpc/outputs.tfファイルから値を持ってきている subnet_ids = module.vpc.private_subnet_ids } modules/ecs/main.tf ECSクラスター、サービス、タスク定義などの主要なリソースを定義しています。 環境に関わらず一意な値はハードコードで指定し、環境ごとに変更される可能性のあるものは変数として定義しています。 var.project_nameを再利用することで変数定義する数を減らしています。 resource "aws_ecs_cluster" "main" { name = "${var.project_name}-cluster" } resource "aws_ecs_service" "main" { name = "${var.project_name}-service" cluster = aws_ecs_cluster.main.id task_definition = aws_ecs_task_definition.main.arn launch_type = "FARGATE" desired_count = var.desired_count network_configuration { subnets = var.subnet_ids assign_public_ip = false } } resource "aws_ecs_task_definition" "main" { family = "${var.project_name}-task" network_mode = "awsvpc" requires_compatibilities = ["FARGATE"] cpu = var.task_cpu memory = var.task_memory container_definitions = jsonencode([ { name = var.container_name image = var.container_image portMappings = [ { containerPort = var.container_port hostPort = var.container_port } ] } ]) } modules/ecs/variables.tf モジュールの入力変数を定義しています。各変数のタイプ、デフォルト値、説明を指定することで、モジュールの使用者が適切な値を設定できるようにしています。 variable "project_name" { description = "Name of the project" type = string } variable "task_cpu" { description = "CPU units for the ECS task" type = number default = 256 } variable "task_memory" { description = "Memory for the ECS task in MiB" type = number default = 512 } variable "container_name" { description = "Name of the container" type = string } variable "container_image" { description = "Docker image for the container" type = string } variable "container_port" { description = "Port exposed by the container" type = number default = 80 } variable "desired_count" { description = "Desired number of tasks" type = number default = 1 } variable "subnet_ids" { description = "List of subnet IDs for the ECS tasks" type = list(string) } modules/ecs/outputs.tf モジュールの出力値を定義しています。他のモジュールや設定で使用する値、特にリソース作成時に動的に変化する値(例:ARN)を定義します。 dev/main.tfのsubnet_idsのように他モジュールで呼び出す際に利用されます。 output "cluster_id" { description = "The ID of the ECS cluster" value = aws_ecs_cluster.main.id } output "service_name" { description = "The name of the ECS service" value = aws_ecs_service.main.name } 実際にapplyしたい方! サンプルコードを用意しました! https://github.com/Kazuhiro-27/terraform_ecs AWSの資格情報の設定が別途必要になるので下記を参考にしてみてください! Terraformの公式ドキュメント おわりに 今回はファイル構成を意識しながらTerraformでECSを実装していきました。 ファイル構成は人それぞれで私はリソース単位でモジュール化していきましたが、機能単位(API、バッチ、ネットワーク、データ処理)でモジュール化するファイル構成もあるようでまだまだ奥が深そうでした。 Terraformはニフティ内で盛んに使われている技術なのでこれからも継続して学習し、知見を深めていきたいです。 最後まで読んでいただきありがとうございました!! 次回は、緑川さんです!どんな記事になるのか楽しみですね!! ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
前回の記事では、ニフティのサポートシステムチームで働く社員に、業務内容やチーム環境についてインタビューを行いました。 今回はインタビューの後編をお届けします。前編はこちらの記事をご覧ください。 【インタビュー】ニフティのお客様サポートシステムの開発エンジニアの仕事について色々教えてもらいました!【サポートシステム前編】 普段プライベートではどんなことをされていますか? H.Oさん 子供向けのアプリを開発したりしています。モグラたたきだったり、ちょっとしたゲーム性のあるものですね。Unity、C♯など、業務では使わない技術に余暇で触れるようにしています。 A.Kさん H.Oさんは休暇中もコードを書いてらっしゃるんですね! H.Oさん そうなんです。意外と余暇の話をする機会がなかったですね。 A.Kさん 余暇は趣味にあてています。ランニング、アクアリウム、キーボードづくりですね。 A.Kさんのアクアリウム H.Nさん アクアリム凄いですよね…!めちゃめちゃお金かかってそうだなと思いました!これだけのアクアリウムを個人で作れるんだな~とびっくりです。 H.Oさん アクアリウムの石って拾ってきたりするんですか? A.Kさん 自然のものは寄生虫とかいるので拾ったりはしないですw あとは、ニフティは部活動が盛んで、ISUCONに真剣に取り組むISUCON部というものもあり、その活動をプライベートでもやっています。別のチームの達人たちに色々と教えてもらっています。 ※「ISUCON」は、LINEヤフー株式会社の商標または登録商標です。 H.Nさん 私は駅ブラしたりしています。出社制度が変わって定期券を持つようになったので、沿線をぶらついていますねモータースポーツを見に行ったり、サバゲ―をやったりとアウトドア系の趣味もできました。 A.Kさん モータースポーツの話とか聞くんですけど、自分がインドアなのでいつも話に乗れなくてすみません。笑 H.Oさん バイクの免許とってなかったっけ? H.Nさん とりました!けど、まだ発行していなくて。笑 年休取って発行しに行こうと思います! H.Oさん 夏は暑くて大変だよね。 H.Nさん そうですね、涼しくなってから乗ろうと思います笑 どんな時に仕事のやりがいを感じますか。 H.Oさん 作ったシステムが利用者に喜んでもらえた時ですね。サポートセンターの社員が使っているシステムですと、フィードバックがすぐにもらえるのでモチベーションに繋がっています。 A.Kさん 自分も同じです。会員サポートページはずっと担当しているので、自分がやりたいといった技術を導入させてもらえるのでやりたいように仕事がやれています。 H.Nさん 昔開発委託していたシステム刷新にやりがいを感じています。新しいものを導入して何かが解決した時は面白いなと思います。開発時の障害をテストコードやIaCで解消できた時は楽しいですね! サポートシステムチームはどういうチームですか? A.Kさん ニフティ全体のサービス情報をお客様とカスタマーサポートに提供しているので、ニフティ全体にどういうサービスがあり、どういう仕組みなのかをしることができるチームです。他のチームよりニフティ全体のサービスについて広く知ることができるチームです。 今回はニフティのサポートシステムチームのインタビューの様子をお届けしました。ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! このインタビューに関する求人情報/ブログ記事 サポートシステムチームの求人情報 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する .is-style-rounded + .has-text-color{ font-size:95%; }
アバター
今回はサポートシステムチーム(以下、サポシス)へのインタビューです! 自己紹介 H.Oさん ニフティには2018年に中途入社して6年目になります。当社サポートセンターで使用されているサポートシステムの開発運用を行っているサブチームのリーダーをしています。主な業務はプロジェクト管理や、担当システムに関する要望や改善に関する活動、開発全般のレビュー等を担当しています。 A.Kさん 新卒6年目で、主な業務は会員サポートページの開発運用です。 6年間同じチームで同じシステムを見ているので、会員サポートページにはそこそこ詳しいと思います。 最近は一緒にメンテしてくれる人が増えて、運用業務の方に余裕ができてきたので、社内勉強会などに出て担当システムにフィードバックできることがないか色々勉強中です。 H.Nさん 新卒4年目で、会員サポートページの開発運用や社内で利用しているメール配信サービスの運用などを行っています。このチームには4年間いますが、2年目に2ヶ月だけジョブローテーションで他のチームにいたことがあります。文化の違いに驚きましたが、自分のチームを見直す良いきっかけになったと思っています。現在は他のチームの魅力的なポイントをこのチームにも取り込めないか日々模索中です笑 サポートシステムについて サポートシステムとは何ですか? H.Oさん ニフティのお客様をサポートするために使われているシステム全般のことですね。具体的には、お客様にご利用いただくものとサポートを行うために社員が利用するツールがあります。 お客様にご利用いただくもの 会員サポートページ https://support.nifty.com/ サポートをおこなうために社員が利用するもの サポートセンターにご連絡いただいたお客様の情報を確認するためのツール お客様との過去のやり取りを確認するためのツール PBX(電話) メール配信システム 担当しているサービスについて どのサービスを担当していますか? A.Kさん H.Nさんと私は 会員サポートページ を担当しています。お客様にご利用いただけるサービスの情報を照会するためのサイトなので、新しいサービスが追加されたらサイトも更新する必要があります。サイトのコーディングだけではなく、企画、デザイナーが考えた内容の反映について調整を行うことも大切な業務です。 コーディングにはどんな技術を使っていますか? A.Kさん フロントエンドはPHP、Smartyで、バックエンドはPHP、FuelPHPです。H.Nさんにはチームに入ってから技術を習得してもらいました。 H.Nさん はい、技術についてはチームに入ってから習得しました。公式ドキュメントをみながら勉強したり、実際に動いている会員サポートページのコードを読みながら習得しました。PHPに関しては動画サイトや、インターネット上の記事をみて勉強しました! A.Kさん H.Nさんは私のトレーニーだったのですが、とても優秀でした。フレームワークについては私が教えたりもしましたが、PHPについては自己学習で身に着けていて凄いなあと思ってましたよ。 H.Nさん そういう風に思われていると思っていなかったので、嬉しいです!A.Kさんは同世代のエンジニアの中でも抜きんでて優秀なので、なんとか追いつけるよう勉強していたつもりですがまだまだ教えていただくことばっかりです。 A.Kさん そんなことないですよ。クラウドの知識とかはH.Nさんのほうが持っているので、得意な分野については逆に教えてもらっています。 H.Oさん 2人とも開発スキルは僕よりも優秀だし、公私問わず勉強熱心で業務にも生かしてくれるので頼りにしています! 業務について 過去にがんばった業務に関するエピソードを教えてください! H.Oさん データセンターを撤退するプロジェクトがありましたが、かなり大規模な内容だったのでチーム一丸となったからこそできたと思います。ほとんどのシステムがブラックボックス化していて、何がどういう風に動いているかわからないものを廃止しなければいけなかったので調査が大変でした。 A.Kさん システムの全容はH.Oさんがまとめてくださり、私は機能単位で調査を担当しました。クラスファイルしか残っていないような状態だったのをむりやりトランスコンパイルしたりして、量も多かったのですごく大変でした。知らないこともたくさんありましたが、自分で調べたり、システムの概要はチームのベテランエンジニアに聞きながら一つ一つ整理していきました。 H.Nさん このプロジェクトに携わったのは入社2年目の時でした。入社したばかりの自分からするとあまりに大きいプロジェクトで笑、基本的にはH.OさんやA.Kさんが調査した内容に基づき、開発を行っていました。プロジェクト進行中にジョブローテで2カ月抜けたことがあり、キャッチアップが大変でした。 ※当時エンジニアのジョブローテーション制度が新しく導入されました。本配属後に他のチームの業務を体験するという取組みでした。現在は本配属前にローテを行っています。 ジョブローテの経験は生かせましたか? H.Nさん サポシスが使っていない技術、フレームワーク、開発手法が体験できたので、サポシスでの開発の参考にしました。具体的にはコンテナ(Docker)開発、Python、IaCです。 H.Oさん H.Nさんがジョブローテで得た知識をサポシスに持ってきてくれました。1年目のメンバーであっても、提案してくれた技術をどんどん取り入れていきますし、アーキテクチャを考えるところから入ってもらっています。 A.Kさん 私の時は配属されていきなり引継ぎだったし、ジョブローテという仕組みもなかったので羨ましく思いました。あとはエンジニア定例という技術勉強の取り組みがあるが、自分の世代は人数が多かったこともあり出る人、出ない人がいて、自分はマネージャーから「出なくていいよね」と言われました笑 H.Oさん A.Kさんが優秀だったからです。 H.Nさん でなくていいよねといわれるのは凄いですね笑 A.Kさんの次の世代からは全員エンジニア定例を受けるようになっています! 今どういった業務を頑張っていますか? H.Oさん 複数のサービスと密結合している古いDBを廃止しようとしています。コスト削減とブラックボックス解消を目的に取り組んでいます。 A.Kさん 私とH.Nさんも参画しています。請求画面に連携しているDBを別チームのDBに変更しています。 H.Nさん お客様の情報をDBから取得するためのAPIの移行を担当しています。 そのプロジェクトはいつまでかかりそうですか?また、その後はどんなプロジェクトを予定していますか? H.Oさん まさにいま佳境という段階で、もうすぐ完了できる見込みです。その後はクラウドのマイグレーションを予定しています。 チームの課題と、取組みについて 現在のチームにはどういった課題がありますか?それに対してどうやって取り組んでいますか? H.Oさん タスクのチケット管理が少しおざなりになっていたことと、新しいシステムを作る時の設計基準がなく、漏れが発生していたことですね。 A.Kさん GitHub Projectsでチケット管理できるように進めています。チームメンバーと決めた要件に従って、GitHub周りの設定を私がやっています。 H.Nさん 最初はNotionで管理しようとしていましたがもともとGitHub Projectsで管理しているものがあったのでそちらに集約することにしました。 後編に続きます! 今回はニフティのサポートシステムチームのインタビューの様子をお届けしました。続きは後編の記事をご覧ください。 【インタビュー】ニフティのお客様サポートシステムの開発エンジニアの素顔に迫りました!【サポートシステム後編】 このインタビューに関する求人情報/ブログ記事 サポートシステムチームの求人情報 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する .is-style-rounded + .has-text-color{ font-size:95%; }
アバター
この記事は、リレーブログ企画「24新卒リレーブログ」の記事です。 はじめに 初めまして、新卒1年目の佐藤です。 現在、ジョブローテの1期目でインフラシステムグループの情報システムチームに所属しており、社内システムの開発と運用を担当しています。 特に、社内で使用しているあるサービスを新しいものに移行する計画があり、その際に必要な両サービス間のデータを同期させる機能を開発しています。 開発にあたっては、社内で利用が増えている Go を採用しました。 また、既存のサービスには API が用意されていないため、Go でスクレイピングを行う必要がありました。 この記事では、Go でスクレイピングを行うためのライブラリを紹介し、そのうちの1つである「rod」の使い方について紹介します。 注意:サイトによってはスクレイピングが禁止されています。利用規約等をご確認ください。 Go について Go は「プログラミングの環境を改善する」ことを目的とし、Googleによって開発されたコンパイラ言語です。C言語を設計した方が開発に携わっており、処理の速さが特徴です。他にも並行処理やコードのシンプルさが挙げられます。 Go で作れた有名なサービスにはメルカリやAbema、開発ツールにはDockerやTerraform、Kubernetesがあるそうです。 NIFTY enginnering ブログでは Go を使用した記事が4回投稿されています。 Go製APIで仕様ドキュメントを生成するツールSwagを初めて使ってみてハマったこと LambdaでGoランタイムが使えなくなるので、Terraformでカスタムランタイムに移行してみた Amazon RDSのメンテナンスイベントをSlackに通知するようにした AWS LambdaでGoランタイムからカスタムランタイムに移行した際にハマったこと Go でスクレイピングができるライブラリ スクレイピングができる Go のライブラリをいくつか紹介します。 ライブラリ名 スター数 最終コミット 最新バージョン リリース日 gocolly/colly 22.9k 2024/06/06 2020/06/09 PuerkitoBio/goquery 13.9k 2024/08/13 2024/04/30 chromedp/chromedp 10.8k 2024/08/02 2023/08/05 go-rod/rod 5.2k 2024/08/19 2024/07/12 tebeka/selenium 2.5k 2021/11/06 2019/08/01 anaskhan96/soup 2.2k 2022/01/16 2022/01/16 playwright-community/playwright-go 2.0k 2024/07/17 2024/07/17 yhat/scrape 1.5k 2016/11/28 2016/11/28 sclevine/agouti 823 2021/04/27 2018/03/04 (2024/08/28現在) Python でスクレイピングをする場合は Selenium か BeautifulSoup の2択だと思います。Python は人気ですし、調べるといろんなブログで使い方がわかりやすく紹介されています。 Goはどうでしょうか。「Go スクレイピング」と調べて出てくるライブラリはおそらく goquery, colly, selenium, agouti の4つです。 しかし、selenium は最新バージョン5年前でセキュリティ的に危ないですし、agouti はメンテナンスされておらず、GitHub のリポジトリも昨年アーカイブになっています。 一方で goquery と colly は記事の数も多く、GitHub リポジトリのスター数も多いことからこのどちらかを使うと良いと思います。 ですが、使ってみると Python の Selenium のような 直感的な使いやすさ がない…と感じると思います。 私の言う直感的な使いやすさとは、 「指定した URL に飛んで、要素を見つけて、情報を取得・操作する」 といった普段ネットサーフィンをするときと同じ流れをコード上で実現できることを指します。 goquery と colly は指定したサイトを解析することには長けていますが、あれこれ入力したりボタンを押したりするような複雑な操作は苦手なのだと個人的に思います。 Python の Selenium のような 直感的な使いやすさ を求める方には rod をおすすめします。 rod とは GitHub リポジトリの README を翻訳したものが以下のものになります。 Rod は DevTools Protocol を直接ベースにした高レベルドライバです。ウェブ自動化とスクレイピングのために設計され、高レベルと低レベルの両方で使用できます。上級開発者は低レベルのパッケージと関数を使用して、簡単にカスタマイズしたり、独自のバージョンの Rod を構築することができます。 https://github.com/go-rod/rod?tab=readme-ov-file#documentation–api-reference–faq AI に要約させると、 Rod は、ウェブ自動化とスクレイピング用のツールで、DevTools Protocolを基盤とし、高レベルと低レベルの両方の操作が可能。上級者向けにカスタマイズ性も提供している。 とのことです。 いざスクレイピング!! 実際にコードを見ていきましょう。 Go の環境構築がお済みでない方はご準備をお願いします。 作業ディレクトリを作り、下のコマンドで go.mod を作成します。このファイルによって使用されるモジュールの依存関係を管理しています。 go mod init <適当なmodule名> 次に、 main.go を作成し、次の内容を書き込みます。 package main import ( "fmt" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/input" "github.com/go-rod/rod/lib/launcher" ) func main() { // ブラウザを起動 url := launcher.New().MustLaunch() browser := rod.New().ControlURL(url).MustConnect() // @niftyのページを開く page := browser.MustPage("https://www.nifty.com") // 検索ボックスに「ニフティトップページ」と入力する page.MustElementX("/html/body/div[1]/div[2]/div/div[1]/div[2]/div/div[2]/div[2]/div[1]/div/div[1]/form/div/div/p/span/label/input").MustInput("ニフティトップページ") // エンターキーを押す page.KeyActions().Press(input.Enter).MustDo() // 検索結果が表示されるまで待機 page.MustWaitStable() // 検索結果のタイトルを取得 results := page.MustElement("h3") fmt.Println(results.MustText()) // ブラウザを閉じる browser.MustClose() } rod のライブラリを取得しましょう。 go get github.com/go-rod/rod 必要なライブラリを go.mod に認識させます。 go mod tidy 最後に、 go run main.go で実行できます。 Chromium というブラウザのインストーラーが走った後、しばらく待つと「@nifty」と出力されます。 main.go の解説 ブラウザの起動 ~ 画面表示 コード内のコメントで大体の流れは分かったと思いますが、補足情報と併せて簡単に main.go の内容について説明します。 まず初めにブラウザを起動します。起動するだけで画面に表示されません。 url := launcher.New().MustLaunch() デフォルトだと裏で Chromium が起動します。実際にブラウザが動いているところを見たい場合は、 url := launcher.New().Headless(false).MustLaunch() 普段使っている Chrome を使いたい場合は、 url := launcher.New().Bin("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome").MustLaunch() のようにパスを指定してあげるとそれを使って実行してくれます。 rod で サポートされているブラウザ の説明をみると、Chrome の他に Microsoft Edge と Firefox などがサポートされているそうです。 続いて、以下のコードでは指定したブラウザを使って画面表示させます。 browser := rod.New().ControlURL(url).MustConnect() もし複数ブラウザを使う場合はコピーして変数名を変えると複数表示されます。 ページ遷移 ~ キー操作 次に、@nifty の検索ページに遷移します。 page := browser.MustPage("https://www.nifty.com") 上の browser と同じようにしてあげると別タブでページ遷移します。 次に検索ボックスを見つけて、「ニフティトップページ」と入力します。 page.MustElementX("/html/body/div[1]/div[2]/div/div[1]/div[2]/div/div[2]/div[2]/div[1]/div/div[1]/form/div/div/p/span/label/input").MustInput("ニフティトップページ") MustElementX では XPath を使って要素を選択します。要素選択の方法は色々と対応していますが、XPath が一番確実だと思います。 XPath は Chrome をお使いの方は右クリック → 検証を押して、 要素を選択して、右クリック → Copy → Copy XPath で取得できます。 あとは MustInput で文字を入力します。 次にエンターキーを押します。 page.KeyActions().Press(input.Enter).MustDo() キー操作の他にもマウス操作も可能です。キー操作では押す・離すが可能で、2つを組み合わせて複数キーのタイピングもできます。 ページ読み込み ~ ブラウザの終了 続いて、ページが読み込まれるのを待たないと要素が表示されず処理が上手くいきません。 page.MustWaitStable() Wait のメソッドも色々と用意されており、 MustWaitIdle や MustWaitDOMStable , MustWaitLoad などがあります。 次に、先ほど説明した方法で要素を選択します。今回は検索結果で出てくる最初のサイトのタイトルを取得しています。 result := page.MustElement("h3") 該当する全ての要素を取得したい場合は、 results := page.MustElements("h3") のように Elements にしてあげると、そのページの h3 要素を全て取得してくれます。ただし、ページによっては下までスクロールしてあげないと取得できない場合があります。 次は取得した要素のテキストを出力します。 fmt.Println(result.MustText()) 指定した h3 要素は HTML で <h3 class="WebSearchItem_title__Fk3pr">@<b>nifty</b></h3> と書かれており、そのテキスト部分を MustText で抽出しています。 もし、選択した要素の class 名や href, id, src を取得した場合は、 fmt.Println(*(result.MustAttribute("class"))) のように MustAttribute で取得できます。 最後にブラウザを閉じて終了です。 browser.MustClose() エラーハンドリングについて ここまで使ってきたメソッドの多くに Must が付いていました。 Must が付くメソッドはエラーを返さないため、 Must〇〇.Must〇〇 のように結合することができ、コードをシンプルにできます。 エラーを返して欲しい場合は、そのメソッドの定義をみると Must が付いていないメソッドを内部で使用しているので、そちらを使ってみてください。 // MustElementの定義 // MustElement is similar to [Page.Element]. func (p *Page) MustElement(selector string) *Element { el, err := p.Element(selector) p.e(err) return el } 終わりに Go でのスクレイピングいかがでしたでしょうか。 rod の使い方は公式がドキュメントを用意しているので、もし気になったら見てみてください。 https://github.com/go-rod/go-rod.github.io https://go-rod.github.io https://pkg.go.dev/github.com/go-rod/rod 私はこの開発で初めて Go を触りましたが、ニフティのキャリアアップ支援のうちの1つ「書籍購入費用補助制度」を利用して技術書を購入し、勉強しました。 この制度は業務に関する書籍(技術書やビジネス書)の購入費用を年間2万円まで会社が負担する制度というものです。 https://recruit.nifty.co.jp/workplace/initiative/careerup/ 勉強していくうちに「Go って面白い!」と感じたので 、今後の業務にも活かせていけたらと思います! 次回は、滝川さんです。 24新卒リレーブログもいよいよクライマックス、残すは2人となりました! ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
ニフティ株式会社でエンジニアリングマネージャーをしています芦川です。 今日は「 InnerSource Gathering Tokyo 2024 」についてお話ししたいと思います。このイベントは、組織の壁を越えたソフトウェア開発のコラボレーションを実現するための秘訣を探ることを目的として開催されました。ニフティはこのイベントにオーガナイザーとして参加し、非常に有意義な経験をしましたので、その内容を共有したいと思います。 目次 InnerSourceとは? InnerSource Gathering Tokyo 2024の概要 オーガナイザーとしての経験 登壇者としての気づき InnerSourceの未来と今後の展望 さいごに InnerSourceとは? まず、InnerSourceについて簡単に説明します。InnerSourceとは、オープンソースの原則とプラクティスを企業や組織内で活用する方法のことです。通常、ソフトウェア開発は特定のチームやプロジェクトに閉じた環境で行われますが、InnerSourceを導入することで、他のチームのメンバーが自由に開発に参加できるようになります。これにより、チーム間のコミュニケーションが活性化し、知識の共有が促進され、個人のスキルアップやコードの再利用が進むといったメリットがあります。 イメージをつかみたい方は、ぜひこちらをご覧ください! ニフティの導入事例は「 インナーソースを導入してみた その① お試し導入編 」を参照ください。 InnerSource Gathering Tokyo 2024の概要 「 InnerSource Gathering Tokyo 2024 」は、2024年8月8日に KDDI DIGITAL GATE で開催されました。このイベントは、InnerSource Commonsが主催するもので、ソフトウェア開発における組織横断的なコラボレーションの秘訣を共有することを目的としています。オーガナイザーとしては、他の会社様のメンバーとともに、非常に多様な視点で議論を重ねてきました。 スピーカー陣も非常に豪華で、InnerSource Commonsのファウンダー Danese Cooper様をはじめ、「世界一流エンジニアの思考法」の著者 牛尾様、「チームトポロジー」の訳者 吉羽様などが登壇し、それぞれの専門知識を活かした講演が行われました。 オーガナイザーとしての経験 私がオーガナイザーとして参加したこのイベントでは、準備期間から当日の運営まで、多くの学びがありました。準備期間は2024年2月から7月までで、機材の確認やリハーサルを経て、約70名のリアル参加者を迎えることができました。 特に印象的だったのは、オーガナイザー間でのコミュニケーションです。Slackやオンラインミーティングを活用しての非同期コミュニケーションが主でしたが、キックオフとしてオフラインイベントを一番最初に実施頂いたからこそ、人柄がよくわかった状態ですすみ、うまくいったのではないかと思います。 こうした活動を通じて、オーガナイザーとしての一体感が生まれ、当日の進行もスムーズに行うことができました。 登壇者としての気づき ニフティ 小松は、スピーカーとしても登壇し、InnerSourceの実践事例を紹介しました。日本ではまだInnerSourceの情報が少ないため、他社の最初の一歩を後押しするために、具体的な事例を中心にお話しました。例えば、InnerSource Commons コミュニティの活用方法や、会社での試験的な導入のプロセスについて説明しました。ビジネス部門の方がコントリビュートした話が特に好評でした。 InnerSourceの未来と今後の展望 今後、ニフティとしてはInnerSourceの取り組みをさらに拡大していきたいと考えています。特にプラットフォームよりのプロダクトはInnerSourceとの相性がよいと感じています。これは、利用者が多く、コントリビュートするモチベーションがうまれやすいからです。そういったプロダクトに対しては、社内インナーソース運営同好会より積極的に働きかけInnerSource化を進める予定です。また、社内で勉強会を開きInnerSourceの理解を促進しつつ、「春のコントリビュート祭り」などハンズオンでまとめて時間を取って導入障壁をさげるなんてどうだろうか、と検討しています。 さいごに 最後に、エンジニアたるもの何か1つでも、どこかのエンジニアコミュニティに参加してみることを強くお勧めします。個人のスキルアップが向上することはもちろんのこと、会社の名前を出すことができれば、相手の方が安心できる材料になり、かつ、その会社の認知度も向上します。 私自身、InnerSource Commonsに参加したことで、InnerSource Commons Japanの一員としてイベント主催のチャンスを得たり、ニフティがインナーソースやっているよ、という認知度が向上したりと、非常に多くのメリットを感じています。また、冒頭のインナーソースマンのビデオなどコミュニティの中での制作物への参加など楽しいことばかりです。 ほんとにおすすめです!もっと若いときからやってればよかった! 「InnerSource Gathering Tokyo 2024」は、ソフトウェア開発における新たな可能性を探る貴重な機会でした。オーガナイザーとしても、スピーカーとしても、多くの学びと気づきを得ることができました。これからもInnerSourceの取り組みを通じて、組織の壁を越えたコラボレーションを推進し、より良いソフトウェア開発環境を構築していきたいと考えています。 皆さんもぜひ、InnerSourceの取り組みを始めてみてはいかがでしょうか? ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
この記事は、リレーブログ企画「24卒リレーブログ」の記事です。 はじめに はじめまして。 新卒1年目の後藤です。 業務の問い合わせ対応にSlackのワークフローを利用していますが、 問い合わせ内容ごとにワークフローを作成しているため、数が多くなっています。 そこで、複数のワークフローを1つにまとめるため、Slack Bolt, AWS Lambdaで条件分岐するワークフローを作ってみました。 Slackのワークフローで条件分岐があったらいいなと思いました。 公式では以下のように記載されています。 ワークフローに条件つきロジックを作成できますか? 現時点では、ワークフロービルダーで条件つきロジックは作成できません。より複雑なロジックを実行するには、 カスタムファンクション を使って Slack アプリを作成する必要があります。 引用: https://slack.com/intl/ja-jp/help/articles/26800170438419 つまり、デフォルトの機能では存在していないということになります。 そこで、Slack Boltを利用しようと考えました。 Slack Boltとは、Slack アプリ開発のための公式フレームワークです。 JavaScript (Node.js), Python, Java で利用することができます。 Bolt 入門ガイド に、詳しくたくさん載っています。 以上のSlack Boltを用いることで、条件分岐ワークフローを実現できるSlackアプリを作成することが可能になります。 早速作っていきます! Slack APIの設定 Part1 まず、最初に取り掛かるのはSlack APIの作成です。 Slack APIとは、独自のアプリケーションをSlackに導入するために作るアプリです。 Slack APIのYour Appsページ 右上の Create New App をクリックします。 上の From a manifest を選択し、アプリをインストールするワークスペースを指定します。 Next を二回選択し、 Create をクリックします。 Basic Information の下部へ行き、 App name にアプリの名前、 Short description にアプリの説明、 Background color で背景色を選択します。 その後、右下の Save Changes をクリックします。 以下の画像のように、左側の OAuth & Permissions 内にある Scopes の Add an OAuth Scope から chat:write と commands を追加します。 左側のメニューで Incoming Webhooks を選択し、 On にします。 OAuth & Permissions 上部の OAuth Tokens の Request to Install をクリックし、コメントを記入し、 Submit Request で送信します。 承認を待ちます。 承知後、 OAuth & Permissions 内にある Install to ~ を選択し、使用するワークスペースを選択します。 その後、 Bot User OAuth Token (xbxo-hogehoge)が必要になるのでメモしてください。 また、左側のメニューで一番上の Basic Information に遷移し、 Signing Secret も必要になるので Show を押してメモしてください。 メモした2つはAWS Lambdaの設定で必要になります。 一旦ここでSlack APIの設定はストップです。 Slack Bolt ここからはSlack Boltについて説明していきます。 まずは任意のディレクトリにpipコマンドを利用してSlack Bolt をインストールし、packageフォルダを作成します。 そのフォルダをvscodeなどで開きます。 今回はPythonを使用するので lambda_function.py という名前でファイルを作成します。 このファイルにLambdaのコードを書いていきます。 cd /(任意のディレクトリ) pip install --target ./package slack_bolt cd package touch lambda_function.py コードは以下をコピペして貼り付けましょう。 コードの @app.command(“/sport_start”) 部分にある通り、Slackのスラッシュコマンド(/sport_start)で起動するようになっています。 import os from slack_bolt import App from slack_bolt.adapter.aws_lambda import SlackRequestHandler app = App( signing_secret=os.environ["SLACK_SIGNING_SECRET"], token=os.environ["SLACK_BOT_TOKEN"], process_before_response=True, ) SPORTS = { "soccer": { "name": "サッカー", "players": [ "サッカーマン1", "サッカーマン2", "サッカーマン3", "サッカーマン4" ] }, "baseball": { "name": "野球", "players": [ "野球マン1", "野球マン2" ] }, "basketball": { "name": "バスケットボール", "players": [ "バスケマン1", "バスケマン2", "バスケマン3" ] } } def create_modal(user_id, channel_id, selected_sport=None): blocks = [ { "type": "section", "block_id": "sport_select", "text": {"type": "mrkdwn", "text": "スポーツを選んでください。"}, "accessory": { "type": "static_select", "action_id": "sport_select", "placeholder": {"type": "plain_text", "text": "スポーツを選択"}, "options": [ {"text": {"type": "plain_text", "text": sport["name"]}, "value": key} for key, sport in SPORTS.items() ] } } ] if selected_sport: blocks.append({ "type": "section", "block_id": "player_select", "text": {"type": "mrkdwn", "text": "選手を選んでください。"}, "accessory": { "type": "static_select", "action_id": "player_select", "placeholder": {"type": "plain_text", "text": "選手を選択"}, "options": [ {"text": {"type": "plain_text", "text": player}, "value": player} for player in SPORTS[selected_sport]["players"] ] } }) return { "type": "modal", "callback_id": "sport_player_modal", "private_metadata": f"{user_id}:{channel_id}", "title": {"type": "plain_text", "text": "スポーツと選手選択"}, "blocks": blocks, "submit": {"type": "plain_text", "text": "送信"} } @app.command("/sport_start") def ask_for_sport(ack, body, client): ack() client.views_open( trigger_id=body["trigger_id"], view=create_modal(body["user_id"], body["channel_id"]) ) @app.action("sport_select") def update_player_options(ack, body, client): ack() selected_sport = body["actions"][0]["selected_option"]["value"] user_id, channel_id = body["view"]["private_metadata"].split(":") client.views_update( view_id=body["view"]["id"], view=create_modal(user_id, channel_id, selected_sport) ) @app.action("player_select") def handle_player_select(ack, body, logger): ack() logger.info(body) @app.view("sport_player_modal") def handle_submission(ack, body, client, view, say): ack() try: user_id, channel_id = view["private_metadata"].split(":") selected_sport = view["state"]["values"]["sport_select"]["sport_select"]["selected_option"]["value"] selected_player = view["state"]["values"]["player_select"]["player_select"]["selected_option"]["value"] message = f"<@{user_id}>さんが好きなスポーツは{SPORTS[selected_sport]['name']}で、好きな選手は{selected_player}です。" say(text=message, channel=channel_id) except Exception as e: print(f"Error in handle_submission: {str(e)}") def lambda_handler(event, context): slack_handler = SlackRequestHandler(app=app) return slack_handler.handle(event, context) Lambdaのコードを作成したら、エクスプローラーなどで、作成したものをzipファイルに固めます。 ここまででSlack Boltはおしまいです。 コードの変更はAWS Lambdaでもできるので変更したい場合はあとでも大丈夫です。 AWS Lambda AWS Lambdaのページに行き、関数を作成します。 以下画像のように設定を行い、右下の関数の作成を押すと関数が作成されます。 ※hogehogeは関数名なので各自適した名前にしてください。 作成した関数を選択し、以下画像の右下の黄色の部分の .zipファイル をクリックし、先ほどzip化したものをアップロードし 保存 をクリックします。 すると、コードが展開されます。 次にコードではなく 設定 の 関数URL を開きます。 関数URLを作成をクリックします。 NONEを選択して保存しましょう。 NONEは誰からでもアクセス可能なため、サービス運用には向いていません。 サービス運用する場合は認証された呼び出し元のみがアクセス可能なAWS_IAMを選択しましょう。 ※NONEとAWS_IAMについては Lambda 関数 URL へのアクセスの制御 で詳しく説明されているのでそちらを参考にしてください。 作成した関数URLをメモしておきましょう。 「環境変数」を選択し、「編集」をクリックします。 環境変数の追加を選択すると増やすことが出来るので2つ追加します。 コードの中にSLACK_SIGNING_SECRETとSLACK_BOT_TOKENがあるので、それらの設定をします。 キーと値は以下のものを記載します。 キー 値 SLACK_SIGNING_SECRET Slack APIの設定でメモした Basic Information の Signing Secret SLACK_BOT_TOKEN Slack APIの設定でメモした OAuth & Permissions の Bot User OAuth Token これでAWS Lambdaの設定は終わりです! Slack APIの設定 Part2 左側のメニューで Interactivity & Shortcuts を選択し、 Off を On にします。 Request URL に先ほどメモした関数URLを記入します。 その後、右下の Save Changes をクリックします。 左側のメニューで Slash Commands を選択して Create New Command をクリックします。 以下の画像ように入力します。 Command は (/sport_start) です。 Request URL は先ほどAWS Lambdaでメモした関数URLです。 Short Description には説明を書いておきましょう。 右下のsaveで保存します。 Slack 最後にSlackを開いてこのアプリを追加したチャンネルに行きます。 追加したチャンネルの インテグレーション に追加したAppがあるか確認してください。 無い場合は、Slack画面左側の…(その他)の自動化を選択し、Appで作成したアプリ名を検索します。 作成したアプリを選択し、画面上部のアプリ名をクリックすると チャンネルにこのアプリを追加する があるのでこちらでチャンネルにアプリを追加してください。 作成したSlackのスラッシュコマンド(/sport_start)をSlackのチャットに入力すると以下のようになります。 送信を押すとメッセージが送信されました! 応用編 以下のコードのように選択肢をどんどん追加することができます。 主な追加箇所は以下です。 ・ SPORTS にポジションをそれぞれ追加 ・ if selected_player を追加 ・ @app.action(“player_select”) を追加 また、スラッシュコマンドも(/sport_start)ではなく、目的に応じたものに変更すると使いやすくなると思います。 質問と選択肢も自分が必要としているものに変更しましょう! import os from slack_bolt import App from slack_bolt.adapter.aws_lambda import SlackRequestHandler app = App( signing_secret=os.environ["SLACK_SIGNING_SECRET"], token=os.environ["SLACK_BOT_TOKEN"], process_before_response=True, ) SPORTS = { "soccer": { "name": "サッカー", "players": { "サッカーマン1": ["FW", "MF"], "サッカーマン2": ["DF", "GK"], "サッカーマン3": ["MF", "DF","GK"], "サッカーマン4": ["FW", "MF"] } }, "baseball": { "name": "野球", "players": { "野球マン1": ["ピッチャー","ファースト"], "野球マン2": ["キャッチャー","ショート","セカンド"] } }, "basketball": { "name": "バスケットボール", "players": { "バスケマン1": ["PG","SG"], "バスケマン2": ["SF","PF"], "バスケマン3": ["C","PG","SF"] } } } def create_modal(user_id, channel_id, selected_sport=None, selected_player=None): blocks = [ { "type": "section", "block_id": "sport_select", "text": {"type": "mrkdwn", "text": "スポーツを選んでください。"}, "accessory": { "type": "static_select", "action_id": "sport_select", "placeholder": {"type": "plain_text", "text": "スポーツを選択"}, "options": [ {"text": {"type": "plain_text", "text": sport["name"]}, "value": key} for key, sport in SPORTS.items() ] } } ] if selected_sport: blocks.append({ "type": "section", "block_id": "player_select", "text": {"type": "mrkdwn", "text": "選手を選んでください。"}, "accessory": { "type": "static_select", "action_id": "player_select", "placeholder": {"type": "plain_text", "text": "選手を選択"}, "options": [ {"text": {"type": "plain_text", "text": player}, "value": player} for player in SPORTS[selected_sport]["players"].keys() ] } }) if selected_player: blocks.append({ "type": "section", "block_id": "position_select", "text": {"type": "mrkdwn", "text": "ポジションを選んでください。"}, "accessory": { "type": "static_select", "action_id": "position_select", "placeholder": {"type": "plain_text", "text": "ポジションを選択"}, "options": [ {"text": {"type": "plain_text", "text": position}, "value": position} for position in SPORTS[selected_sport]["players"][selected_player] ] } }) return { "type": "modal", "callback_id": "sport_player_position_modal", "private_metadata": f"{user_id}:{channel_id}:{selected_sport or ''}:{selected_player or ''}", "title": {"type": "plain_text", "text": "スポーツと選手とポジション選択"}, "blocks": blocks, "submit": {"type": "plain_text", "text": "送信"} } @app.command("/sport_start") def ask_for_sport(ack, body, client): ack() client.views_open( trigger_id=body["trigger_id"], view=create_modal(body["user_id"], body["channel_id"]) ) @app.action("sport_select") def update_player_options(ack, body, client): ack() selected_sport = body["actions"][0]["selected_option"]["value"] user_id, channel_id, _, _ = body["view"]["private_metadata"].split(":") client.views_update( view_id=body["view"]["id"], view=create_modal(user_id, channel_id, selected_sport) ) @app.action("player_select") def update_position_options(ack, body, client): ack() selected_player = body["actions"][0]["selected_option"]["value"] user_id, channel_id, selected_sport, _ = body["view"]["private_metadata"].split(":") client.views_update( view_id=body["view"]["id"], view=create_modal(user_id, channel_id, selected_sport, selected_player) ) @app.action("position_select") def handle_position_select(ack, body, logger): ack() logger.info(body) @app.view("sport_player_position_modal") def handle_submission(ack, body, client, view, say): ack() try: user_id, channel_id, selected_sport, _ = view["private_metadata"].split(":") selected_player = view["state"]["values"]["player_select"]["player_select"]["selected_option"]["value"] selected_position = view["state"]["values"]["position_select"]["position_select"]["selected_option"]["value"] message = f"<@{user_id}>さんが選んだスポーツは{SPORTS[selected_sport]['name']}で、選んだ選手は{selected_player}、選んだポジションは{selected_position}です。" say(text=message, channel=channel_id) except Exception as e: print(f"Error in handle_submission: {str(e)}") def lambda_handler(event, context): slack_handler = SlackRequestHandler(app=app) return slack_handler.handle(event, context) おわりに 今回、Slackに条件分岐ワークフローを実装しました。 調べてみても出てこなかったので一から作ってみました。 Slack BoltとAWS Lambdaはほとんど触ったことがなかったので、大変でした。 今回、触ったことにより少しは詳しくなれたと思います。 メッセージを送信するだけではなく、スプレッドシートに記載する機能やフォームを変更するなどの他の機能を追加できると便利になるので引き続き勉強していこうと思います。 ありがとうございました。 次回は、佐藤さんです。 どんな記事になるのかワクワクですね♪ ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめまして。ニフティ株式会社でマネージャーをしている三谷です。 今回は、私がリーダーをしているインフラシステムグループの業務支援チームを紹介します。 まずは私の自己紹介です。入社後、ISPサービスのバックボーン管理、サポートサービスのシステム運用、情報セキュリティ推進、品質推進、サービス監視運用、関係会社のシステム運用を担当してきました。現在のチームの担当歴は2年目ですが、途中に関係会社に出向した時期があり、それ以前にも同チームを担当していたのでトータルの担当歴は5年目になります。 業務支援チームとは 下記は2024.8.1時点のニフティのシステム部門の組織図です。当チームは、インフラシステムグループに属しており、ニフティのサービスの監視、システム監視、ネットワーク監視、関係会社のシステム運用・監視、社内業務支援、品質推進を担当しています。 業務支援チームの業務 業務カテゴリは以下2つです。 ①監視・業務支援 監視は、お客様が利用するサービスの監視やシステムの監視です。監視するシステムがあり異常を検知するとアラートが上がりそれに対応する作業を行います。関係会社のシステム運用も行っておりジョブ実行等の対応を行います。24時間365日対応するためシフト体制を組んでおり、2人ペアで作業チェックしながら業務を実施しています。 ②品質推進 ニフティの品質ポリシーに従って、提供サービスに関しての品質基準を定めて検査しています。新しいサービスはこの品質検査をクリアする必要があります。またサービスを提供しているシステムの脆弱性がないか調査することも品質管理の一環として実施しています。 チームメンバー紹介 業務支援チームの人数は14名です。チーム構成は、20代の若手、30代の中堅、50代のベテランの混成です。最近若手が多くジョインしてくれたので人材育成に力を入れています。 Aさん 中途入社1年目。IT業界未経験者ながら業務対応のセンスがよく戦力になりつつある若手メンバー。明るい性格でコミュ力が高い期待の星。 Bさん 中途入社2年目。システム構築・運用のメイン担当を引き受けている中堅メンバー。最近はシステム移行と作業自動化に黙々と取り組む職人的存在。 Cさん 中途入社5年目。関係会社のシステム運用の取りまとめ担当の中堅メンバー。抜群のコミュ力を買われて若手育成も担当し熱心に育成活動中。 Dさん 中途入社30年目。過去にもシフト経験豊富なベテランメンバー。得意の分析力とスクリプト等で作業自動化をさりげなく実現するなかなかのアイデアマン。 Eさん 新卒入社34年目。業務を熟知した頼りになるベテランメンバー。作業手順化を担当し業務の安定度は随一。過去に何人もの新人を育成。 チームの文化 コミュニケーション システム作業やトラブル対応などの業務遂行においてメンバー間の緊密なコミュニケーションは欠かせません。若手とベテランの組合せの場合でも若手が余計な気を遣わなくてもよい雰囲気で、良好なコミュニケーション環境をキープしています。 業務内製化 ニフティではできることは内製で行う考えがあり、業務支援チームにおける監視業務も品質管理業務も外部に委託していたものを内製化して自分たちで業務を行っています。業務に関しては自分たちで手順化しマニュアルを作成してどのメンバーでも安定した作業を実施できるようにしますが、実際にやってみるとさまざまな改善点が見つかるので改善のアイデアを出し合っています。 自動化の取り組み 最近はノーコードで処理を自動化する取り組みを行っています。定型的な作業を自動化して手作業を削減しています。新たな案件では何をどう自動化したいのか具体的なアイデアを大事にしています。 以上、インフラシステムグループ 業務支援チームの紹介でした。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに こんにちは、ニフティ 会員システムグループのシニアエンジニア 伊達です。好きなPC周辺機器は柔らかいシリコンUSBケーブルです。絡まりにくく折れ癖もつきにくいので気に入っています。3本持っており、4本目をどうしようかと思っています。 資格取得支援制度に込める想い ニフティでは以下を目的としてこの資格取得支援制度を運用しています。 社員の成長機会の創出と全社的な技術力の向上 自ら学ぶ風土の醸成 もちろんITエンジニアの仕事は資格のあるなしに関わらずできますが、資格を取るという行為をきっかけにして新しい学びを得ることや自身のスキルの再確認・棚卸を行うことは有意義だと思っています。 つまり、制度の活用を通して、 新しい技術を学び、それを体系立てて理解したことを資格取得を通じて確認 これまでの業務経験で培ったスキルを再確認し、さらなる成長に繋げる が自然と行われることを期待しています。 制度の詳細 ニフティの資格取得支援制度は、資格を取得した社員に対し、報奨金を支給する方式を採用しています。試験の種類ごとに1回だけ受験料を支給するなど、いろいろな方式が試されましたが現在はこの方法に落ち着いています。この方式に変更したことで、「確実に合格するぞ」という強い意志を持って準備に取り組んでから、試験に臨んでくれるようになったと感じています。 また、報奨金は試験の受験料+アルファとしており、資格の取得難易度に併せた金額設定をしています。 難しめの試験は参考書代も賄える金額に設定されています。これには十分に学習をしてから受験してほしいという意図が込められています。 対象の資格 2024年8月27日現在、対象の資格は100を超えます。半年ごとに見直しを行い、業界の需要や技術トレンドを考慮しながら、社員に取得したい資格のヒアリングも行い、ニフティとしてエンジニアにスキル取得してほしい分野の資格を随時追加・更新する運用をしています。 以下に対象資格の一部をご紹介します。 IPAの各種資格 ただし、応用情報技術者試験以上の高度な資格を対象としています AWS認定資格、Google Cloud認定資格、Microsoft Azure認定資格 パブリッククラウドを活用したシステム開発を推奨しており、認定資格の取得も推奨しています Oracle Master、MySQL Certification、OSS-DB ニフティでは多くのRDBMSを使用しており、設計・管理運用のスキルは必須となっています Oracle認定Javaプログラマ、Pythonエンジニア認定基礎試験、Ruby技術者認定試験 LPIC、LinuC CCNA、CCNP その他、TOEICなどビジネス系の資格も多数対象となっています。 どの資格が人気か 新人にはプログラミング言語やLinuxについての資格が比較的人気です。IPAのスペシャリスト系資格試験を受ける人もいます。全体ではここ数年はAWS認定資格を受けるエンジニアが多く、延べで数十人は資格を取得しているほどの人気ぶりです。 また、資格取得支援制度外で取得されることが多いのはCSM(Certified ScrumMaster)です。ニフティではスクラムでの開発が広く浸透しており、スクラムマスターも数多くいます。ただ、スクラムマスターになる経緯はさまざまですし、スクラムマスター歴の長さも人によって異なります。スクラムにとって大切な価値観を身に着けてもらうため、スクラムマスターとしてのステップアップを目指す人には社費で研修を受講してもらい、認定スクラムマスターとなって活躍してもらっています。 最後に ニフティの資格取得支援制度について簡単ですが紹介しました。ニフティにはエンジニアに嬉しい制度が多数あります。福利厚生制度シリーズとしてほかの制度についても紹介していきます。 どんな制度があるかについて興味がある方は 福利厚生(ニフティ採用サイト) をご覧ください。
アバター
こんにちは、サイドカーより助手席に乗りたいお姉さんです。運転免許は持っていません。 ECS Fargate で、メインのコンテナ + FireLens(Fluent Bit) のサイドカー構成で、別アカウントの CloudWatch にログを出力してみました。 FireLens とは FireLens とは、ECSのコンテナのログを、様々な分析ツール(CloudWatch, Amazon Kinesis Data Firehose, サードパーティ製ツールなど)へ転送できる仕組みです。 コンテナ内の複数箇所に出力されたログをまとめて転送したり、逆に1つのログを複数箇所に転送したりできるため、利便性が高いです。 FireLens を用いたログ出力の基本は公式ブログに載っていますので、そちらをご参照ください。 https://aws.amazon.com/jp/blogs/news/announcing-firelens-a-new-way-to-manage-container-logs/ Fluent Bitとは Fluent Bit は、システムのログを収集・処理してくれるツールで、非常に軽量です。 FireLens で、コンテナのログドライバーとして使うことができます。 詳細は、公式ドキュメントをご参照ください。 https://docs.fluentbit.io/manual 別アカウントへのログ転送 ECSで Fluent Bit を動かす際は CloudWatch への転送用のプラグインを使うのですが、ドキュメントを見てみると、設定の中に role_arn という項目があります。 role_arn : ARN of an IAM role to assume (for cross account access). https://github.com/aws/amazon-cloudwatch-logs-for-fluent-bit/blob/mainline/README.md#plugin-options for cross account access ということは、別のアカウントへも転送できるんだな~、と思ったのですが、それ以上の説明はどこにも無く、やり方が分からない!!ということでやってみました。 転送先アカウントの設定 まず、転送先のアカウントで必要な設定をしていきます。 IAMロールの作成 「転送元のアカウントから、このアカウントの CloudWatch にログを出力していいよー!」というロールを作ります。 以下の要素を含めます。 信頼されたエンティティ( AssumeRolePolicyDocument )に、転送元アカウントの AssumeRole の許可を追加 Principal に設定されたアカウントに、このロールの権限を与えてOKだよ~という設定です { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::${転送元アカウントのID}:root" }, "Action": "sts:AssumeRole" } ] } CloudWatch logs への書き込み許可をするポリシーをアタッチ ロググループやログストリームの作成を許可します { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowLogGroup", "Effect": "Allow", "Action": [ "logs:DescribeLogStreams", "logs:CreateLogGroup" ], "Resource": "arn:aws:logs:*:${転送先アカウントのID}:log-group:*" }, { "Sid": "AllowLogStream", "Effect": "Allow", "Action": [ "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:*:${転送先アカウントのID}:log-group:*:log-stream:*" } ] } 転送元アカウントの設定 転送元アカウントの設定もしていきます。 ECSタスクロールの作成 先ほど設定した転送先アカウントのロールを、ECSタスクが引き受けられるように、転送元でもタスクロールに AssumeRole を許可するポリシーをアタッチします。 { "Version": "2012-10-17", "Statement": [ { "Sid": "allowAssumeRole", "Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "arn:aws:iam::${転送先アカウントのID}:role/${転送先アカウントで作成したロール名}" } ] } ECS タスク定義の作成 タスク定義を作ります。 今回は、プレーンなhttpdコンテナを立ち上げて、そのログを別アカウントへ転送します。 以下の値を設定していきます。それ以外はデフォルトでOKです。 タスクロール 先ほど作成したタスクロールを選択 コンテナ イメージURIに httpd を入力 名前は何でもOK ログ収集 AWS FireLens 経由でカスタム送信先にログをエクスポート を選択 オプションを設定(キーに、 ドキュメント に記載されたオプションの項目を入れる) Name : どこに転送するか。今回は cloudwatch です。 region : ロググループを作成するリージョン log_group_name , log_stream_name : ロググループ名、ログストリーム名。ECSタスクのIDなどを動的に入れることができます。( https://github.com/aws/amazon-cloudwatch-logs-for-fluent-bit?tab=readme-ov-file#templating-log-group-and-stream-names ) auto_create_group : trueにすると、設定した名前のロググループが存在しない場合に自動で作成してくれます。 role_arn : ここに転送先のアカウントで作成したロールのarnを設定します!!!! arn:aws:iam::${転送先アカウントのID}:role/${転送先アカウントで作成したロール名} コンソールで見るとこんな感じ コンソールで FireLens の設定をすると、自動的に2つ目のコンテナ( Fluent Bit が動くコンテナ)の設定の入力欄が現れますが、今回はデフォルトのままいじりませんでした。 最終的にできたタスク定義がこちらです。 { "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:${転送元アカウントのID}:task-definition/fluentbit-test:1", "containerDefinitions": [ { "name": "httpd", "image": "httpd", "cpu": 0, "portMappings": [], "essential": true, "environment": [], "environmentFiles": [], "mountPoints": [], "volumesFrom": [], "ulimits": [], "logConfiguration": { "logDriver": "awsfirelens", "options": { "log_group_name": "/ecs/httpd", "log_stream_name": "$(ecs_task_id)", "region": "ap-northeast-1", "role_arn": "arn:aws:iam::${転送先アカウントのID}:role/${転送先アカウントで作成したロール名}", "auto_create_group": "true", "Name": "cloudwatch" }, "secretOptions": [] }, "healthCheck": { "command": [ "CMD-SHELL", "date" ], "interval": 30, "timeout": 5, "retries": 3 }, "systemControls": [] }, { "name": "log_router", "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable", "cpu": 0, "memoryReservation": 51, "portMappings": [], "essential": true, "environment": [], "mountPoints": [], "volumesFrom": [], "user": "0", "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/ecs-aws-firelens-sidecar-container", "mode": "non-blocking", "awslogs-create-group": "true", "max-buffer-size": "25m", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "firelens" }, "secretOptions": [] }, "systemControls": [], "firelensConfiguration": { "type": "fluentbit" } } ], "family": "fluentbit-test", "taskRoleArn": "arn:aws:iam::${転送元アカウントのID}:role/ecs-task-role-fluent-bit-test", "executionRoleArn": "arn:aws:iam::${転送元アカウントのID}:role/ecsTaskExecutionRole", "networkMode": "awsvpc", "revision": 5, "volumes": [], "status": "ACTIVE", "requiresAttributes": [ { "name": "ecs.capability.execution-role-awslogs" }, { "name": "com.amazonaws.ecs.capability.docker-remote-api.1.17" }, { "name": "com.amazonaws.ecs.capability.docker-remote-api.1.28" }, { "name": "com.amazonaws.ecs.capability.docker-remote-api.1.21" }, { "name": "com.amazonaws.ecs.capability.logging-driver.awsfirelens" }, { "name": "com.amazonaws.ecs.capability.task-iam-role" }, { "name": "ecs.capability.container-health-check" }, { "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18" }, { "name": "ecs.capability.task-eni" }, { "name": "com.amazonaws.ecs.capability.docker-remote-api.1.29" }, { "name": "com.amazonaws.ecs.capability.logging-driver.awslogs" }, { "name": "com.amazonaws.ecs.capability.docker-remote-api.1.24" }, { "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19" }, { "name": "ecs.capability.firelens.fluentbit" } ], "placementConstraints": [], "compatibilities": [ "EC2", "FARGATE" ], "requiresCompatibilities": [ "FARGATE" ], "cpu": "1024", "memory": "3072", "runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" }, "registeredAt": "2024-08-26T02:19:06.916Z", "registeredBy": "himitsu", "tags": [] } タスクを起動して、ログが転送されることを確認 転送元アカウントで、先ほど作ったタスク定義でコンテナを上げます。 特にアクセス制限も必要無いので、デフォルトVPCに適当なECSクラスターを作成し、そこにタスクを1つ起動させました。 起動後しばらく待つと、コンソールのログのタブに Fluent Bit のログが出てきます。 /ecs/httpd っていうロググループに、${タスクID}っていうログストリームを作ったよ~とかそんなログが出ています。 では、転送先アカウントの方も見てみます。 コンソールから、 CloudWatch のロググループを見てみると、 /ecs/httpd があります。 ロググループを開いてみると、さっきのタスクIDでログストリームが作成されています。 ログイベントはこんな感じで出ています。 1つのログイベントに、ログのメッセージだけでなく、コンテナIDやタスクのarnまで出ているという親切設計 まとめ Fluent Bit のオプションの role_arn に転送先アカウントのロールを設定することで、別アカウントへのログの転送ができました。 今回は標準出力を CloudWatch へ転送するという単純な構成でしたが、もっとカスタムした構成( Fluent Bit の設定ファイルをS3に置いたり、自前で Fluent Bit のコンテナを用意したり)でも別アカウントへの転送は可能なので、是非やってみてください^ヮ^ ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター