TECH PLAY

ニフティ株式会社

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

500

イベント概要 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で ニフティグループに参加する
アバター
iwillblog . こんにちは、Ryuseiです。普段はマイ ニフティというiOS/Androidアプリの開発をしています。 先日、弊社エンジニア3名でiOSDC Japan 2024にリアル参加してきました!(私はDay 1とDay2の一部に参加させていただきました。) 宣言通りブログを書きます! iOSDCとは iOSDC Japan 2024はiOS関連技術をコアのテーマとしたソフトウェア技術者のためのカンファレンスです。 https://iosdc.jp/2024/ iOSDC Japanは今年で9回目の開催らしいですが、過去最高人数の参加者になったみたいですね。 私自身は今回が初参加となります! ブース 今回はスタンプラリーがビンゴになっていて、各ブースでスタンプを押してもらって縦横斜めが揃うとクレープかたこ焼きの引換券がゲットできるというものでした。 全ての協賛ブースを制覇してクレープとたこ焼きの引換券をそれぞれ8枚と全制覇特典のiOSDCパーカーをゲットしました! スパイダープラスさんのブースでは風速1.8m/sを目指すゲームで、なんとピッタリ賞をいただきました!!ありがとうございます! セッション 4レーン同時進行で進んでおり、見たいセッションだらけでどこに行こうか迷って大変でした!! iOSの隠されたAPIを解明し、開発効率を向上させる方法 by noppe 開発時に便利そうなAPIがたくさんで実際に使ってみたいと思いました。 UIDebuggingInformationOverlay がまさしく自分が求めていたものでした。 Ditto SDK 紹介: インターネットなしで快適なデータ同期 by 近藤峻輔 dittoというプロトコルは聞いたことありましたがこのセッションを見て活用場面がたくさんありそうだなと思いました。 FlutterのSDKもあるとのことなので実際に使ってみたいです。 Mergeable Libraryで高速なアプリ起動を実現しよう! by giginet 今までふわっとして浅い理解だったFrameworkについてちょっと詳しくなりました! Mergeable Libraryが登場した背景から説明されていたので自分でも理解できました。 月間4.5億回再生を超える大規模サービスTVer iOSアプリのリアーキテクチャ戦略 by 小森 英明 実例の発表はすごく勉強になります! モジュールごとにリリースするのが参考になりました。 LT バドワイザーを片手にたこ焼きを食べながらのLT最高でした! 登壇者の皆さんが上手すぎて面白く、あっという間に終わってしまいました。 LTに完璧なセット 懇親会 さまざまな働き方をしてる方々とお話しさせていただきとても刺激になりました。 お話ししてくださった皆様ありがとうございます! おわりに 今回初参加でしたが、他カンファレンスよりも盛り上がりがすごく驚きました。 また幕間の動画など細かなところから凝っている点にも驚かされました。 セッションからは新たな技術や、より深い知識を得られてとても勉強になりました。 リアル参加することで熱量を直で感じモチベーションが上がっているのを実感しています。 そして、クレープとたこ焼きとバドワイザー最高でした!! ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
こんにちは。ニフティ株式会社の村山です。 先日 Python のフォーマッタである Black のアップデートを行った際にアップデート前後でフォーマット結果に差分が出たため原因を調べたときのお話です。結論から言ってしまえば、行長判定において日本語が二文字分としてカウントされるようになっていました。 別に大きく困ったわけではないですが小ネタ共有程度に。 Black 23.3.0 から Preview 機能として、行の長さの計算時に Unicode の East Asian Width を加味して計算する機能が追加されました。( リリースノート ) これまでは行の長さは len() によって計算されており、ひらがなもアルファベットも等しく1文字として扱われていました。 East Asian Width は Unicode のプロパティであり、詳細は UAX #11 に記載されていますが、かなり色々無視して大幅にざっくり言うと全角で表示されるような文字は2文字分としてカウントされるということです。 24.1.0 以降ではこれが標準の挙動になっていたみたいです。リリースノートには記載がありませんが、該当箇所を 23.12.1 と 24.1.0 で見比べると Preview かどうかで処理を分ける分岐がなくなっています。 というわけなので、ソース内で日本語を利用している行については、これまでにフォーマッタを素通りしていた行が 24.1.0 以降から突然改行されるようになることがあるみたいでした。 簡単な動作検証もしてみました。 フォーマット前: s = "" # 89文字 s = s + "1234567890123456789012345678901234567890123456789012345678901234567890123456789" # 88文字 s = s + "123456789012345678901234567890123456789012345678901234567890123456789012345678" # 88文字(最後が全角) s = s + "123456789012345678901234567890123456789012345678901234567890123456789012345678" Black 23.12.1: s = "" # 89文字 s = ( s + "1234567890123456789012345678901234567890123456789012345678901234567890123456789" ) # 88文字 s = s + "123456789012345678901234567890123456789012345678901234567890123456789012345678" # 88文字(最後が全角) s = s + "123456789012345678901234567890123456789012345678901234567890123456789012345678" Black 23.12.1( --preview 指定)、24.1.0: s = "" # 89文字 s = ( s + "1234567890123456789012345678901234567890123456789012345678901234567890123456789" ) # 88文字 s = s + "123456789012345678901234567890123456789012345678901234567890123456789012345678" # 88文字(最後が全角) s = ( s + "123456789012345678901234567890123456789012345678901234567890123456789012345678" ) デフォルトの最大行長 88 文字できっちりコードを書かれている方などは今回の変更で割とコードが読みやすくなるのではないでしょうか。私が普段扱っているプロダクトは 132 文字と長めに設定されていたので個人的にはあんまり影響がありませんでしたが… なお、Python のリンタ兼フォーマッタとして普及してきている Ruff においても line-length の計算には East Asian Width を考慮することとなっているようです( ドキュメント )。古いバージョンの Black から Ruff に移行する場合にもこういったことが起こるかもしれません。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに おはようございます。IWSです 私達のチームではECRへのイメージのビルド、プッシュにGitHub Actionsを使っているのですが、ついでにECSのタスク定義も新しいのを作ってくれると便利なタイミングがあるかな〜とおもったので試しに作ってみました。せっかくなのでどういうのを作ったか書き残しておこうかなと思います。 タスク定義を作る あらかじめビルドしてECRにプッシュするところまでは出来ている状態で作っていきます。 タスク定義の作成には AWS CLI の aws ecs register-task-definition を使います。 オプションを1つずつ指定していっても使えますが、大変なのでタスク定義のJSONファイルを渡す方法を使いましょう。 既存のタスク定義のJSONを取得する タスク定義のJSONファイルを1から用意するのは面倒くさいので、今回は元々あるタスク定義を持ってきて必要な部分だけを変えて作ろうかと思います。まずは既存のタスク定義を取得するところからはじめましょう。 タスク定義の取得には aws ecs describe-task-definition --task-definition <タスク定義の名前> を使います。 コマンドを叩くとこのようなJSONが取得できるはずです。 { "taskDefinition": { "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:task-definition/<タスク定義名>:xx", "containerDefinitions": [ { "name": "name", "image": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/<ECR名>:<タグ>", "cpu": 0, "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp", "name": "80-tcp" } ], "essential": true, "environment": [ { "name": "ENV", "value": "development" } ], "mountPoints": [], "volumesFrom": [], "dockerSecurityOptions": [], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "hoge", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "hoge" }, "secretOptions": [] }, "systemControls": [] } ], "family": "<タスク定義名>", "taskRoleArn": "<taskRoleArn>", "executionRoleArn": "<executionRoleArn>", "networkMode": "awsvpc", "revision": xx, "volumes": [], "status": "ACTIVE", "requiresAttributes": [ { "name": "com.amazonaws.ecs.capability.logging-driver.awslogs" }, { "name": "ecs.capability.execution-role-awslogs" }, { "name": "com.amazonaws.ecs.capability.ecr-auth" }, { "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19" }, { "name": "com.amazonaws.ecs.capability.task-iam-role" }, { "name": "ecs.capability.execution-role-ecr-pull" }, { "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18" }, { "name": "ecs.capability.task-eni" } ], "placementConstraints": [], "compatibilities": [ "EC2", "FARGATE" ], "requiresCompatibilities": [ "FARGATE" ], "cpu": "256", "memory": "1024", "registeredAt": "2024-06-27T17:21:10.983000+09:00", "registeredBy": "arn:aws:sts::xxxxxxxxxxxx:assumed-role/Administrator/hogefuga" }, "tags": [] } これをそのまま使えたら楽なのですが、残念ながらこのままだと使えないので少し整えてあげる必要があります。 JSONを整える やることは taskDefinitionの中身を取り出す taskDefinitionArn を削除 revision を削除 status を削除 requiresAttributes を削除 compatibilities を削除 registeredAt を削除 registeredBy を削除 です。 GitHub Actions ではデフォルトで使える jq コマンドで整形すると簡単です。 # 取得したタスク定義JSONからいらない部分を削除 jq '.taskDefinition | del (.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)' > task-def.json jqコマンドで整形後のタスク定義JSON { "containerDefinitions": [ { "name": "name", "image": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/<ECR名>:<タグ>", "cpu": 0, "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp", "name": "80-tcp" } ], "essential": true, "environment": [ { "name": "ENV", "value": "development" } ], "mountPoints": [], "volumesFrom": [], "dockerSecurityOptions": [], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "hoge", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "hoge" }, "secretOptions": [] }, "systemControls": [] } ], "family": "<タスク定義名>", "taskRoleArn": "<taskRoleArn>", "executionRoleArn": "<executionRoleArn>", "networkMode": "awsvpc", "volumes": [], "placementConstraints": [], "requiresCompatibilities": [ "FARGATE" ], "cpu": "256", "memory": "1024" } これでタスク定義が作れるJSONになりました! イメージを変える ここまでで aws ecs register-task-definition に渡せるJSONファイルが用意できました。ですが、中身の設定は当然前のタスク定義のままです。そのため、このままタスク定義を作成しても前と同じものが出来てしまうだけになります。 今度は中身の設定を変えていきましょう! 今回は使用するイメージ( containerDefinitions[0].image )のタグの部分だけを変更します。 containerDefinitions[0].image はこうなっています "image": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/<ECR名>:<タグ>" : の後がタグになっているので split を使って古いタグを取得して置き換えてしまいましょう # imageのタグ部分だけを取得する PREVIOUS_IMAGE_TAG=$(cat task-def.json | jq -r '.containerDefinitions[0].image | split(":")[1]') 古いタグが取得できたらあとは新しく設定するタグと置き換えるだけです。 # $PREVIOUS_IMAGE_TAG(古いタグ) を $IMAGE_TAG(新しいタグ)で置き換え cat task-def.json | jq --arg PREVIOUS_IMAGE_TAG "$PREVIOUS_IMAGE_TAG" --arg IMAGE_TAG "$IMAGE_TAG" '.containerDefinitions[0].image |= sub($PREVIOUS_IMAGE_TAG; $IMAGE_TAG)' > new-task-def.json jq の sub() を使うことで対象の部分を置き換えることができます。jqはなんでもできますね…… "image": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/<ECR名>:<古いタグ>" ↓ "image": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/<ECR名>:<新しいタグ>" これで新しいタスク定義のJSONファイルができました! タスク定義を作成 ここまできたらあとは完成したJSONファイルを使ってタスク定義を作るだけです。 # タスク定義を作成 aws ecs register-task-definition --cli-input-json fileb://new-task-def.json --cli-input-json を使うことでパラメーターをJSONファイルで渡すことが出来ます。 このコマンドが成功すればタスク定義にあたらしいリビジョン番号が増え、設定に新しいイメージタグが書かれているはずです。 どうでしょう?タスク定義は出来ましたか? あとはこのタスク定義を使って aws ecs update-service なりしてあげれば、新しいタスク定義を使用したECSタスクが立ち上がってくると思います。 最後に自分の作ったコードを一部載せておくので良ければ参考にしてください。 タスク定義作成部分コード 事前にAWS credentialsやイメージのビルドなどをしてください。 - name: Task Definition Update id: update-task-definition run: | # Create a new revision of the task definition aws ecs describe-task-definition --task-definition $TASK_DEF_NAME | jq '.taskDefinition | del (.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)' > task-def.json PREVIOUS_IMAGE_TAG=$(cat task-def.json | jq -r '.containerDefinitions[0].image | split(":")[1]') cat task-def.json | jq --arg PREVIOUS_IMAGE_TAG "$PREVIOUS_IMAGE_TAG" --arg IMAGE_TAG "$IMAGE_TAG" '.containerDefinitions[0].image |= sub($PREVIOUS_IMAGE_TAG; $IMAGE_TAG)' > new-task-def.json aws ecs register-task-definition --cli-input-json fileb://new-task-def.json - name: ECS Update id: update-ecs run: | # Update the ECS service with the latest task definition. aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --desired-count $DESIRE_COUNT --task-definition $TASK_DEF_NAME --force-new-deployment まとめ 今回はGitHub Actionsでのタスク定義の作成について書きました。 最近はGitHub Actionsを使ってAWSのリソースを作ったりイメージをPushしたりいろいろなことをやったりしています。今回もそのなかで試しにやってみたことを記事にしてみました。いつか何かしらの参考になればうれしいです。 ここまでお読みいただきありがとうございました! ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
この記事は、リレーブログ企画「24新卒リレーブログ」の記事です。 はじめに こんにちは。 初めまして、新卒1年目のけにです。 現在OJT期間で 課金システムチーム に所属しており、社内向けのAPIの開発に携わっています。そのAPIに対して負荷テストを行うにあたり、チーム内で負荷テストツールとしてk6を採用するという話になりました。 k6は簡単にテストシナリオを書けるツールでありながら、非常に強力な負荷テストを実行することができます。本記事では、そのk6について基本的な使い方からinfluxDBとGrafanaを用いた結果の可視化方法まで、調べた内容について自分の理解の整理も含めて記事にしたいと思います。 負荷テストを行う理由 現在のAPI開発において、ユーザー数の急激な増加や予期しないトラフィックの集中が起こることを想定し、それら状況下においてもシステムが安定して機能することが求められます。そのため、パフォーマンスの検証、限界値の測定、リソース使用状況の把握など、対象のシステムの品質や信頼性、リソースの最適化などを目的とする負荷テストは非常に重要な役割を持ちます。 今回私達のチームでは、開発したAPIが実用に耐えうる構成となっているのかについて実際のアクセス数を元に確認することを主な目的として、スパイクテストとストレステストを行いました。 想定される範囲で急激な負荷の上昇が起きたとしても想定通りの速度が出せるか 想定される以上の高負荷がかかった場合でもAPIは正常に動き十分なレスポンス速度を出すことができるか 長時間負荷がかかった場合にどのような挙動をするのか などです。また、負荷テストを実行した際に CPUやメモリなどのリソースは現在の構成で適切なものになっているか API / サーバーの設定は適切なものになっているか についても確認を行いました。 これらにより、システムの限界値や潜在的な問題点を事前に把握し必要な対策を講じることで、システムの品質と信頼性を向上させユーザー満足度を高めることに繋がります。 ツールの選定 負荷テストを行うことができるツールは様々あります。 それらのうち、無料で利用できる7つを比較してみます。 ツール名 公式 シナリオ記述言語 結果確認 方法 詳細 k6 k6.io , github JavaScript grafana / CUI / Cloud Golangで開発され、高負荷環境でもスムーズに動作、拡張性が高い Apache Bench apache.org CLI CUI 単一のURLへのリクエストを生成するツールのため、シナリオベースでWebアプリケーションをテストすることには向いていない Apache JMeter jmeter.apache.org , github GUI / Java GUI 拡張性が高く、結果可視化方法が豊富 Tsung tsung , github XML tsung-recorder Erlang言語で開発され、データベースやメッセージングシステムのテストを行える Gatling gatling.io , github Scala .html JavaVM上で動作 Vegeta vegeta , github Golang .html / plotコマンド 同時接続数やリクエストレートの詳細な制御が可能 Locust locust.io , github Python webサーバ スケーラブルで、分散実行が可能 負荷テストツールの比較 私達のチームでは、主に Python を使用していることから Locust と、今後チーム内に導入するにあたって学習コストが低い JavaScript で記述でき、現在も盛んに開発が行われている k6 が選択肢に上がりました。それらの内、OSSでアップデートが盛んに行われているため新しい技術に対応可能である点から、 k6 を採用することにしました。 実行環境 準備 今回は以下のGrafanaが用意している k6のリポジトリ を使用します。 k6で行った負荷テスト結果をinfluxDBに保存し、Grafanaを用いて可視化します。 # clone git clone https://github.com/grafana/xk6-output-influxdb.git エンドポイントについては、今回は こちら を使います。 これは、 k6 の実験用HTTP、 WebSocket API のコレクションとなっています。 このうち https://test-api.k6.io/public/crocodiles/{id} に対して負荷テストを行います。 注意点として、これは共有テスト環境であるため高負荷テストは避ける必要があります。 APIの挙動 curlコマンドを用いて一度APIを叩いてみます。 叩くことができるidの一覧は以下の結果のとおりです。 curl https://test-api.k6.io/public/crocodiles/ [ { "id": 1, "name": "Bert", "sex": "M", "date_of_birth": "2010-06-27", "age": 14 }, { "id": 2, "name": "Ed", "sex": "M", "date_of_birth": "1995-02-27", "age": 29 }, { "id": 3, "name": "Lyle the Crocodile", "sex": "M", "date_of_birth": "1985-03-03", "age": 39 }, { "id": 4, "name": "Solomon", "sex": "M", "date_of_birth": "1993-12-25", "age": 30 }, { "id": 5, "name": "The gharial", "sex": "F", "date_of_birth": "2004-06-28", "age": 20 }, { "id": 6, "name": "Sang Buaya", "sex": "F", "date_of_birth": "2006-01-28", "age": 18 }, { "id": 7, "name": "Sobek", "sex": "F", "date_of_birth": "1854-09-02", "age": 169 }, { "id": 8, "name": "Curious George", "sex": "M", "date_of_birth": "1981-01-03", "age": 43 } ] それを元に対象のAPIを叩いてみます。返却値は以下のようになっています。 curl https://test-api.k6.io/public/crocodiles/1/ { "id": 1, "name": "Bert", "sex": "M", "date_of_birth": "2010-06-27", "age": 14 } プロキシ設定の変更 本リポジトリでは、 k6 → influxdb → grafana の順にデータが送られます。 コンテナ間の通信が発生するため、 ~/.docker/config.json にproxyが設定されていると503エラーとなる可能性があります。そのため、設定をしている人は修正が必要になります。 シナリオ シナリオの選定 条件毎に、必要なシナリオに応じてパターンを考える必要があります。 特にGrafanaでは、負荷テストの種類を6つ挙げています。( 参考 テスト名 時間 内容 Smoke test 数分、数秒    最小限の負荷でシステムの機能を検証し、基準となるパフォーマンス値を収集するテスト Average-load test 中(5〜60分) 標準的な負荷下でシステムがどのように動作するかを評価するテスト Stress test 中(5〜60分) トラフィックのピーク時の負荷でシステムがどのように機能するかを調べるテスト Spike test 長時間 突然の使用状況の急増に対してシステムが耐えて機能するかどうかを検証するテスト Breakpoint test 数分 システムの限界を計測するために行うテスト Soak test 必要な限り 数時間から数日の長期間行う平均負荷テスト 負荷テストのシナリオ例 シナリオの実装 k6 テスト スクリプトでは、 load_test.js 内の options オブジェクトを使用してシナリオを構成できます。 各シナリオには一意の名前を付け、 executor タイプとその構成を指定する必要があります。 export const options = { scenarios: { spike: { executor: "ramping-vus", startVUs: 0, stages: [ { duration: "5m", target: 100 }, { duration: "2m", target: 0 }, ], }, stress: { executor: "constant-vus", vus: 50, duration: "10m", }, }, }; 上記は、スパイクテストとストレステストの例です。 スパイクテスト ramping-vus executor を使用 仮想ユーザーの秒間アクセス数を5分掛けて 0 から 100 まで徐々に増やし、2分掛けて 0 まで減らしている ストレステスト constant-vus executor を使用 10 分間、秒間50回の仮想ユーザーからのアクセスを保っている シナリオを複数指定している場合、上から順に実行されます。 executorには他にも種類があり、それぞれ記述方法が異なります。 参考: https://k6.io/docs/using-k6/scenarios/executors/#all-executors 負荷テストの実行 今回は上記のうち、ストレステストとスパイクテストを行ってみます。 実行コード import http from 'k6/http'; // スパイクテストの設定 export const options = { scenarios: { // スパイクテスト // 短時間で一気に負荷が上昇した場合の負荷テスト spike: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '2s', target: 10 }, { duration: '1m', target: 0 }, ], }, // ストレステスト // 短時間で一気に負荷が上昇した場合の負荷テスト stress: { executor: "constant-vus", vus: 50, duration: "10m", }, }, }; export default function () { // APIリクエストを送信 const id = Math.floor(Math.random() * 8); const response = http.get(`https://test-api.k6.io/public/crocodiles/${id}`, { tags: { "id": id, "all": "all" } // Grafana表示用にタグを設定 }); // レスポンスをチェック console.log(`VU: ${__VU}, Iteration: ${__ITER}, ID: ${id}, Status: ${response.status}`); } # コンテナの起動 ## k6以外を立ち上げる docker compose up influxdb grafana -d # テスト実行 docker compose run --rm -T k6 run - < samples/load_test.js 起動画面 結果の確認 1. k6 実行結果 確認できるメトリクスは以下の通りになります。 メトリクス名 説明 data_received 受信したデータ量 data_sent 送信されたデータ量 http_req_blocked リクエストを開始するまでにブロックされた時間 http_req_connecting リモートホストとのTCPコネクションの確立にかかった時間 http_req_duration リクエストを送ってから帰ってくるまでの時間 ( http_req_sending + http_req_waiting + http_req_receiving ) http_req_failed リクエストに失敗した数 http_req_receiving リモートホストからの応答データ受信にかかった時間 http_req_sending リモートホストへのデータ送信にかかった時間 http_req_tls_handshaking リモートホストとのTLSセッションのハンドシェイクにかかった時間 http_req_waiting リモートホストからの応答待ち時間 http_reqs 生成した HTTP リクエストの合計数 iteration_duration 1回のイテレーションの実行に要した時間 iterations virtual usersが実行したデフォルト関数の回数 vus virtual users、同時にアクセスする仮想ユーザー数 vus_max 仮想ユーザーの最大可能数 確認できるメトリクスの一覧 特に確認する必要があるのは http_req_duration , http_reqs , http_req_failed の3つです。 リクエストにかかった時間、リクエストの数、失敗した数です。 この結果から、このAPIのレスポンス速度は約200ms程度であり、ほとんどの場合に於いてその速度を出していることがわかります。 平均値 最小値 中央値 最大値 90パーセン タイル 95パーセン タイル 成功数 失敗数 203.49ms 176.55ms 200.83ms 1.21s 216.69ms 223.42ms 3112回 0回 実行結果 2. grafana コンテナを立ち上げた際に立ち上がっているため、そちらにアクセスします。 アクセス先: http://localhost:3000/dashboards これでは結果が分かりにくいため、凡例とクエリを修正します。 修正先: grafana/dashboards/xk6-output-influxdb-dashboard.json ︎ 修正コード { "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "target": { "limit": 100, "matchAny": false, "tags": [], "type": "dashboard" }, "type": "dashboard" } ] }, "editable": true, "fiscalYearStartMonth": 0, "gnetId": null, "graphTooltip": 1, "iteration": 1677877089957, "links": [], "liveNow": false, "panels": [ { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 0, "y": 0 }, "id": 2, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.2.6", "targets": [ { "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)n |> filter(fn: (r) => r["_measurement"] == "http_reqs")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r["_field"] == "value")n |> group(columns: ["_measurement"])n |> aggregateWindow(every: v.windowPeriod, fn: sum, createEmpty: false)n |> yield(name: "sum")", "refId": "EACH" } ], "title": "Requests Made", "type": "stat" }, { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 4, "y": 0 }, "id": 12, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.2.6", "targets": [ { "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)n |> filter(fn: (r) => r["_measurement"] == "http_req_failed")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r["_field"] == "value")n |> group(columns: ["_measurement"])n |> aggregateWindow(every: v.windowPeriod, fn: sum, createEmpty: false)n |> yield(name: "sum")", "refId": "EACH" } ], "title": "HTTP Failures", "type": "stat" }, { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 8, "y": 0 }, "id": 13, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [ "max" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.2.6", "targets": [ { "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)n |> filter(fn: (r) => r["_measurement"] == "http_reqs")n |> filter(fn: (r) => r["_field"] == "value")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> group()n |> aggregateWindow(every: v.windowPeriod, fn: sum, createEmpty: false)", "refId": "EACH" } ], "title": "Peak RPS", "type": "stat" }, { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "ms" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 12, "y": 0 }, "id": 14, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.2.6", "targets": [ { "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)n |> filter(fn: (r) => r._measurement == "http_req_duration")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r._field == "value")n |> group()n |> quantile(q: 0.95, method: "exact_mean")", "refId": "EACH" } ], "title": "P95 Response Time", "type": "stat" }, { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "decbytes" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 16, "y": 0 }, "id": 15, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.2.6", "targets": [ { "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)n |> filter(fn: (r) => r._measurement == "data_received")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r._field == "value")n |> group()n |> sum()", "refId": "EACH" } ], "title": "Data Received", "type": "stat" }, { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "decbytes" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 20, "y": 0 }, "id": 16, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.2.6", "targets": [ { "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)n |> filter(fn: (r) => r._measurement == "data_sent")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r._field == "value")n |> group()n |> sum()", "refId": "EACH" } ], "title": "Data Sent", "type": "stat" }, { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "left", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "EACH", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "displayName": "${__field.labels.id}", "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byFrameRefID", "options": "VUS" }, "properties": [ { "id": "displayName", "value": "Active VUs" }, { "id": "custom.lineStyle", "value": { "dash": [ 10, 10 ], "fill": "dash" } }, { "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } } ] }, { "matcher": { "id": "byFrameRefID", "options": "RPS" }, "properties": [ { "id": "displayName", "value": "RPS" } ] }, { "matcher": { "id": "byFrameRefID", "options": "EACH" }, "properties": [ { "id": "unit", "value": "ms" }, { "id": "custom.axisPlacement", "value": "right" } ] }, { "matcher": { "id": "byFrameRefID", "options": "ALL" }, "properties": [ { "id": "unit", "value": "ms" }, { "id": "displayName", "value": "ALL" } ] } ] }, "gridPos": { "h": 11, "w": 24, "x": 0, "y": 4 }, "id": 11, "options": { "legend": { "calcs": [ "min", "mean", "max", "lastNotNull" ], "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.1", "targets": [ { "hide": false, "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)n |> filter(fn: (r) => r._measurement == "http_req_duration")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r._field == "value")n |> filter(fn: (r) => r.status == "200")n |> group(columns: ["all"], mode:"by")n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)n |> yield(name: "mean")n", "refId": "ALL" }, { "hide": false, "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)n |> filter(fn: (r) => r._measurement == "http_req_duration")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r._field == "value")n |> filter(fn: (r) => r.status == "200")n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)n |> yield(name: "mean")", "refId": "EACH" }, { "hide": false, "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)n |> filter(fn: (r) => r["_measurement"] == "vus")", "refId": "VUS" }, { "hide": false, "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)n |> filter(fn: (r) => r["_measurement"] == "http_reqs")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r["_field"] == "value")n |> group(columns: ["_measurement"])n |> aggregateWindow(every: 1s, fn: sum, createEmpty: false)n |> yield(name: "sum")", "refId": "RPS" } ], "type": "timeseries" } ], "refresh": false, "schemaVersion": 32, "style": "dark", "tags": [], "templating": { "list": [ { "allValue": null, "current": { "selected": false, "text": "All", "value": "$__all" }, "datasource": null, "definition": "import "influxdata/influxdb/schema"nschema.tagValues(bucket: v.defaultBucket, tag: "testid")", "description": null, "error": null, "hide": 0, "includeAll": true, "label": null, "multi": false, "name": "testid", "options": [], "query": "import "influxdata/influxdb/schema"nschema.tagValues(bucket: v.defaultBucket, tag: "testid")", "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "type": "query" } ] }, "time": { "from": "now-1h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "K6 Test Results", "uid": "4sk8QaJVx", "version": 1 } 修正後結果 凡例をID毎に設定したことで、各IDのレスポンス速度が可視化できるようになりました。 凡例名 概要 ALL 全ID平均の統計値 (クエリを追加) 各ID (タグで変更可能) (凡例を修正) 対象IDの統計値 Active VUs (Virtual Users) 動いたVirtual Users の数 RPS (Requests Per Second) 1秒あたりのリクエスト数 設定した凡例 修正前 修正後 全APIのレスポンスの平均速度 別途クエリを作成し表示しています。 毎秒毎に平均を算出したことで、全IDの平均値の推移を可視化するようにしました。 このAPIでは、レスポンス速度の平均が197msであることがわかり、ほぼ一定の速度を出せていることがわかります。 VUs (Virtual Users) このグラフは一秒間に何回のアクセスがあったかを表しています。 シナリオの通り、開始2秒でアクセス数が20まで跳ね上がり、その後2分をかけて徐々に数が減っている様相が見て取れます。 RPS (response per second) 直前の一秒間に帰ってきたリクエストの数を返しています。 時間経過で徐々に減少していることが見て取れ、想定通りの挙動をしていることがわかります。 3. データのインポート コンテナを立ち上げた際に立ち上がっているため、そちらにアクセスします。 csv形式でデータを保存する場合の手順は以下の通りです。 influxDB ( http://localhost:8086/ ) にアクセスする Data → Buckets → demo に遷移する フィルター設定をする ( demo / http_req_duration を選択) (その他フィルターは場合により設定) submit を押す 下図赤枠のボタンを押す おわりに 今回はinfluxdb, grafanaを用いたk6の使い方についてまとめてみました。k6は簡単に使えるツールでありながら、非常に強力な負荷テストを実行することができます。influxDBとGrafanaを組み合わせることで、テスト結果を視覚的に分析することも可能になります。 他にも、使用しているサーバーのCPU使用率やRAM使用率の計測や、発生したエラーの調査などのアプローチが必要になります。 はじめにも触れている通り、そういった手順が品質や信頼性の担保に繋がります。システムのパフォーマンスを把握し、潜在的な問題を早期に発見するために非常に重要です。そのため負荷テストは単なる技術的な作業ではなく、ユーザー体験の向上やビジネスの成功に直結する取り組みとなります。 今回紹介した手法を参考に、皆さんのプロジェクトでより安定したシステム運用につなげていただければ幸いです。 次回は、後藤さんです。どんな記事になるか楽しみですね! ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
ニフティ社内で使われているGitHub Enterpriseの管理者をしている石川です。 今年の4月にself-hosted runnerとしてCodeBuildを利用できるようになる嬉しいアップデートがありましたね。 CodeBuild-hosted GitHub Actions runnerは、4月時点ではリポジトリとCodeBuildプロジェクトが1対1で利用する方法しか取れませんでしたが、6月のアップデートでOrganizationやEnterpriseレベルでひとつのCodeBuildをRunnerとして指定することができるようになりました! AWS CodeBuild now supports organization and global GitHub webhooks – AWS ということで今回はOrganizationレベルでのRunner設定を試してみます。 CodeBuildの設定 Runnerの利用制限 GitHub Actionsの設定 Jobを実行するまでのセットアップも課金対象 リポジトリごとにビルド費用を算出できるか まとめ CodeBuildの設定 Terraformだと以下のようなコードで設定できます。 resource "aws_codebuild_project" "main" { ... source { type = "GITHUB" location = "CODEBUILD_DEFAULT_WEBHOOK_SOURCE_LOCATION" } environment { compute_type = "BUILD_LAMBDA_1GB" image = "aws/codebuild/amazonlinux-aarch64-lambda-standard:nodejs20" image_pull_credentials_type = "CODEBUILD" type = "ARM_LAMBDA_CONTAINER" } ... } resource "aws_codebuild_webhook" "main" { project_name = aws_codebuild_project.main.name build_type = "BUILD" filter_group { filter { pattern = "WORKFLOW_JOB_QUEUED" type = "EVENT" } } scope_configuration { name = "your-organization-name" scope = "GITHUB_ORGANIZATION" } } CFnでも作れるとよかったのですが、まだ対応していないようです。 Global webhooks and GitHub Enterprise webhooks are not supported by AWS CloudFormation. Filter GitHub organization webhook events (AWS CloudFormation) – AWS CodeBuild デプロイ前に注意が必要なのが、事前にGitHub認証情報をCodeBuildに手動で登録する必要があります。上記コードでは除いてしまってますが、PATやOAuthを使いSecret Managerに認証情報を格納した場合、CodeBuildのサービスロールにシークレットを取得する権限も必要となります。 参考: GitHub and GitHub Enterprise Server access in CodeBuild – AWS CodeBuild CodeBuildのプロジェクトが作成されると同時にOrganizationにWebhookが登録されます。 管理者視点だとちょっと困ったことがあって、Webhooks一覧を見てもどのAWSアカウントのCodeBuildが呼ばれるのかここからだと識別できません。 GitHub認証情報管理の問題もありますし、Organization共通のRunnerは専用のAWSアカウントで管理した方がいいかもしれません。 Runnerの利用制限 Organization全体で使えるようなRunnerとして登録しつつも、利用方法に制限を加えたい場合があります。その場合はWebhookのFilterを使って制限を行います。 参考: aws_codebuild_webhook | Resources | hashicorp/aws | Terraform | Terraform Registry filter_group { filter { pattern = "WORKFLOW_JOB_QUEUED" type = "EVENT" } filter { pattern = "your-workflow-name" type = "WORKFLOW_NAME" } } 上記の例だと特定のワークフロー名で実行されたGitHub Actionのときのみ利用できるRunnerとなります。ほかにもいろいろな条件でFilterを作成することができます。 Terraformに限っては、まだ REPOSITORY_NAME を使っての制限は作れないようなので、ここはアップデートを待ちましょう。 参考: [Enhancement]: Add REPOSITORY_NAME event type filter to aws_codebuild_webhook resource · Issue #38868 · hashicorp/terraform-provider-aws GitHub Actionsの設定 組織レベルでもリポジトリレベルでも変わりません。 WorkflowのYAMLに以下のフォーマットでRunnerを指定すれば動作します。 組織内のリポジトリであれば、これだけで利用できるはかなりお手軽ですね。 runs-on: codebuild-<project-name>-${{ github.run_id }}-${{ github.run_attempt }} 参考: Tutorial: Configure a CodeBuild-hosted GitHub Actions runner – AWS CodeBuild Jobを実行するまでのセットアップも課金対象 CodeBuildで実行する場合、毎回Runnerのセットアップと登録処理が行われます(buildspecの指定はできないため、どんなイメージを用意しても必ず実行される)。これが20〜30秒かかっていて、数秒で終わるJobだろうとこの時間が追加で実行時間として加算されます。 GitHub Actionsよりも格段に安いですが、Job実行時間以外も計算に入れておかないといけない点に注意が必要です。 [Container] 2024/08/21 02:13:32.738199 YAML location is /tmp/codebuild/readonly/buildspec.yml [Container] 2024/08/21 02:13:32.738443 Processing environment variables [Container] 2024/08/21 02:13:33.141679 Moving to directory /tmp/codebuild/output/src653/src/1c4cfb37_4d30_4c6c_bb71_0197866a72cd [Container] 2024/08/21 02:13:33.315509 Ignoring BUILD phase commands for self-hosted runner build. [Container] 2024/08/21 02:13:33.315542 Checking if docker is running. Running command: docker version [Container] 2024/08/21 02:13:33.317171 Warning: Docker not installed. GHA self-hosted runner build triggered by /actions/runs/10482304118/job/29033188127 Creating GHA self-hosted runner workspace folder: actions-runner Downloading GHA self-hosted runner binary % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 25 136M 25 35.1M 0 0 46.6M 0 0:00:02 --:--:-- 0:00:02 46.5M 65 136M 65 90.1M 0 0 51.1M 0 0:00:02 0:00:01 0:00:01 51.1M 100 136M 100 136M 0 0 51.9M 0 0:00:02 0:00:02 --:--:-- 51.9M Configuring GHA self-hosted runner -------------------------------------------------------------------------------- | ____ _ _ _ _ _ _ _ _ | | / ___(_) |_| | | |_ _| |__ / \ ___| |_(_) ___ _ __ ___ | | | | _| | __| |_| | | | | '_ \ / _ \ / __| __| |/ _ \| '_ \/ __| | | | |_| | | |_| _ | |_| | |_) | / ___ \ (__| |_| | (_) | | | \__ \ | | \____|_|\__|_| |_|\__,_|_.__/ /_/ \_\___|\__|_|\___/|_| |_|___/ | | | | Self-hosted runner registration | | | -------------------------------------------------------------------------------- # Authentication √ Connected to GitHub # Runner Registration √ Runner successfully added √ Runner connection is good # Runner settings √ Settings Saved. Running GHA self-hosted runner binary √ Connected to GitHub Current runner version: '2.319.1' 2024-08-21 02:14:00Z: Listening for Jobs 2024-08-21 02:14:04Z: Running job: schedule_task 2024-08-21 02:14:17Z: Job schedule_task completed with result: Succeeded √ Removed .credentials √ Removed .runner Runner listener exit with 0 return code, stop the service, no retry needed. Exiting runner... リポジトリごとにビルド費用を算出できるか CUR/CUR2 で確認しましたが、CodeBuildのビルドステータスにあるイニシエータの情報はありませんでした。CloudWatch Logsに出力できるCodeBuildのログからもリポジトリの情報は標準では出力されておらず。残念ながらCURやCodeBuildのログから算出することはできませんでした。 なので、ちょっと手をかけて算出する必要があります。 ざっくり分かればいい 該当のRunnerが使われているWorkflowを特定 Actions Usage Metricsから実行時間を確認 参考: Viewing usage metrics for GitHub Actions – GitHub Enterprise Cloud Docs 詳細に算出したい EventBridgeには欲しい情報が揃っていたので、それをS3に保存してAthenaで集計 EventBridge → Firehose or Lambda → S3 ← Athena 参考: Build notifications sample for CodeBuild – AWS CodeBuild まとめ 組織レベルのRunnerを作るのは簡単 GitHub Organization ownerの認証情報管理とRunnerの置き場はちゃんと考える必要がある リポジトリごとの利用量や費用算出は一手間かければできる CI/CDを組織全体で共有管理している場合は、とても嬉しいアップデートですね。 一方各アカウントでCI/CDを管理している場合は、Organization owner権限が必要なポイントがあり、複数リポジトリで利用したいから使いたいという用途では少々使いにくい面もあります。 GitHub認証情報をどう管理していくかという問題とも紐づいているため、そこも考慮して利用を検討していくといいのではないかと思います。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに terraform で Amazon EventBridge → Amazon CloudWatch Logs の構成を作るときの例を紹介します。若干詰まった部分も書いているので、参考にしていただけると幸いです。 実装例 例えば、Amazon ECSの停止理由をAmazon CloudWatch Logs に残したい場合は以下の構成になります。 Amazon ECS → Amazon EventBridge → Amazon CloudWatch Logs これをTerraformで宣言すると、以下の様になります。 # ECS stopped tasks resource "aws_cloudwatch_event_rule" "ecs_stopped_tasks_event_rule" { name = "ECSStoppedTasksEvent" description = "Triggered when an Amazon ECS Task is stopped" event_pattern = jsonencode({ source = ["aws.ecs"] "detail-type" = ["ECS Task State Change"] detail = { desiredStatus = ["STOPPED"] lastStatus = ["STOPPED"] } }) state = "ENABLED" } resource "aws_cloudwatch_event_target" "ecs_stopped_tasks_event_target" { target_id = "ECSStoppedTasks" rule = aws_cloudwatch_event_rule.ecs_stopped_tasks_event_rule.name arn = "${aws_cloudwatch_log_group.ecs_stopped_tasks_event.arn}:*" } # ECS stoppped task resource "aws_cloudwatch_log_group" "ecs_stopped_tasks_event" { name = "/aws/events/ECSStoppedTasksEvent" retention_in_days = 90 } # ECS Scheduled tasks resource policy resource "aws_cloudwatch_log_resource_policy" "log_event_policy" { policy_name = "LogEventsPolicy" policy_document = jsonencode({ Version = "2012-10-17", Statement = [ { Effect = "Allow", Principal = { Service = [ "events.amazonaws.com", "delivery.logs.amazonaws.com" ] }, Action = [ "logs:CreateLogStream", "logs:PutLogEvents" ], Resource = ["${aws_cloudwatch_log_group.ecs_stopped_tasks_event.arn}:*"] } ] }) } # 異常終了時のアラーム設定 resource "aws_cloudwatch_log_metric_filter" "task_failed_log_metric_filter" { name = "TaskFailedLogMetricFilter" log_group_name = aws_cloudwatch_log_group.ecs_stopped_tasks_event.name pattern = "failed" metric_transformation { name = "FailedLogCount" namespace = "TaskLogMetrics" value = "1" } } resource "aws_cloudwatch_metric_alarm" "task_failed_log_alarm" { alarm_name = "TaskFailedLogAlarm" comparison_operator = "GreaterThanOrEqualToThreshold" evaluation_periods = "1" metric_name = aws_cloudwatch_log_metric_filter.task_failed_log_metric_filter.metric_transformation[0].name namespace = aws_cloudwatch_log_metric_filter.task_failed_log_metric_filter.metric_transformation[0].namespace period = "60" statistic = "Sum" threshold = "1" alarm_description = "Alarm when there are task failed log entries" actions_enabled = true alarm_actions = [ aws_sns_topic.unpaid_notificate.arn(任意の通知先) ] } 上記のコードを見てみると、次のような見慣れないリソースが登場します。 aws_cloudwatch_log_resource_policy この設定、実はコンソールから設定・確認できないパラメータになっています。 AWS CLI からは以下の様に確認することができます。 aws logs describe-resource-policies --no-cli-pager { "resourcePolicies": [ { "policyName": "AWSLogDeliveryWrite20150319", "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"AWSLogDeliveryWrite\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"delivery.logs.amazonaws.com\"},\"Action\":[\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Resource\":\"arn:aws:logs:ap-northeast-1:786813063316:log-group:/aws/api_gw/unpaid-api:log-stream:*\",\"Condition\":{\"StringEquals\":{\"aws:SourceAccount\":\"AWSAccountID\"},\"ArnLike\":{\"aws:SourceArn\":\"arn:aws:logs:ap-northeast-1:AWSAccountID:*\"}}}]}", "lastUpdatedTime": 1712559794739 }, { "policyName": "LogEventsPolicy", "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"delivery.logs.amazonaws.com\",\"events.amazonaws.com\"]},\"Action\":[\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Resource\":\"arn:aws:logs:ap-northeast-1:AWSAccountID:log-group:/aws/events/ECSStoppedTasksEvent:*\"}]}", "lastUpdatedTime": 1720579910972 } ] } リソースポリシーは「〇〇からのアクセスは許可する」というルールです。 コンソールからAmazon EventBridgeを作成すると自動で作られるようですが、Terraformで宣言する場合は明示的に宣言してあげる必要があります。 おわりに 今回はTerraformを利用する場合のAmazon EventBridgeとAmazon CloudWatch Logsを連携する方法について紹介しました。 Terraformを利用して構築する場合、ご参考になれば幸いです。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
この記事は、リレーブログ企画「24新卒リレーブログ」の記事です。 はじめに こんにちは。初めまして、新卒1年目の塚崎です。 現在、ジョブローテの1期目として、第一開発チーム( https://engineering.nifty.co.jp/blog/26940 )でとあるサイトをリニューアルするプロジェクトを進めています。このリニューアルではフロントエンドでNext.jsを採用し、バックエンドではGraphQLを採用しています。今回のリニューアルで私は主にフロントエンドの実装を担当しているのですが、GraphQLについても学んだので、今回はGraphQLを使ったAPIサーバーの実装について記事にしたいと思います。 GraphQLとは? GraphQL( https://graphql.org/ )とは、Meta社によって開発されたWeb APIのクエリ言語です。GraphQLでは、クライアントが必要なデータだけを指定し、サーバーから取得することができるため、REST APIの課題であったデータの過剰取得を防ぎ、効率良くデータの取得が行えます。また、REST APIでは複数のエンドポイントからデータの取得を行うのに対して、GraphQLでは単一のエンドポイントから一度のリクエストで全てのデータを取得することができます。他にもスキーマと呼ばれる型やクエリを定義する仕組みによって、安全に開発ができることも大きなメリットです。 特徴 柔軟なデータ取得 クライアント側で必要なデータだけを指定して取得可能 単一のエンドポイント 単一のエンドポイントで全てのデータ操作が可能 強力な型システム スキーマを定義することで型の安全性が保証される 階層的な構造 データの関係性をクエリの構造に反映できる リアルタイム機能 サブスクリプション機能を利用し、リアルタイムにデータを取得できる バージョン管理が不要 新しいフィールドの追加を容易に行うことができ、バージョン管理が不要 GraphQLサーバーの実装 実際にGraphQLサーバーを実装し、データの取得までをやってみます。 まずは任意の場所でGraphQL用のディレクトリ( graphql-server-example )を作成します。 mkdir graphql-server-example cd graphql-server-example npmで初期化します。 npm init --yes && npm pkg set type="module" 依存ライブラリである@apollo/serverとgraphqlをインストールします。 npm install @apollo/server graphql package.json を開き、 scripts に "start": "node index.js" を追加します。 スキーマの作成 今回は例としてアーティスト情報を返すAPIサーバーを構築してみます。 まず index.js を作成し、以下のようにスキーマ(APIの型やクエリを定義するもの)を書きます。 typeDefs はスキーマを定義する変数です。VSCodeでは以下の拡張機能を追加し、テンプレートリテラルの開始に #graphql と書くことでシンタックスハイライトが機能するようになります。 今回はID、アーティスト名、ジャンルの3つをアーティストオブジェクトとして定義しました。またクエリとして全てのアーティストオブジェクトを取得するクエリも定義しています。 拡張機能(GraphQL: Syntax Highlighting) https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql-syntax import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; const typeDefs = `#graphql type Artist { id: String name: String genres: [String] } type Query { allArtists: [Artist] } `; データの作成 次にサーバーから返すデータを作成します。 通常、クライアントに返すデータはデータベースと接続し、そこから取得を行いますが、今回はデータを配列でハードコーディングすることで擬似的に用意します。 const artists = [ { id: "2c431017-0a62-46de-805c-a2e4c401264f", name: "Bring Me The Horizon", genres: ["Metalcore", "Alternative Metal"], }, { id: "f575282f-aa8a-4636-8258-3ce2279871a6", name: "In Flames", genres: ["Alternative Metal"], }, { id: "2400debb-45aa-4467-991f-64063e7753aa", name: "Dream Theater", genres: ["Progressive Metal"], }, ]; リゾルバの作成 ここまででスキーマを定義し、データも用意することができました。あとはサーバーからフロントへデータを返す処理を追加すれば良さそうです。 データを返すリゾルバを定義します。リゾルバとは、データベースなどからデータを取得し、スキーマで定義された型に合わせてデータを返す処理を担当します。 今回、スキーマで定義した allArtists は全てのアーティストオブジェクトを返すクエリのため、用意した配列のデータをそのまま返す処理を追加しています。 実際にはデータベースからデータを取得した際に、それがそのまま返せるケースは少ないので、リゾルバでスキーマと合うように整形してあげる必要があります。 const resolvers = { Query: { allArtists: () => artists, }, }; サーバーの起動 最後にApollo Serverを初期化する処理を追加してあげましょう。 const server = new ApolloServer({ typeDefs, resolvers, }); const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, }); console.log(`Server ready at: ${url}`); サーバーを起動します。 npm start サーバーを起動するとターミナルに以下が出力されていると思うので、リンクをブラウザで開きます。 Server ready at: http://localhost:4000/ リンクを開くと以下のような画面に遷移します。 この画面ではクエリを実行したり、クエリのレスポンス結果を確認したりすることができます。 それでは実際にクエリを実行してみましょう。 実行結果です。クエリで指定したフィールドの値を取得することができました。 { "data": { "allArtists": [ { "id": "2c431017-0a62-46de-805c-a2e4c401264f", "name": "Bring Me The Horizon", "genres": [ "Metalcore", "Alternative Metal" ] }, { "id": "f575282f-aa8a-4636-8258-3ce2279871a6", "name": "In Flames", "genres": [ "Alternative Metal" ] }, { "id": "2400debb-45aa-4467-991f-64063e7753aa", "name": "Dream Theater", "genres": [ "Progressive Metal" ] } ] } } フィールドの追加 次にアーティスト数を取得するフィールドを追加してみましょう。 const typeDefs = `#graphql type Artist { id: String name: String genres: [String] } type Query { allArtists: [Artist], # 以下を追加 totalArtists: Int } `; フィールド( totalArtists )を追加したので、それに対応するリゾルバを作成する必要があります。今回は、アーティスト数を返すリゾルバとしたので、単純にアーティストの配列の長さを返します。 const resolvers = { Query: { allArtists: () => artists, // 以下を追加 totalArtists: () => artists.length, }, }; サーバーを再起動し、変更を反映させます。 リンクを開き画面の左側を見ると、フィールドとして totalArtists が追加されていることが分かります。 クエリを実行してみましょう。 実行結果です。全てのアーティスト情報とアーティスト数を取得することができました。このようにクエリには複数のフィールドを追加して、実行することもできます。こういったケースでは、REST APIでは2つのエンドポイントに対してリクエストを送信する必要がありますが、GraphQLでは1回のリクエストで複数のデータを取得することができます。 { "data": { "allArtists": [ { "id": "2c431017-0a62-46de-805c-a2e4c401264f", "name": "Bring Me The Horizon", "genres": [ "Metalcore", "Alternative Metal" ] }, { "id": "f575282f-aa8a-4636-8258-3ce2279871a6", "name": "In Flames", "genres": [ "Alternative Metal" ] }, { "id": "2400debb-45aa-4467-991f-64063e7753aa", "name": "Dream Theater", "genres": [ "Progressive Metal" ] } ], "totalArtists": 3 } } 最後に各アーティストがリリースしたアルバム情報を取得できるように実装を追加してみます。 アルバムオブジェクトをスキーマとして定義します。またアーティストオブジェクトにリリースしたアルバム情報をリストで返すフィールド( releasedAlbums )も追加してあげましょう。 const typeDefs = `#graphql type Artist { id: String name: String genres: [String] # 追加 releasedAlbums: [Album] } # 追加 type Album { artistId: String name: String } type Query { allArtists: [Artist], totalArtists: Int } `; アルバム情報のデータを用意します。 const albums = [ { artistId: "2c431017-0a62-46de-805c-a2e4c401264f", name: "Post Human: Nex Gen", }, { artistId: "2c431017-0a62-46de-805c-a2e4c401264f", name: "That's the Spirit", }, { artistId: "2c431017-0a62-46de-805c-a2e4c401264f", name: "Sempiternal", }, { artistId: "f575282f-aa8a-4636-8258-3ce2279871a6", name: "Foregone", }, { artistId: "f575282f-aa8a-4636-8258-3ce2279871a6", name: "Whoracle", }, { artistId: "2400debb-45aa-4467-991f-64063e7753aa", name: "Images and Words", }, ]; リゾルバを追加します。配列の filter メソッドを使用することで、アーティストがリリースしたアルバム情報をリストで取得しています。 const resolvers = { Query: { allArtists: () => artists, totalArtists: () => artists.length, }, // 以下を追加 Artist: { releasedAlbums: (artist) => { return albums.filter((album) => album.artistId === artist.id); }, }, }; サーバーを再起動し、変更を反映させます。 クエリを実行してみます。 以下のようにデータを取得することができたと思います。 { "data": { "allArtists": [ { "id": "2c431017-0a62-46de-805c-a2e4c401264f", "name": "Bring Me The Horizon", "genres": [ "Metalcore", "Alternative Metal" ], "releasedAlbums": [ { "name": "Post Human: Nex Gen" }, { "name": "That's the Spirit" }, { "name": "Sempiternal" } ] }, { "id": "f575282f-aa8a-4636-8258-3ce2279871a6", "name": "In Flames", "genres": [ "Alternative Metal" ], "releasedAlbums": [ { "name": "Foregone" }, { "name": "Whoracle" } ] }, { "id": "2400debb-45aa-4467-991f-64063e7753aa", "name": "Dream Theater", "genres": [ "Progressive Metal" ], "releasedAlbums": [ { "name": "Images and Words" } ] } ], "totalArtists": 3 } } まとめ 今回は、とあるプロジェクトのリニューアルでGraphQLについて学び、APIサーバーの実装をやってみました。実際にプロジェクトに導入し、実装を進める中で必要なデータだけを簡単に取得できる点や事前にスキーマを固めることでバックエンドの開発を待たずにフロントエンドの実装が進められる点がメリットとして実感できました。しかし、今回学んだ内容はGraphQLの基礎的な内容になるため、キャッシュやスキーマファーストといったパフォーマンスや設計に関する部分もこれから学んでいく必要があると感じました。ニフティでは、書籍購入費用補助制度やUdemyなども使えるので、それらを活用してこれからも勉強していきたいです。 次回は、けにさんです。どんな記事になるか楽しみですね! 参考資料 初めてのGraphQL ― Webサービスを作って学ぶ新世代API Introduction to GraphQL | GraphQL( https://graphql.org/learn/) Introduction to Apollo Server | Apollo GraphQL Docs( https://www.apollographql.com/docs/apollo-server/ ) ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター