【AWS Amplify Gen 2】radikoのお気に入り番組を管理するWebサイトの構築

概要

Web上でラジオが聴けるサービスradikoで、お気に入りの番組を管理したい。
アプリ版ではお気に入りの番組をフォローできるが、ブラウザ版ではその機能がないため、聴取履歴を辿るなどして番組ページを表示する必要があり、やや使いづらい。
そこで、お気に入り番組を保存し、その番組へのリンクを表示するというシンプルな機能を実現するWebサイトを構築した(下図)。

リポジトリGitHub - vulture039/radio-extension

スクリーンショット

目次

全体の構成

なるべくお金を掛けずにWebサイトを運用するという方針で、以下の通り選定した。

分類 採用技術 採用理由
インフラ Amplify Gen2 バックエンドとフロントエンドを統合管理できる
DB DynamoDB NoSQLだと従量課金のため低コスト
バックエンド Lambda サーバレスだと従量課金のため低コスト
フロントエンド Vue.js 利用経験あり

【参考】
趣味Webサービスをサーバーレスで作る ― 格安編 - maybe daily dev notes
個人開発・スタートアップで採用すべき最強のアーキテクチャを考えた

最終的には下図のような構成となった。

構成図

実装

Vue.js

Amplifyプロジェクトを先に作成すると、既存ディレクトリに対してVueプロジェクトを追加できない。そのため、まずVueプロジェクトを作成する。
Amplify Gen2はTypeScript Baseであるため、Vueプロジェクト作成時にTypeScriptを追加し、その他はデフォルト設定とした。

$ npm create vue@latest
 > Add TypeScript? … Yes

$ cd <your-project-name> 
$ npm install 
$ npm run dev

Vue.jsのクイックスタートより

Amplify

Amplifyプロジェクトを作成する。

$ npm create amplify@latest

sandboxを作成・起動する。sandboxについては後述

$ npx ampx sandbox

DynamoDB

Set up Amplify Dataを参考に記述する。
以下、詰まったポイント。

  • フロントから/amplify/data/resourceのSchemaをimportした際のエラー
    File '/xxx/amplify/data/resource.ts' is not listed within the file list of project '/xxx/tsconfig.app.json'. Projects must list all files or use an 'include' pattern.
    → tsconfig.app.jsonのincludeに"amplify/**/*.ts"を追加する
  • 画面表示時のブラウザでのコンソールエラー
    Uncaught (in promise) Error: Client could not be generated.
    → `Amplify.configure()した後にcreateAppする

Lambda

Set up Amplify REST APIを参考に、LambdaでREST APIを動かす。

フロントからのリクエストは、Fetch dataを参考に実装する。
なお、Vueで<script setup lang="ts">としている場合は、catch (error: any)とする必要がある。: any をつけないと、errorの中身を参照した際に'error' is of type 'unknown'.となる。

API認証

REST APICognito User Pool Authorizationに記載のあるAmplify.configureの設定を合わせ、フロントのmain.tsを以下のように記述した。 これにより、各APIのリクエストにヘッダが付与される。

import outputs from '../amplify_outputs.json';

Amplify.configure(outputs, {
  API: {
    REST: {
      headers: async () => {
        return { Authorization: await getIdToken() };
      },
    },
  },
});

const existingConfig = Amplify.getConfig();
Amplify.configure({
  ...existingConfig,
  API: {
    ...existingConfig.API,
    REST: outputs.custom.API,
  },
});

Amplify.configureを1つにまとめたかったが、意図したように設定されなかったため、一旦諦めた。

ユーザ認証

フロントエンド

Set up Amplify Authの通り、'npm add @aws-amplify/ui-vue'ui-vueを追加した後、App.vueで以下のようの利用する。

import { Authenticator } from "@aws-amplify/ui-vue";
import "@aws-amplify/ui-vue/styles.css";

  <authenticator>
    <template v-slot="{ user, signOut }">
      <h1>Hello {{ user.username }}!</h1>
      <button @click="signOut">Sign Out</button>
      <!-- ログイン後に表示するコンポーネントをここに記述 -->
    </template>
  </authenticator>

【参考】
Amazon Cognitoのサインインに使えるユーザー属性
External identity providers - AWS Amplify Gen 2 Documentation

バックエンド

今回はCogintoを利用するため、authorizationTypeをデフォルトのIAMからCOGNITOに変更する。

const cognitoAuth = new CognitoUserPoolsAuthorizer(apiStack, "CognitoAuth", {
  cognitoUserPools: [backend.auth.resources.userPool],
});
const programsPath = radioExtensionApi.root.addResource("programs", {
  defaultMethodOptions: {
    authorizationType: AuthorizationType.COGNITO, // 変更
    authorizer: cognitoAuth, // 追加
  },
});

sandboxについて

Use cloud sandbox in dev environment - Vue - AWS Amplify Gen 2 Documentation
Amplifyでは、AWS Cloud上で使い捨て可能なsandboxと呼ばれる環境がある。
sandboxは開発者ごとに独立して利用することができる。

上記の公式ドキュメントの図を見ると、バックエンドのリソースがsandbox上で動作すると思われる。つまりフロントエンドはlocalで起動し、sandbox上で動くバックエンドに対してリクエストする形になると考えられる。
なお、接続情報等はsandboxを立ち上げた際amplify_outputs.jsonに出力される。

環境変数

バックエンドのリソースからamplify_outputs.jsonを参照すると、sandboxは起動するが、本番環境でのデプロイに失敗する。

まずデプロイ前に、Amplify > All apps > {app-name} > Hosting: Build settings から、amplify.ymlを編集する。
npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID--debugを追加する。
その状態でデプロイすると、以下のようなエラーメッセージが出力される。

2025-02-02T04:55:31.499Z [INFO]: [DEBUG] 2025-02-02T04:55:31.499Z: ✘ [ERROR] Could not resolve "../../../amplify_outputs.json"
                                 amplify/functions/programs-api-function/handler.ts:4:20:
                                 4 │ import outputs from "../../../amplify_outputs.json";
                                 ╵                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

そこで、以下のように設定することとした。

  • sandbox
    .env.localに環境変数を記述し、npx dotenv-cli -e .env.local -- ampx sandbox でsandbox起動時に環境変数を読み込み
  • 本番環境
    Amplify > All apps > {app-name} > Hosting: Environment variables から環境変数を設定

ログの確認

Function内でconsole.logを記述するとログを出力できる。
自分で追加したものを含む実行時のログは、下記の手順で確認可能。
Functions, Log groupsの数が多く、確認対象がどれか分からないためAPI Gatewayから順に辿ることとした。

【手順】

  • API Gateway > APIs から対象の Resources を選択
  • 確認したいエンドポイントを選択
    Test タブでAPIを叩ける。
  • Lambda integration のバルーンに表示される Function のリンクを押下
  • Lambda > Functions > Monitor タブから View CloudWatch logs を押下
  • CloudWatch > Log groups > Log streams タブから該当の Log stream を選択
    Last event time を頼りに選択する。

エラー

$ npx ampx sandboxを実行した際のエラーと対応をまとめる。

The given region has not been bootstrapped

% npx ampx sandbox
The given region has not been bootstrapped. Sign in to console as a Root user or Admin to complete the bootstrap process, then restart the sandbox.

このときブラウザが開かれるが、以下の表示

CDKToolkit is already setup successfully in {region} Region. You may now close this browser window.

npx ampx sandboxを再実行しても同様のエラー

対応

  • IAM > Users からユーザを選択
  • Permissions policiesにAdministratorAccessを追加
  • cdk bootstrap aws://{account-id}/{region}を実行
  • 再度npx ampx sandboxを実行

xxx is not authorized to perform

bootstrap時、権限が足りないと以下のようなエラーとなる。

11:47:57 PM | CREATE_FAILED        | AWS::ECR::Repository    | ContainerAssetsRepository
Resource handler returned message: "User: arn:aws:iam::{account-id}:user/{user-name} is not authorized to perform: ecr:CreateRepository on resource: (以下略)

11:47:57 PM | CREATE_FAILED        | AWS::SSM::Parameter     | CdkBootstrapVersion
Resource handler returned message: "User: arn:aws:iam::{account-id}:user/{user-name} is not authorized to perform: ssm:DeleteParameter on resource: (以下略)

11:48:01 PM | DELETE_FAILED        | AWS::SSM::Parameter     | CdkBootstrapVersion
Resource handler returned message: "User: arn:aws:iam::{account-id}:user/{user-name} is not authorized to perform: ssm:DeleteParameter on resource: (以下略)

対応
ロールバックにも失敗するため、CloudFormation > StacksからCDKToolkitを手動で削除し、権限を追加した。

Failed to bundle asset

% npx ampx sandbox
Failed to instantiate nodejs function construct
Caused By: Failed to bundle asset xxx

npx ampx sandbox--debugを付与し、詳細を確認したところCould not resolve "luxon"だった。

対応
パッケージが足りていなかったため、npm install luxonして再度sandboxを起動した。

所感

Amplify Gen2の公式ドキュメントはあるものの、ChatGPTを利用しつつ情報を探し回り、手探りで実装を試すなど、動かすのに苦労した。
sandboxについては、反映までしばらく待ったり、一部ホットスワップが効かない部分もあったり、使いこなすのにある程度慣れが必要だと思った。
また、sandboxでは動作していても、本番環境へのデプロイ時にビルドが失敗してしまい、その解消に試行錯誤を要した。
しかし、フロントエンド・バックエンドを問わず、AWSのリソースをまとめてコードとして管理できるのは便利だと思った。
AWSやTypeScriptに馴染みがなかったので実装に時間が掛かったが、なんとか動かすところまでは持っていけて良かった。

ニキシー管時計の製作

目次

作ったもの

概要

ニキシー管時計を製作した。2021年頃に完成させたものなので今更ではあるが、製作時のメモから主要な内容を記事にまとめておく。

複数の作例や、そこで使用されていた各ICのデータシート、アプリケーションノート等を参考に回路を設計した。ダイナミック点灯方式を採用し、ニキシー管の制御にはアノード・カソードともにシフトレジスタを用いることでArduino Nanoのピン数に収まるようにした。時刻の取得にはRTCモジュールではなく、その他の数字を表示することも考慮してRaspberry Pi Zero Wを使用した。
回路設計が完了した後はプリント基板の設計をした。制御用の回路は部品点数が多くなったため、自動配線ツールを利用した。PCBWayで注文したプリント基板に各部品を実装し、最後に基板底面のサイズに合わせたケースを3Dプリンタで出力した。

困った点

製作時に困った点とその対応を以下にまとめた。

昇圧されない

最初はNPN型のトランジスタを使用していたが、PNP型に変更した。また同一のICが使用された昇圧回路キットと自作の回路を比較し、分圧回路の抵抗値を変更した。

明るさが安定しない

シフトレジスタをストレージレジスタ内蔵の74HC595に変更した。

ゴーストの出現

ニキシー管のアノード側の接地抵抗の値を大きくした。また下記の制御順に設定し、それを切り替える時間を調整した。
切り替えの時間については「人の目の時間分解能は約50msから100ms程度(wiki)」であることから、ダイナミック点灯の1ループが50ms未満となれば良いはず。時間が短すぎると電荷を逃しきれずゴーストが発生する一方、長すぎるとちらついて見えるため、調整が必要となる。

【制御順】

  1. カソード(数字)の指定
  2. アノード(点灯管)の指定 -> 点灯
  3. アノードのリセット -> 消灯
  4. カソードのリセット

ラズパイによるニキシー管の制御

表示する数字の指定とニキシー管の制御をラズパイに集約しようとしたが、綺麗に点灯しなかった。Arduinoと比較してラズパイのsleepの精度が低かったため、ニキシー管の明るさが安定しなかったと思われる。一方で明るさを安定させるためにsleepの時間を長くするとゴーストが出現してしまうため、以下のように機能を分担させた。

【機能の分担】

ラズパイからArduinoへの通信ができない

ラズパイとArduinoのGNDを共通化できていなかったため修正した。また、通信の方法をソフトウェアシリアルからハードウェアシリアルに変更した。

資料まとめ

回路図

制御用

マウント用

PCB

制御用

マウント用

コード

Arduino

Raspberry Pi 以前は新型コロナウイルス感染症の新規陽性者数(日別)を表示していたが、データ集計が終わっていた。 そこで、ビットコインの最終取引価格を表示する機能に変更した。

その他画像

回路の仮組み

部品の配置確認 部品が干渉するなどの問題がないかを実際に並べてみた様子。
激落ちくんの上に印刷した紙を貼り付けて確認した。

到着した基板

リンクまとめ

参考

利用ツール等

【Notion】テキストブロック内での改行をShift + EnterからEnterに変更する

概要

Notionのテキスト入力操作では以下のようにキーが割り当てられている。

  • 新規テキストブロックの作成:Enter
  • テキストブロック内での改行:Shift + Enter

ひとまとまりの文章を入力する際、改行のたびに「Shift + Enter」を入力するのはストレスを感じる。 デフォルトの設定から変更する方法は見つけられなかったため、JavaScriptを追加してキー入力を置き換えてみる。

手順

JavaScript

扇形PCデスクの自作(組立編)

前回:扇形PCデスクの自作(設計編)

天板の到着

当初の予定日より数日早く出荷され、注文確定から年末年始を含めて6週間程度で到着した。

天板の加工

位置を測りつつ脚を仮置きした。 ボルトで固定する位置に、穴の中心と円周が分かるようにシャープペンシルで印を付けた。 ここに鬼目ナットを埋め込むための下穴を開けていく。

いきなり径の大きな穴を開けると位置がズレる恐れがあるので、直径1mm~11mmまでドリル刃を交換しながら1mmずつ穴を広げていった。 また、穴の深さが分かるようにドリル先にマスキングテープで目印を付け、貫通させないように慎重に掘り進めた。
穴が浅すぎても鬼目ナットがきれいに埋め込めないため、千枚通しに目印を付けて適宜穴の深さを確認した。

穴が掘れたら、木くずを除去して鬼目ナットを埋め込んだ。

完成

天板と脚をボルトで固定した後、裏返して完成。

振り返り

無事に脚を取り付けることができ、満足できる仕上がりとなった。
今回の反省点をまとめると主に以下の3つ。

ボルトの長さを間違えた

最初は皿ボルトを購入したのだが、必要な長さよりも短かった。 調べてみると、基本的にはボルトの長さ表記はボルトの頭を含まないが、皿ボルトについては頭を含めた全長を指すらしい。

注意したいのは、他の小ネジやボルトなどの頭のあるネジの寸法は首下からの長さなのですが皿頭の場合は全長、丸皿頭の場合は丸みを除いた皿部の最大径から端までが長さとなります。
参考:十字穴付き皿小ねじ | 富田螺子株式会社

長さが短かったことに加えてそもそも皿頭だと収まりが良くなかったので、ボタンボルトを買い直した。

下穴の加工が面倒だった

今回は自分で下穴を開けたが、貫通させないように少しずつ穴を広げていく作業に時間がかかった。 また、天板を床に置いて作業していたため、やや腰に負担がかかった。 天板を購入したマルトクショップでは穴開け加工の有料オプションもあったので、依頼した方が良かった気がする。

もう少し安定感がほしかった

実用上は特に問題ない程度の安定感はあるが、机に力を加えてみると少し揺れるような状態となった。 どっしりとした机にするため、脚と脚の間に棒を入れて補強するなどの手もあるかもしれない。

扇形PCデスクの自作(設計編)

背景

これまで使用していたPCデスクは奥行きが45cmで、作業するには手狭だと感じていた。 サイズ・デザイン共に満足できる製品を探すよりは自作したほうが早そう、ということでPCデスクを自作した。 自作といっても天板の設計のみで、脚は既製品を購入して組み合わせただけなので、そこまで難しいものではなかった。

天板の設計

天板はマルトクショップという木材通販サイトで購入した。 このサイトでは樹種やサイズ等をフォームに入力して木材を注文することができる。 また、フォームに入力することができないものについても図面を送ると見積もりをしていただけるようなので、図面を作成して送ってみることにした。

天板の形状は、自分が座った位置から等距離に色々なものを置けると便利なのでは?という考えから扇形にしてみた。 部屋に配置したときにデッドスペースができそうだが、そこは後々考えることにする。

Fusion 360で以下の3Dモデルを作成した。

top_board_3d

Fusion 360では、3Dモデルを元に2Dの図面に出力できる便利な機能がある。 2Dの図面に出力後、寸法や加工用の注釈を追加したものが以下の図面。

top_board_2d

木材加工については、面取り加工塗装など、マルトクショップのサイトに分かりやすくまとめられている。 一通り読んだので少し詳しくなった気がする。

樹種や塗装方法の指定とともにpdfの図面を送ると、切り出し元の木材のサイズと木目方向を確認する図面の返信が届いた。 確認後、見積もりとともに決済用のリンクが送られて来るので、支払いを行うと注文が完了となる。

最終的に以下の見積もりとなった。

アカシア(節・白太有り) 集成材フリーカット 25*840*1838mm | 面内側C面(表)+ 磨き その他糸面+磨き ◆ 反り止め:無し ◆自然塗料クリア塗装
※別紙図面加工 | 三方向使用
単価:37680 円 数量:1 金額:37680 円

脚の選定

机に使用する脚を検討し、価格の観点からニトリの組み合わせデスク「プレフェ」用の脚を採用した。 机の手前側にスペースをなるべく取りたいので、両端にT型脚を使用することにした。 また、天板の重心を考慮すると両端のみだとバランスが悪いので、真ん中に脚を追加した。 真ん中には2本セットのうち1本を使用したので、1本余った。

固定方法の検討

天板と脚の固定方法は、付け外しができるような形にしておくと引っ越しの際に都合が良い。 そこで、木ネジで脚を固定するのではなく、天板に鬼目ナットを埋め込んで脚をボルトで固定することにした。

「プレフェ」に付属しているボルトを実測してみたところ、M8の1.0mmピッチであった。 それに適合する鬼目ナットはあまり見当たらなかったため、ボルトを別で用意した。

購入品まとめ

購入したものを表にまとめた。 部品のほか、下穴を開けるために必要な径のドリル刃が手元になかったので、合わせて購入した。 合計で49,089円だった。

部品

品名 価格※ 数量 リンク
天板 37,680 1 https://shop.woodworks-marutoku.com/
T型脚 2,500 2 https://www.nitori-net.jp/ec/product/6240424/
昇降脚2本入り 2,500 1 https://www.nitori-net.jp/ec/product/6240422/
鬼目ナット 呼びM8長さ13mm 1パック(6個) 395 4 https://www.monotaro.com/p/4175/5131/
六角穴付ボタンボルト 呼びM8長さ15mm 1パック(29個) 791 1 https://www.monotaro.com/p/0552/5283/

工具

品名 価格※ 数量 リンク
六角軸木工用ドリルセット 945 1 https://www.monotaro.com/p/6087/9316/
六角軸TiN鉄ドリル ドリル径11mm 593 1 https://www.monotaro.com/p/5469/4999/

※ 価格は購入時点の税込み価格

組立編に続く

【Mac】SpringToolSuite4(STS)でパッケージが同一階層にまとめて表示されない

概要

事象

MacのSpringToolSuite4で「Package Explorer > Package Presentation > Hierarchical」を選択した際に、以下のように同一階層にまとめて表示されない場合がある。

> aaa
   > bbb ← 同一階層に"aaa.bbb"のようにまとめて表示してほしいが、別の階層として表示されてしまう
      > ccc
      > ddd
   .DS_Store ← STS上は表示されない

原因

表示するディレクトリに.DS_Storeファイルが生成されている。

対処

以下のコマンドで.DS_Storeファイルを削除する。

find . -name .DS_Store -print -delete

補足

SpringToolSuite4ではパッケージの表示形式としてHierarchicalとFlat の2つがあり、それぞれ以下のように表示される。

  • Hierarchical
> aaa.bbb  
  > ccc  
  > ddd
> aaa.bbb.ccc
> aaa.bbb.ddd

Hierarchical表示の場合、他にファイルがない場合は同一階層にパッケージがまとめて表示されるが、.DS_Storeファイルが存在する場合には別の階層として表示されてしまう。この状態だとパッケージの階層が深い場合に開きたいファイルを開く手間が増える。
一方でFlat表示にすれば同一階層に表示されるものの、パッケージの数が多い場合にやや見づらくなる。
どのタイミングで.DS_Storeファイルが生成されるのかは不明だが、このファイルを削除することで期待通りに表示される。

削除コマンドについて

findコマンドで、対象ディレクトリ以下の指定したファイルを再帰的に削除する。

find <target_directory> -name <file_name> -print -delete

-name:findする対象のファイル名を指定
-print:対象のファイルを表示
-delete:対象のファイルを削除

Androidアプリのリバースエンジニアリング手順

Androidアプリのソースコードが気になったので、リバースエンジニアリングをしてみた。 手順をまとめた。

関連するファイルの種類

  • apk
    Androidアプリのパッケージ。Google Playではこのファイル形式でアプリが配信される。
  • dex
    Java(Kotlin)のファイルをAndroid環境で実行可能な形式に変換圧縮したもの [1]
  • jar
    Javaのファイルをまとめて圧縮したもの。
  • class
    Javaのコードをコンパイルしたもの 。

手順

実機からapkを取得

AndroidスマホをPCに接続し、以下のコマンドで実機に入っているパッケージの一覧を取得する。今回は、ヨドバシカメラ電子書籍アプリdolyを見てみることにした。

> adb shell pm list packages
...
package:com.yodobashi.doly
...

次に、取り出したいパッケージのパスを取得する。

> adb shell pm path com.yodobashi.doly                
package:/data/app/com.yodobashi.doly-nPQjQrYSk9EIi7lTI85-GA==/base.apk

そして、取得したパスを指定し、base.apkを実機からコピーする。

> adb pull /data/app/com.yodobashi.doly-nPQjQrYSk9EIi7lTI85-GA==/base.apk
/data/app/com.yodobashi.doly-nPQjQrYSk9EIi7lTI85-GA==...lled, 0 skipped. 33.9 MB/s (36025531 bytes in 1.013s)

apkからdexを取り出す

コピーしたbase.apkをbase.zipにリネームし、解凍する。
中に入っているclasses.dexを取り出す。

dexからjarへ変換

d2j-dex2jarを使用し、classes.dexをjarへ変換する。 sourceforgeからDLし、d2j-dex2jar.batを実行する。

> ./d2j-dex2jar.bat ./classes.dex
dex2jar ./classes.dex -> .\classes-dex2jar.jar
Detail Error Information in File .\classes-error.zip
Please report this file to http://code.google.com/p/dex2jar/issues/entry if possible.

エラーが出たようなのでclasses-error.zipを確認すると、summary.txtには以下のように書かれていた。

There are 40 methods fail to translate.

一部変換に失敗したよ、とのことだが、だいたいは変換できたようなので次へ進む。

jar(class)をjavaとして読む

JD-GUIで、変換したjarを開く。すると、jarの中に入っているclassファイルを逆コンパイルして、javaファイルとして表示してくれる。

アプリのソースコード

JD-GUIのスクショ

ソースコードを見てみると、クラス、メソッド、変数の名前がアルファベット1文字になっていたり、不要と思われるクラスがいくつも存在したり、難読化されている。 一方、そのままの名前で読めそうな部分もあった。

参考

Android リバースエンジニアリング
Android リバースエンジニアリング 入門
[1] dexファイルってなんだっけ