TECH PLAY

KINTOテクノロジーズ

KINTOテクノロジーズ の技術ブログ

936

みなさん、こんにちは。KINTOテクノロジーズのモバイル開発グループのマーティンです!このガイドでは、TFLite(TensorFlow Lite)モデルをゼロから構築する方法を簡単に説明します。それでは、早速始めましょう。 この記事は KINTOテクノロジーズアドベントカレンダー2024の9日目の記事です🎅🎄 準備 データセットを準備する方法は基本的に2通りあります。1つは、ローカルでアノテーションプロセスを行う方法、もう1つは、オンラインでデータセットにアノテーションを行い、チームメンバーと初期の工数をより効率的に共有しながら進める方法です。このガイドでは、Roboflow (1) の使用に重点を置いています。 Roboflow のモデルエクスポート機能を使用すると、トレーニング済みのモデルをさまざまな形式でエクスポートできるため、独自のアプリケーションに簡単に展開したり、さらに微調整したりすることができます。今回の場合、TFlite モデルをトレーニングしたいので、下の画像に示すように TFRecord 形式にエクスポートする必要があります。 ただし、Roboflowのようなサードパーティのオンラインアノテーションツールを用いずに、ローカルで画像にアノテーションを付けたい場合は、無料のPythonライブラリlabelImgを試してみてください: https://github.com/HumanSignal/labelImg 一般的に、ローカルでもオンラインでも、まず画像のデータセットを収集し、それらにラベルを付けて、対応する境界ボックス分類メタデータ (xml) ファイルを取得する必要があります。(この場合は、VOC [Visual Object Classes] Pascal メタデータを作成) Pascal VOCの詳細情報は、こちらで確認できます: https://roboflow.com/formats/pascal-voc-xml Google Cloud Platform の標準ランタイムインスタンスを作成したら、それを Colab ノートブックに接続する必要があります。基本的に、Google Colab は、組織内のデータサイエンスチームと機械学習チームに、安全でスケーラブルな共同作業プラットフォームを提供します。それが完了したら、まずTensorFlow(テンソルフロー)を動作させるために必要なライブラリをインポートする必要があります(ステップ1) GCP標準インスタンスの作成: Colab Enterprise( https://cloud.google.com/colab/docs ) ノートブックの作成: Google Cloud バケットを接続する(ステップ 4) 実行 TensorFlow オブジェクト検出 API をインストールします(このガイドのステップ 5) トレーニングに必要な TFRecord ファイルを生成します。(これには、csv ファイルを生成するための generate_tfrecord.pyスクリプトが必要です) モデルパイプライン構成ファイルを編集し、事前にトレーニングされたモデルのチェックポイントをダウンロードします。 モデルのトレーニングと評価 モデルをエクスポートして、TFlite(TensorFlow Lite)形式に変換します。 展開 TFlite モデルを Android / iOS / IoT デバイスに展開します。 それでは、始めましょう Colab Enterprise ノートブック内で実行すべき手順を詳しく説明します: 1) ライブラリをインポートします !pip install tensorflow==2.13.0 import os import glob import xml.etree.ElementTree as ET import pandas as pd import tensorflow as tf print(tf.__version__) 2) GCPクラウドストレージバケットに* customTF2 、 training 、 data *フォルダを作成します(初回のみ必要) GCPクラウドストレージバケットに customTF2 という名前のフォルダを作成します。 customTF2 フォルダ内に training と data という2つのサブフォルダを作成します( training フォルダはトレーニング中にチェックポイントが保存される場所です) GCSバケット内のフォルダ構造の作成: 3) 以下を generate_tfrecord.py ファイルとしてダウンロードして保存し、バケットの CustomTF2 フォルダにアップロードします。(初回のみ必要) from __future__ import division from __future__ import print_function from __future__ import absolute_import import os import io import pandas as pd import tensorflow as tf import argparse from PIL import Image from tqdm import tqdm from object_detection.utils import dataset_util from collections import namedtuple, OrderedDict def __split(df, group): data = namedtuple('data', ['filename', 'object']) gb = df.groupby(group) return [data(filename, gb.get_group(x)) for filename, x in zip(gb.groups.keys(), gb.groups)] def create_tf_example(group, path, class_dict): with tf.io.gfile.GFile(os.path.join(path, '{}'.format(group.filename)), 'rb') as fid: encoded_jpg = fid.read() encoded_jpg_io = io.BytesIO(encoded_jpg) image = Image.open(encoded_jpg_io) 幅、高さ = 画像.サイズ filename = group.filename.encode('utf8') image_format = b'jpg' xmins = [] xmaxs = [] ymins = [] ymaxs = [] classes_text = [] classes = [] for index, row in group.object.iterrows(): if set(['xmin_rel', 'xmax_rel', 'ymin_rel', 'ymax_rel']).issubset(set(row.index)): xmin = row['xmin_rel'] xmax = row['xmax_rel'] ymin = row['ymin_rel'] ymax = row['ymax_rel'] elif set(['xmin', 'xmax', 'ymin', 'ymax']).issubset(set(row.index)): xmin = row['xmin'] / width xmax = row['xmax'] / width ymin = row['ymin'] / height ymax = row['ymax'] / height xmins.append(xmin) xmaxs.append(xmax) ymins.append(ymin) ymaxs.append(ymax) classes_text.append(str(row['class']).encode('utf8')) classes.append(class_dict[str(row['class'])]) tf_example = tf.train.Example(features=tf.train.Features( feature={ 'image/height': dataset_util.int64_feature(height), 'image/width': dataset_util.int64_feature(width), 'image/filename': dataset_util.bytes_feature(filename), 'image/source_id': dataset_util.bytes_feature(filename), 'image/encoded': dataset_util.bytes_feature(encoded_jpg), 'image/format': dataset_util.bytes_feature(image_format), 'image/object/bbox/xmin': dataset_util.float_list_feature(xmins), 'image/object/bbox/xmax': dataset_util.float_list_feature(xmaxs), 'image/object/bbox/ymin': dataset_util.float_list_feature(ymins), 'image/object/bbox/ymax': dataset_util.float_list_feature(ymaxs), 'image/object/class/text': dataset_util.bytes_list_feature(classes_text), 'image/object/class/label': dataset_util.int64_list_feature(classes), })) return tf_example def class_dict_from_pbtxt(pbtxt_path): # open file, strip \n, trim lines and keep only # lines beginning with id or display_name with open(pbtxt_path, 'r', encoding='utf-8-sig') as f: data = f.readlines() name_key = None if any('display_name:' in s for s in data): name_key = 'display_name:' elif any('name:' in s for s in data): name_key = 'name:' if name_key is None: raise ValueError( "label map does not have class names, provided by values with the 'display_name' or 'name' keys in the contents of the file" ) data = [l.rstrip('\n').strip() for l in data if 'id:' in l or name_key in l] ids = [int(l.replace('id:', '')) for l in data if l.startswith('id')] names = [ l.replace(name_key, '').replace('"', '').replace("'", '').strip() for l in data if l.startswith(name_key)] # id と display_names を1つの辞書に結合します class_dict = {} for i in range(len(ids)): class_dict[names[i]] = ids[i] return class_dict if __name__ == '__main__': parser = argparse.ArgumentParser( description='Create a TFRecord file for use with the TensorFlow Object Detection API.', formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('csv_input', metavar='csv_input', type=str, help='Path to the CSV input') parser.add_argument('pbtxt_input', metavar='pbtxt_input', type=str, help='Path to a pbtxt file containing class ids and display names') parser.add_argument('image_dir', metavar='image_dir', type=str, help='Path to the directory containing all images') parser.add_argument('output_path', metavar='output_path', type=str, help='Path to output TFRecord') args = parser.parse_args() class_dict = class_dict_from_pbtxt(args.pbtxt_input) writer = tf.compat.v1.python_io.TFRecordWriter(args.output_path) path = os.path.join(args.image_dir) examples = pd.read_csv(args.csv_input) grouped = __split(examples, 'filename') for group in tqdm(grouped, desc='groups'): tf_example = create_tf_example(group, path, class_dict) writer.write(tf_example.SerializeToString()) writer.close() output_path = os.path.join(os.getcwd(), args.output_path) print('Successfully created the TFRecords: {}'.format(output_path)) 4) GCSバケットをマウントし、GCSFUSEをインストールしてフォルダをリンクします。 from google.colab import auth auth.authenticate_user() !echo「deb https://packages.cloud.google.com/apt gcsfuse-bionic main」> /etc/apt/sources.list.d/gcsfuse.list !curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - !apt -qq update !apt -qq install gcsfuse !gsutil ls -r gs://your-cloud-storage-bucket-name !mkdir customTF2 !gcsfuse --implicit-dirs your-cloud-storage-bucket-name customTF2 5) TensorFlow モデルのGitレポジトリをクローンし、TensroFlow オブジェクト検出APIをインストールします。 %cd /content # colab クラウドvmに tensorflowモデルをクローンする !git clone --q https://github.com/tensorflow/models.git # protosをコンパイルするために/models/research フォルダにナビゲートします。 %cd models/research # protosをコンパイルします。 !protoc object_detection/protos/*.proto --python_out=. # TensorFlowオブジェクト検出 API をインストールします。 !cp object_detection/packages/tf2/setup.py . !python -m pip install . 6) モデルビルダーをテストします (推奨) %cd /content/models/research # モデルビルダーのテスト !pip install 'tf-models-official >=2.5.1, <2.16.0' !python object_detection/builders/model_builder_tf2_test.py 7)事前にトレーニングされたモデルチェックポイントをダウンロードします(初めての場合のみ必要) 現在の作業ディレクトリは /content/customTF2/customTF2/data/ ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz を  データ  フォルダにダウンロードして解凍します。 その他の Tensorflow 2.x の検出チェックポイントのリストは こちら 。 %cd /content/customTF2/customTF2/data/ #事前トレーニング済みモデル ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz をダウンロードしてデータフォルダに入れて解凍します。 !wget http://download.tensorflow.org/models/object_detection/tf2/20200711/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz !tar -xzvf ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz * 8)モデルパイプライン構成ファイルを入手して変更を加え、 データ フォルダに入れます(クラス番号の量を変更するたびに必要) * 現在の作業ディレクトリは /content/customTF2/customTF2/data/ ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config を /content/models/research/object_detection/configs/tf2 からダウンロードします。必要な変更を加え、 /content/customTF2/customTF2/data/ フォルダーにアップロードします。 または colab内 で /content/models/research/object_detection/configs/tf2 からの構成ファイルを編集し、編集済みの構成ファイルを /content/customTF2/customTF2/data フォルダにコピーします。 パイプライン構成ファイルは、前のステップでダウンロードしたモデルチェックポイントフォルダ内にもあります。 次の変更を行う必要があります。 num_classes を自分のクラス数に変更します。 test.record パス、 train.record パス、 labelmap パスを、これらのファイルを作成した場所のパスに変更します(トレーニング中は現在の作業ディレクトリに対する相対パスである必要があります) fine_tune_checkpoint を、ステップ 12 でダウンロードしたチェックポイントがあるディレクトリのパスに変更します。 fine_tune_checkpoint_type を、分類タイプに応じて数値 classification または detection に変更します。 batch_size を、使用する GPU の能力に応じて 8 の倍数に変更します(例:24、128、...、512) - 通常、標準的な colab enterprise インスタンスでは、小さなデータセットには 24、大きなデータセットには 32 が適しています。 num_steps を、検出器にトレーニングさせたいステップ数に変更します。 #編集した構成ファイルを configs/tf2 ディレクトリから GCP ストレージの data/ フォルダにコピーします。 !cp /content/models/research/object_detection/configs/tf2/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config /content/customTF2/customTF2/data 次のステップでは、公式の TensorBoard ツールを使用して、実行状況やグラフを可視化し、学習と分類の損失を時間経過とともに確認したいと思います。グラフの読み方やツールの使い方に関する詳細情報は、こちらをご覧ください: https://www.tensorflow.org/tensorboard/get_started#:~:text=TensorBoard%20is%20a%20tool%20for,during%20the%20machine%20learning%20workflow。 9) TensorBoardをロードします (推奨) # cload tensorboard %cd /content/customTF2/customTF2/training # !pip install tensorboard # tensorboard --inspect --logdir /content/customTF2/customTF2/training # !gcloud init # !gcloud auth application-default login %reload_ext tensorboard %tensorboard --logdir '/content/customTF2/customTF2/training' 10) モデルをトレーニングします colab vm の object_detection フォルダに移動します %cd /content/models/research/object_detection 10 (a) model_main_tf2.py を使用したトレーニング(推奨方法) ここで、 PIPELINE_CONFIG_PATH はパイプライン構成ファイルを指し、 MODEL_DIR はトレーニングのチェックポイントとイベントが書き込まれるディレクトリを指します。 最良の結果を得るためには、損失が0.1未満になった時点でトレーニングを停止してください。それが難しい場合は、損失がしばらくの間、顕著な変化を示さなくなるまでモデルをトレーニングしてください。理想的な損失は0.05未満です(モデルを過学習させずに、損失をできるだけ低く抑えるようにしてください)。モデルがすでに収束している場合(例えば、損失がそれ以上顕著に減少せず、減少に時間がかかる場合)、損失を下げようとしてトレーニングステップを高くしすぎないようにしてください。 !pip install tensorflow==2.13.0 以下のコマンドをcontent/models/research/object_detection ディレクトリから実行してください。 """ PIPELINE_CONFIG_PATH=path/to/pipeline.config MODEL_DIR=path to training checkpoints directory NUM_TRAIN_STEPS=50000 SAMPLE_1_OF_N_EVAL_EXAMPLES=1 python model_main_tf2.py -- \ --model_dir=$MODEL_DIR --num_train_steps=$NUM_TRAIN_STEPS \ --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \ --pipeline_config_path=$PIPELINE_CONFIG_PATH \ --alsologtostderr """ !python model_main_tf2.py --pipeline_config_path=/content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --model_dir=/content/customTF2/customTF2/training --alsologtostderr 10 (b) Evaluation using model_main_tf2.py (Optional, just if you want more customization) これを並行して実行するには、別の Colab ノートブックを開き、上記のトレーニングコマンドと同時にこのコマンドを実行します(その際、gcpストレージをマウントし、TF gitリポジトリをクローンし、TF2オブジェクト検出 API をインストールするのを忘れないでください)。これにより、検証損失、mAP などが表示され、モデルのパフォーマンスがどのようになっているかをより良く把握できます。 ここで、 {CHECKPOINT_DIR} はトレーニングジョブによって生成されたチェックポイントが格納されているディレクトリを指します。評価イベントは {MODEL_DIR/eval} に書き込まれます。 以下のコマンドをcontent/models/research/object_detection ディレクトリから実行してください。 """ PIPELINE_CONFIG_PATH=path/to/pipeline.config MODEL_DIR=path to training checkpoints directory CHECKPOINT_DIR=${MODEL_DIR} NUM_TRAIN_STEPS=50000 SAMPLE_1_OF_N_EVAL_EXAMPLES=1 python model_main_tf2.py -- \ --model_dir=$MODEL_DIR --num_train_steps=$NUM_TRAIN_STEPS \ --checkpoint_dir=${CHECKPOINT_DIR} \ --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \ --pipeline_config_path=$PIPELINE_CONFIG_PATH \ --alsologtostderr """ !python model_main_tf2.py --pipeline_config_path=/content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --model_dir=/content/customTF2/customTF2/training/ --checkpoint_dir=/content/customTF2/customTF2/training/ --alsologtostderr モデルの再トレーニング (接続が切れた場合) 接続が切れたり、colab vmのセッションが失われた場合、トレーニングは中断した場所から再開できます。チェックポイントは training フォルダ内のクラウドストレージに保存されています。トレーニングを再開するには、 ステップ 1, 4, 5, 6, 9, 10 を実行してください。 トレーニングに必要なすべてのファイル(記録ファイル、編集済みpipeline configファイル、label_mapファイル、モデルチェックポイントフォルダなど)が揃っているため、これらを再作成する必要はありません。 model_main_tf2.pyスクリプトは、1000ステップごとにチェックポイントを保存します。 トレーニングは、最後に保存されたチェックポイントから自動的に再開されます。 ただし、最後のチェックポイントからトレーニングが再開されない場合は、パイプライン構成ファイルに1つの変更を加えることができます。 fine_tune_checkpoint を、最新のトレーニング済みチェックポイントが書き込まれている場所に変更し、最新のチェックポイントを指すように以下のように設定します。 fine_tune_checkpoint: "/content/customTF2/customTF2/training/ckpt-X" (where ckpt-X is the latest checkpoint) 11) トレーニング済みモデルをテストします 推論グラフをエクスポート 現在の作業ディレクトリは /content/models/research/object_detection検出です %cd /content/models/research/object_detection !pip install tensorflow==2.13.0 # #推論グラフをエクスポート !python exporter_main_v2.py --trained_checkpoint_dir=/content/customTF2/customTF2/training --pipeline_config_path=/content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --output_directory /content/customTF2/customTF2/data/inference_graph 訓練済みのオブジェクト検出モデルを画像でテストします(好みのテスト画像を提供して image_path を調整します) 現在の作業ディレクトリは /content/models/research/object_detection検出です %cd /content/models/research/object_detection # ラベルテキストの異なるフォントタイプ。(このステップはオプション) !wget https://www.freefontspro.com/d/14454/arial.zip !unzip arial.zip -d . %cd utils/ !sed-i「s/font = ImageFont.TrueType ('arial.ttf', 24) /font = ImageFont.TrueType ('arial.ttf', 50)/」visualization_utils.py %cd .. %cd /content/models/research/object_detection !pip install tensorflow=="2.12.0" #保存された_モデルのローディング import tensorflow as tf import time import numpy as np import warnings warnings.filterwarnings('ignore') from PIL import Image from google.colab.patches import cv2_imshow from object_detection.utils import label_map_util from object_detection.utils import visualization_utils as viz_utils IMAGE_SIZE = (12, 8) # Output display size as you want import matplotlib.pyplot as plt PATH_TO_SAVED_MODEL="/content/customTF2/customTF2/data/inference_graph/saved_model" print('Loading model...', end='') # 保存したモデルを読み込み、検出機能を構築します。 detect_fn=tf.saved_model.load(PATH_TO_SAVED_MODEL) print('Done!') #ラベル_マップのローディング category_index=label_map_util.create_category_index_from_labelmap("/content/customTF2/customTF2/data/label_map.pbtxt",use_display_name=True) def load_image_into_numpy_array(path): return np.array(Image.open(path)) # テストイメージに置き換えます image_path = "/content/customTF2/customTF2/data/images/your_test.jpg" #print('Running inference for {}... '.format(image_path), end='') image_np = load_image_into_numpy_array(image_path) # 入力はテンソルでなければなりません。`tf.convert_to_tensor` を使用して変換します。 input_tensor = tf.convert_to_tensor(image_np) # モデルは画像のバッチを想定しているため、`tf.newaxis` で軸を追加してください。 input_tensor = input_tensor[tf.newaxis, ...] detections = detect_fn(input_tensor) # すべての出力はバッチテンソルです。 # numpy 配列に変換し、インデックス [0] を使用してバッチディメンションを削除します。 # 対象とするのは最初の num_detections だけです。 num_detections = int(detections.pop('num_detections')) detections = {key: value[0, :num_detections].numpy() for key, value in detections.items()} detections['num_detections'] = num_detections # 検出_クラスは整数でなければなりません。 detections['detection_classes'] = detections['detection_classes'].astype(np.int64) image_np_with_detections = image_np.copy() viz_utils.visualize_boxes_and_labels_on_image_array( image_np_with_detections, detections['detection_boxes'], detections['detection_classes'], detections['detection_scores'], category_index, use_normalized_coordinates=True, max_boxes_to_draw=200, min_score_thresh=.8, # この値を調整して True として分類される最小確率ボックスを設定します agnostic_mode=False) %matplotlib inline plt.figure(figsize=IMAGE_SIZE, dpi=200) plt.axis("off") plt.imshow(image_np_with_detections) plt.show() トレーニング済みの SSD (シングルショット検出器) モデルから TFLite モデルへの変換 12) tf-nightlyをインストールします TFLiteコンバーターは、tf-nightlyでうまく機能します。 %cd /content/models/research/object_detection !pip install tensorflow=="2.12.0" !pip install numpy==1.26.4 !pip install tf-nightly 13) SSDTFLiteグラフをエクスポートします 現在の作業ディレクトリは /content/models/research/object_detection検出です # !pip3 uninstall keras # !pip3 install keras==2.14.0 !pip3 install --upgrade tensorflow keras !pip3 install tensorflow=="2.12.0" # !pip3 install --upgrade tensorflow keras # !pip3 install tensorflow=="2.13.1" # !pip3 install numpy --upgrade # !pip3 uninstall numpy # !pip3 install numpy=="1.22.0" # !pip3 install tensorflow --upgrade #!python --version %cd /content/models/research/object_detection !python export_tflite_graph_tf2.py --pipeline_config_path /content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --trained_checkpoint_dir /content/customTF2/customTF2/training --output_directory /content/customTF2/customTF2/data/tflite 14) TF 保存モデルを TFLite モデルに変換 現在の作業ディレクトリは /mydrive/customTF2/data/ です %cd /content/customTF2/customTF2/data/ 入力テンソル名と出力テンソル名を確認 !saved_model_cli show--dir /content/customTF2/customTF2/data/tflite/saved_model--tag_set serve—all TFLite への変換: 方法 (a) または方法 (b) のいずれかを使用します。 方法 (a) コマンドラインツール tflite_convert を使用-(基本モデル変換) # デフォルトの推論タイプは浮動小数点です。 %cd /content/customTF2/customTF2/data/ !tflite_convert --saved_model_dir=tflite/saved_model --output_file=tflite/detect.tflite 方法 (b) Python API を使用-(最適化などによる高度なモデル変換用) %cd /mydrive/customTF2/data/ #'''******************************** # 浮動小数点推論用 #*********************************''' #import tensorflow as tf saved_model_dir = '/content/customTF2/customTF2/data/tflite/saved_model' #converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) #tflite_model = converter.convert() #open("/content/customTF2/customTF2/data/tflite/detect.tflite", "wb").write(tflite_model) #'''************************************************** # 最適化による浮動小数点推論用 #***************************************************''' import tensorflow as tf converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir,signature_keys=['serving_default']) converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.experimental_new_converter = True converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS, tf.lite.OpsSet.SELECT_TF_OPS] tflite_model = converter.convert() with tf.io.gfile.GFile('/mydrive/customTF2/data/tflite/detect.tflite', 'wb') as f: f.write(tflite_model) #'''********************************** # ダイナミックレンジ量子化用 #************************************* # このモデルは重みが量子化され、少し小さくなりましたが、他の変数データはまだfloat形式です。''' # import tensorflow as tf # converter = tf.lite.TFLiteConverter.from_saved_model('/content/customTF2/customTF2/data/tflite/saved_model',signature_keys=['serving_default']) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # tflite_quant_model = converter.convert() # with tf.io.gfile.GFile('/content/customTF2/customTF2/data/tflite/detect.tflite', 'wb') as f: # f.write(tflite_quant_model) # '''*********************************************************************** # 整数と浮動小数点のフォールバック量子化(デフォルトの最適化を使用) # ************************************************************************** # これで、すべての重みと変数データが量子化され、元のTensorFlow Liteモデルと比較してモデルが大幅に小さくなりました。 # ただし、float形式のモデル入力および出力テンソルを従来使用しているアプリケーションとの互換性を維持するために、 # TensorFlow Lite コンバーターはモデルの入力テンソルと出力テンソルをフロートのままにします''' # import tensorflow as tf # import numpy as np # saved_model_dir = '/content/customTF2/customTF2/data/tflite/saved_model' # def representative_dataset(): # for _ in range(100): # data = np.random.rand(1, 320, 320, 3) # yield [data.astype(np.float32)] # converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # converter.representative_dataset = representative_dataset # tflite_quant_model = converter.convert() # with open('/content/customTF2/customTF2/data/tflite/detect.tflite', 'wb') as f: # f.write(tflite_quant_model) # '''********************************* # 全整数量子化用 # ************************************ # 内部量子化は前の浮動小数点フォールバック量子化方法と同じままです。 # しかし、入力と出力のテンソルは、ここでも整数形式になっていることがわかります。''' # import tensorflow as tf # import numpy as np # saved_model_dir = '/content/customTF2/customTF2/data/tflite/saved_model' # def representative_dataset(): # for _ in range(100): # data = np.random.rand(1, 320, 320, 3) # yield [data.astype(np.float32)] # converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # converter.representative_dataset = representative_dataset # converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] # converter.inference_input_type = tf.uint8 # converter.inference_output_type = tf.uint8 # tflite_quant_model_full_int = converter.convert() # with open('/content/customTF2/customTF2/data/tflite/detect.tflite', 'wb') as f: # f.write(tflite_quant_model_full_int) トレーニング後の量子化の詳細は、 こちら をご覧ください。これらについては、 こちら のColabノートブックでも読むことができます。 15) TFLite メタデータを作成します !pip install tflite_support_nightly %cd /content/customTF2/customTF2/data/ %d tflite/ !mkdir tflite_with_metadata %cd .. データ フォルダ内の各行にクラスの名前が書き込まれた labelmap.txt ファイルを作成します。 最後に、次のセルを実行して、メタデータが添付された detect.tflite モデルを作成します。 現在の作業ディレクトリは /content/customTF2/customTF2/data/ %cd /content/customTF2/customTF2/data/ !pip uninstall tensorflow !pip install tensorflow=="2.13.1" # TFLite にメタデータを添付 from tflite_support.metadata_writers import object_detector from tflite_support.metadata_writers import writer_utils import flatbuffers import platform from tensorflow_lite_support.metadata import metadata_schema_py_generated from tensorflow_lite_support.metadata import schema_py_generated from tensorflow_lite_support.metadata.python import metadata from tensorflow_lite_support.metadata.python import metadata_writers import flatbuffers import os from tensorflow_lite_support.metadata import metadata_schema_py_generated as _metadata_fb from tensorflow_lite_support.metadata.python import metadata as _metadata from tensorflow_lite_support.metadata.python.metadata_writers import metadata_info from tensorflow_lite_support.metadata.python.metadata_writers import metadata_writer from tensorflow_lite_support.metadata.python.metadata_writers import writer_utils ObjectDetectorWriter = object_detector.MetadataWriter _MODEL_PATH = "/content/customTF2/customTF2/data/tflite/detect.tflite" _LABEL_FILE = "/content/customTF2/customTF2/data/labelmap.txt" _SAVE_TO_PATH = "/content/customTF2/customTF2/data/tflite/tflite_with_metadata/detect.tflite" writer = ObjectDetectorWriter.create_for_inference( writer_utils.load_file(_MODEL_PATH), [127.5], [127.5], [_LABEL_FILE]) writer_utils.save_file(writer.populate(), _SAVE_TO_PATH) # 入力されたメタデータと関連ファイルを確認します。 displayer = metadata.MetadataDisplayer.with_model_file(_SAVE_TO_PATH) print("Metadata populated:") print(displayer.get_metadata_json()) print("Associated file(s) populated:") print(displayer.get_packed_associated_file_list()) model_meta = _metadata_fb.ModelMetadataT() model_meta.name = "SSD_Detector" model_meta.description = ( "Identify which of a known set of objects might be present and provide " "information about their positions within the given image or a video " "stream.") # 入力情報を作成します。 input_meta = _metadata_fb.TensorMetadataT() input_meta.name = "image" input_meta.content = _metadata_fb.ContentT() input_meta.content.contentProperties = _metadata_fb.ImagePropertiesT() input_meta.content.contentProperties.colorSpace = ( _metadata_fb.ColorSpaceType.RGB) input_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.ImageProperties) input_normalization = _metadata_fb.ProcessUnitT() input_normalization.optionsType = ( _metadata_fb.ProcessUnitOptions.NormalizationOptions) input_normalization.options = _metadata_fb.NormalizationOptionsT() input_normalization.options.mean = [127.5] input_normalization.options.std = [127.5] input_meta.processUnits = [input_normalization] input_stats = _metadata_fb.StatsT() input_stats.max = [255] input_stats.min = [0] input_meta.stats = input_stats # 出力情報を作成します。 output_location_meta = _metadata_fb.TensorMetadataT() output_location_meta.name = "location" output_location_meta.description = "The locations of the detected boxes." output_location_meta.content = _metadata_fb.ContentT() output_location_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.BoundingBoxProperties) output_location_meta.content.contentProperties = ( _metadata_fb.BoundingBoxPropertiesT()) output_location_meta.content.contentProperties.index = [1, 0, 3, 2] output_location_meta.content.contentProperties.type = ( _metadata_fb.BoundingBoxType.BOUNDARIES) output_location_meta.content.contentProperties.coordinateType = ( _metadata_fb.CoordinateType.RATIO) output_location_meta.content.range = _metadata_fb.ValueRangeT() output_location_meta.content.range.min = 2 output_location_meta.content.range.max = 2 output_class_meta = _metadata_fb.TensorMetadataT() output_class_meta.name = "category" output_class_meta.description = "The categories of the detected boxes." output_class_meta.content = _metadata_fb.ContentT() output_class_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.FeatureProperties) output_class_meta.content.contentProperties = ( _metadata_fb.FeaturePropertiesT()) output_class_meta.content.range = _metadata_fb.ValueRangeT() output_class_meta.content.range.min = 2 output_class_meta.content.range.max = 2 label_file = _metadata_fb.AssociatedFileT() label_file.name = os.path.basename("labelmap.txt") label_file.description = "Label of objects that this model can recognize." label_file.type = _metadata_fb.AssociatedFileType.TENSOR_VALUE_LABELS output_class_meta.associatedFiles = [label_file] output_score_meta = _metadata_fb.TensorMetadataT() output_score_meta.name = "score" output_score_meta.description = "The scores of the detected boxes." output_score_meta.content = _metadata_fb.ContentT() output_score_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.FeatureProperties) output_score_meta.content.contentProperties = ( _metadata_fb.FeaturePropertiesT()) output_score_meta.content.range = _metadata_fb.ValueRangeT() output_score_meta.content.range.min = 2 output_score_meta.content.range.max = 2 output_number_meta = _metadata_fb.TensorMetadataT() output_number_meta.name = "number of detections" output_number_meta.description = "The number of the detected boxes." output_number_meta.content = _metadata_fb.ContentT() output_number_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.FeatureProperties) output_number_meta.content.contentProperties = ( _metadata_fb.FeaturePropertiesT()) # サブグラフ情報を作成します。 group = _metadata_fb.TensorGroupT() group.name = "detection result" group.tensorNames = [ output_location_meta.name, output_class_meta.name, output_score_meta.name ] subgraph = _metadata_fb.SubGraphMetadataT() subgraph.inputTensorMetadata = [input_meta] subgraph.outputTensorMetadata = [ output_location_meta, output_class_meta, output_score_meta, output_number_meta ] subgraph.outputTensorGroups = [group] model_meta.subgraphMetadata = [subgraph] b = flatbuffers.Builder(0) b.Finish( model_meta.Pack(b), _metadata.MetadataPopulator.METADATA_FILE_IDENTIFIER) metadata_buf = b.Output() 質問されたら、'Y'で進みます 16) TFLite モデルをダウンロードします お疲れ様でした。これで完了です! まとめ Google Colab Enterpriseは、機械学習のための強力なクラウドベースのプラットフォームであり、TensorFlow Liteモデルを構築する上で理想的な環境です。このプラットフォームを1年以上使用してみて、最も時間がかかるのはデータ準備と初期の試行錯誤段階であることが分かりました。この段階では、データセットの特定の部分を認識する際の課題を特定し、画像が誤って分類される誤検知に対処するために、著しい反復とテストが必要です。 *Android ロボットのヘッダー画像は、Google が作成および共有した画像から複製または変更されたものであり、クリエイティブ コモンズ 3.0 帰属ライセンスに記載されている条件に従って使用されています。
アバター
This article is part of day 6 of KINTO Technologies Advent Calendar 2024 . Introduction Hello. I am Uehara ( @penpen_77777 ), and I am part of the KINTO FACTORY Development Group. I joined in July 2024, and I was assigned to the backend development of KINTO FACTORY. This time, using sample code, I will introduce the data conflicts that you should be cautious of when processing S3 events, and how to resolve them. Intended Readers Those who are concerned with AWS S3 event notifications being duplicated or the order of notifications being changed. Those with basic knowledge of Rust, S3, DynamoDB, Lambda, and Terraform. Having this knowledge will make it easier to understand when reading the sample code. S3 Event Overview An S3 event^[AWS document regarding S3 event notifications https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/EventNotifications.html] is an event that occurs when an operation such as uploading or deleting an object to S3 is triggered. By detecting S3 events using Lambda functions or SNS, various processes concerning S3 can be automated. Issues with S3 Events Something you should be cautious of when processing S3 events is that the event notifications could be duplicated or the order could be changed. For example, let’s think about the process of creating an object after deleting an object for the same object key. In this case, first you will be notified of the object delete event, then you can expect to be notified of the create event (remove the same object => see the diagram of the expected S3 event reception order when created). However, there are times when the S3 event order isn’t secured, and the create event notification comes before the delete event notification (remove the same object => see the diagram of the order of reception of possible S3 events when created). As a result, the processing result by the event that deletes the object will be the latest, and the issue of data consistency not being guaranteed depending on the processing content occurs. gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion Object deletion :done, cre1, 00:00:01, 1s Receive delete event / Process :done, cre2, 00:00:03, 1s section Upload object Upload object :done, cre1, 00:00:02, 1s Receive create event / Process :active, cre2, 00:00:04, 1s gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion Object deletion :done, cre1, 00:00:01, 1s Receive delete event / Process :active, cre2, 00:00:04, 1s section Upload object Upload object :done, cre1, 00:00:02, 1s Receive create event / Process :done, cre2, 00:00:03, 1s As a solution to this problem, there is a way to guarantee the order of events using the sequencer key included in S3 events^[document regarding S3 event structure. https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/notification-content-structure.html] One way to determine the sequence of events is to use the sequencer key. There is no guarantee that event notifications will be delivered in the order in which the event occurred. However, notifications and delete objects from events that create objects (PUT) include the sequencer. This can be used to determine the order of events for a particular object key. When comparing the sequencer strings of two event notifications for the same object key, you can see that the event notification with the larger 16 hexadecimal value sequencer is the event that occurred later. When maintaining a separate database or index of Amazon S3 objects using event notifications, I recommend that you compare and save the sequencer value each time you process an event notification. Please note the following: The sequencer cannot be used to determine the order of events for multiple object keys. The length of the sequencer may be different. To compare these values, first input 0 on the right of the shorter value, and then perform a lexicographic comparison. To Summarize: The sequencer is a value included in an object's PUT or DELETE event, and can be used to determine the order of events. Lexicographically compare the sequencers. The larger the value, the later the event that occurred. If the lengths differ, compare after inputting 0 on the right side of the shorter value. It cannot be used to determine the order of S3 events between multiple objects. It is used to determine the order of PUT and DELETE events for the same object. For example, if you want to implement the S3 event sequencer comparison in Rust, you can implement it as follows: Define the fields and constructor of the structure S3Sequencer to express the nature of the S3 sequencer. // 1. Define the structure S3Sequencer use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct S3Sequencer { bucket_name: String, object_key: String, sequencer: String, } // 2. Define the S3Sequencer constructor // Take the bucket name, object key, and sequencer as argument impl S3Sequencer { pub fn new(bucket_name: &str, objcet_key: &str, sequencer: &str) -> Self { Self { bucket_name: bucket_name.to_owned(), object_key: objcet_key.to_owned(), sequencer: sequencer.to_owned(), } } } Next, to determine the order of events by comparing the size of the S3Sequencer, use the PartialOrd ^[Mathematically speaking, it can represent a partially ordered set. There is also a full ordered set that implements the ‘Ord’ trait. It is interesting that traits are intentionally divided to implement comparison methods. I will implement https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html https://ja.wikipedia.org/wiki/%E9%A0%86%E5%BA%8F%E9%9B%86%E5%90%88#%E5%8D%8A%E9%A0%86%E5%BA%8F%E9%9B%86%E5%90%88] and PartialEq trait^[PartialEq trait definition https://doc.rust-lang.org/std/cmp/trait.PartialEq.html] By implementing these two, you can compare the size of the sequencers using comparison operators such as == , < , or > as shown below. let seq1 = S3Sequencer::new("bucket1", "object1", "abc123"); let seq2 = S3Sequencer::new("bucket1", "object1", "abc124"); if seq1 < seq2 { println!("seq1 is an older event than seq2"); } else if seq1 == seq2 { println!("seq1 and seq2"); } else { println!("seq1 is a newer event than seq2"); } Implement the partial_cmp method required to implement the PartialOrd trait as shown below. impl PartialOrd for S3Sequencer { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { // Sequencers with different bucket names cannot be compared if self.bucket_name != other.bucket_name { return None; } // Sequencers with different object keys cannot be compared if self.object_key != other.object_key { return None; } // Compare by adding 0 to the end of the shorter one to match with the longer one let max_len = std::cmp::max(self.sequencer.len(), other.sequencer.len()); let self_sequencer = self .sequencer .chars() .chain(std::iter::repeat('0')) .take(max_len); let other_sequencer = other .sequencer .chars() .chain(std::iter::repeat('0')) .take(max_len); Some(self_sequencer.cmp(other_sequencer)) } Since there is no meaning in comparing sequencers with different bucket names and object keys, return None using early return if they are different. You can compare the sequencers once you confirm that the bucket names and object keys are the same, but the process will be in the following order. Compare the length of the sequencers, and store the longer one in max_len Add 0 to the end of the shorter sequencer to match max_len Compare the sequencers created in 2 in lexicographic order and return the size The PartialEq trait is implemented as follows: impl PartialEq for S3Sequencer { fn eq(&self, other: &Self) -> bool { self.partial_cmp(other) .map_or(false, |o| o == std::cmp::Ordering::Equal) } } Determine if the sequencer comparison results are equal using the partial_cmp method of the PartialOrd trait. With the above implementations, it is now possible to compare sequencers for S3 events. Example of S3 Event Processing Implementation Considering Data Consistency Architecture Diagram Now, I will introduce how to guarantee the order of S3 events using sequencers, along with sample code The sample code converts the image file uploaded to the S3 bucket to grayscale on Lambda and saves it to the S3 bucket for output. Below is an architecture diagram. ![sample code architecture diagram. Convert the image file uploaded to the S3 bucket to grayscale on Lambda and save it to the S3 bucket for output. Use DynamoDB to implement lock process.](/assets/blog/authors/uehara/2024-12-02-how-to-gurantee-s3-event-order/architecture.svg =600x) When an image file is uploaded to the image input bucket, the Lambda function is triggered through an S3 event. The launched Lambda function checks DynamoDB to confirm that it is not processing, then sets the processing flag and processes the image file. Once processing has finished, it clears the processing flag and waits for the next image file to be processed. If the order of the create and delete event notifications is reversed, problems such as accidently deleting images that should exist could occur. For example, let’s assume that the process is implemented as follows. Image file A is deleted from the input bucket Image file A is re-uploaded to the input bucket Lambda receives a delete event (corresponding to 1) and deletes image file A from the output bucket Lambda receives a create event (corresponding to 2), processes image file A, converts it to grayscale, and saves it in the output bucket gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion (1) Object deletion :done, cre1, 00:00:01, 1s (3) Event reception, Object deletion :done, cre2, 00:00:03, 1s section Upload object (2) Upload object :done, cre1, 00:00:02, 1s (4) Event reception, convert to grayscale :active, cre2, 00:00:04, 1s However, there is a possibility of the notification order for 3 and 4 being reversed in S3 events, so the following flow may occur. Image file A is deleted from the input bucket Image file A is re-uploaded to the input bucket Lambda receives a create event (corresponding to 2), processes image file A, converts it to grayscale, and saves it in the output bucket Lambda receives a delete event (corresponding to 1) and deletes image file A from the output bucket gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion (1) Object deletion :done, cre1, 00:00:01, 1s (4) Event reception, Object deletion :active, cre2, 00:00:04, 1s section Upload object (2) Upload object :done, cre1, 00:00:02, 1s (3) Event reception, convert to grayscale :done, cre2, 00:00:03, 1s In this case, the problem occurs that even though image file A exists in the input bucket, the grayscaled image file A' does not exist in the output bucket. In order to prevent problems such as this, use the S3 event sequencer to implement exclusive processing. Additionally, it is also necessary to implement exclusive processing to manage image processing status by DynamoDB. Use DynamoDB conditional write to set an in-progress flag in order to prevent multiple Lambda functions from processing the same image file at the same time. The sample code is published on GITHUB. Take a look using the link below. (You will need to build an AWS infrastructure to run it, but it has been made easy to try using the terraform code.) https://github.com/kinto-technologies/techblog-s3-sequencer-example Implementing Sample Code with Rust This time, I will implement a Lambda function using Rust. It is convenient to use cargo-lambda to implement Lambda functions. https://www.cargo-lambda.info/ I will omit the details of how to use cargo-lambda. Creating an Entry Point When you run the initialization command on cargo-lambda, main.rs is automatically generated as shown below. cargo lambda init use lambda_runtime::{ run, service_fn, tracing::{self}, Error, }; mod handler; mod image_task; mod lock; mod s3_sequencer; #[tokio::main] async fn main() -> Result<(), Error> { tracing::init_default_subscriber(); run(service_fn(handler::function_handler)).await } Since handler:function_handler is specified as the entry point for the Lambda function, I will write the implementation in handler.rs The function_handler is implemented as follows. use crate::image_task::ImageTask; use aws_lambda_events::event::s3::S3Event; use lambda_runtime::{tracing::info, Error, LambdaEvent}; pub async fn function_handler(event: LambdaEvent<S3Event>) -> Result<(), Error> { // Convert S3 events to ImageTask let tasks: Vec<_> = event .payload .records .into_iter() .map(ImageTask::try_from) .collect::<Result<Vec<_>, _>>()?; // Use futures::future::join_all to create the task to be executed let execute_tasks = tasks.iter().map(|task| task.execute()); // Use join_all to execute or wait all tasks // Store the execution results in ret let ret = futures::future::join_all(execute_tasks).await; // Output the execution results to log for (t, r) in tasks.iter().zip(&ret) { info!("object_key: {}, Result: {:?}", t.object_key, r); } // Return error is there is an error if ret.iter().any(|r| r.is_err()) { return Err("Some tasks failed".into()); } // Successful termination Ok(()) } Convert the S3 event vector to an ImageTask structure vector. Since the conversion method implements the TryFrom trait, you just have to call the try_form method. Create an image processing task based on the ImageTask structure vector. Use the tokio crate join_all function to execute all tasks in parallel. Output the results returned by join_all in 3 to the log. If there is an error, return the error and terminate the Lambda function abnormally. If there are no errors it will successfully terminate. Image Processing Implementation The ImageTask structure used in 1 is defined as below and holds the information necessary to execute Lambda. #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "type")] pub enum TaskType { Grayscale, Delete, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImageTask { pub bucket_name: String, #[serde(rename = "id")] pub object_key: String, pub sequencer: S3Sequencer, pub task_type: TaskType, pub processing: bool, } Field name Description bucket_name S3 bucket name object_key Object key sequencer S3 event sequencer task_type Enumeration indicating the type of task (Grayscale, Delete) processing Processing flag Implement specific image processing within the execute method of the ImageTask structure. impl ImageTask { pub async fn execute(&self) -> Result<(), Error> { // 1. Acquire lock let lock = S3Lock::new(&self).await?; // 2. Process according to the task type match self.task_type { TaskType::Grayscale => { // Convert image to grayscale and save in output bucket let body = lock.read_input_object().await?; let format = image::ImageFormat::from_path(&self.object_key)?; let img = image::load_from_memory_with_format(&body, format)?; let img = img.grayscale(); let mut buf = Vec::new(); img.write_to(&mut Cursor::new(&mut buf), format)?; lock.write_output_object(buf).await?; } // Delete image from output bucket TaskType::Delete => lock.delete_output_object().await?, } // 3. Release lock lock.free().await?; Ok(()) } } Apply exclusive processing to prevent S3 object data inconsistencies Process according to the task type If a file was added to a previous bucket, convert the image to grayscale and save it in the output bucket If a file was deleted, delete the file from the output bucket Release lock when processing has finished Implement Lock Processing Define the S3Lock structure to implement lock processing. pub struct S3Lock { dynamodb_client: aws_sdk_dynamodb::Client, table_name: String, s3_client: aws_sdk_s3::Client, input_bucket_name: String, input_object_key: String, output_bucket_name: String, output_object_key: String, } The specific lock acquisition process is implemented in the constructor. The code is a bit long, but roughly speaking, it is as follows. If writing to DynamoDB is successful, the lock is considered to have been acquired. If writing fails, try again every 2 seconds. If the lock cannot be acquired within 30 seconds, a timeout occurs. The lock processing sequence diagram is shown below. sequenceDiagram participant ImageTask participant S3Lock participant DynamoDB ImageTask->>S3Lock: Acquire lock loop alt timeout (30 seconds) S3Lock->>ImageTask: Return error (timeout) end S3Lock->>DynamoDB: Acquire processing status DynamoDB->>S3Lock: Return results alt record exists S3Lock->>S3Lock: Sequencer comparison alt is old S3Lock->>ImageTask: Return error (skip) else is new S3Lock->>S3Lock: Retry acquiring lock end else S3Lock->>DynamoDB: Acquire lock with conditional write DynamoDB->>S3Lock: Write return result alt write successful S3Lock->>ImageTask: Lock acquisition successful else failure S3Lock->>S3Lock: retry end end end The code in the constructor is as follows: impl S3Lock { pub async fn new(task: &ImageTask) -> Result<Self, Error> { let table_name = std::env::var("DYNAMODB_TABLE_NAME").unwrap(); let output_bucket_name = std::env::var("OUTPUT_BUCKET_NAME").unwrap(); let require_lock_timeout = Duration::from_secs( std::env::var("REQUIRE_LOCK_TIMEOUT") .unwrap_or_else(|_| "30".to_string()) .parse::<u64>() .unwrap(), ); let interval_retry_time = Duration::from_secs( std::env::var("RETRY_INTERVAL") .unwrap_or_else(|_| "2".to_string()) .parse::<u64>() .unwrap(), ); let config = aws_config::load_defaults(aws_config::BehaviorVersion::v2024_03_28()).await; let s3_client = aws_sdk_s3::Client::new(&config); let dynamodb_client = aws_sdk_dynamodb::Client::new(&config); // Acquire lock Measure execution time let start = Instant::now(); loop { // It will time out if the lock cannot be acquired for more than 30 seconds. if start.elapsed() > require_lock_timeout { return Err("Failed to acquire lock, timeout".into()); } Acquire sequencer from DynamoDB with strong read consistency let item = dynamodb_client .get_item() .table_name(table_name.clone()) .key("id", AttributeValue::S(task.object_key.clone())) .consistent_read(true) .send() .await?; Compare sequencer if acquired item exists if let Some(item) = item.item { let item: ImageTask = from_item(item)?; if task.sequencer <= item.sequencer { If the sequencer itself is old, there is no need to process it, so skip it return Err("Old sequencer".into()); } If the sequencer itself is new, wait until other processing has finished if item.processing { warn!( "Waiting for other process to finish task, retrying, remaining time: {:?}", require_lock_timeout - start.elapsed() ); thread::sleep(interval_retry_time); continue; } } // Acquire lock with conditional write to DynamoDB // If the record exists at that time, only write if the processing flag is false let resp = dynamodb_client .put_item() .table_name(table_name.clone()) .set_item(Some(to_item(&task).unwrap())) .condition_expression("attribute_not_exists(id) OR processing = :false") .expression_attribute_values(":false", AttributeValue::Bool(false)) .send() .await; Once acquired, exit the loop and continue processing If it could not be acquired, continue trying until the lock can be acquired. match resp { Ok(_) => break, Err(SdkError::ServiceError(e)) => match e.err() { PutItemError::ConditionalCheckFailedException(_) => { warn!( "Failed to acquire lock, retrying, remaining time: {:?}", require_lock_timeout - start.elapsed() ); thread::sleep(Duration::from_secs(2)); continue; } _ => return Err(format!("{:?}", e).into()), }, Err(e) => return Err(e.into()), } } return Ok(Self { dynamodb_client, output_bucket_name, s3_client, table_name, input_bucket_name: task.bucket_name.clone(), input_object_key: task.object_key.clone(), output_object_key: task.object_key.clone(), }); } } The process to release the lock is implemented as shown below, and the lock is released by updating the processing flag to false. impl S3Lock { pub async fn free(self) -> Result<(), Error> { // Release DynamoDB lock // Only update the processing flag self.dynamodb_client .update_item() .table_name(self.table_name) .key("id", AttributeValue::S(self.input_object_key)) .update_expression("SET processing = :false") .expression_attribute_values(":false", AttributeValue::Bool(false)) .send() .await?; Ok(()) } } Since I want to force a lock when touching an S3 object, I have created a method for manipulating S3 objects in S3Lock^[I feel that reusability will be lower if I create it in S3Lock, but for simplicity I will define it in the same structure. Though there seems to be a better way...] impl S3Lock { pub async fn read_input_object(&self) -> Result<Vec<u8>, Error> { // Acquire S3 object let object = self .s3_client .get_object() .bucket(&self.input_bucket_name) .key(&self.input_object_key) .send() .await?; let body = object.body.collect().await?.to_vec(); Ok(body) } pub async fn write_output_object(&self, buf: Vec<u8>) -> Result<(), Error> { // Save S3 object let byte_stream = ByteStream::from(buf); self.s3_client .put_object() .bucket(&self.output_bucket_name) .key(&self.output_object_key) .body(byte_stream) .send() .await?; Ok(()) } pub async fn delete_output_object(&self) -> Result<(), Error> { // Delete S3 object self.s3_client .delete_object() .bucket(&self.output_bucket_name) .key(&self.output_object_key) .send() .await?; Ok(()) } } Trying it Out Let’s try using the sample code. I need to prepare an image that I want to convert to grayscale, so this time I will use Hyogo Prefectural Park Awaji Hanasajiki^[a beautiful flower garden on Awaji Island https://awajihanasajiki.jp/about/] . I took the photo. I will use the image from https://awajihanasajiki.jp/about/ . Infrastructure First, use terraform apply to build the AWS infrastructure. Then, clone the GitHub repository and implement the following command. cd terraform # Modify variables.tf and provider.tf appropriately. terraform init terraform apply ![sample code architecture diagram. Convert the image file uploaded to the S3 bucket to grayscale on Lambda and save it to the S3 bucket for output. Use DynamoDB to implement lock process.](/assets/blog/authors/uehara/2024-12-02-how-to-gurantee-s3-event-order/architecture.svg =600x) Upload the Image File to the S3 Bucket If infrastructure is completed, upload the image file to the input S3 bucket. The Lambda function process begins when the upload finishes, and the item is added to the DynamoDB table. The image file is saved in the output S3 bucket when the process finishes, and the processing flag of the DynamoDB item is set to false. You can see the image added to the output S3 bucket and converted to grayscale. Delete the Image File from the S3 Bucket When an object is deleted from the input bucket, it is also deleted from the output bucket. Confirm that Exclusive Processing is Working When the processing flag of an item added to DynamoDB is set to true, processing will wait even if an S3 event that should be processed arrives. To confirm this behavior, let's intentionally set the DynamoDB processing flag to true and upload a file with the same name. If you look at CloudWatch Logs, you can see that it waits for other processes to complete without processing newly generated S3 events. The log message will indicate that Lambda is waiting. Setting the DynamoDB processing flag back to false will restart processing. When the DynamoDB item's processing flag is set back to false, the log will indicate that processing is completed. Thanks to exclusive processing, the order of processing is guaranteed even if delete and upload events occur at approximately the same time. Conclusion This time, I introduced sample code for image processing that uses a sequencer to guarantee the order of S3 events. To guarantee the order of S3 events, you need to utilize a sequencer to compare the order of events. I implemented the sample code in Rust as my own hobby, but it should be possible to implement something similar in other languages. Please feel free to use this as a reference.
アバター
Introduction Hello, I'm Shimakawa, a member of the Cloud Infrastructure Group. The Cloud Infrastructure Group is responsible for everything from designing to operating the company's entire infrastructure, including AWS. As generative AI adoption grows across various products in our company, the Cloud Infrastructure Group has been actively supporting these initiatives. In this article, I’ll share my experience building an Amazon Bedrock Knowledge Base using Terraform. I’ll also touch on the RAG Evaluation announced at re:Invent 2024. Configuration Here is the architecture we will be building. We will use OpenSearch Serverless as the Vector store for Amazon Bedrock Knowledge Base, and specify S3 as the data source. Building with Terraform The directory structure is as follows. I will explain each file’s content in detail. The Terraform version used in this setup is 1.7.5 . $ tree . ├── aoss.tf # OpenSearch Serverless ├── bedrock.tf # Bedrock resources ├── iam.tf # iam ├── s3.tf # S3 for bedrock ├── locals.tf # variable definitions ├── provider.tf # provider definitions └── terraform.tf # Backend settings, etc. This section defines the variables. locals { env = { environment = "dev" region_name = "us-west-2" sid = "test" } aoss = { vector_index = "vector_index" vector_field = "vector_field" text_field = "text_field" metadata_field = "metadata_field" vector_dimension = 1024 } } We specify the AWS provider, the OpenSearch provider version, and the S3 backend to store the tfstate. The S3 bucket used here was created manually and is not included in this Terraform code. terraform { required_providers { # https://registry.terraform.io/providers/hashicorp/aws/ aws = { source = "hashicorp/aws" version = "~> 5.0" } opensearch = { source = "opensearch-project/opensearch" version = "2.2.0" } } backend "s3" { bucket = "***-common-bucket" region = "ap-northeast-1" key = "hogehoge-terraform.tfstate" encrypt = true } } This defines the AWS and OpenSearch providers. The OpenSearch provider is used to create and manage indexes. provider "aws" { region = local.env.region_name default_tags { tags = { SID = local.env.sid Environment = local.env.environment } } } provider "opensearch" { url = aws_opensearchserverless_collection.collection.collection_endpoint aws_region = local.env.region_name healthcheck = false } Create OpenSearch Serverless resources and an index. I referred to Deploy Amazon OpenSearch Serverless with Terraform . For this setup, the security policy is set to public, but ideally, access should be restricted using a VPC endpoint. data "aws_caller_identity" "current" {} # Creates a collection resource "aws_opensearchserverless_collection" "collection" { name = "${local.env.sid}-collection" type = "VECTORSEARCH" standby_replicas = "DISABLED" depends_on = [aws_opensearchserverless_security_policy.encryption_policy] } # Creates an encryption security policy resource "aws_opensearchserverless_security_policy" "encryption_policy" { name = "${local.env.sid}-encryption-policy" type = "encryption" description = "encryption policy for ${local.env.sid}-collection" policy = jsonencode({ Rules = [ { Resource = [ "collection/${local.env.sid}-collection" ], ResourceType = "collection" } ], AWSOwnedKey = true }) } # Creates a network security policy resource "aws_opensearchserverless_security_policy" "network_policy" { name = "${local.env.sid}-network-policy" type = "network" description = "public access for dashboard, VPC access for collection endpoint" policy = jsonencode([ ###References for using VPC endpoints # { # Description = "VPC access for collection endpoint", # Rules = [ # { # ResourceType = "collection", # Resource = [ # "collection/${local.env.sid}-collection}" # ] # } # ], # AllowFromPublic = false, # SourceVPCEs = [ # aws_opensearchserverless_vpc_endpoint.vpc_endpoint.id # ] # }, { Description = "Public access for dashboards and collection", Rules = [ { ResourceType = "collection", Resource = [ "collection/${local.env.sid}-collection" ] }, { ResourceType = "dashboard" Resource = [ "collection/${local.env.sid}-collection" ] } ], AllowFromPublic = true } ]) } # Creates a data access policy resource "aws_opensearchserverless_access_policy" "data_access_policy" { name = "${local.env.sid}-data-access-policy" type = "data" description = "allow index and collection access" policy = jsonencode([ { Rules = [ { ResourceType = "index", Resource = [ "index/${local.env.sid}-collection/*" ], Permission = [ "aoss:*" ] }, { ResourceType = "collection", Resource = [ "collection/${local.env.sid}-collection" ], Permission = [ "aoss:*" ] } ], Principal = [ data.aws_caller_identity.current.arn, iam_role.bedrock.arn, ] } ]) } resource "opensearch_index" "vector_index" { name = local.aoss.vector_index mappings = jsonencode({ properties = { "${local.aoss.metadata_field}" = { type = "text" index = false } "${local.aoss.text_field}" = { type = "text" index = true } "${local.aoss.vector_field}" = { type = "knn_vector" dimension = "${local.aoss.vector_dimension}" method = { engine = "faiss" name = "hnsw" } } } }) depends_on = [aws_opensearchserverless_collection.collection] } Create the Knowledge Base and data source. data "aws_bedrock_foundation_model" "embedding" { model_id = "amazon.titan-embed-text-v2:0" } resource "aws_bedrockagent_knowledge_base" "this" { name = "test-kb" role_arn = iam_role.bedrock.arn knowledge_base_configuration { type = "VECTOR" vector_knowledge_base_configuration { embedding_model_arn = data.aws_bedrock_foundation_model.embedding.model_arn } } storage_configuration { type = "OPENSEARCH_SERVERLESS" opensearch_serverless_configuration { collection_arn = aws_opensearchserverless_collection.collection.arn vector_index_name = local.aoss.vector_index field_mapping { vector_field = local.aoss.vector_field text_field = local.aoss.text_field metadata_field = local.aoss.metadata_field } } } depends_on = [iam_role.bedrock] } resource "aws_bedrockagent_data_source" "this" { knowledge_base_id = aws_bedrockagent_knowledge_base.this.id name = "test-s3-001" data_source_configuration { type = "S3" s3_configuration { bucket_arn = "arn:aws:s3:::****-dev-test-***" ### Masked bucket name } } depends_on = [aws_bedrockagent_knowledge_base.this] } Set the service role that bedrock will use. resource "aws_iam_role" "bedrock" { name = "bedrock-role" managed_policy_arns = [aws_iam_policy.bedrock.arn] assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Sid = "" Principal = { Service = "bedrock.amazonaws.com" } }, ] }) } resource "aws_iam_policy" "bedrock" { name = "bedrock-policy" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = ["bedrock:InvokeModel"] Effect = "Allow" Resource = "*" }, { Action = [ "s3:GetObject", "s3:ListBucket", ] Effect = "Allow" Resource = "***-dev-test-***" ### ARN of the S3 bucket that was }, { Action = [ "aoss:APIAccessAll", ] Effect = "Allow" Resource = "arn:aws:aoss:us-west-2:12345678910:collection/*" }, ] }) } Create an S3 bucket for use with Bedrock. Also, configure CORS. Below is a reference image of the error. resource "aws_s3_bucket" "bedrock" { bucket = "***-dev-test-***" ### Masked bucket name } resource "aws_s3_bucket_cors_configuration" "this" { bucket = aws_s3_bucket.bedrock.id cors_rule { allowed_headers = ["*"] allowed_methods = [ "GET", "PUT", "POST", "DELETE" ] allowed_origins = ["*"] } } Execution Use terraform apply to create all resources at once. Verifying the Created Resources Check that the Knowledge Base has been created in Bedrock and that the data source is available. Next, verify that an OpenSearch collection has been created and that an index has been set. Testing the Knowledge Base Upload some sample text to S3 to use as a data source. Dogs like meat. Cats like fish aws s3 cp ./test001.txt s3://[S3 Bucket Name]/test001.txt Next, synchronize the data source. Now, let’s test it by asking a question. (Using Claude 3.5 Sonnet for the Prompt) The system correctly retrieves answers from the provided text while refraining from answering questions about information that is not present. This was a brief overview of setting up a Knowledge Base and OpenSearch Serverless using Terraform. Trying out RAG Evaluation Next, we will test the RAG evaluation announced at re:Invent 2024 on the Knowledge Base we created. @ card Preparation First, prepare a dataset file in JSONL format for evaluation. This file contains prompts with their expected answers. {"conversationTurns":[{"referenceResponses":[{"content":[{"text":"Cats' favorite food is fish."}]}],"prompt":{"content":[{"text":"What do cats like? {"conversationTurns":[{"referenceResponses":[{"content":[{"text":"Dogs’ favorite food is meat."}]}],"prompt":{"content":[{"text":"What do dogs like? Since the evaluation references data from S3, upload the dataset file to an S3 bucket. aws s3 cp ./dataset001.txt s3://[S3 Bucket Name]/datasets/dataset001.txt Creating a Job Next, we will create a job. While this can be done through the management console, this time, I executed it using the CLI. @ card aws bedrock create-evaluation-job \ --job-name "rag-evaluation-complete-stereotype-docs-app" \ --job-description "Evaluates Completeness and Stereotyping of RAG for docs application" \ --role-arn "arn:aws::iam:<region>:<account-id>:role/AmazonBedrock-KnowledgeBases" \ --evaluation-context "RAG" \ --evaluationConfig file://knowledge-base-evaluation-config.json \ --inference-config file://knowledge-base-evaluation-inference-config.json \ --output-data-config '{"s3Uri":"s3://docs/kbevalresults/"}' You need to specify the JSONL file and the destination for saving the results in knowledge-base-evaluation-config.json . Checking the Job After waiting about 15 to 20 minutes, the job was completed, so I checked the results. I started by reviewing the summary. The responses were almost exactly as expected, so there wasn’t much surprise. However, the Correctness and Completeness scores were both 1, indicating that the system performed as expected. The only exception was the Helpfulness score, which was 0.83. When I checked the evaluation comments, it stated: “The answer is neither particularly interesting nor unexpected, but in this context, it doesn't need to be.” I think the fact that this context does not have to be the case is what is causing the score to drop. Final Thoughts Our company has been increasingly integrating generative AI, including Amazon Bedrock, and its real-world applications are expanding. Moving forward, I plan to explore more features and ensure we are well-prepared to meet project requirements. I hope this blog post serves as a useful reference. Thank you for reading!
アバター
AIが急速に進化する2025年、エンジニアにとって「AIをどう活用するか」が重要なスキルとなってきました。 しかし、AIを効果的に活用するには、適切なプロンプト(指示の出し方)を理解する必要があり、そのためには 慣れや知見 が不可欠です。 そこで、AIとコーディングを行う最初のステップとして、今回のテーマである TDD(test-driven development: テスト駆動開発) と AI を活用した開発を紹介します。 TDD × AIのメリット ✅ 実装コストが激減!エンジニアは"テストを書くだけ"でOK テストを書くだけで、複雑な指示もプロンプトも不要。あとはAIが自動でコードを生成。 ✅ 開発速度が爆速化!細かな往復が劇的に減少 TDDのステップごとにAIが即座にコードを生成するので、開発効率が驚異的にアップ。 ✅ 圧倒的なコード品質!テストでしっかりAIの暴走をコントロール 明確なテストでAIの生成コードをしっかり制御。結果的にバグの少ないコードになる。 TDDとは? 大前提となるTDD(Test-Driven Development: テスト駆動開発)について簡単に説明します。 https://www.amazon.co.jp/dp/4274217884 「TDD(Test-Driven Development: テスト駆動開発)」はKent Beck氏が👆の書籍で20年以上前に提唱した手法です。 上の図の「まずテストを書いて、次にテストを通過する実装をする、最後にリファクタリング」というシンプルなサイクルを繰り返すことで、品質の高いコードを生み出すことができます。 また、テストが実装の起点になるので、 テスタブルな構造を担保しつつ、実装を進めることが出来ます。 TDDを実践する :::message VSCodeの GitHub CopilotのAgentモードを使って実施しています。 ::: https://code.visualstudio.com/docs/copilot/copilot-edits#_use-agent-mode-preview 今回の方法では、エンジニアがテストを書き、生成AIが実装・リファクタリングを担うことで、コード品質を維持しつつ効率的に開発を進めます。 今回は パスワードのバリデーション を例に、TDD × AIの流れを説明していきます。 まずは、 文字数のバリデーション から始めていきましょう。 :::message alert 最初からすべての仕様を詰め込まず、細かいステップで進めることが重要です! ::: ステップ1: 文字数バリデーションテストの作成 対象になるPasswordバリデーションのクラスとテストファイルを用意しましょう。 この段階ではどちらも空のファイルで問題ありません。 ファイルが作成できたらそのままPasswordバリデーションクラスを書きたくなるところですが、今回はTDDなのでテストを書いていきます。 @Test("パスワードの文字数が8文字以上16文字以下でバリデーションが成功すること") func testPasswordLengthValidation() async throws { let sut = PasswordValidator() let validPasswordShort = "Valid123" let validPasswordLong = "ValidPassword123" let invalidPasswordShort = "Short01" let invalidPasswordLong = "ThisPasswordIsWay" #expect(sut.validate(validPasswordShort) == true) #expect(sut.validate(validPasswordLong) == true) #expect(sut.validate(invalidPasswordShort) == false) #expect(sut.validate(invalidPasswordLong) == false) } この段階では PasswordValidator クラスや validate メソッドが未実装のため、当然エラーが発生します。 次に、AIにプロンプトを入力して実装を依頼します。 ![input prompt](/assets/blog/authors/HiroyaHinomori/2025_03_tdd_x_ai_02.png =500x) すると、数十秒後にAIが実装を生成してくれます。 struct PasswordValidator { // パスワードの長さの制約を定数として定義 private let minimumPasswordLength = 8 private let maximumPasswordLength = 16 /// パスワードが有効かどうかを検証します /// - Parameter password: 検証するパスワード /// - Returns: パスワードが有効な場合はtrue、そうでない場合はfalse func validate(_ password: String) -> Bool { let passwordLength = password.count return passwordLength >= minimumPasswordLength && passwordLength <= maximumPasswordLength } } 次に、数字・文字・記号のミックス判定を追加してみます。 ステップ2: 数字、文字、記号の含有バリデーションの追加 @Test("パスワードの文字列の中に数字、文字、記号が含まれている場合にバリデーションが成功すること") func testPasswordCharacterValidation() async throws { let sut = PasswordValidator() let validPassword = "ValidPassword12!" let invalidPasswordNoNumber = "InvalidPassword!" let invalidPasswordNoCharacter = "1234567890!" let invalidPasswordNoSymbol = "InvalidPassword1" #expect(sut.validate(validPassword) == true) #expect(sut.validate(invalidPasswordNoNumber) == false) #expect(sut.validate(invalidPasswordNoCharacter) == false) #expect(sut.validate(invalidPasswordNoSymbol) == false) } この段階でエラーが発生しますが、気にせずテストケースをAIに渡して実装を生成します。 struct PasswordValidator { // パスワードの長さの制約を定数として定義 private let minimumPasswordLength = 8 private let maximumPasswordLength = 16 /// パスワードが有効かどうかを検証します /// - Parameter password: 検証するパスワード /// - Returns: パスワードが有効な場合はtrue、そうでない場合はfalse func validate(_ password: String) -> Bool { let passwordLength = password.count guard passwordLength >= minimumPasswordLength && passwordLength <= maximumPasswordLength else { return false } let hasNumber = password.rangeOfCharacter(from: .decimalDigits) != nil let hasLetter = password.rangeOfCharacter(from: .letters) != nil let hasSymbol = password.rangeOfCharacter(from: .symbols) != nil || password.rangeOfCharacter(from: .punctuationCharacters) != nil return hasNumber && hasLetter && hasSymbol } } ここまで 10分もかからず 実装できました。 通常のコーディングではプロンプトに色々な条件や仕様を伝える必要がありますが、この方法では テスト条件を満たした実装をして というお願いをするだけです。 実装内容に関してはTestに全て書かれているので、複雑なプロンプトの指定はほぼ不要になります。 AIとのコミュニケーションをさらに効率化するには 実装のルールや制約を事前に「copilot-instructions.md」に書いておけば、毎回AIに細かい指示を伝える必要もありません。 日本語で返答してください。 ### コーディングルール - テストはswift-testingを使用してください。 - 実装には基本的にマジックナンバーは使わないこと - DRYの原則に則って実装してください - KISSの原則に則って実装してください - YAGNIの原則に則って実装してください AI時代に活躍するエンジニアになるために AIは万能ではありません。しかし、だからといって諦めるのはとてももったいないです! 「AIが得意なこと」と「人間が担うべきこと」を冷静に見極めること が重要です。 「TDD × AI」で、AIコーディングの癖を理解し、次世代の開発スピードと品質を手に入れましょう🚀
アバター
1.start-(概要) 初めまして! KINTOテクノロジーズのCloud Infrastructure GでInfrastructure Architectを担当している劉(YOU)です。 今年の1月に入社して、techblogには初執筆なのでこれからよろしくお願いします! AWSの認定は23年10月のSAAを始じめ、25年2月のMLAを最後に、1年4ヶ月でやっとAWS認定12冠を取りました。せっかくなので、AWS認定12冠を達成しながら感じた個人的な意見と情報を共有します。 先に話しますと、AWS認定12冠とは、AWS社が主催する資格認定を全て取得したことを指します。その基準は毎年変更されるんですけど、 https://aws.amazon.com/jp/blogs/psa/2024-japan-aws-all-certifications-engineers/ AWS JAPAN APN ブログ からその詳細を前もって発表して、選出された方々を表彰します。2024年は1,222名が正式に「AWS All Certifications Engineers」として認められています。公式記事では、 ”AWS 認定資格を全て取得し・維持する事” は AWS のテクノロジーを正しく理解し、お客様に信頼性の高い最新の技術をご案内できる基盤をお持ちだという証になります。 と述べています。 AWS以外でも、AzureとかGCPとかクラウドサービスを提供している会社は多いですが、クラウドサービスの量と質、高い占有率からできる汎用性、圧倒的なアップデートスピード、どう考えてもAWSは クラウド業界の標準だと言い切れます。 そして、最近AIの注目が高くなることと伴い、クラウドの重要性も上がっています。「クラウドとかAIとか、私と関係ないだろう」と思う方がいらっしゃるかも知れませんが、現時点でほぼ全ての業務にパソコンを使うようになった事と同じく、もうじきAIを日常で使うことになる時代がやって来ると思います。 AIその物になるモデルとモデルを動かすためのコンピュータ、それが簡単に提供されるプラットフォームがクラウドであり、時代を追いつくためにもクラウドを習得する必要があると言うことです。それで、AWS及びクラウドを勉強するために資格はなぜ必要なのか? その答えを次から説明して行きます。 2.status-(現状) 残念ながら、資格はあってもなくてもクラウド活用に大きい影響を与えません。例を挙げると、今からクラウドを英語だと見なして考えてください。英語を活用するためにTOEICを準備して、高得点をしました。それが本当の意味で英語を上達することだと思いますか? どれだけ試験のテクニックが良くても、単語と文法などを暗記しても、実際に英語を要求される時に使いこなせなかったら無駄です。 しかし、TOEICが英語の実力向上に役立たないって言ったらそれは違うと断言できます。意味が無かったら数々の大学・企業からTOEICの点数を基準として評価する訳がないです。ビジネス英語力を点数に換算する試験がTOEICだからこそ、ただの点数ではなくて能力として認められてることになります。 そう言う意味で、AWS認定12冠はクラウドに対して分かりやすい天井です。 実体化されていない知識の塊を資格という形で見えるようにします。こう言う見える化によって得られる効果を整理しますと、 明確な目標設定(Goal):AWS社が証明してくれるロードマップを従った認定なので、階段式でスケジューリング可能 努力の源(Motivation):受験日を決めて勉強することで、頑張れる環境が作られる 知識の保証(Knowledge):資格を取るための最低限の知識が保証される 振り返り(Remind):そもそもクラウドに詳しい人であっても、資格で要求される知識を点検することができる キャッチアップ(Discovery):試験はアップデートを沿って変わっていくので、触れる機会がない情報を勉強することができる になります。 逆に英語で置換して見ても、違和感なく受け入れられる内容ではないでしょうか?結局、資格を取ったらクラウド力が上がる、クラウドやりたいから資格取ろうではなく、鍛えること、それ自体に意味があると思います。 AWS認定のこれから 続いて、私が一年ちょっと超える時間をAWS認定準備しながら感じた「これからAWS認定はどうなる?」を突っ込んで行きます。 :::message あくまでも、 根拠のない自分勝手の推測なので、AWSで公式の何かがあることではありません。 引用の時にはご注意お願いします。 ::: 私がAWS認定の準備し始めた時期は、22年ChatGPTが流行ってAIに対する関心が大きくなってる状況でした。AWSもAIを中心にするサービスをどんどん出して、24年から資格の構成を大きく変更させました。 既存のスペシャル資格三つを24年4月で削除し、 AWS Certified Data Analytics – Specialty (DAS) AWS Certified Database – Specialty (DBS) AWS Certified: SAP on AWS – Specialty (PAS) DASとDBSを代行するために資格が24年3月に登場、 AWS Certified Data Engineer – Associate (DEA) その後、Amazon QとかAmazon Bedrockなど新しく出たAIサービスと、Amazon Sagemaker周りの強化されたAIサービスのロードマップを提示するために24年10月、 AWS Certified AI Practitioner (AIF) AWS Certified Machine Learning Engineer – Associate (MLA) 結構、大変革だったので試験を準備している個人の立場でもちょっと困りました。勉強してた内容が大幅に変更されることと同然だったので、先に計画してた試験日程も変えるしか無かったです。これからも、今後の技術トレンドであるAIを中心に変わっていくことは確かです。 あくまでも推測で過ぎませんが、変更が起きる可能性が高い資格は AWS Certified Machine Learning Engineer – Specialty (MLS) だと思います。 MLSが最後にアップデートされたのは22年7月のなのでAIF, MLAに比べたら内容が古くなっています。現状のSpecialtyのままアップデートされることもありますが、新しいprofessional資格に改編される可能性が高いです。 その理由としては、既存のパスがPractitioner→Associate→Professionalに繋がる仕組みだからです。 ^1 同様にAIF → MLA資格の次に来るProfessional資格が必要になります。単純にスペシャリティがプロフェッショナルになって最新化される…ことはAWS側が決める所ですが、そうなったらDEAの上位資格も想定するしかないです。 (仮)AWS Certified Machine Learning Engineer – Professional (MLP) (仮)AWS Certified Data Engineer – Professional (DEP) これが普通に考えられる予測ですが、これはこれで問題が生じます。AWS社の認定は12冠を象徴として維持してるそうなので、二つが増えれば資格の数が13個を超えてしまうことです。 それを回避する方法があって、まずは上記の資格が増やされる分、曖昧になったSpecialtyを減らすことです。 (仮)AWS Certified Security – Specialty (SCS) AWS Certified Advanced Networking – Specialty (ANS) SCSとANSはもう消えている他のスペシャリティとは違って、プロフェッショナルから携わった知識を深く振り込む内容で構成された認定です。両方とも出る内容が60%以上がプロフェッショナルと重複しつつ、SCSは組織全体のセキュリティを重視し、ANSはオンプレミスとのネットワーキングが主になっています。その中、現状のままではやむを得ない欠陥があります。 SCSはAIのアップデートされなかったので、AIに対するセキュリティの内容が含まれていません。AIの発展が早くなっていながら、AIに対するセキュリティーやコンプライアンスも大事にされてるので、SCSにAIの内容が入れるか、各プロフェッショナルに溶かすかの問題です。すでにAIのトレンドに合わせて統廃合されたスペシャリティ認定が多いので後者がなり得ると思ってます。 ANSの場合、SCSと似ているポジションですが、ネットワークはAIができてもAWS内部に限ってはそんなに差がありません。OpenAIを使うにはAzureを、Geminiを使うにはGCPを、その他のクラウドベンダが運営しているAIを使うにはマルチクラウドが確かに必要になりますが、自社製品じゃないとAWS社が積極的に支援しないスタンスなので、マルチクラウドに関係する認定のアップデートは今の所はないです。代わりに、アンチクラウドの流れからハイブリッドクラウドが浮上していますので、ANS認定の仕組みは維持すると見込んでいます。 とにかく、資格の数を減らしたら12冠は担保できるのでこう言うやり方もあることと、二つのプロフェッショナルを増やさずにDevOps Engineerみたいに一つに納める事もあります。 (仮)AWS Certified MLOps Engineer – Professional (MOP) AWSで紹介している MLOps は、「ML アプリケーション開発 (Dev) と ML システムのデプロイおよび運用 (Ops) を統合する ML カルチャーとプラクティスです」だと記述してます。 ^2 これはMLに関する全体作業を意味してるので、まさにDEAで扱うデータエンジニアリング、データ分析を通して、AIF→MLA→MLSで使われる機械学習の全体を活用できますので、新しいプロフェッショナルがただ一つだけ必要だったらこれで通用できる仕方だと思ってます。 AWS認定の類型 そして、認定の種類だけではなく、試験の類型にも変化が起きています。 既存の試験はSOAのラボ試験が中止になってから、選択肢で正解を選ぶ形式のみが評価方式でした。客観的に定量評価ができる要素は長所ですが、実装とは関係が深くない知識になることも否定できません。AWS社もこれを意識しているかなと思いまして、AIFとMLAから新しい出題形式が出ました。 AIF試験ガイド を見ると、 並べ替え: 指定されたタスクを完了することを目的とした 3~5 つの答えのリストが提示される。設問に対する点数を得るには、正解を選択し、正しい順序に並べる必要がある。 内容一致: 3~7 つのプロンプトのリストと一致する答えのリストが提示される。設問に対する点数を得るは、すべてのペアを正しく一致させる必要がある。 ケーススタディ : 1 つのシナリオに、そのシナリオに関する設問が 2 つ以上含まれている。ケーススタディの各設問のシナリオは同じである。ケーススタディの各設問は個別に採点される。ケーススタディでは正解した設問ごとに点数が得られる。 上記の三つの類型は私の試験にも多い数ではありませんが、試験ガイドで述べている事と同じく出題されました。問題のレベルとしては選択肢の問題と同様で、AWS試験の秘密保証のため問題の詳しい形式は言えませんが、私が感じた出題形式の評価はこうなります。 並べ替えと内容一致は、選択肢の類似性から正解を類推することが出来なくなりました。本当に実施するべきの手順だったり、提示される単語や説明を結び付く、問題から要求する内容を熟知しないと解けない形です。 ケーススタディの基本は設問=選択肢なんですが、単一のケースで複数の問題を提示するやり方です。ここはケースを多角的に接近することもできるし、長問・多問からは知識の応用より読解力を求められる状況があるから、それが解消されます。我らの現実世界もケースがあったら一問一答で絶えず、ケースごとにシミュレーションすることが一般的なので、このケーススタディは受験者としていい類型だと思います。 AWS社は認定に関してこれからも出題形式もそうですし、SOAのラボみたいに「本当に実装できますか?」を目指して変化して行くと考えられます。こういう変更は単発で起きることではなく、連続的に他の認定にも反映されるので、AWS試験を準備していらっしやる方々はキャッチアップして備えて行きましょう! 3.stance-(心構え) 私も周りの人達と話す時に職種不問でよく出る話題なんですが、 「AWSとは関係ない仕事してるけど、これ勉強して本当に使える?」 「AWS資格取るとしたらどこから始まる?」 「何をネタに勉強してる?」 と質問を貰います。私はクラウドエンジニアとして資格を取っていまして、クラウドで業務をするための知識が元々必要になります。実務で使っているからこそ、クラウドに関わる頻度が他の人と比べて非常に多いです。その為、資格を取得したらすぐにクラウドに関わる業務ができるようになるとは言えません。 今まで全然使っていなかったのに、資格を取ったらすぐ使えるようになることはあんまりないです。資格は言わば、何らかのクーポンのような物です。1万円以上に限って10%割引をしてくれるガソリンスタンドのクーポンができたとしても、車がそもそもないから無用、クーポンの店が遠いから行けない、10%割引額を達成するお金がない、クーポンは色んな理由で使えきれないケースがいっぱいです。こう見たらクーポンを使うための条件は明確です。 自分・知り合いが車を持っていたり、車を持つ計画がある。 クーポンを使えるガソリンスタンドとの距離が近い。 クーポンを使える余力がある。 自分自身にそのクーポンが欲しい理由が整えているかを確認してください。 言い換えますと、「資格を活かせるように動けますか?」という意味です。クーポンを手に入れたとして、車が自動で生成されたり、ガソリンスタンドが勝手に家の前にできたり、クーポンを使えるお金が急に現れたり、そんな出来事は現実では起こらないですよね?クラウドもAWSも同じです。クラウドが己の業務と当てはまらない方々、例えば ITと関係ないビジネス系 インフラは専門ではない開発系 オンプレに特化したインフラ系 が挙げられます。 車は高いから買えない人には何を推奨しますか?カーリース・サブスクは月額払いだけできれば車を使えることができます。これがITではクラウドです。私は「技術を借りる」がクラウドの本質だと思ってます。技術を教わることが高かったら借りればいい話です。領域によって詳しくは違うかも知れませんが、知っている事だけで技術の視界が覆ると確信してます。 ガソリンスタンドが遠くて行けないって言ったら、それはそれで十分です。無理矢理に行くことまではないです。ただし、通勤途中で寄り道に行けそうな距離だったらどうしますか?開発系の方にとって、クラウドはそんなに遠い所にある訳ではありません。むしろ、視線を少し横に移すだけで活用できることが溢れるているかもです。 最後に、どれだけお得になるクーポンを持っていても、使わずにいたらない事と同然です。車をすでに持っていて、ガソリンスタンドがすぐ前に出来ても、いつも通っていたガソリンスタンドだけ利用してたらクーポンは使えません。1万円を前払いできないとか、他社のガソリンカードがあるとか、検証できない店は行きたくないとか、それぞれ抱えた理由は千差万別だと思ってます。しかし、否定できない事実はインフラ系の方々は、他の誰よりもクラウドを始めることに特化していることです。オンプレだけやってたら、クラウドはIaaS(Infra as a Service)とかPaaS(Platform as a Service)として提供していて馴染みがないと思われます。それでも、基本構造はインフラの知識の上で作られているから、ビジネス系と開発系に比べれば極めて簡単です。「クラウドまでやれる余力がない」ではなく、「クラウドまでやる余力を作ってみよう」はどうですか? 私もキャリアを開発系でスタートしていましたが、個人的に勉強したクラウドの知識があってクラウド業務も任されました。その後は資格を取りながらクラウド職に転向もできました。多分、私がやろうとしてる事とやっている仕事に限って集中していたらできなかったと思います。AWS認定12冠の達成もチャンスを増やす感覚であります。資格取得で得た知識のうち、KTCに合流してから実践で使える知識は50%前後程度です。しかし、使ってない50%の知識もこれから活かせるように頑張っています。KTCはAIファーストを今年の目標に決めているので、私もAI活動を盛り上げて行くつもりです。 AIファーストと繋ぐKTCの今年目標に興味がある方は、弊社副社長の景山が記事を掲載しているのでぜひ読んでみてください。 https://blog.kinto-technologies.com/posts/2024-12-25-LookBack2024/ 公式から推薦する AWS認定パス もありますので、ここもご参考お願いします! 4.strategy-(攻略法) 勉強法は私以外にもたくさんの方が推薦してくれていると思いますので、観点を変えてAWS認定を効率よく攻略する戦法をテーマにお話しします。 正攻法 真面目に勉強する方法は極簡単です。前述してた AIF試験ガイド で出ている内容をゼロから習得することです。ここはベース知識がなくて誠実に勉強して行きたい方や、受験日程に焦らずにやりたい方にお勧めします。 全部5段階でやってます。 情報収集:検索、SNS、YouTube、Blogなどを参考して己が好むソースを探す。 ソース決定:下記のソースの中で、最も自分に適することを決めます。 AWS公式 AWS社で提供してくれるドキュメントは最新反映されてて、信用度も高いし、内容も上品です。私も他のやり方で勉強しても公式はいつも参照します。一部無料のAWS Traning Centerの活用もすごく助けになりますので、ぜひ活用してください。有料の物は使っていませんが、下で紹介する外部学習サイトと似てる効果だと感じます。 YouTube 無料情報の量は一番多いですが、アップローダーさんによって質も量もバラバラですし、最新化も望めない短所があります。でも、映像や音声で学習できることと言語の縛りさえなければ短所も薄くなります。聞いてみていつでもお気軽にやめる事もできることが嬉しいです。 書籍 アナログ勉強法が好きだったら推しです。ターゲットを絞って情報の質を保証することが本の魅力です。買う前にある程度内容把握もできるし、一冊で求める内容が集約されていることが長所です。しかし、情報の最新化には弱いことは変化が早いAWS認定には向いてないことを注意してください。即時、勉強に入って認定の更新前に試験を受ける事じゃなかったら避けた方がいいです。 外部学習サイト Udemyみたいに有料で提供するソースを記述します。お金を払う分、YouTubeを超える質を持ちながら最新化も早いです。端的に言うと、YouTubeと書籍の長所を合体させた感じなので、個人的に愛用しています。気を付ける所はAWS認定の場合、資格同士に被る内容が結構ありますので目次をよく見て購入してください。 学習開始:試験によって学習時間は違いますが、2-3ヶ月を想定して勉強することが最適でした。 検証:AWS公式とかでやってる模擬試験で自己評価 試験:日程を予め取って受験します。オンラインでもオフラインでも受験できますが、一貫的な試験環境のためにもオフラインを推奨します。オフラインでの試験で何かの問題が起こったら、試験主催側が調整してくれるので安心ですし、試験結果もオフライン試験の方が早く届くのでお得です。 速攻法 逆に「私はAWS知っているから、初めから勉強するまでもない。知らない所だけ勉強したい!」と考えてる方や、「資格を早く取って勉強してもいいから、最速で取りたい!」と思ってる方もいます。私も幾つかの試験はそうだったので、コスパ最高の攻略法を話します。 :::message 生成AI:私はPerplexityを使ってましたが、どんな生成AIでも大丈夫です!そして、紹介するプロンプトは記事用の例であって、個人が向いてるプロンプトやり方でしてもOKです。 ::: キーワード整理 サービスの内容を要約してピンポイントだけ知ることができます。 :::details 回答 以下に、AWSの監視サービスであるCloudWatchを中心に、関連するサービス(CloudTrail、Config、WAF)との比較を整理します。また、これらに含まれない他の監視ツールも簡単に触れます。 CloudWatchの整理 概要 Amazon CloudWatchは、AWSリソースやアプリケーションをリアルタイムで監視するためのサービスです。主に次の機能を提供します: メトリクス収集:CPU使用率、ディスクI/O、ネットワークトラフィックなどのリソースデータを収集。 ログ監視:アプリケーションログやシステムログを収集し、分析可能。 アラーム設定:特定の条件(しきい値)に基づいて通知や自動アクションを実行。 イベント管理:特定のイベントをトリガーとして処理を自動化(例:インスタンス起動時の通知)。 主なユースケース リソースのパフォーマンス監視 異常検知とアラート通知 システム全体の稼働状況の可視化 CloudWatchと関連サービスの比較 サービス名 主な目的 特徴 主なユースケース CloudWatch メトリクス&ログ監視 AWSリソースやアプリケーションの稼働状況をリアルタイムで監視可能。 ほぼリアルタイムで通知。 リソース監視、パフォーマンス最適化、異常検知 CloudTrail 操作履歴の記録 誰がいつ何をしたか(APIコール)の履歴を記録。 コンプライアンスやセキュリティ監査向け。 セキュリティ監査、不正操作検出 AWS Config 設定変更の追跡&評価 リソース構成変更を記録し、ルール違反を検出可能。 設定状態をスナップショットで保存。 設定管理、コンプライアンス遵守、トラブルシューティング AWS WAF Webアプリケーション層のセキュリティ対策 SQLインジェクションやDDoS攻撃などからWebアプリケーションを保護。 CloudFrontと連携可能。 セキュリティ強化、攻撃遮断 比較ポイント 目的と対象範囲: CloudWatchはシステム全体の稼働状況を監視。 CloudTrailは操作履歴に特化。 Configは設定変更とルール評価。 WAFはセキュリティ対策専用。 リアルタイム性: CloudWatchはほぼリアルタイムで通知可能。 CloudTrailは遅延が発生する場合あり。 ユースケース重複: udTrailやConfigで記録したデータをCloudWatch Logsに送信し、統合的に管理することも可能。 他のAWS外部監視ツールとの比較 AWS以外でも利用可能な監視ツールには以下があります: ツール名 特徴 メリット デメリット Zabbix オープンソース。 高度なカスタマイズ性 無料で利用可能。 幅広い環境に対応 導入・運用が複雑 Mackerel SaaS型日本製ツール 簡単導入。 複数環境一元管理が可能 AWS専用機能が少ない Site24x7 マルチクラウド対応 ハイブリッドクラウド環境にも対応 AWS専用ツールより細かい設定が難しい 選択基準 AWS専用ならCloudWatchが最適。 マルチクラウド環境ではSite24x7やMackerelが有効。 高度なカスタマイズが必要ならZabbix。 試験準備に役立つポイント 各サービスの役割とユースケースを明確に理解する。 CloudWatchと他サービス(CloudTrail, Config, WAF)の連携例を把握する。 コスト面(従量課金制)やリアルタイム性なども試験問題に出る可能性あり。 必要なら追加質問してください! ::: 試験問題生成 キーワードの概念を分かったら、簡単な問題を生成して自己評価します。 :::details 回答 問題 1: あなたは、EC2インスタンスのCPU使用率が特定のしきい値を超えた場合に通知を受け取りたいと考えています。この目的を達成するためにどのAWSサービスを使用するべきですか? 選択肢: • A. AWS CloudTrail • B. Amazon CloudWatch • C. AWS Config • D. AWS WAF 問題 2: セキュリティチームが、過去3か月間に発生したAWSリソースへのAPIコールを追跡し、特定のユーザーアクティビティを調査したいと考えています。この目的に最適なサービスはどれですか? 選択肢: • A. Amazon CloudWatch • B. AWS CloudTrail • C. AWS Config • D. Amazon Athena 問題 3: ある企業が、AWSリソースの設定変更を追跡し、コンプライアンス違反を検出したいと考えています。この目的に最適なサービスはどれですか? 選択肢: • A. AWS CloudTrail • B. Amazon CloudWatch Logs • C. AWS Config • D. AWS WAF 問題 4:あなたは、WebアプリケーションをSQLインジェクションやクロスサイトスクリプティング(XSS)攻撃から保護したいと考えています。この目的に最適なAWSサービスはどれですか? 選択肢: • A. Amazon CloudWatch Logs • B. AWS WAF • C. AWS Shield • D. AWS Config ::: 速攻法は私が主に使ってる使用法二つだけ説明できなかったんですけど、使用者によって無限にカスタマイズできる生成AI学習法です。紹介した方法以外にも、細かい質問を数十個を投げてリマインドすることも結構やっています。生成AIが参考できる資料をInputに入れたりしたら、もっと豊かなキーワード整理や試験問題を生成してくれます。 私もAIFとかMLAは速攻方を70%割合で使って、1-2周くらいで取ったんで効果は保証します! (1st-art).最初から始まる美学 AWS認定12冠達成して、私が感じた色々を呟いてみました。 気づいてるか知れませんが、実はタイトルにも本文にもトリックを入れました。最初に戻ってみたらすぐ分かると思います。 1.start-(概要) → (1st-art).最初から始まる美学 こんな面倒臭い仕業を入れた理由は、私の1年4ヶ月の12冠の挑戦はスタート(アート)したから得られた一つの絵だと話かったです。どんな結果物が誕生するか、絵を描く時には知らないと思います。小学生の頃、「私の未来を描いて下さい」って聞いて、私が描いた未来は消防士でした。そして、中学生の頃は小説家でした。現在はクラウドエンジニアで全然違う仕事をしています。 そうだとして、私の幼い頃の絵が意味がなかったことでしょうか。私はその絵を描きながら、自分の夢に向き合ったことに意味があったと信じます。私は今、「AWS認定12冠」と言う絵を完成しました。次も新しい絵を描いて行くつもりです。ここtechblogに書いたこの記事も一つの絵になりますし、KTCでの仕事も別の絵になれると思ってます。 記事を読んでいただきありがとうございました!
アバター
This article is the entry for day 18 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Nice to meet you, I’m Tsuyoshi Yamada, an Android engineer at KINTO Technologies. In this article, we’ll share some handy techniques to help you take the first step in migrating an app built with Android View to a Jetpack Compose -based UI or gradually integrating Jetpack Compose into an existing Android View-based UI. 1. Introduction Development of Jetpack Compose , Android’s Declarative UI , was announced a few years ago, and version 1.0 was officially released in July 2021. Many Android developers quickly embraced the concept of Jetpack Compose, harnessing the flexibility and extensibility of Kotlin , which had already become the second officially supported language for Android development after Java. It seems that the adoption of Jetpack Compose in Android app development has been gradually increasing. With the introduction of declarative UI, developers can write UI more intuitively with less code compared to traditional view-based UI, leading to improved development efficiency and productivity. With the recent release of Compose Multiplatform , the scope of Jetpack Compose skills has expanded beyond Android. Moving forward, we might see more libraries being developed exclusively for Jetpack Compose. To bring these benefits to our existing apps, we have started migrating some of our development projects from Android View-based UIs to Jetpack Compose-based UIs. However, traditional view-based UIs rely heavily on procedural elements, making it challenging to adopt Jetpack Compose’s declarative style seamlessly. This article introduces some practical techniques to compensate for elements that may have been lost or become less visible due to Jetpack Compose’s simplified approach. In particular, we’ll cover tracking Composable locations, checking debug information, and ensuring smooth interoperability between Views and Composables—all with the goal of making the transition from a View-based UI to a Jetpack Compose-based UI as seamless as possible. 2. Aligning and Debugging Composables Replacing the existing View expressions with new Composables came with a big challenge—not just figuring out how to represent the same content, but also whether it could be positioned correctly. Even for someone familiar with Android Views, that was a major concern. To address this, we’ll first go over how to align a Composable’s position with a View’s and how to retrieve and check debugging information to assist in this process. 2.1. Checking the position of Composable and View The View , available since Android API level 1, is built on Java’s object-oriented principles. It not only represents rectangular screen elements but also effectively handles containment relationships between Views, interactions, and extending functionality through subclasses. For example, you can log the positions of a single View or multiple Views inside a ViewGroup using the following code: private fun Resources.findResourceName(@IdRes resId: Int): String = try { getResourceName(resId) } catch (e: Resources.NotFoundException) { "?" } fun View.logCoordinators(logger: (String) -> Unit, res: Resources, outLocation: IntArray, rect: Rect, prefix: String, density: Float) { getLocationInWindow(outLocation) rect.set(outLocation[0], outLocation[1], outLocation[0] + width, outLocation[1] + height) var log = "$prefix${this::class.simpleName}(${res.findResourceName(id)})${rect.toShortString()}(${rect.width().toFloat() / density}dp, ${rect.height().toFloat() / density}dp)" if (this is TextView) { log += "{${if (text.length <= 10) text else text.substring(0, 7) + "..."}}" } logger(log) if (this is ViewGroup) { val nextPrefix = "$prefix " repeat(childCount) { getChildAt(it).logCoordinators(logger, res, outLocation, rect, nextPrefix, density) } } } fun View.logCoordinators(logger: (String) -> Unit = { Log.d("ViewLogUtil", it) }) = logCoordinators(logger, resources, IntArray(2), Rect(), "", resources.displayMetrics.density) Here, View$getLocationInWindow(IntArray) is a function that retrieves the top-left coordinates of the Activity’s window where the View is located. If you’re familiar with Android Views, checking how this code works should be pretty straightforward. One thing to keep in mind is that calling these functions directly from Activity$onResume() , for example, won’t work as expected because the View layout isn’t fully set up yet, so you won’t be able to get meaningful information. In most cases, you’ll need to call them from a callback like View$addOnLayoutChangeListener(OnLayoutChangeListener) . A lot of developers might not be sure how to achieve the same thing in Jetpack Compose, and since it’s not easy to check if Composable behaves the same way, the migration process can feel like a hassle. In Jetpack Compose, you can use the Modifier extension function onGloballyPositioned((LayoutCoordinator) -> Unit) to get a Composable's position like this: @Composable fun BoundsInWindowExample() { Text( modifier = Modifier .padding(16.dp) .background(Color.White) .onGloballyPositioned { Log.d("ComposeLog", "Target boundsInWindow: ${it.boundsInWindow()}") }, text = "Hello, World!", style = TextStyle( color = MaterialTheme.colorScheme.onSecondary, fontSize = 24.sp ) ) } By passing a callback to onGloballyPositioned(...) , you can get the position coordinates every time the Composable’s position updates. In this case, LayoutCoordinator.boundsInWindow() is an extension function that retrieves the top, bottom, left, and right bounds of the Composable’s rectangle in the Activity coordinate system. Getting the positions of all Composables inside a single Composable at once seems tricky for now, but grabbing the position of an individual Composable is easy. Plus, in many cases, you can get position data without worrying about complex lifecycles like Activities. onGloballyPositioned(...) There’s also onPositioned(...) , a callback similar to onGloballyPositioned(...) , which gets called after the relative position within the parent Composable is determined. Besides boundsInWindow() , there are also boundsInRoot() and boundsInParent() , which you can use depending on the situation, but we’ll spare you the details for now. 2.2. Creating a Composable to Check Screen Display Now that we’ve figured out how to get the position in Composable using boundsInWindow() , which is compatible with View$getLocationInWindow(IntArray) , we can log the position changes during testing to check its behavior. By doing this while developing the Composable, we’ll gradually get used to Compose and be able to recreate something similar to View. This method is simple and effective, but since LogCat floods with text, it can be hard to read—especially when dealing with a lot of information. To make things easier, let’s try creating a separate Composable just for checking the screen display. If you create a debug display area in a part of your app just for testing purposes and keep it constantly updated, you won’t have to scramble to find key logs among the endless stream of LogCats. ...Of course, we’ve known this since the Android View days, but actually implementing it always felt like a hassle... I can almost hear the sighs. With Jetpack Compose, a declarative UI , creating such a debug area is much easier with minimal effort: class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK != Configuration.UI_MODE_NIGHT_YES setContentView(R.layout.activity_main) findViewById<WebView>(R.id.webView).let { webView -> webView.loadUrl("https://blog.kinto-technologies.com/") } val targetRect = mutableStateOf(Rect.Zero) // androidx.compose.ui.geometry.Rect findViewById<ComposeView>(R.id.composeTargetContainer).let { containerComposeView -> containerComposeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) containerComposeView.setContent { KtcAdventCalendar2024Theme { ScrollComposable(targetRect) } } } val visibleRect = mutableStateOf(Rect.Zero) val outLocation = IntArray(2) findViewById<View>(R.id.layoutMain).addOnLayoutChangeListener { v, left, top, right, bottom, _, _, _, _ -> v.getLocationInWindow(outLocation) visibleRect.value = Rect(outLocation[0].toFloat(), outLocation[1].toFloat(), outLocation[0].toFloat() + (right - left), outLocation[1].toFloat() + (bottom - top)) } findViewById<ComposeView>(R.id.composeTargetWatcher).let { watcherComposeView -> watcherComposeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) watcherComposeView.setContent { KtcAdventCalendar2024Theme { TargetWatcher(visibleRect.value, targetRect.value) } } } } } The app in the sample code is in the middle of being converted to Jetpack Compose. Right now, it’s still using a View with Activity$setContentView(Int) , and we’re introducing Composable within that View by using ComposeView . Here, mutableStateOf(...) is used to share the rectangular position information between View and Composable, allowing us to observe how it behaves on the screen. The screen layout looks like this: We’re working on making the HorizontalScrollView part composable. To help with that, the bottom part of the screen will be used to display debug information: ![Screen composition](/assets/blog/authors/tsuyoshi_yamada/advent-calendar_sample_screen-area.png =252x) The layout XML file for MainActivity as follows: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:layout_marginVertical="48dp"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/layoutMain" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <WebView android:id="@+id/webView" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginHorizontal="16dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/scrollView" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:ignore="NestedWeights" /> <HorizontalScrollView android:id="@+id/scrollView" android:layout_width="0dp" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/webView" app:layout_constraintTop_toTopOf="parent"> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeTargetContainer" android:layout_width="wrap_content" android:layout_height="match_parent" /> </HorizontalScrollView> </androidx.constraintlayout.widget.ConstraintLayout> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeTargetWatcher" android:layout_width="match_parent" android:layout_height="300dp" android:paddingTop="16dp" /> </LinearLayout> MainActivity.kt and activity_main.xml are in the middle of being converted to Jetpack Compose, so they currently contain a mix of Views and Composables. As of this writing, WebView and some other elements don’t yet have Composable equivalents, so for now, a hybrid approach is necessary. [^1] In activity_main.xml, ComposeView inside HorizontalScrollView is intended to replace an existing View with a Composable, while the other ComposeView is placed to track the position of the Composable above. The Composable that replaces the View is structured as follows and implements onGloballyPositioned(...) to check the position of the part labeled "Target". @Composable fun ScrollComposable(targetRect: MutableState<Rect>) { val textStyle = TextStyle( textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSecondary, fontSize = 24.sp, fontWeight = FontWeight.W600 ) Row( modifier = Modifier .fillMaxSize(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Red), contentAlignment = Alignment.Center ) { Text("1", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Magenta), contentAlignment = Alignment.Center ) { Text("2", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(MaterialTheme.colorScheme.primary) .onGloballyPositioned { targetRect.value = it.boundsInWindow() }, contentAlignment = Alignment.Center ) { Text("Target", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Cyan), contentAlignment = Alignment.Center ) { Text("4", style = textStyle) } } } Here's a Composable that keeps an eye on this: Here, the Composable itself uses onGloballyPositioned(...) to get its own size. @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { if (visibleRect.width <= 0f || visibleRect.height <= 0f) return val rootAspectRatio = visibleRect.width / visibleRect.height val density = LocalDensity.current // For calculating toDp() val targetColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.25F) var size by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .fillMaxSize() .onGloballyPositioned { coordinates -> size = coordinates.size } ) { if (size.width <= 0F || size.height <= 0F) return@Box val watchAspectRatio = size.width.toFloat() / size.height val (paddingH: Float, paddingV: Float) = if (rootAspectRatio < watchAspectRatio) { (size.width - size.height * rootAspectRatio) / 2 to 0F } else { 0F to (size.height - size.width / rootAspectRatio) / 2 } with(density) { Box( modifier = Modifier .padding(horizontal = paddingH.toDp(), vertical = paddingV.toDp()) .fillMaxSize() .background(Color.Gray) ) } if (targetRect.width <= 0f || targetRect.height <= 0f) return@Box with(density) { Box( modifier = Modifier .padding( // Caution: exception is thrown if padding is negative start = max( 0F, marginOf( size.width, paddingH, visibleRect.left, visibleRect.right, targetRect.left ) ).toDp(), end = max( 0F, size.width - marginOf( size.width, paddingH, visibleRect.left, visibleRect.right, targetRect.right ) ).toDp(), top = max( 0F, marginOf( size.height, paddingV, visibleRect.top, visibleRect.bottom, targetRect.top ) ).toDp(), bottom = max( 0F, size.height - marginOf( size.height, paddingV, visibleRect.top, visibleRect.bottom, targetRect.bottom ) ).toDp() ) .fillMaxSize() .background(targetColor) ) } } } private fun marginOf( sizePx: Int, halfPx: Float, visibleFrom: Float, visibleTo: Float, px: Float ): Float { val alpha = (px - visibleFrom) / (visibleTo - visibleFrom) return (1 - alpha) * halfPx + alpha * (sizePx - halfPx) } This Composable function illustrates the relationship between the Activity screen and the Composable labeled as "Target". Setting the padding is a bit tricky, but it’s not rocket science. Composable is technically a function, but it maintains UI state by continuously holding values with MutableState and remember . At the same time, automatically recompose when persistent information changes, reducing the need for tedious event handling and allowing for a more declarative coding style. When you scroll the HorizontalScrollView in the upper right corner of the screen left and right, the Composable at the bottom follows along and updates its position. By retrieving the position of a View or Composable inside a ScrollView using View$getLocationInWindow(IntArray) or LayoutCoordinator.boundsInWindow() , you can also get off-screen coordinates. This allows you to check whether elements can move properly in and out of the visible screen.   (However, this does not necessarily apply when scrolling using Modifier.horizontalScroll(...) , Modifier.verticalScroll(...) , or similar methods in a Composable.) In a conventional View, displaying this kind of debug information required modifying both the layout XML file and the Java/Kotlin code. Since these changes wouldn’t be reflected in the released app, it felt like a bit of a hassle. Data binding was also introduced, allowing layout XML files to track variable updates, but it wasn’t the most intuitive approach. Jetpack Compose lets you to design screens as if you were writing information directly into the design, making it easy to see your implementation results almost instantly. Displaying everything graphically isn’t always the best approach, but having more options for expression can make it easier to dive into coding. [^1]: A wrapper that uses WebView functionality as a Composable is available in the accompanist library, but it is currently deprecated . Even in the official implementation, making a View fully Composable doesn’t seem easy. Since it’s common for Views and Composables to coexist for a long time in general app development, there’s no need to worry—go ahead and embrace Composables with confidence. 2.3. Writing Debug Information Declaratively Displaying debug information with Composables allows you to add details even more easily with fewer steps: @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { // ... // ... Text( text = when { targetRect in visibleRect -> "Target: inside screen" visibleRect.overlaps(targetRect) -> "Target: crossing edge of screen" else -> "Target: outside screen" }, modifier = Modifier.align(Alignment.TopStart), style = TextStyle( textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurface, fontSize = 24.sp, fontWeight = FontWeight.W600 ) ) } } /** * "operator fun receiver.contains" defines in and !in (syntax: other in(!in) receiver) */ private operator fun Rect.contains(other: Rect) = left <= other.left && top <= other.top && right >= other.right && bottom >= other.bottom The above uses Text(...) to add debug information. The style argument in Text(...) can be omitted if the default settings are sufficient. You can display information with almost the same amount of typing as Log.d(...) or println(...) . Unlike those methods, the information doesn’t disappear as you scroll. One of the advantages of Declarative UI is that it makes building a UI as effortless as "debugging with print statements". Build and run the app, then scroll horizontally in the top-right scroll view to see the app in action at the bottom of the screen, like this: Off-screen display Partially over screen borders On-screen display 3. Procedural Processing with LaunchedEffect So far, we’ve discussed the significance of Declarative UI , but when it comes to handling events triggered by state changes or adding animations to show those changes, some Procedural processing is also necessary. If you can’t write procedural code like event handling, which has traditionally been done in View, moving to Jetpack Compose won't be possible. In Jetpack Compose, LaunchedEffect is commonly used to handle actions based on state changes. @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { // ... // ... var currentState by remember { mutableStateOf(TargetState.INSIDE) } var nextState by remember { mutableStateOf(TargetState.INSIDE) } var nextState by remember { mutableStateOf(TargetState.INSIDE) } var stateText by remember { mutableStateOf("") } var isTextVisible by remember { mutableStateOf(true) } nextState = when { visibleRect.overlaps(targetRect) -> TargetState.CROSSING else -> TargetState.OUTSIDE } LaunchedEffect(key1 = nextState) { if (stateText.isNotEmpty()) { if (currentState == nextState) return@LaunchedEffect stateText = when (nextState) { TargetState.INSIDE -> "Target: entered screen" TargetState.OUTSIDE -> "Target: exited screen" TargetState.CROSSING -> if (currentState == TargetState.INSIDE) "Target: exiting screen" else "Target: entering screen" } currentState = nextState repeat(3) { isTextVisible = true delay(250) isTextVisible = false delay(250) } } stateText = when (nextState) { TargetState.INSIDE -> "Target: inside screen" TargetState.CROSSING -> "Target: crossing edge of screen" TargetState.OUTSIDE -> "Target: outside screen" } isTextVisible = true } if (isTextVisible) { Text( text = stateText, modifier = Modifier.align(Alignment.TopStart), ) } } } enum class TargetState { INSIDE, CROSSING, OUTSIDE } You can set multiple keys for LaunchedEffect . If you only want to run the process the first time the Composable is called, you can use LaunchedEffect(Unit) { ... } . Whenever the nextState specified as a key changes, the corresponding process will be executed accordingly. The code above will make the text flash for 1.5 seconds after a state change, showing the current state compared to the previous one, and then display it statically. You can handle events by specifying a state variable as the key of LaunchedEffect and writing the processing inside the block to run when the state changes. Inside the LaunchedEffect block, you can write time-consuming processes using suspendfunctions like delay(...) . If a state change happens before the block’s processing finishes, the current processing is canceled, and the new state’s processing starts from the beginning in response to the change. The LaunchedEffect block handles procedural processing, while the Text(...) follows a Declarative approach based on the values of the variables provided procedurally. For handling changes in the UI, it’s best to use LaunchedEffect . Besides, there are other effects suited for different situations, such as SideEffect , as well as DisposableEffect , which handles processing according to the lifecycle of Activity and Fragment. On the other hand, it’s also important not to overuse these processes to keep the code from becoming unnecessarily complex. For example, it’s recommended to handle event processing triggered by responses from the Internet or inputs from sensors like NFC in the ViewModel, while keeping procedural code in Composable limited to UI-related elements. 4. Interoperability between ComposeView and AndroidView When using a Composable in an Activity or Fragment, you can display it with ComposeView , as shown above. Conversely, if you want to use something like the aforementioned WebView inside a Composable, you can embed a View into a Composable using AndroidView or AndroidViewBinding . For instructions, please refer to Here (Using views in Compose) . This article won’t go into details, but thanks to the AndroidView Composable, even if you’ve made progress in converting your app to Composable but find that replacing certain Views is challenging or time-consuming, you can still continue development by integrating both Compose and View. This interoperability is extremely powerful—you can call AndroidView inside a Composable invoked by ComposeView , then nest another ComposeView inside it to call a Composable again, and even embed AndroidView within that, creating a layered structure that can continue further. By keeping the option to use Views within Composables, you can minimize the risk of wasted effort if the transition to Composable doesn’t progress as expected or if replacing Views takes significantly longer than a development sprint. 5. Preview Function So far, we’ve focused on the flexibility of Composable, which allows you to reflect on the development process. It supports techniques for alignment and debugging at a level comparable to Views, enables procedural processing, and offers the option to revert parts to Views through powerful interoperability when facing challenges in the transition to Composable. Here, we’ll talk about one of the key benefits of Composable—the simple yet powerful preview capabilities of the Preview function. 5.1. Create a Preview function Android Studio’s preview feature allowed you to visualize Views in layout XML files, but Jetpack Compose takes it a step further with even more powerful preview functions. Just create a function with the @Preview annotation, and your Composable will be displayed in the preview: // ... private class TargetWatcherParameterProvider : PreviewParameterProvider<TargetWatcherParameterProvider.TargetWatcherParameter> { class TargetWatcherParameter( val visibleRect: Rect, val targetRect: Rect ) override val values: Sequence<TargetWatcherParameter> = sequenceOf( TargetWatcherParameter( visibleRect = Rect(0f, 0f, 100f, 300f), targetRect = Rect(90f, 80f, 110f, 120f) ), TargetWatcherParameter( visibleRect = Rect(0f, 0f, 300f, 100f), targetRect = Rect(80f, 90f, 120f, 110f) ) ) } @Preview @Composable private fun PreviewTargetWatcher( @PreviewParameter(TargetWatcherParameterProvider::class) params: TargetWatcherParameterProvider.TargetWatcherParameter ) { KtcAdventCalendar2024Theme { TargetWatcher(params.visibleRect, params.targetRect) } } The above example demonstrates how to use PreviewParameterProvider to supply multiple parameters to a single preview function and display them in the preview. In the layout XML file of the Android View, you can’t achieve this using the tools:??? attribute to configure the preview. However, can display a preview without using PreviewParameterProvider —just call a Composable function inside a function annotated with @Preview and @Composable . My recommendation is to create a preview function as soon as you start working on a new Composable. Just the advantage of having powerful preview capabilities from the start when creating a new Composable is reason enough to switch to Jetpack Compose. Being able to check the display of debug information with the preview function is another advantage of using Composable for debugging. Recently, using preview functions for UI testing with libraries like roborazzi has been gaining attention. From a testing efficiency standpoint, creating preview functions is definitely worthwhile. 5.2. Try running the Preview function As explained here (Running the Preview) , you can run the Preview function on an actual Android device or emulator by clicking the Run '...' icon on the left side of Android Studio. This is similar to the previous functionality for executing a specific Activity, but the Preview function is more powerful. It is easier to write and it can run in a simplified environment without needing elements like intents. Callbacks for UI actions, such as button taps, are also executed, allowing you to use the Preview function for testing as if it were a simple app. However, it’s not recommended to overload a Composable with too many features just to make it more testable in the Preview function. To keep Composable purely as a declarative UI, it’s best to separate business logic into other classes or functions, such as a ViewModel or a Presenter in Circuit, and focus on writing only UI-related code. 6. Conclusion I hope this article will encourage more developers to take the leap from Android View to Jetpack Compose. Transitioning to a UI system with a different approach isn’t always straightforward, and the fear of setbacks is understandable. My hope is that your first steps feel as smooth as possible and that the risk of wasted effort is kept to a minimum. 7. References Android API reference [Android] Functions for Retrieving a View’s Position In-Depth Guide to Jetpack Compose Modifier What can Advanced / Lesser Known Modifiers do for your UI? — A Comprehensive Exploration in Jetpack Compose: A Journey Through Advanced and Notes The Android robot is reproduced or modified from work created and shared by Google and used under the terms of the Creative Commons Attribution 3.0 License. ^2
アバター
1.start-(概要) 初めまして! KINTOテクノロジーズのCloud Infrastructure GでInfrastructure Architectを担当している劉(YOU)です。 今年の1月に入社して、techblogには初執筆なのでこれからよろしくお願いします! AWSの認定は23年10月のSAAを始じめ、25年2月のMLAを最後に、1年4ヶ月でやっとAWS認定12冠を取りました。せっかくなので、AWS認定12冠を達成しながら感じた個人的な意見と情報を共有します。 先に話しますと、AWS認定12冠とは、AWS社が主催する資格認定を全て取得したことを指します。その基準は毎年変更されるんですけど、 https://aws.amazon.com/jp/blogs/psa/2024-japan-aws-all-certifications-engineers/ AWS JAPAN APN ブログ からその詳細を前もって発表して、選出された方々を表彰します。2024年は1,222名が正式に「AWS All Certifications Engineers」として認められています。公式記事では、 ”AWS 認定資格を全て取得し・維持する事” は AWS のテクノロジーを正しく理解し、お客様に信頼性の高い最新の技術をご案内できる基盤をお持ちだという証になります。 と述べています。 AWS以外でも、AzureとかGCPとかクラウドサービスを提供している会社は多いですが、クラウドサービスの量と質、高い占有率からできる汎用性、圧倒的なアップデートスピード、どう考えてもAWSは クラウド業界の標準だと言い切れます。 そして、最近AIの注目が高くなることと伴い、クラウドの重要性も上がっています。「クラウドとかAIとか、私と関係ないだろう」と思う方がいらっしゃるかも知れませんが、現時点でほぼ全ての業務にパソコンを使うようになった事と同じく、もうじきAIを日常で使うことになる時代がやって来ると思います。 AIその物になるモデルとモデルを動かすためのコンピュータ、それが簡単に提供されるプラットフォームがクラウドであり、時代を追いつくためにもクラウドを習得する必要があると言うことです。それで、AWS及びクラウドを勉強するために資格はなぜ必要なのか? その答えを次から説明して行きます。 2.status-(現状) 残念ながら、資格はあってもなくてもクラウド活用に大きい影響を与えません。例を挙げると、今からクラウドを英語だと見なして考えてください。英語を活用するためにTOEICを準備して、高得点をしました。それが本当の意味で英語を上達することだと思いますか? どれだけ試験のテクニックが良くても、単語と文法などを暗記しても、実際に英語を要求される時に使いこなせなかったら無駄です。 しかし、TOEICが英語の実力向上に役立たないって言ったらそれは違うと断言できます。意味が無かったら数々の大学・企業からTOEICの点数を基準として評価する訳がないです。ビジネス英語力を点数に換算する試験がTOEICだからこそ、ただの点数ではなくて能力として認められてることになります。 そう言う意味で、AWS認定12冠はクラウドに対して分かりやすい天井です。 実体化されていない知識の塊を資格という形で見えるようにします。こう言う見える化によって得られる効果を整理しますと、 明確な目標設定(Goal):AWS社が証明してくれるロードマップを従った認定なので、階段式でスケジューリング可能 努力の源(Motivation):受験日を決めて勉強することで、頑張れる環境が作られる 知識の保証(Knowledge):資格を取るための最低限の知識が保証される 振り返り(Remind):そもそもクラウドに詳しい人であっても、資格で要求される知識を点検することができる キャッチアップ(Discovery):試験はアップデートを沿って変わっていくので、触れる機会がない情報を勉強することができる になります。 逆に英語で置換して見ても、違和感なく受け入れられる内容ではないでしょうか?結局、資格を取ったらクラウド力が上がる、クラウドやりたいから資格取ろうではなく、鍛えること、それ自体に意味があると思います。 AWS認定のこれから 続いて、私が一年ちょっと超える時間をAWS認定準備しながら感じた「これからAWS認定はどうなる?」を突っ込んで行きます。 :::message あくまでも、 根拠のない自分勝手の推測なので、AWSで公式の何かがあることではありません。 引用の時にはご注意お願いします。 ::: 私がAWS認定の準備し始めた時期は、22年ChatGPTが流行ってAIに対する関心が大きくなってる状況でした。AWSもAIを中心にするサービスをどんどん出して、24年から資格の構成を大きく変更させました。 既存のスペシャル資格三つを24年4月で削除し、 AWS Certified Data Analytics – Specialty (DAS) AWS Certified Database – Specialty (DBS) AWS Certified: SAP on AWS – Specialty (PAS) DASとDBSを代行するために資格が24年3月に登場、 AWS Certified Data Engineer – Associate (DEA) その後、Amazon QとかAmazon Bedrockなど新しく出たAIサービスと、Amazon Sagemaker周りの強化されたAIサービスのロードマップを提示するために24年10月、 AWS Certified AI Practitioner (AIF) AWS Certified Machine Learning Engineer – Associate (MLA) 結構、大変革だったので試験を準備している個人の立場でもちょっと困りました。勉強してた内容が大幅に変更されることと同然だったので、先に計画してた試験日程も変えるしか無かったです。これからも、今後の技術トレンドであるAIを中心に変わっていくことは確かです。 あくまでも推測で過ぎませんが、変更が起きる可能性が高い資格は AWS Certified Machine Learning Engineer – Specialty (MLS) だと思います。 MLSが最後にアップデートされたのは22年7月のなのでAIF, MLAに比べたら内容が古くなっています。現状のSpecialtyのままアップデートされることもありますが、新しいprofessional資格に改編される可能性が高いです。 その理由としては、既存のパスがPractitioner→Associate→Professionalに繋がる仕組みだからです。 ^1 同様にAIF → MLA資格の次に来るProfessional資格が必要になります。単純にスペシャリティがプロフェッショナルになって最新化される…ことはAWS側が決める所ですが、そうなったらDEAの上位資格も想定するしかないです。 (仮)AWS Certified Machine Learning Engineer – Professional (MLP) (仮)AWS Certified Data Engineer – Professional (DEP) これが普通に考えられる予測ですが、これはこれで問題が生じます。AWS社の認定は12冠を象徴として維持してるそうなので、二つが増えれば資格の数が13個を超えてしまうことです。 それを回避する方法があって、まずは上記の資格が増やされる分、曖昧になったSpecialtyを減らすことです。 (仮)AWS Certified Security – Specialty (SCS) AWS Certified Advanced Networking – Specialty (ANS) SCSとANSはもう消えている他のスペシャリティとは違って、プロフェッショナルから携わった知識を深く振り込む内容で構成された認定です。両方とも出る内容が60%以上がプロフェッショナルと重複しつつ、SCSは組織全体のセキュリティを重視し、ANSはオンプレミスとのネットワーキングが主になっています。その中、現状のままではやむを得ない欠陥があります。 SCSはAIのアップデートされなかったので、AIに対するセキュリティの内容が含まれていません。AIの発展が早くなっていながら、AIに対するセキュリティーやコンプライアンスも大事にされてるので、SCSにAIの内容が入れるか、各プロフェッショナルに溶かすかの問題です。すでにAIのトレンドに合わせて統廃合されたスペシャリティ認定が多いので後者がなり得ると思ってます。 ANSの場合、SCSと似ているポジションですが、ネットワークはAIができてもAWS内部に限ってはそんなに差がありません。OpenAIを使うにはAzureを、Geminiを使うにはGCPを、その他のクラウドベンダが運営しているAIを使うにはマルチクラウドが確かに必要になりますが、自社製品じゃないとAWS社が積極的に支援しないスタンスなので、マルチクラウドに関係する認定のアップデートは今の所はないです。代わりに、アンチクラウドの流れからハイブリッドクラウドが浮上していますので、ANS認定の仕組みは維持すると見込んでいます。 とにかく、資格の数を減らしたら12冠は担保できるのでこう言うやり方もあることと、二つのプロフェッショナルを増やさずにDevOps Engineerみたいに一つに納める事もあります。 (仮)AWS Certified MLOps Engineer – Professional (MOP) AWSで紹介している MLOps は、「ML アプリケーション開発 (Dev) と ML システムのデプロイおよび運用 (Ops) を統合する ML カルチャーとプラクティスです」だと記述してます。 ^2 これはMLに関する全体作業を意味してるので、まさにDEAで扱うデータエンジニアリング、データ分析を通して、AIF→MLA→MLSで使われる機械学習の全体を活用できますので、新しいプロフェッショナルがただ一つだけ必要だったらこれで通用できる仕方だと思ってます。 AWS認定の類型 そして、認定の種類だけではなく、試験の類型にも変化が起きています。 既存の試験はSOAのラボ試験が中止になってから、選択肢で正解を選ぶ形式のみが評価方式でした。客観的に定量評価ができる要素は長所ですが、実装とは関係が深くない知識になることも否定できません。AWS社もこれを意識しているかなと思いまして、AIFとMLAから新しい出題形式が出ました。 AIF試験ガイド を見ると、 並べ替え: 指定されたタスクを完了することを目的とした 3~5 つの答えのリストが提示される。設問に対する点数を得るには、正解を選択し、正しい順序に並べる必要がある。 内容一致: 3~7 つのプロンプトのリストと一致する答えのリストが提示される。設問に対する点数を得るは、すべてのペアを正しく一致させる必要がある。 ケーススタディ : 1 つのシナリオに、そのシナリオに関する設問が 2 つ以上含まれている。ケーススタディの各設問のシナリオは同じである。ケーススタディの各設問は個別に採点される。ケーススタディでは正解した設問ごとに点数が得られる。 上記の三つの類型は私の試験にも多い数ではありませんが、試験ガイドで述べている事と同じく出題されました。問題のレベルとしては選択肢の問題と同様で、AWS試験の秘密保証のため問題の詳しい形式は言えませんが、私が感じた出題形式の評価はこうなります。 並べ替えと内容一致は、選択肢の類似性から正解を類推することが出来なくなりました。本当に実施するべきの手順だったり、提示される単語や説明を結び付く、問題から要求する内容を熟知しないと解けない形です。 ケーススタディの基本は設問=選択肢なんですが、単一のケースで複数の問題を提示するやり方です。ここはケースを多角的に接近することもできるし、長問・多問からは知識の応用より読解力を求められる状況があるから、それが解消されます。我らの現実世界もケースがあったら一問一答で絶えず、ケースごとにシミュレーションすることが一般的なので、このケーススタディは受験者としていい類型だと思います。 AWS社は認定に関してこれからも出題形式もそうですし、SOAのラボみたいに「本当に実装できますか?」を目指して変化して行くと考えられます。こういう変更は単発で起きることではなく、連続的に他の認定にも反映されるので、AWS試験を準備していらっしやる方々はキャッチアップして備えて行きましょう! 3.stance-(心構え) 私も周りの人達と話す時に職種不問でよく出る話題なんですが、 「AWSとは関係ない仕事してるけど、これ勉強して本当に使える?」 「AWS資格取るとしたらどこから始まる?」 「何をネタに勉強してる?」 と質問を貰います。私はクラウドエンジニアとして資格を取っていまして、クラウドで業務をするための知識が元々必要になります。実務で使っているからこそ、クラウドに関わる頻度が他の人と比べて非常に多いです。その為、資格を取得したらすぐにクラウドに関わる業務ができるようになるとは言えません。 今まで全然使っていなかったのに、資格を取ったらすぐ使えるようになることはあんまりないです。資格は言わば、何らかのクーポンのような物です。1万円以上に限って10%割引をしてくれるガソリンスタンドのクーポンができたとしても、車がそもそもないから無用、クーポンの店が遠いから行けない、10%割引額を達成するお金がない、クーポンは色んな理由で使えきれないケースがいっぱいです。こう見たらクーポンを使うための条件は明確です。 自分・知り合いが車を持っていたり、車を持つ計画がある。 クーポンを使えるガソリンスタンドとの距離が近い。 クーポンを使える余力がある。 自分自身にそのクーポンが欲しい理由が整えているかを確認してください。 言い換えますと、「資格を活かせるように動けますか?」という意味です。クーポンを手に入れたとして、車が自動で生成されたり、ガソリンスタンドが勝手に家の前にできたり、クーポンを使えるお金が急に現れたり、そんな出来事は現実では起こらないですよね?クラウドもAWSも同じです。クラウドが己の業務と当てはまらない方々、例えば ITと関係ないビジネス系 インフラは専門ではない開発系 オンプレに特化したインフラ系 が挙げられます。 車は高いから買えない人には何を推奨しますか?カーリース・サブスクは月額払いだけできれば車を使えることができます。これがITではクラウドです。私は「技術を借りる」がクラウドの本質だと思ってます。技術を教わることが高かったら借りればいい話です。領域によって詳しくは違うかも知れませんが、知っている事だけで技術の視界が覆ると確信してます。 ガソリンスタンドが遠くて行けないって言ったら、それはそれで十分です。無理矢理に行くことまではないです。ただし、通勤途中で寄り道に行けそうな距離だったらどうしますか?開発系の方にとって、クラウドはそんなに遠い所にある訳ではありません。むしろ、視線を少し横に移すだけで活用できることが溢れるているかもです。 最後に、どれだけお得になるクーポンを持っていても、使わずにいたらない事と同然です。車をすでに持っていて、ガソリンスタンドがすぐ前に出来ても、いつも通っていたガソリンスタンドだけ利用してたらクーポンは使えません。1万円を前払いできないとか、他社のガソリンカードがあるとか、検証できない店は行きたくないとか、それぞれ抱えた理由は千差万別だと思ってます。しかし、否定できない事実はインフラ系の方々は、他の誰よりもクラウドを始めることに特化していることです。オンプレだけやってたら、クラウドはIaaS(Infra as a Service)とかPaaS(Platform as a Service)として提供していて馴染みがないと思われます。それでも、基本構造はインフラの知識の上で作られているから、ビジネス系と開発系に比べれば極めて簡単です。「クラウドまでやれる余力がない」ではなく、「クラウドまでやる余力を作ってみよう」はどうですか? 私もキャリアを開発系でスタートしていましたが、個人的に勉強したクラウドの知識があってクラウド業務も任されました。その後は資格を取りながらクラウド職に転向もできました。多分、私がやろうとしてる事とやっている仕事に限って集中していたらできなかったと思います。AWS認定12冠の達成もチャンスを増やす感覚であります。資格取得で得た知識のうち、KTCに合流してから実践で使える知識は50%前後程度です。しかし、使ってない50%の知識もこれから活かせるように頑張っています。KTCはAIファーストを今年の目標に決めているので、私もAI活動を盛り上げて行くつもりです。 AIファーストと繋ぐKTCの今年目標に興味がある方は、弊社副社長の景山が記事を掲載しているのでぜひ読んでみてください。 https://blog.kinto-technologies.com/posts/2024-12-25-LookBack2024/ 公式から推薦する AWS認定パス もありますので、ここもご参考お願いします! 4.strategy-(攻略法) 勉強法は私以外にもたくさんの方が推薦してくれていると思いますので、観点を変えてAWS認定を効率よく攻略する戦法をテーマにお話しします。 正攻法 真面目に勉強する方法は極簡単です。前述してた AIF試験ガイド で出ている内容をゼロから習得することです。ここはベース知識がなくて誠実に勉強して行きたい方や、受験日程に焦らずにやりたい方にお勧めします。 全部5段階でやってます。 情報収集:検索、SNS、YouTube、Blogなどを参考して己が好むソースを探す。 ソース決定:下記のソースの中で、最も自分に適することを決めます。 AWS公式 AWS社で提供してくれるドキュメントは最新反映されてて、信用度も高いし、内容も上品です。私も他のやり方で勉強しても公式はいつも参照します。一部無料のAWS Traning Centerの活用もすごく助けになりますので、ぜひ活用してください。有料の物は使っていませんが、下で紹介する外部学習サイトと似てる効果だと感じます。 YouTube 無料情報の量は一番多いですが、アップローダーさんによって質も量もバラバラですし、最新化も望めない短所があります。でも、映像や音声で学習できることと言語の縛りさえなければ短所も薄くなります。聞いてみていつでもお気軽にやめる事もできることが嬉しいです。 書籍 アナログ勉強法が好きだったら推しです。ターゲットを絞って情報の質を保証することが本の魅力です。買う前にある程度内容把握もできるし、一冊で求める内容が集約されていることが長所です。しかし、情報の最新化には弱いことは変化が早いAWS認定には向いてないことを注意してください。即時、勉強に入って認定の更新前に試験を受ける事じゃなかったら避けた方がいいです。 外部学習サイト Udemyみたいに有料で提供するソースを記述します。お金を払う分、YouTubeを超える質を持ちながら最新化も早いです。端的に言うと、YouTubeと書籍の長所を合体させた感じなので、個人的に愛用しています。気を付ける所はAWS認定の場合、資格同士に被る内容が結構ありますので目次をよく見て購入してください。 学習開始:試験によって学習時間は違いますが、2-3ヶ月を想定して勉強することが最適でした。 検証:AWS公式とかでやってる模擬試験で自己評価 試験:日程を予め取って受験します。オンラインでもオフラインでも受験できますが、一貫的な試験環境のためにもオフラインを推奨します。オフラインでの試験で何かの問題が起こったら、試験主催側が調整してくれるので安心ですし、試験結果もオフライン試験の方が早く届くのでお得です。 速攻法 逆に「私はAWS知っているから、初めから勉強するまでもない。知らない所だけ勉強したい!」と考えてる方や、「資格を早く取って勉強してもいいから、最速で取りたい!」と思ってる方もいます。私も幾つかの試験はそうだったので、コスパ最高の攻略法を話します。 :::message 生成AI:私はPerplexityを使ってましたが、どんな生成AIでも大丈夫です!そして、紹介するプロンプトは記事用の例であって、個人が向いてるプロンプトやり方でしてもOKです。 ::: キーワード整理 サービスの内容を要約してピンポイントだけ知ることができます。 :::details 回答 以下に、AWSの監視サービスであるCloudWatchを中心に、関連するサービス(CloudTrail、Config、WAF)との比較を整理します。また、これらに含まれない他の監視ツールも簡単に触れます。 CloudWatchの整理 概要 Amazon CloudWatchは、AWSリソースやアプリケーションをリアルタイムで監視するためのサービスです。主に次の機能を提供します: メトリクス収集:CPU使用率、ディスクI/O、ネットワークトラフィックなどのリソースデータを収集。 ログ監視:アプリケーションログやシステムログを収集し、分析可能。 アラーム設定:特定の条件(しきい値)に基づいて通知や自動アクションを実行。 イベント管理:特定のイベントをトリガーとして処理を自動化(例:インスタンス起動時の通知)。 主なユースケース リソースのパフォーマンス監視 異常検知とアラート通知 システム全体の稼働状況の可視化 CloudWatchと関連サービスの比較 サービス名 主な目的 特徴 主なユースケース CloudWatch メトリクス&ログ監視 AWSリソースやアプリケーションの稼働状況をリアルタイムで監視可能。 ほぼリアルタイムで通知。 リソース監視、パフォーマンス最適化、異常検知 CloudTrail 操作履歴の記録 誰がいつ何をしたか(APIコール)の履歴を記録。 コンプライアンスやセキュリティ監査向け。 セキュリティ監査、不正操作検出 AWS Config 設定変更の追跡&評価 リソース構成変更を記録し、ルール違反を検出可能。 設定状態をスナップショットで保存。 設定管理、コンプライアンス遵守、トラブルシューティング AWS WAF Webアプリケーション層のセキュリティ対策 SQLインジェクションやDDoS攻撃などからWebアプリケーションを保護。 CloudFrontと連携可能。 セキュリティ強化、攻撃遮断 比較ポイント 目的と対象範囲: CloudWatchはシステム全体の稼働状況を監視。 CloudTrailは操作履歴に特化。 Configは設定変更とルール評価。 WAFはセキュリティ対策専用。 リアルタイム性: CloudWatchはほぼリアルタイムで通知可能。 CloudTrailは遅延が発生する場合あり。 ユースケース重複: udTrailやConfigで記録したデータをCloudWatch Logsに送信し、統合的に管理することも可能。 他のAWS外部監視ツールとの比較 AWS以外でも利用可能な監視ツールには以下があります: ツール名 特徴 メリット デメリット Zabbix オープンソース。 高度なカスタマイズ性 無料で利用可能。 幅広い環境に対応 導入・運用が複雑 Mackerel SaaS型日本製ツール 簡単導入。 複数環境一元管理が可能 AWS専用機能が少ない Site24x7 マルチクラウド対応 ハイブリッドクラウド環境にも対応 AWS専用ツールより細かい設定が難しい 選択基準 AWS専用ならCloudWatchが最適。 マルチクラウド環境ではSite24x7やMackerelが有効。 高度なカスタマイズが必要ならZabbix。 試験準備に役立つポイント 各サービスの役割とユースケースを明確に理解する。 CloudWatchと他サービス(CloudTrail, Config, WAF)の連携例を把握する。 コスト面(従量課金制)やリアルタイム性なども試験問題に出る可能性あり。 必要なら追加質問してください! ::: 試験問題生成 キーワードの概念を分かったら、簡単な問題を生成して自己評価します。 :::details 回答 問題 1: あなたは、EC2インスタンスのCPU使用率が特定のしきい値を超えた場合に通知を受け取りたいと考えています。この目的を達成するためにどのAWSサービスを使用するべきですか? 選択肢: • A. AWS CloudTrail • B. Amazon CloudWatch • C. AWS Config • D. AWS WAF 問題 2: セキュリティチームが、過去3か月間に発生したAWSリソースへのAPIコールを追跡し、特定のユーザーアクティビティを調査したいと考えています。この目的に最適なサービスはどれですか? 選択肢: • A. Amazon CloudWatch • B. AWS CloudTrail • C. AWS Config • D. Amazon Athena 問題 3: ある企業が、AWSリソースの設定変更を追跡し、コンプライアンス違反を検出したいと考えています。この目的に最適なサービスはどれですか? 選択肢: • A. AWS CloudTrail • B. Amazon CloudWatch Logs • C. AWS Config • D. AWS WAF 問題 4:あなたは、WebアプリケーションをSQLインジェクションやクロスサイトスクリプティング(XSS)攻撃から保護したいと考えています。この目的に最適なAWSサービスはどれですか? 選択肢: • A. Amazon CloudWatch Logs • B. AWS WAF • C. AWS Shield • D. AWS Config ::: 速攻法は私が主に使ってる使用法二つだけ説明できなかったんですけど、使用者によって無限にカスタマイズできる生成AI学習法です。紹介した方法以外にも、細かい質問を数十個を投げてリマインドすることも結構やっています。生成AIが参考できる資料をInputに入れたりしたら、もっと豊かなキーワード整理や試験問題を生成してくれます。 私もAIFとかMLAは速攻方を70%割合で使って、1-2周くらいで取ったんで効果は保証します! (1st-art).最初から始まる美学 AWS認定12冠達成して、私が感じた色々を呟いてみました。 気づいてるか知れませんが、実はタイトルにも本文にもトリックを入れました。最初に戻ってみたらすぐ分かると思います。 1.start-(概要) → (1st-art).最初から始まる美学 こんな面倒臭い仕業を入れた理由は、私の1年4ヶ月の12冠の挑戦はスタート(アート)したから得られた一つの絵だと話かったです。どんな結果物が誕生するか、絵を描く時には知らないと思います。小学生の頃、「私の未来を描いて下さい」って聞いて、私が描いた未来は消防士でした。そして、中学生の頃は小説家でした。現在はクラウドエンジニアで全然違う仕事をしています。 そうだとして、私の幼い頃の絵が意味がなかったことでしょうか。私はその絵を描きながら、自分の夢に向き合ったことに意味があったと信じます。私は今、「AWS認定12冠」と言う絵を完成しました。次も新しい絵を描いて行くつもりです。ここtechblogに書いたこの記事も一つの絵になりますし、KTCでの仕事も別の絵になれると思ってます。 記事を読んでいただきありがとうございました!
アバター
This article is the entry for day 18 in the KINTO Technologies Advent Calendar 2024 🎅🎄 I’m Nakanishi from the Manabi-no-Michi-no-Eki (Learning Roadside Station) team. This year, the Learning Roadside Station Project was officially launched and later established as a team. As part of this initiative, we are also running an in-house podcast, and for this year’s Advent Calendar, we’re excited to share some of its episodes with you. What is the "Learning Roadside Station"? "Learning Roadside Station" is a project launched to enhance the accessibility and effectiveness of the study sessions that are frequently held within the company. This initiative aims to promote knowledge sharing by supporting study sessions led by dedicated volunteers within the company. Figma Study Session The KTC Learning Roadside Station Podcast features interviews with people who organize study sessions within the company. This segment is called “A Peek into the Study Session Next Door”. Today's podcast guests are T.M. and M.K., who are leading the Figma Study Session. First of all, could you tell us about the work you both do? Let's start with T.M.-san. Interview T.M.-san: I am a member of the Retailer Digital Transformation (DX) Planning Team of the DX Planning Promotion Group in the Mobility Product Development Division. As a designer, I primarily work on developing products related to digital transformation (DX) for our company's retail stores. Lately I’ve been focusing on designing a mobile app, which we plan to release in the fall. Since our team was formed only about a year ago, we are now entering the phase of fully commercializing the project. Akeda-san: Thank you. Now, let’s hear from M.K.-san. M.K.-san: I work as a designer in the Marketing Planning Division. My main responsibilities include visual design and UI/UX design, and recently, I have been working on the website design for a new vehicle subscription service. Akeda-san: Thank you. Now, regarding the Figma study session held last Friday, what was the reason for holding it? T.M.-san: It all started when Nakanishi-san casually dropped by and said, “Why don’t you host a Figma study session?” However, even before that, I had felt that there was room for improvement in how we were using Figma at KTC, so it was perfect timing. So, together with M.K., we discussed the best format, and held our first session last Friday. Akeda-san: Nakanishi-san, what made you reach out and suggest the study session? Nakanishi: Since T.M. joined the company, I felt that he had a strong desire to improve the design. We had already talked about how it would be great to hold a Figma study session, so I felt that now was the right time and reached out. Akeda-san: Thank you. What was the reaction of the participants when you actually held the study session? T.M.-san: The response was quite positive. It seemed like many of the participants wanted to learn more about Figma. There were a lot of questions during the Q&A session, and it was clear that everyone was interested. M.K.-san: I felt the same way. It was a great opportunity to see that many people are interested in Figma and improving UI/UX. Akeda-san: What kind of questions were asked? T.M.-san: The questions covered a wide range of topics, from technical aspects to discussions about UI/UX initiatives at KTC. There were especially many questions from engineers, which showed that they, too, have a strong interest in UX. Akeda-san: Please tell us about future plans for the study group. T.M.-san: In the short term, we aim to improve productivity and efficiency by providing the most relevant themes and content. In the long term, I want to use Figma as a way to increase the presence of designers and contribute to the success of businesses. M.K.-san: I completely agree. We also want to create more opportunities for engineers and designers to develop a common language and communicate with each other. Akeda-san: Thank you. Finally, do you have a message for everyone listening to this podcast? M.K.-san: Design is not just about visuals. It's important for everyone to have an opinion. Let’s communicate freely and openly. T.M.-san: To create a great product, everyone should think like a designer. Please feel free to reach out to us anytime! Akeda-san: Thank you very much. That wraps up today's podcast. T.M.-san and M.K.-san, thank you for joining us. Your passion for the Figma study session and design really came through. I’m excited to see how KTC's design continues to evolve. This time, we have provided details about the Figma study group, the background to its operation, and future prospects. Stay tuned for the next study session!
アバター
Introduction Hi, my name is Pann Nu Wai, and I am part of the QA Group at KINTO Technologies. As a Test Automation Specialist for the App team in the QA Group, I am responsible for building and maintaining the test automation environment for the KINTO Kantan Moushikomi App , as well as writing test specifications and test scripts. Previously, I wrote a technical article about DarkMode automation testing using Appium . I have been working with automated testing using Appium for three years. In my team, we follow a process where test specifications are defined first before writing the automated test source code. So, in this post, I’d like to share an approach to creating test specifications that has proven highly useful when writing Java source code for Appium. Before diving into the details, I’ll first explain why this method of creating test specifications is essential. Benefits of Creating Test Specifications Saves time when writing automated test source code. Easy to understand even for non-automation testers. Improves readability by organizing specifications by screen, source code class, and operation function. In this article, we will use the login scenario specification for the KINTO Kantan Moushikomi App as a sample test specification. Login steps Here, we will explain each step of logging in to the KINTO Kantan Moushikomi App. Step 1 Step 2 Step 3 Collecting Test Data for Login As the first step in creating a login test specification, it is essential to gather test data, including the test environment, test account, password, report name (automatically generated after test execution), and test execution file. Now, let’s begin with the iOS login scenario specification. Data Name Data Information Environment Stg4 Test Account ******@gmail.com Password ******** Report Name For iOS iOS.poc.login For Android android.poc.login Executable Source File For iOS iOS.poc.login.xml For Android android.poc.login.xml Login Scenario Specification for iOS Confirmation Function Screen Operation Points to Check Executable File (xml) Source File Method Log in Main Screen Click on the My Page logo The login button can be pressed iOS.poc.login.xml iOS.MainPage clickMyPagingLogo My Page Screen Click on "Application Details" Press "Log in here!" iOS.MyPagingPage clickApplyTab clickLoginHereButton Login Screen Enter the above test account in the "Email address (KINTO ID)" field. Enter the above password in the "Login Password" field. Click "Log in to My KINTO" iOS.LoginPage fillMailAddress fillPassword clickToMyKintoLoginButton Now let’s create a Java class for each screen in the scenario specification. First, click the My Page logo on the main screen (MainPage.java). MainPage.java public class MainPage extends Base { public static final String MY_PAGING_LOGO = "//XCUIElementTypeButton[@name="My Page"]"; /** * Test method for clicking the "My Page" logo on the main screen * * This method uses XPath to locate the "My Page" logo on the main page * and performs a click action. * */ @Test(groups = "MainPage") public void clickMyPagingLogo() { driver.findElementByXPath(MY_PAGING_LOGO).click(); } } Step 2: Click "Application Details" on the My Page screen, then click "Log in here." MyPagingPage.java public class MyPagingPage extends Base { public static final String APPLY_TAB = "//XCUIElementTypeButton[@name="Application Details"]"; public static final String LOGIN_HERE_BUTTON = "//XCUIElementTypeButton[@name="Log in to My KINTO"]"; /** * Test method to click "Application Details" on the My Page screen * * This method uses XPath to identify the "Application Details" on the My Page screen, * and performs a click action. * */ @Test(groups = "MyPagingPage", dependsOnGroups = "MainPage") public void clickApplyTab() { driver.findElementByXPath(APPLY_TAB).click(); } /** * A test method to click "Log in here" on the My Page screen * * This method uses XPath to identify "Log in here" on the My Page screen, * and performs a click action. * */ @Test(groups = "MyPagingPage", dependsOnMethods = "clickApplyTab") public void clickLoginHereButton() { driver.findElementByXPath(LOGIN_HERE_BUTTON).click(); } } Next, in step 3, enter your email address and password on the Login Screen, then press the Login button. LoginPage.java public class LoginPage extends Base { public static final String EMAIL_TEXT_FIELD = "//XCUIElementTypeApplication[@name="KINTO Easy Application"]/XCUIElementTypeOther[2]/XCUIElementTypeTextField"; public static final String PASSWORD_TEXT_FIELD = "//XCUIElementTypeApplication[@name="KINTO Easy Application"]/XCUIElementTypeOther[3]/XCUIElementTypeSecureTextField"; public static final String ENTER_KEY = "//XCUIElementTypeButton[@name="Return"]"; public static final String TO_MY_KINTO_LOGIN_BUTTON = "//XCUIElementTypeButton[@name="Log in to My KINTO"]"; /** * A test method to enter a test account in the "Email address (KINTO ID)" field on the login screen * * This method uses XPath to search for the "email address" on the login screen * and enter the test account (Parameter) in the xml file. * */ @Parameters("email") @Test(groups= "LoginPage", dependsOnGroups = "MyPagingPage") public void fillMailAddress(String email) { driver.findElementByXPath(EMAIL_TEXT_FIELD).click(); driver.getKeyboard().sendKeys(email); }   /** * A test method to enter a password in the "Login Password" field on the login screen. * * This method uses XPath to get the login password from the login screen. * and enter the password (Parameter) for the xml file. * */ @Parameters("password") @Test(groups= "LoginPage", dependsOnGroups = "MyPagingPage") public void fillPassword(String password) { driver.findElementByXPath(PASSWORD_TEXT_FIELD).click(); driver.getKeyboard().sendKeys(password); driver.findElementByXPath(ENTER_KEY).click(); } /** * A test method to click "Log in to My KINTO" on the login screen * * This method uses XPath to identify "Log in to My KINTO" on the login screen, * and performs a click action. * */ @Test(groups= "LoginPage", dependsOnGroups = "MyPagingPage") public void clickToMyKintoLoginButton() { driver.findElementByXPath(TO_MY_KINTO_LOGIN_BUTTON).click(); } } The xml file below is the test execution file for automated testing. Each function is written in sequence based on the test specification. <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="iOS.poc.login"> <test verbose="2" name="iOS.poc.login"> <classes> <class name="iOS.MainPage"> <methods> <include name="clickMyPagingLogo"/> </methods> </class> <class name="iOS.MyPagingPage"> <methods> <include name="clickApplyTab"/> <include name="clickLoginHereButton"/> </methods> </class> <class name="iOS.LoginPage"> <methods> <parameter name="email" value="******.gmail.com"/> <parameter name="password" value="*********"/> <include name="fillMailAddress"/> <include name="fillPassword"/> <include name="clickToMyKintoLoginButton"/> </methods> </class> </classes> </test> </suite> Summary In this article, we've outlined how to create test specifications for Appium Java source code. However, we believe this approach will be valuable not only for those working with Appium but also for developing automated test source code across different frameworks.
アバター
Self-Introduction Hello. This is Koyama ( @_koyasoo ) from KINTO Technologies. Since the beginning of this year, I've been dedicated to promoting Agile practices and working full-time as a Scrum Master, supporting my team in growing stronger every day. Today, I’d like to share some of the things we’ve been working on. Speaking of retrospectives... Do you hold them regularly? When it comes to retrospectives, the KPT (Keep/Problem/Try) format often comes to mind. It’s practically synonymous with the practice. KPTs are widely used in many workplaces, but is it actually effective? Back in June, I attended Scrum Fest Osaka, and out of all the sessions I joined, one left a particularly strong impression on me. OODA!!!!!! (If you know, you know) Yep, it’s Ikuo ( @dora_e_m ) san. Ikuo san’s session on retrospectives really left an impression on me. Until then, I had only used KPT for retrospectives, so when I heard the words, "To keep retrospectives meaningful, we need to avoid falling into a rut," they really hit home. That made me think, "I do retrospectives all the time, I should be able to apply this right away." Or so I thought. @ card As mentioned on page 22 of the session deck, it was eye-opening to see how simply switching from KPT to YWT (which is quite similar) led to a flood of opinions. It’s amazing how just shifting your perspective can bring out so many ideas... Quoted from page 22 of Ikuo san’s document So in this article, I’ll be sharing five retrospective techniques that I’ve actually tried out since then. Summary of How to Choose the Right Retrospective Method As Ikuo san mentioned in his session, switching up between different methods to fit the situation can really help a team grow and improve. Below is a summary of each method’s key features; feel free to use it as a reference. When to Use It! Things to Keep in Mind KPT A versatile method that works anytime. Just be careful not to rely on it too much. Hot Air Balloon Great for thinking about your team’s future. Puts more focus on current challenges rather than reflecting on the past. LeanCoffee Great for discussing various topics, not just retrospectives. Can be a bit tiring since discussions are held under time pressure. Celebration Grid Ideal for fact-based discussions. Hard to generate opinions when there’s little actual implementation and more personal impressions. FunDoneLearn When you want to reflect on the positives. Negatives often get overlooked. Elephants, dead fish, and vomit When the team seems to be building up frustration. Facilitating to keep the team from falling apart. Next, let’s dive into each retrospective method in detail. Let’s explore retrospectives! I hope this article encourages you (especially if you’ve only used KPT so far) to take the first step in trying a different approach! With that in mind, I’ll walk you through how to put it into practice with as much detail as possible. Feel free to use whatever works best for you. Just give it a try, you might be surprised. It’s actually not that different from KPTs! By the way, the examples in this article mainly use the online whiteboard tool Miro . @ card 1. Hot Air Balloon This reflection method involves replacing the "hot air balloon" in the center with your own product and thinking about what kind of "baggage" it carried, what "updrafts" helped it rise, and what "clouds" might become obstacles in the future. All you need is an image of a hot air balloon and three types of sticky notes to differentiate the categories. Here’s the hot air balloon our team came up with. Diagram of a hot air balloon Here’s how it goes: First, we spent 5 minutes writing about "updrafts", followed by an 8-minute discussion. Next, we did the same with "luggage"—writing for 5 minutes, then followed by an 8-minute discussion. Then, we repeated the process with "clouds"—5 minutes of writing, followed by an 8-minute discussion. Finally, we wrapped up with a 10-minute discussion on the question: "What’s important for making a hot air balloon fly higher?" The discussion took place from this perspective (10 minutes). This method naturally encourages discussions that reflect on the present and envision the future. Compared to KPT, it breaks down problems into current issues and anticipated challenges, making discussions more focused and effective. 2. LeanCoffee Lean Coffee is a method that starts with gathering topics, then breaks them into short time-boxed discussions to carry out various conversations about them. You can make this work by setting up an area where people can add and edit sticky notes for topics and another area to process selected notes one by one for discussion. Miro had a template for this, so I gave it a try. LeanCoffee Diagram Here’s how it works. First, participants will come up with a topic (8 minutes). Giving them a suggested theme can help spark ideas and make it easier to share opinions. Use features like polling to find out which topics interest the group the most. Discussions will follow the cycle below, starting with the topic that gets the most votes. Each discussion begins with 5 minutes, including time to introduce the topic. After 5 minutes, the conversation will pause. At that point, ask participants if they’d like to continue discussing the topic. You can use a poll to decide. If they want to continue, add 3 more minutes. If not, move on to the next topic. After those 3 minutes, pause again and check if they’d like to keep going. If they want to continue, add 1 more minute. If not, move on to the next topic. Once the final minute is up, that topic wraps up. If they want to keep the conversation going, set aside extra time for it and wrap up the discussion within that time. It also gives your insight into the current interests and trends among team members. You can also see trends in the interests of your team members at any given time. Plus, it might help reinforce awareness of timeboxing among the team. Facilitating discussions can make it tricky to step in and stop them, so try using a timer or pausing at natural breaks in the conversation. Be mindful that this method relies on sticking to the timebox—if not, it could fall apart. 3. Celebration Grid This method involves discussing completed actions by dividing them into six quadrants based on two axes: one for "success" and "failure", and another for "wrong ways", "experimental ways", and "known ways". As the name suggests, the focus is on keeping a positive mindset—celebrating every outcome, whether it’s a success or a failure. It seems that people often use the diagrams from this site as a guide. @ card CelebrationGrid template As shown in the diagram, each area varies in size based on the likelihood of an event occurring, and they carry the following meanings: The discussion followed these categories. Wrong way Experimental way Known Way Success Lucky! It was a great experience! You did the right thing! Failure It was inevitable. It’s ok, there was a lesson in it. Unlucky Celebration Grid in action Here’s how it works. Ask participants to specify a time period and list "what they have done" (5 minutes). Guide them to consider where each item belongs as they list them. Take a deeper dive into each one. Wrap up by celebrating the many insights gained. At first glance, this method may seem complicated, but it's actually quite simple. Since discussions are based on actual “events and facts", participants can stay grounded and discuss things without personal biases. However, because participants must first list "facts", some may find it harder to express their opinions. To make the process smoother, it’s best to include people who have been actively involved in the work. 4. FunDoneLearn As the name suggests, this method involves listing Fun (what was enjoyable), Done (what was accomplished), and Learn (what was learned.) Write down the elements that fit into each category where the circles overlap. You can use a template like a Venn diagram with overlapping circles to make organizing easier. Making the overlapping areas larger will give more space for sticky notes and make them easier to place. FunDoneLearn Diagram It’s not something that needs a detailed explanation, but here’s how to do it: Set a time limit and have participants put sticky notes (5 minutes). Discuss with each one. This method keeps the focus positive, incorporating an element of Fun. This approach helps create a generally positive and happy atmosphere for the review. On the other hand, since it focuses less on negative aspects like Problem, it may not be the best fit if there are many issues to address. 5. Elephants, dead fish and vomit This method helps identify issues from three different angles. Members are encouraged to openly discuss things they might normally hesitate to say, categorized as follows: Elephants – Issues that everyone is aware of. Dead Fish – Issues that could cause trouble if left unaddressed. Vomit – Issues that are on one's mind. You can facilitate this exercise using a simple diagram with an elephant, a fish, and vomit. However, to prevent personal conflicts among members, it’s a good idea to clearly outline the ground rules in a visible way. Drawing of an elephant, dead fish, and vomit Here’s how it works. First, start by explaining the rules. Make it clear that the goal of this method is not to create conflict within the team, but to come up with ways to address existing issues. This is a key point to keep in mind when using this method. Ask participants to write thoughts with opinions on sticky notes that fit each category (8 minutes). In my team, to keep the discussion from becoming too negative, we encouraged participants to add a pink sticky note if they wanted to reframe an issue into a positive perspective. With this method, seeing other’s notes while brainstorming might sometimes cause discomfort. To prevent this, we make sure that sticky notes in progress are not visible to others. If you are using Miro, enabling Private Mode is a good idea. Once ready, disable Private Mode and start discussing each topic. Since this method involves addressing negative aspects, it requires a bit more sensitivity compared to other approaches. That said, the atmosphere doesn’t tend to feel too negative. Comments like "Oh, so that’s what you were worried about!" or "I was thinking the same thing!" are likely to come up. This method is highly effective in aligning the team’s approach to problem-solving. What Applies to Any Retrospective Having conducted six retrospectives, including KPT, I’ve noticed that there are more commonalities than you might expect. The ultimate goal is always to "agree on the next action as a team". Writing the rules in large letters helps prevent confusion. It’s totally fine to share opinions during the session! Time guidelines: 5 minutes for basic topics, 8–10 minutes for deeper discussions. Discussion flow: First, explain the sticky notes, then either "share your own thoughts" or "randomly ask someone who might have an opinion". (Keeping it casual makes it easier for everyone to speak up. Haha!) If you’re also sharing your own thoughts, preparing your sticky notes in advance helps you stay focused on facilitating. Most importantly, as long as everyone is aligned on "agreeing on the next action", any method will work. Once that’s decided, the retrospective will be worthwhile. You can almost forget about everything else. Feedback from Participants I always feel a bit anxious after implementing a new retrospective method with new members. Would KPT have been just fine...? Did I give too many instructions, leaving little time for actual reflection...? If you ever feel this way (like I do), don’t hesitate to ask your team for feedback! You’ll probably hear nothing but positive responses. It was refreshing to do a different kind of retrospective. It was fun. (Hot Air Balloon) We were able to talk while being conscious of the time box, so we were able to talk about a variety of topics, which is good as we usually end up talking about the same thing. The issues became clear. (LeanCoffee) We made a lot of mistakes, but this helped us distinguish between good mistakes and bad ones. (Celebration Grid) It was great to understand what makes my team members enjoy their work. I liked that we could simply share fun experiences. (FunDoneLearn) I’m glad that the issues I had in mind became a shared understanding within the team. I appreciate how direct and open the discussion was. (Elephants, dead fish, vomit) Summary Rather than sticking to just one method, teams can grow stronger by choosing and applying the retrospective format that best fits the situation. I’ve only tried six so far, but I'm excited to explore even more! As mentioned at the beginning, if you’ve only used KPT, I highly encourage you to try others too. Start by following the steps outlined in this article. Once you’re comfortable, why not tweak and adapt approaches to better fit your team? I’d be happy if this article helps Scrum Masters who are looking for better ways to run retrospectives.
アバター
こんにちは!KINTOテクノロジーズのクリエイティブ室でデザイナーをしているmayuです。 私は普段、アプリのUI/UXデザインをメインで担当していますが、今回は会社のイベントで社員に配布するノベルティの制作を手掛けました。 この記事では、ノベルティ制作の企画からデザインまでの舞台裏をお話しします。 ノベルティ制作に携わる方々のヒントになれば嬉しいです。 ノベルティの選定 テーマは「一体感を感じられるもの」 今回のイベントでは「一体感を感じられるもの」を大前提としたノベルティを目指しました。 そこで、以下のような条件をもとにアイデアを出しました。 普段話さない人とコミュニケーションを取るきっかけになる 団結意識を高める イノベーションを促進する 年齢・性別を問わず、誰でも嬉しいもの 複数人のニーズがある 誰でもすぐ利用できる手軽さ 予算は数百円〜千円程度/人 長く価値が続く さまざまな案が出ましたが、最終的に「 マグネットカードスタンド 」と「 オリジナルネームカード 」を制作することに決定しました。 「マグネットカードスタンド」と「ネームカード」を選んだ理由 マグネットカードスタンド: デスクに置いて使うことで、自然と声をかけやすくなりコミュニケーションの促進につながる。 KINTOテクノロジーズのロゴや車の形を取り入れることで、会社への愛着やモチベーション向上が期待できる。 シンプルなデザインで、日常的に使いやすく、誰でも活用できる。 ネームカード: 社員一人一人に名前を入れたネームカードを作ることで、初対面でも話しかけやすく、社内の交流を促進。 「見切れKTCデザイン」によって、会社全体としての一体感をデザインで表現。 当日は名札として、イベント後はデスクに置いて使い続けられる。 マグネットカードスタンドの制作 1. 業者選定と依頼 マグネットカードスタンドの制作は、オリジナルグッズ専門サイト「 MOKU 」に依頼しました。 MOKUはカスタマイズ性が高く、デザインデータを入稿するだけでオリジナルのマグネットカードスタンドが作れることが決め手でした。 2. プロトタイピング 紙を使って簡易的なプロトタイプを試作し、サイズ感や使い勝手を確認しました。 実際のデスクに置いてみて、視認性や実用性を検証します。 3. マグネットカードスタンドのデザイン作成 Adobe Illustratorを使って、規定のテンプレートにロゴを配置したデザインデータを作成。 KINTOテクノロジーズのロゴが際立つシンプルなデザインに仕上げました。 4. 取扱説明書のデザイン作成 使い方が分かりやすいように、オリジナルの取扱説明書も作成しました。 こちらもAdobe Illustratorを使って、規定のテンプレートに沿ってデザインデータを作成。 5. データ入稿&納品 デザインデータを入稿し、約3週間で納品されました!(数量は500個で発注) ネームカードの制作 1. ネームカードのデザイン作成 Figmaで名前、部署、そしてSlackで使っているアイコンを配置したオリジナルデザインを作成。 マグネットカードスタンドとサイズが合うよう、プロトタイプを作って確認しました。 こだわりポイントは、この「 見切れKTC 」。KTCは「KINTOテクノロジーズ」の略称です。 並んでいる小さな四角は社員を表現しており、「社員一人一人が集まってKTCになる」という意味を込めました。 黒を基調としたシンプルでスタイリッシュなデザインで、テックカンパニーらしさも演出しました。 2. データの自動生成 全員分のデータを手作業で作るのは大変なので、社内のエンジニアさんに協力してもらい HTMLでデータを自動生成しました。 CSVファイルから社員情報を取り込み、テンプレートに自動反映させる仕組みを構築しました。 3. 印刷・カット作業 会社のプリンターで印刷し、ひたすら手作業でカット。 大変でしたが、その分かなりのコストカットに成功しました!笑 プロジェクトの結果と学び ノベルティ配布後、社員の皆さんからは以下のような嬉しいフィードバックをいただきました。 「声をかけやすくなった!」 「デザインが可愛い!」 「Slackのアイコンが入っているので愛着がわく!」 ノベルティを通じて一体感が生まれ、私自身も大きなやりがいを感じました。 また、「ただデザインするだけではなく、どう使われるか?」を考えながら制作する大切さを改めて実感しました。 目的に沿ったデザインの力 を発揮できたと思います。 さいごに 今回のプロジェクトを通じて得た学びを、これからのデザイン業務にも活かしていきたいと思います。 もし「KINTOテクノロジーズって楽しそう!」と感じていただけたら、ぜひ 採用ページ もご覧ください!お話しできるのを楽しみにしています。 ご覧いただき、ありがとうございました!
アバター
Integrating Native Features into Flutter Apps – Our Approach to Adding an Android-Specific Camera Analysis Library Hello. My name is Osugi, and I’m part of the Toyota Woven City Payment development group. Our team develops the payment system used in Woven by Toyota ’s Toyota Woven City , covering a wide range of payment-related functions, from backend to Web frontend and mobile applications. So far, we’ve been using Flutter to develop a mobile app for Proof of Concept (PoC). In this article, we have summarized the trial and error we went through to overcome the challenges we faced when developing new functions by incorporating a new camera analysis library that is only available natively on Android/iOS into the PoC app. Introduction Integrating native functions into a Flutter app doesn’t just add to the development workload—it also increases maintenance costs, making development more challenging. In our project, considering the development timeline and available resources, we chose not to integrate native functions directly into the Flutter app. Instead, we developed a separate PoC app and a native app for camera analysis, linking them together to carry out the PoC. After completing the PoC, when we considered integrating the Flutter app with the camera analysis app, we found that the information on design guidelines and implementation methods for Flutter's native linking function was fragmented, and we felt that there were few systematic guidelines, especially for Android's complex UI configuration. In this article, we’ll focus on Android and share design principles and practical methods for incorporating native UI into a Flutter app. Hopefully, this will be helpful for engineers facing similar challenges. :::message At the time of writing, the sample code was created using Flutter v3.24.3 / Dart v3.5.3 ::: App Overview For the purposes of this article, we’ve simplified the app developed during the actual PoC. The app follows these specifications: Specifications When you press the start button, the camera preview will be displayed. The camera analysis function runs on the preview image, and the analysis results are sent as notifications. In this article, I would like to talk about this app. Data Integration Between Flutter and Native Android We implemented data exchange between Flutter and Android native using MethodChannel and EventChannel , enabling camera control from Flutter and analysis result notifications from Android native. MethodChannel is used for commands like starting and stopping the camera, while EventChannel is used for sending analysis result notifications. The sequence diagram below illustrates this process: sequenceDiagram actor u as User participant f as Flutter participant mc as MethodChannel participant ec as EventChannel participant an as Android Native u ->> f: press start button activate f f ->> mc: start camera mc ->> an: set up camera an -->> mc: mc -->> f: result deactivate f loop an ->> ec: analyzed result ec ->> f: send analyzed data f ->> f: show data end u ->> f: press stop button activate f f ->> mc: stop camera mc ->> an: reset camera an -->> mc: mc -->> f: result deactivate f Next, I would like to talk about how to display the Android native camera preview UI on the Flutter side. How to display native Android UI in a Flutter app There are three main ways to display native Android UI in a Flutter app: Texture widget – Displays an image rendered on an Android native Surface within the Flutter Widget tree. PlatformView – Embeds, displays, and controls Android native UI inside the Flutter widget tree. Intent – Launches a new Activity. We’ll go over the characteristics of each method and how to implement them. Texture Widget The Texture widget displays an image rendered on an Android native Surface within the Flutter Widget tree. In other words, it allows Flutter to draw native UI images directly to the GPU. This approach works well for use cases where latency isn’t a major concern, such as camera previews and video playback. However, for UI animations requiring real-time performance, adjustments must be made on the native side. This means a solid understanding of both Flutter and Android native development is necessary. Additionally, the Texture widget itself does not detect user interactions like touch events, so this must be handled on the Flutter side using GestureDetector or similar. That said, if it aligns with your requirements, it can be implemented relatively easily using the approach shown below. Implementation Steps First, obtain TextureRegistry . For Flutter apps, use FlutterEngine.FlutterRenderer ,which implements TextureRegistry . For Flutter plugins, retrieve it from FlutterPluginBinding. // For Flutter apps val textureRegistry = this.flutterEngine.renderer // For Flutter plugin val textureRegistry = this.flutterPluginBinding.textureRegistry Next, create a textureEntry , which is a SurfaceTexture , from the textureRegistry , then set up a SurfaceProvider to provide a Surface to the CameraX preview instance. Once this is done, you’re all set. This Surface acts as the drawing buffer mentioned earlier. val textureEntry = textureRegistry.createSurfaceTexture() val surfaceProvider = Preview.SurfaceProvider { request -> val texture = textureEntry?.surfaceTexture() texture?.setDefaultBufferSize( request.resolution.width, request.resolution.height ) val surface = Surface(texture) request.provideSurface(surface, cameraExecutor) { } } val preview = Preview.Builder().build().apply { setSurfaceProvider(surfaceProvider) } // To meet the requirements for camera analysis mentioned at the beginning of the article, // this can be achieved by setting up a cameraProvider and configuring the Preview and analysis processing for the camera. try { camera = cameraProvider?.bindToLifecycle( this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis, // Set the camera image analysis process here ) } catch(e: Exception) { Log.e(TAG, "Exception!!!", e) } Then, simply return the ID of the TextureEntry associated with the Surface to Flutter as the return value of MethodChannel . fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when(call.method) { "startCamera" -> { result.success(textureEntry.id()) } "stopCamera" -> { stopCamera() } else -> result.notImplemented() } } To render a native SurfaceTexture on the Flutter side, simply set the textureId obtained from MethodChannel to the Texture widget, and the camera preview will appear in the Flutter app. static const platform = MethodChannel('com.example.camera_preview_texture/method'); int? _textureId; Future<void> onPressed() async { try { final result = await platform.invokeMethod<int>('startCamera'); if (result != null) { setState(() { _textureId = result; }); } } on PlatformException catch (e) { print(e.message); } } Widget build(BuildContext context) { if (_textureId == null) { return const SizedBox(); } return SizedBox.fromSize( size: MediaQuery.of(context).size, child: Texture( textureId: _textureId!, ), ); } For an implementation using the Texture widget, the mobile_scanner serves as a great reference. PlatformView PlatformView allows embedding Android native UI into Flutter’s widget tree, making it possible to display and control it. There are three rendering modes for PlatformView : Virtual Display ( VD ), Hybrid Composition ( HC ), and TextureLayerHybridComposition ( TLHC )[^1]. When using the PlatformView API, TLHC is selected by default. However, if the Android native UI tree contains SurfaceView , it will fall back to VD or HC [^2]. In addition, Texture improves frame rate synchronization between Flutter and Android native, which was not possible with the Texture widget. It also allows user interaction control and supports displaying UI elements beyond just camera previews and videos. Implementation Steps In this sample code using PlatformView , the camera preview screen is implemented with Jetpack Compose. To use Jetpack Compose in a Flutter app, add the following dependencies and configuration to app/build.gradle : android { ~ ~ buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.4.8" } } dependencies { implementation("androidx.activity:activity-compose:1.9.3") implementation(platform("androidx.compose:compose-bom:2024.04.01")) implementation("androidx.compose.material3:material3") } Now, let’s dive into the details of the implementation. Implementing PlatformView requires the following three steps: Implement NativeView that inherits PlatformView Implement NativeViewFactory that inherits PlatformViewFactory Register PlatformViewFactory to FlutterEngine 1. Implementing NativeView For a general implementation, please refer to Official . One key difference from the official approach is that this implementation uses Jetpack Compose. Here, the CameraPreview (built with Jetpack Compose) is embedded into the Android native View tree using ComposeView . class NativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?, methodChannel: MethodChannel, eventChannel: EventChannel) : PlatformView { private var nativeView: ComposeView? = null override fun getView(): View { return nativeView!! } override fun dispose() {} init { nativeView = ComposeView(context).apply { setContent { CameraPreview(methodChannel, eventChannel) } } } } In the Jetpack Compose implementation, PreviewView from CameraX, which is a View , is Composed using AndroidView . As a side note, AndroidView can also be used for Fragment . @Composable fun CameraPreview(methodChannel: MethodChannel, eventChannel: EventChannel) { val context = LocalContext.current val preview = Preview.Builder().build() val previewView = remember { PreviewView(context) } suspend fun startCamera(context: Context) { val cameraProvider = context.getCameraProvider() cameraProvider.unbindAll() // To meet the requirements for camera analysis mentioned at the beginning of the article, // this can be achieved by setting up a cameraProvider and configuring the Preview and analysis processing for the camera. cameraProvider.bindToLifecycle( LocalLifecycleOwner.current, CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build(), preview, analysis, // Set the camera image analysis process here ) preview.surfaceProvider = previewView.surfaceProvider } suspend fun stopCamera(context: Context) { val cameraProvider = context.getCameraProvider() cameraProvider.unbindAll() } LaunchedEffect(Unit) { fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when(call.method) { "startCamera" -> { runBlocking { CoroutineScope(Dispatchers.Default).launch { withContext(Dispatchers.Main) { startCamera(context) } } } result.success("ok") } "stopCamera" -> { runBlocking { CoroutineScope(Dispatchers.Default).launch { withContext(Dispatchers.Main) { stopCamera(context) } } } } else -> result.notImplemented() } } methodChannel.setMethodCallHandler(::onMethodCall) } AndroidView(factory = { previewView }, modifier = Modifier.fillMaxSize()) } Next, 2. implement NativeViewFactory and 3. register it to FlutterEngine as follows. class MainActivity: FlutterFragmentActivity() { ~ ~ override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) val methodChannel = MethodChannel( flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL ) val eventChannel = EventChannel( flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL ) flutterEngine .platformViewsController .registry .registerViewFactory(VIEW_TYPE, NativeViewFactory(methodChannel, eventChannel)) } } class NativeViewFactory( private val methodChannel: MethodChannel, private val eventChannel: EventChannel ) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { override fun create(context: Context, viewId: Int, args: Any?): PlatformView { val creationParams = args as Map<String?, Any?>? return NativeView( context, viewId, creationParams, methodChannel, eventChannel ) } } Finally, here is the implementation on the Flutter side. PlatformViewsService.initSurfaceAndroidView() is an API for using either TLHC or HC . PlatformViewsService.initAndroidView() allows you to use either TLHC or VD . PlatformViewsService.initExpensiveAndroidView() forces the use of HC . class CameraPreviewView extends StatelessWidget { final String viewType = 'camera_preview_compose'; final Map<String, dynamic> creationParams = <String, dynamic>{}; CameraPreviewView({super.key}); @override Widget build(BuildContext context) { return PlatformViewLink( viewType: viewType, surfaceFactory: (context, controller) { return AndroidViewSurface( controller: controller as AndroidViewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{}, ); }, onCreatePlatformView: (params) { return PlatformViewsService.initSurfaceAndroidView( id: params.id, viewType: viewType, layoutDirection: TextDirection.ltr, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), onFocus: () { params.onFocusChanged(true); }, ) ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) ..create(); }, ); } } By using PlatformView this way, you can integrate Android native UI into your Flutter app. Intent Intent is an Android feature (not specific to Flutter) that allows launching an Activity separate from the MainActivity where Flutter runs. With Intent, you can navigate to another screen within your app, launch external apps, and exchange data between Activities. The two methods mentioned above (Texture widget and PlatformView) have been reported to have performance issues [^3]. To resolve these issues, a deep understanding of both Flutter and Android native is essential. In some cases, building a separate Android app might actually help keep development costs down. However, this poses a different challenge. If your team only has Flutter engineers, you will need to catch up on Android development. If the app is developed as an external application, the interface between apps must include security measures and be designed with lifecycle considerations in mind. For instance, the following measures may be necessary: Validate the data exchanged between activities. Restrict access so that only a specific app can call it. Ensure the called app functions correctly even if the calling app has been killed. Now let’s take a look at how to use Intent in Flutter. First, we’ll go over how to call another Activity from a Flutter app. Calling Activity (MainActivity where the Flutter app runs) override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { if (call.method!!.contentEquals("startCamera")) { val dummyData = call.argument<String>("dummy_data") ?: return result.error( "ERROR", "data is invalid", null ) // In case of screen transition val intent = Intent(this, SubActivity::class.java) // For external apps val packageName = "com.example.camera_preview_intent" val intent = activity.packageManager.getLaunchIntentForPackage(packageName) ?: return result.error( "ERROR", "unexpected error", null ) intent.setClassName(packageName, ".SubActivity") // Store the sending data intent.putExtra("EXTRA_DUMMY_DATA", dummyData) intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) activity.startActivityForResult(intent, REQUEST_CODE) } } override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) { eventSink = sink } override fun onCancel(arguments: Any?) { eventSink = null } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) { val result = data.getStringExtra("RESULT_DATA") ?: "", eventSink?.success(result) return true } return false } Next, let’s implement the Activity that gets called from the Flutter app. Once a specific operation is completed, you can use Intent to return data, as shown below. Target Activity val intent = Intent() intent.putExtra("RESULT_DATA", resultData) activity.setResult(Activity.RESULT_OK, intent) finish() By using Intent this way, you can avoid dealing with complex UI control on both the Flutter and native Android sides while enabling data exchange between Flutter and native Android Activities. However, security and data integrity must be carefully considered in this approach. Summary In this article, we've discussed how to incorporate native functionality into Flutter apps, with a focus on Android. Data communication between Flutter and Android native was achieved using MethodChannel and EventChannel . Here’s how to incorporate Android native UI into Flutter: Texture widget Great for camera previews and video displays, and relatively easy to implement. However, it requires handling user interactions and may have some performance issues. PlatformView Lets you integrate native UI into Flutter’s widget tree while enabling user interaction control. Supports embedding View, Fragment, and Jetpack Compose. Performance can also be an issue. Intent Allows seamless screen transitions and launching of other apps, making it possible to directly display Android’s UI and exchange data. However, security and data handling require careful attention. As mentioned above, each method comes with its own strengths and limitations when integrating Android native features into a Flutter app. The best choice depends on your project’s specific needs. Notes The thumbnail of the Droid is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons Attribution 3.0 License. [^1]: Hosting native Android views in your Flutter app with Platform Views [^2]: Android Platform Views [^3]: Performance
アバター
This article is the entry for day 16 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Hi, I’m Nakanishi from the Manabi-no-Michi-no-Eki (Learning Roadside Station) team. This year, the Manabi-no-Michi-no-Eki project was officially launched and later established as a team. As part of this initiative, we're also hosting an in-house podcast, and for this year’s Advent Calendar, we’d love to share more episodes with you. What is the Manabi-no-Michi-no-Eki (Learning Roadside Station)? It’s a project aimed at making the frequently held in-house study sessions more accessible and effective. The initiative is led by passionate volunteers within the company, with the goal of supporting study sessions and fostering a culture of knowledge sharing across the organization. 10X Innovation Culture Program The Learning Roadside Station Podcast features interviews with employees who organize study sessions within the company. This segment is called “A Peek into the Study Session Next Door”. For today’s podcast, we’re joined by Awata-san and HOKA-san, who are working on the 10X Innovation Culture Program being provided by Google. Usually, HOKA-san conducts the interviews, but today, Akeda-san and I will be taking on that role. So, without further delay, let's jump right into the interview. Interview Awata-san: Thank you for having me. My regular work focuses on platform engineering, ensuring that database-based operations are accessible to everyone. Besides that, I’m interested in corporate culture, so I’m doing a variety of activities. HOKA-san: I usually work in the Human Resources Group’s Organizational Human Resources Team. We plan and conduct training by identifying needs and challenges through interviews with both new and existing employees. Akeda-san: Please tell us what prompted you to hold these study sessions. Awata-san: As part of our Google Cloud Enterprise User Group ( Jagu’e’r ) I took part in a subcommittee for thinking about corporate culture and innovation. As part of our activities, we decided to try the 10X Innovation Culture Program, gathered around 15 volunteers, and went ahead with it. HOKA-san was also among them, and things took off from there. HOKA-san: Yes, that’s right. When we held our first event at the Google office in Shibuya, the reaction from the participants was extremely good. Collaborating in a workshop with people we usually had no interaction with opened up new opportunities for communication. Akeda-san: Next, please tell us some details about the events. How did they expand after the initial session? Awata-san: We initially held them at the Google office, then subsequently shifted to holding them in-house. The in-house events also drew a large number of participants and got an extremely positive reaction. HOKA-san: Seeing KTC employees engage so positively, the Google team also expressed high praise. We hope to go on spreading this program further both inside and outside the company. Akeda-san: What are the prospects for the future? Awata-san: In the future, we’d like to become certified facilitators, and get to spread 10X to other companies as well. HOKA-san: First, we plan to roll it out to other in-house groups and cultivate a culture of innovation across KTC. Akeda-san: What kind of organization do you envision for KTC? Awata-san: I want to make it a vibrant hive of thinking outside the box, flexible communication, and collaboration. HOKA-san: I want to create a culture where people can take on challenges without fearing failure. To achieve this, I plan to utilize the 10X methods. Akeda-san: Finally, could you share a message with all our listeners? Awata-san: Culture isn’t something that can be imposed; it naturally emerges from people's actions. If you're interested, we’d love for you to join us. HOKA-san: If you're interested, feel free to start by just taking a look—don’t hesitate to reach out. Through the 10X Innovation Culture Program, we aim to make KTC a more collaborative and supportive organization to work in. If you’re interested, please contact Awata-san or HOKA-san. In this article, we shared insights into the 10X Innovation Culture Program, its background, and what the future may hold for it. We hope you’re looking forward to the next study session as well!
アバター
This article is part of Day 15 of the KINTO Technologies Advent Calendar 2024 🎅🎄 Hi, I’m Nakanishi from Learning Roadside Station. This year, the Learning Roadside Station project was officially launched and structured as an organization. As part of our initiatives, we also run an in-house podcast, and for this year's Advent Calendar, we’d like to share more about it. What is "Learning Roadside Station"? "Learning Roadside Station" is a project launched to make the frequent study sessions held within the company more convenient and effective. The aim is to support the holding of study sessions, mainly by volunteers within the company, and to promote knowledge sharing within the company. Osaka Tech Lab Information Sharing Meeting In the KTC Learning Roadside Station Podcast, we interview people who hold in-house study sessions. This segment is called "Surprise! Our Neighbor’s Study Group." Today's podcast guests are Okita-san and Fukuda-san, who are leading the Osaka Tech Lab Information Sharing Meeting. Could you start by introducing yourselves? Interview Okita-san: Yes, my name is Okita. I belong to the mobile app development group, and as a development PM, I am responsible for connecting the mobile development team with other groups. I look forward to our discussion today. Fukuda-san: My name is Fukuda. I joined KTC (formerly KINTO Corporation) in July 2020 and worked in the Production Group. I took 10 months of parental leave, and in February 2024, I returned to work. I am now part of the Creative Division, where I manage the operation and renewal of KTC's corporate website. I look forward to our discussion today. Hoka-san: Thank you. Could you tell us about what inspired you to start the Osaka Tech Lab Information Sharing Meeting? Fukuda-san: Osaka Tech Lab was launched in April 2022. At first, it was just Tomonaga-san from the Analysis Group running it alone. As more members joined, we started hearing comments like, "I don't even know what the person sitting next to me is working on." That’s when we decided to start an information-sharing meeting to improve communication. Okita-san: Fukuda-san was the founder of the Osaka Tech Lab Information Sharing Meeting. At first, we started with self-introductions, and by sharing our hobbies, we aimed to find common ground and build connections among members. Hoka-san: I see. So, Okita-san, you have been involved since the very first session and helped promote the initiative. How has it evolved over time? Okita-san: In the beginning, there were only a few members, so we could just gather and share ideas casually. Now, in our 17th session, the number of members has grown, and naturally, the format of our meetings has evolved as well. Hoka-san: What does it mean that Osaka Tech Lab is the main player in this event? Okita-san: The purpose is to stimulate communication within Osaka Tech Lab. We encourage members to share their work and initiatives, fostering stronger horizontal connections across teams. Additionally, our discussions often lead to tech blog content, helping us document and share insights more effectively. Hoka-san: That sounds great! It feels like the initiative is truly taking shape. Have you noticed any changes in the reactions of the participants or the atmosphere? Fukuda-san: At first, the meetings were casual and conversational, but over time, they have become a space for discussing challenges. We now also use these sessions to talk about how to improve our office environment. Okita-san: For example, we didn’t have a clock in the office, so we installed one, and also added bookshelves. It’s a continuous collaborative effort where we share ideas and implement improvements together. Hoka-san: What are your plans for the future of the Osaka Tech Lab Information Sharing Meeting? Okita-san: As our organization grows, I want to maintain the friendly and open atmosphere we have built while continuing to prioritize communication. Fukuda-san: I want to promote KTC from Osaka. We’ll consider whether to continue the information-sharing meetings in their current form or evolve into a new format. Hoka-san: What if other department members want to participate? Okita-san: We are recruiting LT speakers every month, so we’d love for more people to participate! If you’re interested, feel free to reach out to any Osaka Tech Lab member. Fukuda-san: After the information-sharing meeting, we also hold a beer bash, so we hope everyone uses it as an opportunity to connect and communicate in a relaxed setting. Hoka-san: Lastly, do you have a message for our audience? Okita-san: Please visit Osaka! We’d love to have you here. Fukuda-san: Bring an LT (Lightning Talk) and join our information-sharing meetings! We’d love to have you here. Hoka-san: Thank you both for your time today. I can really see how your efforts in Osaka are creating a positive impact across the company. Thank you Okita-san and Fukuda-san. That's the summary of the interview. This article conveys the importance and role of the Osaka Tech Lab Information Sharing Meeting to our readers. This time, we have provided details about the Osaka Tech Lab information sharing session, the background to its operation, and future prospects. Please look forward to the next study session!
アバター
Introduction Hello! My name is Yoo, and I am a member of the New Car Subscription Development Group at KINTO Technologies. While our approach may not be perfect, we continuously strive to improve challenges step by step. In this article, I’d like to share how we implemented Redis Pub/Sub in Spring Boot to dynamically change the system date. Background and Motivation When conducting QA and testing, there are many cases where it is necessary to change the system date to verify specific behaviors. This is especially important for subscription-based services, where business logic often depends on specific dates. For example, testing requires validation of processes related to start and end dates of the period, monthly fees, settlement charges for mid-term cancellations, maintenance inspections, and vehicle inspections. Previously, the system date was defined in the configuration file, meaning that every time the date needed to be changed, the container had to be redeployed. As a result, each test or QA cycle required more than five minutes just for redeployment, significantly impacting efficiency. In this article, I will introduce how we solved this issue and improved our workflow. Benefits of introducing Redis Pub/Sub By implementing Redis Pub/Sub, we optimized system date changes in test environments, making them more efficient and responsive. As a result, we have successfully reduced the workload for testing and QA, leading to improved operational efficiency. Specifically, container redeployment is no longer required. Instead, by simply sending a message (the desired setting value) to the corresponding setting item (topic), each container can instantly receive the update and apply the changes in real time. Even in multi-container environments, all subscribers receive the message simultaneously, allowing system settings to be updated across multiple containers without requiring a restart. Additionally, system date changes are now logged, making it possible to track and review change history when needed. Furthermore, with Spring Boot Profile settings, this feature can be enabled exclusively in designated test environments, preventing accidental application to production or other environments. *For more details on Profiles, see here . What is Redis Pub/Sub Redis Pub/Sub is one of the messaging patterns used in message queuing systems. Message Queuing is a method of asynchronous communication commonly used in serverless and microservices architectures to enable real-time event notification in distributed systems. This mechanism is widely used not only as a database and cache but also as a message broker, as it supports scalable and stable communication between different software modules. Main components Topic: The subject or category that subscribers listen to. Publisher: Sends messages related to a specific topic. Subscriber: Receives messages from publishers for subscribed topics. Keyspace Notifications Redis can monitor real-time changes to keys and values by receiving events that impact the Redis dataset in various ways. How is it implemented? System date change mechanism We implemented an API as a publisher to send messages to designated topics. When an event occurs for a subscribed topic (key), multiple containers (subscribers) receive the message and update the settings in real time. System configuration It is built using Java and Spring Boot. Applications are containerized and run in a cloud environment. Adding the necessary library to build.gradle implementation 'org.springframework.data:spring-data-redis' Implementing the RedisConfig class @AllArgsConstructor @Configuration public class RedisTemplateConfig { private final RedissonClient redissonClient; @Bean public RedisTemplate<String, String> redisTemplate() { RedisTemplate<String, String> template = new RedisTemplate<>(); template.setConnectionFactory(new RedissonConnectionFactory(redissonClient)); template.setDefaultSerializer(new StringRedisSerializer()); return template; } @Bean public RedisMessageListenerContainer redisContainer() { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(new RedissonConnectionFactory(redissonClient)); return container; } } Implementing the Publisher Implement an API that sends messages to a designated topic. @RestController public class SystemTimeController { private final SomeService service; @PostMapping("/update") public void updateSystemTime(@RequestParam String specifiedDateTime) { service.publish(specifiedDateTime); } } @Service @RequiredArgsConstructor public class SomeService { private final RedisTemplate<String, String> redisTemplate; // Define the topic key private static final String FOO_TOPIC = "foo-key"; public void publish(String specifiedDateTime) { // Send a message to the specified topic redisTemplate.opsForValue().set(FOO_TOPIC, specifiedDateTime); } } Implementing the Subscriber The subscriber receives messages when an event occurs for a subscribed topic (key). @Slf4j @Component @Profile("developer1, developer2") // Only enabled for the specified test environment profiles public class FooKeyspaceEventMessageListener extends KeyspaceEventMessageListener { private final RedisMessageListenerContainer listenerContainer; private final RedisTemplate<String, String> redisTemplate; private static final String FOO_TOPIC = "foo-key"; @Override public void init() { doRegister(listenerContainer); } public FooKeyspaceEventMessageListener( RedisMessageListenerContainer listenerContainer, RedisTemplate<String, String> redisTemplate) { super(listenerContainer); this.listenerContainer = listenerContainer; this.redisTemplate = redisTemplate; } @Override protected void doHandleMessage(Message message) { // Retrieve the system date from Redis String systemTime = updateSystemTimeConfig(redisTemplate.opsForValue().get(FOO_TOPIC)); // Create and invoke the method to update the system date updateSystemTimeConfig(systemTime); log.info("Receive a message about FOO_TOPIC: {}", message); } } Finally Thank you for reading this article to the end. There are still many areas for improvement, but we strive to address challenges step by step and grow with each iteration. While the structure and implementation may not be perfect, I believe that gradual progress in the right direction is what truly matters. We will continue to learn, ensuring that these small advancements accumulate and lead to even better outcomes. I hope we can continue this journey of growth together. Thank you very much.
アバター
はじめに こんにちは!Yao Xieです。 KINTO Technologies のモバイルアプリ開発グループで、 KINTO かんたん申し込みアプリ のAndroidアプリを開発しています。本記事では、AGSL(Android Graphics Shading Language)を活用してカスタムUIコンポーネントを向上したり、Androidアプリで高度な画像処理をする方法を紹介します。 AGSLとは AGSL (Android Graphics Shading Language)は、Android 向けに設計されたGPUベースのシェーディング言語のことです。Skia Shading Language (SKSL)をベースにしたAGSLは、高度なグラフィックエフェクトを生みだせるAndroid特有の最適性を備えています。AGSLはAndroidのレンダリングパイプラインと完全に統合しているので、複雑なビジュアルエフェクトを効率良くスムーズに実装できます。 GLSLからSKSLへ、そしてAGSLへ グラフィックスシェーディング言語は、現代のアプリで求められている、高品質なグラフィックへの需要に応えるために大きく進化してきました。簡単にまとめると: GLSL(OpenGLシェーディング言語): リジナルのシェーディング言語で、2D・3DグラフィックのレンダリングにOpenGLと併用されます。GLSLのおかげで、GPU上で動作するカスタムシェーダーを書き出すことができます。 SKSL(Skiaシェーディング言語): Skiaグラフィックスライブラリの一部として導入されています。SKSL は 2DグラフィックをレンダリングするためにAndroidなどいろいろなプラットフォームで使われています。 AGSL(Android Graphics Shading Language): Android用に特化してデザインしてあるシェーディング言語です。SKSLの機能をベースに、Androidのレンダリングパイプラインとスムーズに統合できるように調整してあります。 GLSL、SKSL、AGSLの主な違い AGSLはモバイルデバイス向けに最適化されていて、GLSLよりもパフォーマンスが高く、消費電力が低いです。Androidレンダリングパイプラインと統合していることで、より効率良くグラフィックをレンダリングすることができます。 GLSL: OpenGL用の、C言語に似た構文です。 クロスプラットフォーム対応ではあるものの、OpenGL ESのバリエーションの影響でAndroidでは制限があります。 SKSL: GLSLに似ていますが、Skiaの2Dグラフィック向けに最適化してあります。 主にSkia内部で使用されていて、Androidの直接的な開発ではあまり利用できません。 AGSL: SKSLをベースにしつつ、 Android特有の向上性を備えています。 Androidのグラフィックパイプラインと完全に統合していて、最適なパフォーマンスを発揮します。 AGSLの仕組みは? 下の図は、AGSLシェーダー文字列がAndroidのグラフィックレンダリングシステムやデータフロープロセスの中でどの位置にあるかを示す階層図(上から下への順序)です。(概念的な図なので、正確なシステムアーキテクチャではありません。) はじめに Step 1:グラデーションシェーダを定義する AGSLを使用して、テキストにのみ滑らかなグラデーションエフェクトを加えるシェーダーファイルを作成します。コンポーザブルインプットによって、グラデーションがテキストのアルファマスクにしっかりと適切に反映できます。 @Language("AGSL") val gradientTextShader = """ uniform float2 resolution; // Text size uniform float time; // Time for animation uniform shader composable; // Input composable (text mask) half4 main(float2 coord) { // Normalize coordinates to [0, 1] float2 uv = coord / resolution; // Hardcoded gradient colors half4 startColor = half4(1.0, 0.65, 0.15, 1.0); // Orange half4 endColor = half4(0.26, 0.65, 0.96, 1.0); // Blue // Linear gradient from startColor to endColor half4 gradientColor = mix(startColor, endColor, uv.x); // Optional: Add a subtle animation (gradient shifting) float shift = 0.5 + 0.5 * sin(time * 2.0); gradientColor = mix(startColor, endColor, uv.x + shift * 0.1); // Use the alpha from the input composable mask half4 textAlpha = composable.eval(coord); // Combine the gradient color with the composable alpha return gradientColor * textAlpha.a; } """.trimIndent() Step 2:シェーダ用Modifierを作成する テキストにグラデーションシェーダを適用するカスタム Modifierを定義します。このシェーダは、ダイナミックなタイムパラメータを活用して、グラデーションをアニメーション化します。 fun Modifier.gradientTextEffect(): Modifier = composed { val shader = remember { RuntimeShader(gradientTextShader) } var time by remember { mutableStateOf(0f) } // Increment animation time LaunchedEffect(Unit) { while (true) { time += 0.016f // Simulate 60 FPS delay(16) } } this.graphicsLayer { shader.setFloatUniform("resolution", size.width, size.height) shader.setFloatUniform("time", time) renderEffect = RenderEffect .createRuntimeShaderEffect(shader, "composable") .asComposeRenderEffect() } } Step 3:シェーダをテキストコンポーネントに適用する UIでModifier.gradientTextEffectを使用して、グラデーションエフェクトを適用します。 @Composable fun GradientTextDemo() { Box( modifier = Modifier .fillMaxSize() .padding(16.dp), contentAlignment = Alignment.Center ) { Text( text = "Gradient Text", fontSize = 36.sp, fontWeight = FontWeight.Bold, color = Color.White, modifier = Modifier.gradientTextEffect() ) } } 結果 AGSLで他にできることってこれだけ? AGSLの機能は基本をはるかに超えて、ダイナミックで魅力的で、高パフォーマンスなアプリ体験を作り上げるのをサポートしてくれます。ここからは、AGSLでアプリをさらにレベルアップする方法を、実例をもとに探ってみましょう。 1.UIコンポーネントを強化 AGSLを使えば、ユーザーの心を捉えて、アプリの目的を際立たせる魅力的なUI要素を作成することができます。 アニメーションボーダー: カード、ボタン、または画像の周囲にマーキーエフェクトや点滅エフェクトを作成します。 カスタムグラデーション: ダイナミックに流れる、アニメーション化したGPUアクセラレーショングラデーションを実装すします。 Dynamic Glow エフェクト: ボタンやスライダーに、光るハイライト後光を追加します。 例:運転スキルのトレーニングアプリ 運転スキルのトレーニングアプリを開発していると想像してみてください。目標は、インターフェイスを視覚的に惹きつけるものにして、ユーザーが「トレーニング開始」ボタンのように大切な要素を操作できるようにすることです。AGSLがDynamic Glowエフェクトをどのように実現するのかをご紹介します。 AGSL シェーダコード: @Language("AGSL") val glowButtonShader = """ // Shader for a glowing rounded rectangle button uniform shader button; // Input texture or color for the button uniform float2 size; // Button size uniform float cornerRadius; // Corner radius of the button uniform float glowRadius; // Radius of the glow effect uniform float glowIntensity; // Intensity of the glow layout(color) uniform half4 glowColor; // Color of the glow // Signed Distance Function (SDF) for a rounded rectangle float calculateRoundedRectSDF(vec2 position, vec2 rectSize, float radius) { vec2 adjustedPosition = abs(position) - rectSize + radius; // Adjust for rounded corners return min(max(adjustedPosition.x, adjustedPosition.y), 0.0) + length(max(adjustedPosition, 0.0)) - radius; } // Function to calculate glow intensity based on distance float calculateGlow(float distance, float radius, float intensity) { return pow(radius / distance, intensity); // Glow falls off as distance increases } half4 main(float2 coord) { // Normalize coordinates and aspect ratio float aspectRatio = size.y / size.x; float2 normalizedPosition = coord.xy / size; normalizedPosition.y *= aspectRatio; // Define normalized rectangle size and center float2 normalizedRect = float2(1.0, aspectRatio); float2 normalizedRectCenter = normalizedRect / 2.0; normalizedPosition -= normalizedRectCenter; // Calculate normalized corner radius and distance float normalizedRadius = aspectRatio / 2.0; float distanceToRect = calculateRoundedRectSDF(normalizedPosition, normalizedRectCenter, normalizedRadius); // Get the button's color half4 buttonColor = button.eval(coord); // Inside the rounded rectangle, return the button's original color if (distanceToRect < 0.0) { return buttonColor; } // Outside the rectangle, calculate glow effect float glow = calculateGlow(distanceToRect, glowRadius, glowIntensity); half4 glowEffect = glow * glowColor; // Apply tone mapping to the glow for a natural look glowEffect = 1.0 - exp(-glowEffect); return glowEffect; } """.trimIndent() 結果 ボタンが明滅する後光を出して、関心を引き付けながら車のライトのような雰囲気を作り出せます。 https://youtube.com/shorts/CW1yBgJyDo4?rel=0 2.高度な画像処理を実行する AGSLはリアルタイムの画像操作に優れていて、ダイナミックでインタラクティブなエフェクトを作ることができます。AGSL を使用すれば、GPUアクセラレーターを活かした高速な画像処理エフェクトを作成できます。 カスタムフィルタ: セピア、ピクセル化、ビネットなどアート風なエフェクトを追加します。 ダイナミックブラー: モーションブラーや被写界深度エフェクトなど、リアルタイムでぼかしを適用します。 カラー調整: UI上で明るさ、コントラスト、彩度をダイナミックに調整します。 例:画像の波紋エフェクト アプリに月の画像があると想像してみてください。月が水面に映るような波紋エフェクトを追加して、もっとインタラクティブで関心を引くインターフェースにしたいなと思っているとします。 AGSL シェーダコード: @Language("AGSL") val rippleShader = """ // Uniform variables: inputs provided from the outside uniform float2 size; // The size of the canvas in pixels (width, height) uniform float time; // The elapsed time for animating the ripple effect uniform shader composable; // The shader applied to the composable content being rendered // Main function: calculates the final color at a given fragment (pixel) coordinate half4 main(float2 fragCoord) { // Scale factor based on the canvas width for normalization float scale = 1 / size.x; // Normalize fragment coordinates float2 scaledCoord = fragCoord * scale; // Calculate the center of the canvas in normalized coordinates float2 center = size * 0.5 * scale; // Calculate the distance from the current fragment to the center float dist = distance(scaledCoord, center); // Calculate the direction vector from the center to the fragment float2 dir = scaledCoord - center; // Apply a sinusoidal wave based on the distance and time float sin = sin(dist * 70 - time * 6.28); // Offset coordinates by applying the wave effect in the direction of the fragment float2 offset = dir * sin; // Calculate the texture coordinates with the ripple effect applied float2 textCoord = scaledCoord + offset / 30; // Sample the composable shader using the adjusted texture coordinates return composable.eval(textCoord / scale); } """.trimIndent() 結果 このシェーダーがあれば、最小限のパフォーマンスコストでアプリの画像に深みやエレガントさを加えることができます。 https://www.youtube.com/shorts/80QOTzNUHLg?rel=0 3. プロシージャルグラフィックを有効にする プロシージャルグラフィックスは、視覚に訴えるようなインターフェースを静的なアセットに頼ることなく作成するのにピッタリです。 パターンの生成: ストライプ、グリッド、ノイズなどのプロシージャルテクスチャを作成します。 シェイプアニメーション: モーフィングシェイプや移動パターンをデザインします。 3D風エフェクト: 奥行きや遠近感を、実際の3Dレンダリングなしで表現でき ます。 例:アニメーションローディング画面 ローディング画面は単調になりがちですが、AGSLを使うとダイナミックな芸術作品のように変身します。例えば、アプリの読み込み中にきらきらなアニメーション球体を表示して、ユーザーの目を引く演出が作成できます。 AGSL シェーダコード: @Language("AGSL") val lightBallShader = """ uniform float2 size; // The size of the canvas in pixels (width, height) uniform float time; // The elapsed time for animating the light effect uniform shader composable; // Shader for the composable content half4 main(float2 fragCoord){ // Initialize output color float4 o = float4(0.0); // Normalize coordinates relative to the canvas center float2 u = fragCoord.xy * 2.0 - size.xy; float2 s = u / size.y; //ライトボールエフェクトを計算するループ for (float i = 0.0; i < 180.0; i++) { float a = i / 90.0 - 1.0; // Calculate a normalized angle float sqrtTerm = sqrt(1.0 - a * a); // Circular boundary constraint float2 p = cos(i * 2.4 + time + float2(0.0, 11.0)) * sqrtTerm; // Oscillation term // Compute position and adjust with distortion float2 c = s + float2(p.x, a) / (p.y + 2.0); // Calculate the distance factor (denominator) float denom = dot(c, c); // Add light intensity with color variation float4 cosTerm = cos(i + float4(0.0, 2.0, 4.0, 0.0)) + 1.0; o += cosTerm / denom * (1.0 - p.y) / 30000.0; } // Return final color with an alpha of 1.0 return half4(o.rgb, 1.0); } """.trimIndent() 結果 このシェーダーでアプリのローディング画面が未来的でスタイリッシュになり、待ち時間が短く感じられてより楽しめるものになります。 https://youtube.com/shorts/pUTU0KRmFek?rel=0 4. アプリのパフォーマンスを向上させる AGSLはパフォーマンス重視な場面で力を発揮し、レンダリングタスクをGPUに任せることで、スムーズで効率的なアニメーションを実現します。 効率的なアニメーション: 複雑なリアルタイムエフェクトをスムーズに処理します。 バッテリー最適化 消費電力を最小限に抑えつつ、目を見張るようなエフェクトを実現します。 例:マップビューでの天気アニメーション プロダクトマネージャーから、マップビューに天気アニメーションのオーバーレイを追加するように頼まれたとします。従来の方法はパフォーマンス集約型ですが、GSLはCPUオーバーヘッドを最小限にしつつ、Androidの最適化したレンダリングパイプラインを活かして、効率よくGPUレンダリングができます。 雨のAGSLシェーダーコード: @Language("AGSL") val rainShader = """ uniform float time; // The elapsed time for animating the rain uniform float2 size; // The size of the canvas in pixels (width, height) uniform shader composable; // Shader for the composable content // Generate a pseudo-random number based on input float random(float st) { return fract(sin(st * 12.9898) * 43758.5453123); } half4 main(float2 fragCoord) { // Normalize fragment coordinates to the [0, 1] range float2 uv = fragCoord / size; // Rain parameters float speed = 1.0; // Speed of raindrops float t = time * speed; // Time-adjusted factor for animation float density = 200.0; // Number of rain "drops" per unit area float length = 0.1; // Length of a raindrop float angle = radians(30.0); // Angle of the rain (in degrees) float slope = tan(angle); // Slope of the rain's trajectory // Compute grid position and animated raindrop position float gridPosX = floor(uv.x * density); float2 pos = -float2(uv.x * density + t * slope, fract(uv.y - t)); // Calculate the raindrop visibility at this fragment float drop = smoothstep(length, 0.0, fract(pos.y + random(gridPosX))); // Background and rain colors half4 bgColor = half4(0.0, 0.0, 0.0, 0.0); // Black transparent background half4 rainColor = half4(0.8, 0.8, 1.0, 1.0); // Light blue raindrop color // Blend the background and raindrop color based on drop visibility half4 color = mix(bgColor, rainColor, drop); return color; // Output the final color for the fragment } """.trimIndent() 結果 このシェーダーは雨をリアルに再現できる上に、雲や雪にも対応でき(後者2つのコードはここでは省略します)、ローエンドのデバイスでもスムーズに動作します。 https://youtube.com/shorts/l63i3mQ_n2Y?rel=0 さいごに 見た目が素晴らしく、高いインタラクティブ性と最適なパフォーマンスを備えたエフェクトがAndroid アプリで作成できる。AGSLはそんな多機能なツールです。UIコンポーネントの強化、高度な画像処理、プロシージャルグラフィックスの生成、アニメーションが多い場面でのパフォーマンス向上など、AGSLを使えばアプリが一段と際立ちます。 AGSLがあれば、可能性は創造力次第です。さっそく試して、アプリに命を吹き込みましょう!
アバター
This article is the entry for day 14 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Hi, I’m Nakanishi from the Manabi-No-Michi-No-Eki (Learning Roadside Station) team. This year, the Learning Roadside Station project was officially initiated and subsequently reorganized into a team. As part of our initiatives, we also run an in-house podcast, and for this year’s Advent Calendar, we’d like to share more about it. What is the Manabi-No-Michi-No-Eki (Learning Roadside Station)? The Manabi-No-Michi-No-Eki (Learning Roadside Station) project aims to enhance the accessibility and effectiveness of the frequently held in-house study sessions. The initiative is driven by dedicated volunteers within the company, aiming to support study sessions and promote a culture of knowledge-sharing throughout the organization. Reading Session to understand the General Managers’ Meeting Minutes The Learning Roadside Station Podcast showcases interviews with individuals who organize study groups at KTC. This segment is called "A Peek into the Study Session Next Door". This time, we will be speaking with Omori-san and Takagi-san, the hosts of the "Reading Session for General Manager’s Meeting Minutes." Interview Hoka-san: Could you start by introducing yourselves? Omori-san: Yes, I’m Omori from Corporate IT. I usually handle PC kitting at the Muromachi 16th Floor Center. I am a member of the Asset Platform Team, responsible for managing work devices and SaaS account licenses. I am also in charge of preparing and collecting devices for new employees. Takagi-san: Yes, I’m Takagi, also part of the Corporate IT team. I work in the Tech Service Team and commute between Jimbocho and Muromachi. As a member of the Service Desk, I manage internal inquiries and offer problem resolution. Specifically, I am responsible for managing Self-Service Management (GSM) and OPIT Management. Hoka-san: Thank you. Could you share how the "Reading Session for General Managers’ Meeting Minutes" began? Omori-san: It all began with Kinchan in Nagoya, who initially proposed the idea. In Corporate IT, we rarely have direct access to frontline business information. Therefore, we started this session to enhance productivity by sharing the General Managers’ Meeting minutes, discussing them, and learning from one another. Takagi-san: I feel the same way too. By reviewing the meeting minutes, we can anticipate business trends and apply that insight to our work. For example, we can proactively prepare before official requests are made, enhancing work efficiency. Hoka-san: What kind of impact has this session had so far? Omori-san: Although it may not always directly relate to our daily tasks, reviewing the meeting minutes helps us understand the background of projects, allowing us to make better and more informed proposals. This improves the quality of operations. Takagi-san: I completely agree. Reading the meeting minutes allows us to grasp business movements and respond more efficiently to unexpected requests. The insights gained from the meeting minutes are invaluable for making informed business decisions and proposals. Hoka-san: What are your future plans for this session? Takagi-san: I’d like this session to also serve as a platform for facilitators to challenge themselves and enhance their skills. We aim to encourage more new participants, creating a lively and engaging learning environment. Omori-san: I completely agree. To deepen business understanding, I’d like to continue reading and discussing the meeting minutes, enabling all participants to apply this knowledge to their work. We also aim to gather and organize information, allowing people to catch up later if needed. Hoka-san: Finally, is there any message you’d like to share with the listeners? Omori-san: This session is open to everyone. If you’re interested, please feel free to join us! Let's enhance our business knowledge and elevate work quality together. Takagi-san: We’re planning to create a Slack channel to share announcements. Participating in this session will help deepen your understanding of the business and enhance your work. Hoka-san: Thank you both for your time today. This time, we explored the details of the Reading Session for the General Managers' Meeting Minutes, including its operational background and future prospects. We hope you look forward to the next study session!
アバター
登壇レポート 岡(okapi) 2025年2月20日にAppiumの勉強会「 Appium Meetup Tokyo 」に登壇してきました。 発表したスライドは、「効率的なアプリ自動化のためのガイドラインと実践方法」( https://speakerdeck.com/kintotechdev/xiao-lu-de-naapurizi-dong-hua-notamenogaidoraintoshi-jian-fang-fa ) で確認できますので、ぜひご覧ください。 Appium Meetup Tokyoを行った背景 弊社の新規開発アプリの不具合発生率は、Webに比べると高い傾向(10倍近いことも) ↓ テスト負荷が高いので、アプリのテスト自動化をしなくては ↓ Appiumで自動化作業開始 ↓ 情報を探してもAppiumについて学ぶ場所が見つからない ↓ であれば自分達で開催してしまおう!! ということで登壇してきました。 発表資料で意識した点 ①  弊社では、QA業務に理解のある開発エンジニアがとても多いので、前向きに作業を進められやすく、いっしょに品質を高めて、作り上げられるといった利点があります。 その点をアピールするため、QAと開発チームで協力して「自動化しやすいアプリの作成」をしている点を主として説明しました。 ②  質問もしやすいように、Appiumが分からない人でも理解できる資料を意識して作成していたため、 オンラインで7件、オフラインで7件の計14件の質問をいただき、盛り上がって嬉しかったです。 質問例 こちらにKINTOかんたん申し込みアプリログインの各ステップについて教えます。 Q1 : 開発とQAと協力している点で、IDを振る際に同じような要素がたくさんあるページではどのようにIDを振っていますか? 例えば、先ほどスライドに映っていたページで車種がたくさん表示される画面ではどのようにしていますか? A1: IDはオブジェクトや画面名ごとに定義しており、必ず一意になるように設定しています。 車種が表示されるページでは、開発チームで使用している車種情報の仕様書に沿ってIDを振っています。 Q2: IDは、iOSとAndroidで別で定義することもあるのでしょうか? A2: Appiumで使用するIDについては、全てiOSとAndroidで共通のIDを設定しています。 登壇した感想 外部での発表は初めてでしたが、事前に社内の合同勉強会で練習を行い、本発表前にも十分な準備をしたため、問題なく登壇することができました。私たちのように外部での発表経験がない方も、まずは身近な人や社内で練習を重ね、その後に外部で発表すると、発表が苦手な方でも取り組みやすいので、おすすめです。 練習風景です ↓ 今後に向けた意気込み 日本国内にはAppiumを学ぶ場所がほとんど存在しないため、我々のAppiumに関する知見やノウハウを引き続き「Appium Meetup Tokyo」( https://autifyjapan.connpass.com/event/342867/ ) で発信し、良いコミュニティを築けるように頑張ります! 登壇レポート パンヌウェイ (Pann Nu Wai) 今回はAppiumの取り組みについてお話ししたいと思います。 Appium Meetup Tokyoを行った背景 社内でテスト自動化を主に行っているパンヌウェイです。 Appium の取り組みを広げるために様々な活動を行っていますが、その一環として社内の合同勉強会で話をさせてもらいました。 この機会を通じて、私たちのチームがどのようにしてテスト自動化を進めているのかを共有しました。そして合同勉強会での発表内容をブラッシュアップし、今回はAppium Meetup Tokyoで発表することができました。 発表資料で意識した点 チーム内でAppium を利用して、モバイルアプリ自動テストできたことを4つのステップとして説明しました。 自動化テストエンジニアではない方向けにも、テスト仕様書を読めばソースコードも把握できるように、仕様書作成から自動化テストのパフォーマンスまで含めて記述しています。 登壇した感想 外国人として日本語の発音は少し苦手ですが、登壇に向けて、何度も練習を重ねた結果、当日の登壇は無事に成功しました。 多くの方々からフィードバックをいただき、大変有意義な時間を過ごすことができました。 練習風景です ↓ 今後に向けた意気込み また、私は外部にもAppiumに関する情報を発信していきたいと考えています。 社内の取り組みを外部に広めることで、多くの人々と知識や経験を共有し、さらにテスト自動化の分野を発展させていきたいと思っています。 次回の登壇に向けて、さらに内容を充実させ、より多くの価値を提供できるように頑張ります。これからもどうぞよろしくお願いいたします。 最後に 本記事は「Appium Meetup Tokyo」の登壇レポートですが、 第1回イベントの「開催レポート」( https://blog.kinto-technologies.com/posts/2025-02-20-Appium-Meetup-Tokyo-開催レポート/ ) も執筆していますので、併せてお読みいただけると嬉しいです。
アバター
Introduction Hello, I'm Kuwahara from the SCoE Group at the Osaka Tech Lab in KINTO Technologies (KTC). SCoE stands for Security Center of Excellence, a term that might not be widely recognized yet. In April 2024, KTC restructured the CCoE team into the SCoE group. To learn more about the SCoE Group, check out the article SCoE Group: Leading the Evolution of Cloud Security . For more details about the Osaka Tech Lab, KTC's Kansai base, visit Introduction to Osaka Tech Lab . The mission of the SCoE Group is to "implement real-time guardrail monitoring and improvement activities" across AWS, Google Cloud, and Azure environments. These activities focus on three key areas: Preventing security risks Continuously monitoring and analyzing security risks Responding promptly when a security risk arises In this post, I’ll provide a closer look at the work of KTC’s cloud security engineers. A Day in the Life of a Cloud Security Engineer To provide a clearer picture, I’d like to walk you through a typical day for a cloud security engineer (please note that due to the sensitive nature of the field, some aspects cannot be shared in detail.) Checking alerts The first thing we do in the morning is check whether there are any high-risk alerts. We use CSPM (Cloud Security Posture Management) and threat detection services to understand the security status of the entire cloud environment and check whether there are any alerts that require immediate action. KTC uses services such as AWS Security Hub , Amazon GuardDuty , and Sysdig Secure for CSPM and threat detection services. In checking alerts, the following are considered: Alert prioritization : Alerts are classified and prioritized based on their severity and scope of impact. Alert Triage : Identify the cause of an alert and take necessary action. Management of false positives (”over-detection”) : Security tools can sometimes produce false positives. This may cause activities that are actually not problematic to be reported as alerts. A cloud security engineer also manages these as part of alert handling. Identification of operations required for work : This is related to managing false positives, but some alerts may be triggered by operations required for work. For example, this includes maintenance tasks regularly performed by the person in charge of each product. A cloud security engineer identifies these activities and responds to them appropriately. This allows you to understand the security status of your entire cloud environment and check for any alerts that require immediate action. Information Gathering and Catch-up Next, a cloud security engineer catches up on cybersecurity trends and the latest information on cloud services such as AWS. The following are used as information sources. X (formerly Twitter) : Cloud security engineers follow cybersecurity experts and industry leaders on X (formerly Twitter). They share the latest threat information and countermeasures, allowing for real-time information gathering. Official news and blogs from AWS and Google Cloud : Official information from cloud service providers is an important source of information about new feature releases and security updates. This helps cloud security engineers stay informed about new service launches, the latest technological trends, and best practices. Other news sites : By regularly checking news sites and blogs focused on cybersecurity, cloud security engineers can understand trends across the industry and catch up with the latest threats and attack methods. Threat Detection with SIEM KTC uses Splunk Cloud Platform as its SIEM (Security Information and Event Management). It aggregates security-related logs in Splunk and provides an environment for cross-sectional analysis and monitoring of logs. That day, I discovered a suspicious log on the Splunk dashboard. The log stated: "Attempted to create a resource for a service restricted by Google Cloud's organizational policy, but the operation failed." We were able to determine the general activity from the information in the dashboard for Google Cloud Audit logs that we created using Splunk, but we will investigate in more detail. First, we identify users who are repeatedly retrying to create resources for services restricted by Google Cloud organizational policies. User information is masked before being logged in Google Cloud's audit log (audicy_denied), so users cannot be identified from this log alone. We identify users by analyzing cross-sectional logs together with terminal logs, etc. We created a query to use for this analysis and identified the users in question. Next, we create queries to further analyze the behavior of the identified users and analyze the logs. It appears that the identified user is attempting to use the AI/ML service, Vertex AI . Since no requests for the use of Compute services were made in the relevant project, the use of Compute services is restricted by the organizational policy. When using Notebook with Vertex AI, a Compute Engine (GCE) instance is launched. Therefore, this is a violation of the organizational policy. Ultimately, we determined that this was a harmless activity, citing a omission of information about the services to be used when applying for a new Google Cloud project. Cost optimization for Cloud Vendor-Native Security Services Security services provided by cloud vendors are charged on a pay-as-you-go billing basis, so as cloud resources increase, the security service charge also increases. Our idea of ​​"security" is "security for business," and "security that hinders business" is unacceptable. Therefore, the "balance between security and cost" is also an important point, and cost optimization of security services is also included in the SCoE Group's mission. On that day, I investigated the potential for cost optimization of several security services that accounted for a high proportion of the overall cost. The graph above shows the services that were targeted in the analysis this time. Among them, I paid particular attention to AWS Config . (Specific item names have been masked.) AWS Config is a service for auditing, evaluating, and recording the configuration of AWS resources. Until November 2023, the only recording method for AWS Config was “a method that records every time a change in resource configuration has occurred." This method is called "recording frequency: continuous recording." In other words, if the frequency of resource changes is high, the number of records in AWS Config increases, and the usage fee increases proportionally. As an example, let's look at network-related events. The data below is a graph showing the number of VPC and network-related configuration changes in AWS account over a one-week period. You can see that CreateNetworkInterface and DeleteNetworkInterface , which correspond to the creation and deletion of Elastic Network Interfaces (ENIs), occur approximately 17,000 times per day. KTC is using Fargate, an Amazon Elastic Container Service (ECS). For this reason, the creation/ deletion of ENI occur each time an ECS task (container) starts/stops. Under these circumstances, if you have AWS Config set to “Recording frequency: Continuous recording," the number of AWS Config records associated with these changes will be huge, and the amount you are charged will increase accordingly. However, starting in November 2023, a new feature that allows you to select "Recording Frequency: Daily Recording" was added to AWS Config. This new feature allows us to adjust the recording frequency for each resource type, providing the flexibility in balancing security and cost. In general, this setting is believed to help optimize the cost of using AWS Config. However, this is only the case if you are not using AWS Control Tower . AWS Control Tower is a service for centrally managing the governance of multiple AWS accounts. If you use AWS Control Tower to manage AWS Config in your AWS account, check Guidance for creating and modifying AWS Control Tower resources . Please pay attention to the following sentence at the beginning of the guidance: Do not modify or delete any resources created by AWS Control Tower, including resources in the management account, in the shared accounts, and in member accounts. If you modify these resources, you may be required to update your landing zone or re-register an OU, and modification can result in inaccurate compliance reporting. As this statement indicates, modifying or deleting resources created by AWS Control Tower by any means other than AWS Control Tower is not recommended . Specifically, as of December 2024, AWS Control Tower does not provide the feature to modify the frequency of AWS Config recording. Therefore, changing the recording frequency of AWS Config under AWS Control Tower management is not recommended, and the official documentation also states that it may cause problems. Taking into account the content of the official documentation, I also contacted AWS Support just to be sure and received the same opinion. In this way, when “a setting itself is possible but poses the risk of problems or is not recommended,” it becomes difficult to maintain stable cloud security and governance. The result could be "security that hinders business" . In light of the above, we decided to postpone changing the recording frequency of AWS Config for now and submitted an improvement request to AWS Support. I believe that proposing such an improvement request to enhance the convenience of cloud services is a modest yet very important initiative. Preparation for a Security Study Session Finally, I created presentation materials for our regularly held in-house security and privacy study sessions. The SCoE Group has formulated "Cloud Security Guidelines" that summarize the key points of cloud security for"requirements definition," "design," and "development" phases of product development, and has made them available in-house. This set of guidelines is an important resource for ensuring compliance with the security policies of the group companies to which KTC belongs, minimizing security risks, and supporting efficient development. I host study sessions to raise awareness and enhance understanding of the Cloud Security Guidelines. In the study sessions, I provide detailed explanations of each item in the guidelines, while also incorporating specific cases and practical advice. On that day, I created presentation materials on IAM (Identity and Access Management) best practices, ensuring the materials were concise enough to fit within a 20-minute timeframe. Conclusion I’ve shared a glimpse into a day in the life of a cloud security engineer at KTC. While this is just a snapshot, I hope it helped you gain a better understanding of what the role entails. The SCoE Group is currently looking for new team members. Whether you have hands-on experience in cloud security or are simply passionate about the field, we’d love to hear from you. Feel free to reach out to us. For more information, please check here .
アバター
This article is part of Day 13 of the KINTO Technologies Advent Calendar 2024 Hi, I’m Nakanishi from Learning Roadside Station. This year, the Learning Roadside Station project was officially launched and structured as an organization. As part of our initiatives, we also run an in-house podcast, and for this year’s Advent Calendar, we’d like to share more about it. What is "Learning Roadside Station"? “Learning Roadside Station” is a project aimed at making in-house study sessions, which are frequently held, more accessible and effective. The initiative is led by passionate volunteers within the company, with the goal of supporting study sessions and fostering a culture of knowledge sharing across the organization. Factory Automotive Study Group The Learning Roadside Station Podcast features interviews with employees who organize study groups within the company. This segment is called "Surprised! My Neighbor's Study Group." This time, we interviewed Miura-san from the Factory Team. Interview Hoka-san: Thank you, Miura-san. Could you introduce yourself and tell us about your role? Miura-san: Thank you very much. Officially, I am the Team Leader of the KINTO Factory Team, part of the Project Promotion Group in the Project Development Division. The Factory Team develops products not only with KTC but also in collaboration with KINTO's General Planning Department. Hoka-san: For this interview, we’d like to focus on the fact that you are studying automobiles within the Factory Team. Could you tell us more about that? Miura-san: First of all, I want everyone to enjoy their work. I believe that understanding the products we sell makes the work more engaging and rewarding. Since we handle automobile-related products, having technical knowledge about cars makes the development process even more enjoyable. Having a background in the automotive industry, I felt that sharing my knowledge would help us make better proposals, which is why I started the study group. Hoka-san: How did you decide to share that knowledge? Miura-san: We hold online study sessions. At first, session was for an hour, but now we've reduced it to 30 minutes and hold it once a month. Recently, we’ve been discussing the evolution of in-car networks and navigation systems. Hoka-san: How have the participants responded to the study sessions? Miura-san: Based on survey feedback, many participants found the information fresh and valuable since they wouldn’t normally come across such details in their daily work. The number of participants has not decreased, and they are listening with interest. Hoka-san: How long have you been running these sessions? Miura-san: At first, the sessions were one hour long, but now it’s 30 minutes and we hold them once a month. Hoka-san: What topics have you covered in your study sessions so far? Miura-san: Recently, we talked about the evolution of in-car networks and navigation systems. We also discussed how the automotive industry is evolving based on insights from CES in Las Vegas. Hoka-san: Did you attend CES in Las Vegas? Miura-san: I wasn’t able to attend in person, but I shared my own thoughts based on the exhibition content that are open to the public on the Internet. Hoka-san: Do you have any upcoming study sessions planned? Miura-san: Next, we’ll be discussing the vehicle installation process. Using dealer manuals, we’ll explore how different parts are assembled and installed in cars. Hoka-san: What should I do if I want to join the study sessions? Miura-san: Our sessions are conducted online, so anyone interested is free to participate. We’re also working on creating a system to visualize and organize study session information for easier access. Hoka-san: What inspired you to start the study group? Miura-san: It started as a team-building initiative. Our group transitioned from a project team to a formal team, and I wanted to ensure that everyone had a deeper understanding of automobiles. Hoka-san: Lastly, do you have a message for your colleagues? Miura-san: Since we work in the automotive industry, I believe that deepening our knowledge of cars makes our work more enjoyable and meaningful. If you’re interested, I encourage you to join our study sessions! Hoka-san: Thank you very much, Miura-san. I hope you will continue to share the fascinating world of automobiles with your colleagues through these study sessions. This time, we shared insights about the Factory Team, the background to its operations, and future prospects. Please look forward to the next study session!
アバター