5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

富士山登頂記念に、AWS Blocks という山も少しずつ登ってみる(第1弾:五合目から八合目の山小屋へ ── 「地名→標高」アプリをローカル完結で)

5
Last updated at Posted at 2026-08-10

本記事は、BIPROGY / ユニアデックス社内AWSコミュニティ「BIPROGY AWS SPARK」の定期投稿企画第11回目の記事です。他の定期投稿企画の記事は、インデックスページをご覧ください。

fuji002.JPG
(麓からの写真です。雪のように見えますが、「雲」です。)

先日、富士山に登ってきました。下りは寝不足もあり、苦労しましたが、会社のウォーキングイベントで1ヶ月の期間中に毎日平均1万歩を達成していたおかげで、なんとか登頂も達成することができました。地道な積み上げがとても大切ですね。

そんなこともあり、以前から気になっていた AWS の新しいフルスタックフレームワーク AWS Blocks(プレビュー)を少しずつ触ったり、積み上げたりしていきたいと思いました。名前はよく目にするものの、実際に何ができるのかは、正直なところ掴めていませんでした。

そこで、山を登るように、AWS Blocks も少しずつ動かして確かめていくことにしました。第一歩となる今回は、「地名を入れると標高を返すアプリ」を、AWS アカウント不要・ローカル完結・費用0円で作ります。

AWS Blocks は 2026年7月末時点でパブリックプレビュー段階のサービスです。
本記事で使う CLI・API・自動生成されるコードや挙動は、今後のバージョンで変更される可能性があります。バージョン差により、記載どおりでは動かない箇所が出るかもしれません。本文中のコマンド出力・スクリーンショットは、いずれも執筆時点の実機(WSL2)で採取した実物です。最新の仕様は公式ドキュメントをご確認ください。

この記事でやること

AWS の新しいフルスタックフレームワーク AWS Blocks(2026年時点でプレビュー)を、WSL2 上で

  1. 導入(プロジェクト作成〜ローカル起動)
  2. 設計・開発ApiNamespace に独自APIを追加、React + MapLibre で地図UI)
  3. 動作確認(「富士山」で検索 → 座標と標高を取得 → 地図にピン)

まで、AWS アカウント不要・ローカル完結・0円で通します。題材は「地名を入れると、その地点の標高を返して地図にピンを立てる」アプリです。ジオコーディングに OpenStreetMap(Nominatim)、標高取得に国土地理院APIを使います。

標高・座標データはいずれも無料の公開API(国土地理院 / OpenStreetMap)で取得していて、AWS アカウントは不要です。それらのデータを「AWS のサービスを使って取得できるかな?」という問いに本物の AWS サービス(Amazon Location Service)で答えるのは第2弾で。まずはローカルで“登山口”まで。

検証環境

項目 バージョン
OS WSL2 (Debian GNU/Linux) / 12.14
Node.js v24.14.1(AWS Blocks 要件: 22以降
npm 11.13.0(要件: 10以降)
ブラウザ Windows 側の Chrome

1. AWS Blocks とは何か

一言で言うと、「バックエンドを1ファイルに宣言的に書くと、ローカルではエミュレータ、AWS では本物のマネージドリソースとして同じコードが動く」フルスタックフレームワークです。
AWS Blocksの開発者ガイドには、IFC (Infrastructure from Code) の機能を備えていると記載されています。

特徴は大きく3つあります。

1. 同じコードがローカルと本番で動く

DistributedTable はローカルではインメモリ、AWS では DynamoDB。AuthBasic はローカルではJWT、AWSではユーザ管理基盤。ApiNamespace はローカルではHTTPサーバ、AWSでは Lambda + API Gateway ── というように、同じ宣言が環境に応じて実体化します。

2. 接続先の設定が、アプリのコードから消える

フロントエンドはバックエンドの API を import { api } from 'aws-blocks'直接 import して呼びます。API Gateway の構築も、デプロイして初めて決まる URL の受け渡しも、SDK の初期化も、アプリのコードには出てきません。そしてバックエンドの引数や戻り値の型が、中間のインフラを意識することなくそのままフロント側の型になります。バックエンド側で引数や戻り値の型を変えれば、フロントは即コンパイルエラーです。API の変更漏れが「実行時の障害」から「ビルド時のエラー」に変わる ── 設計上の効きどころはここだと思います。

3. Building Block の組み合わせでアプリを作る

DistributedTable / AuthBasic / ApiNamespace / Database(PostgreSQL) / FileBucket / Realtime / AsyncJob / Agent(AI) などの部品を組み合わせて構成します。

Amplify や App Studio との違いを雑に言えば、AWS Blocks は 「コードファーストで、ローカル環境とAWS環境が対称」 な点が推しどころです。

この時点で、私はこう考えました。 「ローカルでは AWS なしで動くのが売りなら、地名検索も“ローカルは AWS を使わず”無料の公開API(Nominatim / 国土地理院)で済ませよう」と。

── これは半分正解で、半分は落とし穴でした。上の DistributedTable のような Block プリミティブなら、自動モックのおかげで「ローカルは AWS なし」が普通に叶います。でも(第2弾で使う)Amazon Location は Block ではないので自動モックが無く、“ローカルで AWS を避ける”には自分で代替を書く必要がありました。この気づきが第2弾の主題です。

※念のため:これは「AWS Blocks では AWS サービスを使えない」という意味ではありません。デプロイすれば Block は本物の AWS になりますし、非Block の AWS サービスも普通に呼べます。


2. WSL2 への導入

2.1 前提の確認

node --version   # v24.14.1
npm --version    # 11.13.0

要件は Node 22以降 / npm 10以降。満たしていればOKです。

2.2 プロジェクトの作成

AWS Blocks 公式の初期化コマンドを実行します。

npm create @aws-blocks/blocks-app@latest elevation-app -- --template react

npm create @scope/foo は npm の仕様で @scope/create-foo パッケージに解決されます。つまりこのコマンドは実体として @aws-blocks/create-blocks-app を実行しています。

テンプレートは --help で一覧できます(実出力):

Available templates: default, bare, react, backend, nextjs, auth-cognito, amplify, demo

  default        Vite + lit-html frontend with auth, DynamoDB, and realtime (safe default)
  react          Vite + React 19 frontend with auth, DynamoDB, and realtime todo app
  nextjs         Next.js 16 App Router with Server + Client Components calling Blocks
  backend        Backend-only — Blocks API + CDK, no frontend
  ...

今回は地図UIを React で書くために react を選びました。実行結果(実出力):

Creating Blocks app in /home/.../elevation-app...
Installing dependencies...
added 345 packages, and audited 366 packages in 50s
✓ Blocks app created!

Next steps:
  cd elevation-app
  npm run dev
Then open http://localhost:3000

生成される構造(react テンプレート):

elevation-app/
├── aws-blocks/
│   ├── index.ts          # バックエンド(Block定義 + API定義)
│   ├── index.cdk.ts      # CDK スタック定義
│   ├── index.handler.ts  # Lambda ハンドラ
│   ├── client.js         # 自動生成フロントクライアント
│   └── scripts/          # server.ts / sandbox.ts / deploy.ts / destroy.ts ...
├── src/
│   ├── App.tsx           # フロント(React)
│   └── main.tsx
├── cdk.json / vite.config.ts / tsconfig.json
└── package.json          # dev = tsx watch aws-blocks/scripts/server.ts

2.3 ローカル起動

$ cd elevation-app
$ npm run dev

実際のログ(実出力):

Loading backend...
Deploying local resources...
  🔌 Attaching dev server (from @aws-blocks/bb-realtime/ws-server)
📝 Generating client code...
AWS Blocks local server running on http://localhost:3000

Windows 側のブラウザで http://localhost:3000 を開くと、認証つきの Todo スターターアプリが立ち上がります。

ローカル起動直後(サインイン画面)

curl でも 200 が返ることを確認できます:

$ curl -I http://localhost:3000
HTTP/1.1 200 OK

dev サーバは *:3000(= 0.0.0.0 相当)で待ち受けるので、Windows 側から localhost:3000 で届きます。もし届かない場合は hostname -I で WSL2 の IP を調べ、http://<WSL2のIP>:3000 を試してください。


3. 設計・開発:地名から標高を返す独自API + 地図UI

スターターは Todo アプリですが、ここに独自機能を1つ足す形で AWS Blocks の開発体験を味わってみます。

3.1 設計

  • バックエンドApiNamespacesearchElevation(place) メソッドを追加する。
    1. Nominatim(OpenStreetMap) で地名 → 緯度経度
    2. 国土地理院 標高API で座標 → 標高
    • 認証は不要(requireAuth を呼ばないメソッドは public として動く)。
  • フロントmaplibre-gl で OpenStreetMap タイルの地図を描画し、検索したら flyTo でカメラ移動+ピン。

3.2 バックエンド実装(aws-blocks/index.ts

ApiNamespace の定義に、次のメソッドを1つ追加するだけです。

export const api = new ApiNamespace(scope, 'api', (context) => ({

  /** 地名 → 座標 → 標高(認証不要)。requireAuth を呼ばないので public。 */
  async searchElevation(place: string) {
    // 1) 地名 → 緯度経度(Nominatim は User-Agent 必須)
    const geoUrl =
      `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(place)}&format=json&limit=1`;
    const geoRes = await fetch(geoUrl, {
      headers: { 'User-Agent': 'aws-blocks-trial/1.0 (elevation-demo)' },
    });
    const hits = (await geoRes.json()) as Array<{ lat: string; lon: string; display_name: string }>;
    if (!Array.isArray(hits) || hits.length === 0) {
      throw new Error(`場所が見つかりませんでした: ${place}`);
    }
    const latitude = Number(hits[0].lat);
    const longitude = Number(hits[0].lon);
    const address = hits[0].display_name;

    // 2) 座標 → 標高(国土地理院 標高API)
    let elevation: number | null = null;
    try {
      const gsiUrl =
        `https://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php` +
        `?lon=${longitude}&lat=${latitude}&outtype=JSON`;
      const gsiRes = await fetch(gsiUrl);
      const gsi = (await gsiRes.json()) as { elevation: number | string };
      const e = Number(gsi.elevation);
      elevation = Number.isFinite(e) ? e : null;
    } catch (err) {
      console.error('国土地理院APIの取得に失敗:', err);
    }

    return { address, latitude, longitude, elevation };
  },

  // ... 既存の createTodo / listTodos などはそのまま ...
}));

3.3 フロント実装

地図ライブラリを入れます。

npm install maplibre-gl

maplibre-gl は標準で型定義が含まれているため、パッケージをインストールするだけで TypeScript がそのまま使えます(この環境では maplibre-gl@5.24.0 が入りました)。

src/ElevationMap.tsx を新規作成します(抜粋)。エラー処理は割愛します。ポイントは import { api } from 'aws-blocks' でバックエンドを直接呼び、型がそのまま効くことです。

import { useEffect, useRef, useState } from 'react';
import { api } from 'aws-blocks';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';

export function ElevationMap() {
  const [place, setPlace] = useState('富士山');
  const [result, setResult] = useState<{ address: string; latitude: number; longitude: number; elevation: number | null } | null>(null);
  const mapContainer = useRef<HTMLDivElement>(null);
  const map = useRef<maplibregl.Map | null>(null);
  const marker = useRef<maplibregl.Marker | null>(null);

  // 初回のみ:OSM の無料ラスタタイルで地図を描画
  useEffect(() => {
    if (!mapContainer.current || map.current) return;
    map.current = new maplibregl.Map({
      container: mapContainer.current,
      style: {
        version: 8,
        sources: {
          osm: {
            type: 'raster',
            tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
            tileSize: 256,
            attribution: '© OpenStreetMap contributors',
          },
        },
        layers: [{ id: 'osm', type: 'raster', source: 'osm' }],
      },
      center: [139.6917, 35.6895], // 東京
      zoom: 8,
    });
  }, []);

  const search = async () => {
    // ★ ここが AWS Blocks の肝:型安全にバックエンドAPIを直接呼ぶ
    const res = await api.searchElevation(place.trim());
    setResult(res);
    if (map.current) {
      map.current.flyTo({ center: [res.longitude, res.latitude], zoom: 12 });
      marker.current?.remove();
      marker.current = new maplibregl.Marker()
        .setLngLat([res.longitude, res.latitude])
        .addTo(map.current);
    }
  };

  return (
    <section>
      <h2>🗻 地名から標高を調べる(AWS Blocks デモ)</h2>
      <input value={place} onChange={e => setPlace(e.target.value)} onKeyDown={e => e.key === 'Enter' && search()} />
      <button onClick={search}>検索</button>
      {result && (
        <div>
          <p>場所: {result.address}</p>
          <p>座標: 緯度 {result.latitude.toFixed(4)} / 経度 {result.longitude.toFixed(4)}</p>
          <p>標高: {result.elevation !== null ? `${result.elevation} m` : '取得できませんでした'}</p>
        </div>
      )}
      <div ref={mapContainer} style={{ width: '100%', height: 400 }} />
    </section>
  );
}

あとは src/App.tsx の先頭で読み込むだけ:

import { ElevationMap } from './ElevationMap';
// ... <h1>Blocks App</h1> の直後に ...
<ElevationMap />

3.4 ハマりどころ(プレビュー版ならでは)

実際に手を動かして踏んだ罠を共有します。

生成直後のテンプレートが tsc で型エラーになる

npm run typecheck を走らせると、いきなりコケました(実出力):

aws-blocks/index.handler.ts(4,44): error TS2345:
  Argument of type 'typeof import(".../aws-blocks/index")' is not assignable
  to parameter of type '() => Promise<any>'.

原因は、生成された index.handler.tscreateLambdaHandlerモジュールをそのまま渡していたこと。現行の @aws-blocks/core の型(および d.ts の例)は「動的 import を返すファクトリ関数」を要求しています。次のように直すと通ります:

// Before(生成直後・型エラー)
import * as backend from './index.js';
export const handler = createLambdaHandler(backend);

// After(修正)
export const handler = createLambdaHandler(() => import('./index.js'));

npm run dev は tsx 実行で型チェックしないため起動は通ってしまうのが逆に厄介でした。プレビュー版のテンプレートとランタイムのバージョン差に由来するものと思われます。

修正後は型チェックがクリーンに通ります:

$ npm run typecheck
# エラーなし(EXIT 0)

4. 動作確認

4.1 まず API 単体をローカルサーバ経由で叩く

AWS Blocks のローカルサーバは JSON-RPC 2.0POST /aws-blocks/api を受けます。ブラウザを開く前に、curl で疎通を確認できます:

curl -X POST http://localhost:3000/aws-blocks/api \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","method":"api.searchElevation","params":["富士山"],"id":1}'

レスポンス(実出力):

{"jsonrpc":"2.0","result":{"address":"富士山, 小山町, 駿東郡, 静岡県, 日本","latitude":35.3628384,"longitude":138.7307677,"elevation":3537.8},"id":1}

このとき dev サーバ側にも RPC のログが流れます(実出力):

[rpc-in]  {"jsonrpc":"2.0","method":"api.searchElevation","params":["富士山"],"id":1}
[rpc-call] api.searchElevation ["富士山"]
[rpc-ok]  api.searchElevation {"address":"富士山, ...","elevation":3537.8}

別の地名(阿蘇山)でも動きます:

{"result":{"address":"阿蘇山, 南阿蘇村, 阿蘇郡, 熊本県, 日本","latitude":32.8827019,"longitude":131.0947404,"elevation":1354.8}}

4.2 ブラウザで地図を動かす

http://localhost:3000 を開くと、地図と検索フォームが表示されます(初期位置は東京)。

起動直後の地図UI

検索窓に「富士山」と入れて検索すると、地図が富士山周辺へ flyTo で移動し、ピンが立ち、標高が表示されます。

富士山を検索した結果

4.3 ちょっとした気づき:「富士山」は山頂を指さない

検索してみて気づいたのは、「富士山」で立つピンは山頂ではないということ。Nominatim が「富士山」という語で返すのは山頂ピンポイントではなく“代表点”で、その位置は山頂火口のほぼ中心です。標高もその地点の値になるため、返ってきた 3537.8m は火口最深部(大内院)の 3538.7m(国土地理院)と誤差1m弱でほぼ一致します。自分が立った剣ヶ峰の 3776m ではなく、そこから 火口の深さぶん(約237m)下、火口の底の値でした。

ズームすると、地図のピンには、富士山(剣ヶ峰)の山頂の標高が記載されていました。
地図のピンは剣ヶ峰山頂

山頂ピンポイントの標高が必要であれば、山頂の座標を直接渡す一手間が要ります。
または、国土地理院は、日本の主な山岳標高(1003山)について、最高地点の山名、正確な緯度・経度、標高をまとめたCSV/GeoJSONデータを公式ページで公開していますので、そこからの取得も可能です。

余談:ひとくちに「富士山の山頂」と言っても

今回のアプリで「富士山」を検索すると火口の底(大内院)に落ちましたが、そもそも「山頂の標高」というのが、調べてみると一筋縄ではありませんでした。剣ヶ峰の狭い一角だけでも、"高さの基準" がいくつも同居しているのです。

  • 記念撮影の石碑:3776m … 撮影ポイントの、あの「日本最高峰富士山剣ヶ峰3776m」の碑。一般に知られる「富士山=3776m」はこの値です。
  • 二等三角点「富士山」:3775.51m … 測量の基準点。ただし石柱には「三角點」とあるだけで、標高の数字は刻まれていません。3775.51m は2014年の再計算による現行の測量成果で、現地の古い説明板には旧値の3775.63m が書かれています。
  • 実際の最高地点:3776.12m … 三角点の"すぐ北にある岩の頂"。三角点は地盤の都合で最高点そのものには置けず、約61cm 低い場所に設置されています。「3776m」はこの最高地点を四捨五入した値です。
  • 電子基準点「富士山」:3777.5m … GNSS 観測用の基準点。ピラー(柱)で嵩上げされているぶん高く、国土地理院も「これで富士山の標高3776mを変更するものではない」としています。

つまり「山頂の標高」と言った瞬間に、どの点を指すのか(石碑か・三角点か・真の最高地点か・電子基準点か)で答えが変わります。ジオコーダが返す "代表点" が火口の底になるのも、この「山頂とはどの点か」という曖昧さの一種だと考えると、今回の結果もむしろ自然に思えてきます。位置情報を扱うときは「その座標が"何を指す点"なのか」を意識する必要がある ── というのが、山を登った後に標高を調べてみて得た、ちょっとした学びでした。


5. まとめと次の一歩

  • AWS Blocks を使うと、バックエンドの宣言1ファイル + 型安全に直接呼べるフロントで、フルスタックアプリをローカル完結(0円・AWS未接続)で回せました。
  • 独自機能の追加は「ApiNamespace にメソッドを1つ足す」だけ。フロントは api.searchElevation() を型付きで即呼べました。
  • プレビュー版ゆえのハマり(テンプレの型エラー)もありましたが、修正は1行でした。

次のステップは、同じコードを AWS へデプロイすることです(DistributedTable が DynamoDB に、ApiNamespace が Lambda + API Gateway に化ける)。プロジェクトにはすでに以下のスクリプトが用意されています:

npm run sandbox   # 個人検証用のサンドボックス環境へデプロイ
npm run deploy    # 本番デプロイ
npm run destroy   # 後片付け(リソース削除)

ローカルで動いたものが、そのまま AWS のマネージドインフラに乗る ── というのが AWS Blocks の狙いどころでした。

🏔️ 次回予告:今回は八合目の山小屋まで。ここで一泊したら、次はいよいよ山頂でご来光 ── 同じコードを実 AWS へデプロイし、地名検索を「本物の Amazon Location」で動かします。

fuji003.JPG


参考


BIPROGYグループの技術への取り組み

We Are Hiring!

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?