<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ja"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="/rss.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" hreflang="ja" /><updated>2026-09-28T16:17:47+09:00</updated><id>/rss.xml</id><title type="html">JWork Tech Blog</title><subtitle>開発者向けブログ</subtitle><entry><title type="html">Flutterアプリでネイティブコードを呼ぶ方法 Android編</title><link href="/articles/flutter-platform-channel-android/" rel="alternate" type="text/html" title="Flutterアプリでネイティブコードを呼ぶ方法 Android編" /><published>2026-08-14T00:00:00+09:00</published><updated>2026-08-14T00:00:00+09:00</updated><id>/articles/flutter-platform-channel-android</id><content type="html" xml:base="/articles/flutter-platform-channel-android/"><![CDATA[<h2 id="概要">概要</h2>
<p>Flutterアプリを作成している際、ネイティブコードを使いたい時があります。<br />
ここではAndroidでの実装方法を2パターンご紹介します。<br />
外部パッケージの導入は保守や脆弱性対応の対象も増えます。<br />
実装量もそれほど多くないため、選択肢として把握しておくと便利です。</p>

<h2 id="パターン1-flutterからネイティブコードを呼ぶ">パターン1 Flutterからネイティブコードを呼ぶ</h2>
<p>Flutterからネイティブコードを呼ぶ場合、MethodChannelを使用します。 <br />
Android側で入り口（MethodChannel）を名前付きで用意しておき、名前を元にFlutter側で呼ぶイメージです。</p>

<h3 id="android側">Android側</h3>
<pre><code class="language-Kotlin">import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class MainActivity(): FlutterActivity() {
    
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "jp.co.jwork.sample.app/sample")
            .setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result -&gt;
            if (methodCall.method == "input") {
                val args: List&lt;Any&gt; = methodCall.arguments as List&lt;Any&gt;
                // ... Androidでやりたい処理 ...
                result.success("ok") // "ok"部がレスポンスになります。エラーの場合、result.errorも使えます。
            } else {
                result.notImplemented()
            }
        }
    }
}
</code></pre>

<h3 id="flutter側">Flutter側</h3>
<pre><code class="language-Dart">import 'dart:async';

import "package:flutter/services.dart";

class Caller {

    static const MethodChannel _methodChannel = const MethodChannel('jp.co.jwork.sample.app/sample');

    Future run(String param) async {
        try {
            return await _methodChannel.invokeMethod("input", [ param ]);
        } on PlatformException {
            // エラーハンドリング
        }
    }
}
</code></pre>

<h2 id="パターン2-androidからイベントをflutter側に流す">パターン2 AndroidからイベントをFlutter側に流す</h2>
<p>AndroidからFlutterへ渡す場合、EventChannelを使用できます。<br />
Flutter側で入り口（EventChannel）を名前付きで用意しておき、名前を元にAndroid側で呼ぶイメージです。<br />
なお、1回きりで良い場合は、同じ流れでパターン1のMethodChannelを使うことでも実現可能です。</p>

<h3 id="android側-1">Android側</h3>
<pre><code class="language-Kotlin">import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
class MainActivity(): FlutterActivity() {
    
    private var mInputEventSink: EventChannel.EventSink? = null

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        EventChannel(flutterEngine.dartExecutor.binaryMessenger, "jp.co.jwork.sample.app/sampleEvent")
            .setStreamHandler(object : EventChannel.StreamHandler {
                override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                    mInputEventSink = events
                }

                override fun onCancel(arguments: Any?) {
                    mInputEventSink = null
                }
        })
    }

    // 何らかのAndroid側処理の値
    private fun onInput(value: String) {
        mInputEventSink?.success(value)
    }
}
</code></pre>

<h3 id="flutter側-1">Flutter側</h3>
<pre><code class="language-Dart">import 'dart:async';

import "package:flutter/services.dart";

class Receiver {

    static const EventChannel _eventChannel = const EventChannel("jp.co.jwork.sample.app/sampleEvent");
    
    StreamSubscription? _listenEventStreamSubs;
    
    // 引数で関数を受けていますが、単純にStreamSubscriptionを返す作りでも良いと思います。
    Future&lt;void&gt; listen(void Function(String value) onListen) async {
        await _listenEventStreamSubs?.cancel();
        _listenEventStreamSubs = null;
        _listenEventStreamSubs = _eventChannel.receiveBroadcastStream().listen((event) {
            if (event is String) {
                onListen(event);
            }
        }, onError: (error) {
            // エラーハンドリング
        }, cancelOnError: true);
    }

    // 不要になったら閉じます。
    Future&lt;void&gt; cancel() async {
        await _listenEventStreamSubs?.cancel();
        _listenEventStreamSubs = null;
    }
}
</code></pre>

<h2 id="まとめ">まとめ</h2>
<p>MethodChannel、EventChannelで簡単にAndroidコードとやりとりが出来ました。<br />
汎用的なものならpluginとして切り出し、使いまわせるようにするのも良いと思います。</p>]]></content><author><name>sh.sato</name></author><category term="app" /><summary type="html"><![CDATA[FlutterのPlatform Channelsを使って、Androidのネイティブコードと連携する流れを紹介します。]]></summary></entry><entry><title type="html">Flutterアプリでネイティブコードを呼ぶ方法 iOS編</title><link href="/articles/flutter-platform-channel-ios/" rel="alternate" type="text/html" title="Flutterアプリでネイティブコードを呼ぶ方法 iOS編" /><published>2026-08-14T00:00:00+09:00</published><updated>2026-08-14T00:00:00+09:00</updated><id>/articles/flutter-platform-channel-ios</id><content type="html" xml:base="/articles/flutter-platform-channel-ios/"><![CDATA[<h2 id="概要">概要</h2>
<p>Flutterアプリを作成している際、ネイティブコードを使いたい時があります。<br />
ここではiOSでの実装方法を2パターンご紹介します。<br />
外部パッケージの導入は保守や脆弱性対応の対象も増えます。<br />
実装量もそれほど多くないため、選択肢として把握しておくと便利です。</p>

<h2 id="パターン1-flutterからネイティブコードを呼ぶ">パターン1 Flutterからネイティブコードを呼ぶ</h2>

<h3 id="ios側">iOS側</h3>
<pre><code class="language-Swift">import Flutter

@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {

  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
    ...
    let methodChannel = FlutterMethodChannel(name: "jp.co.jwork.sample.app/sample", binaryMessenger: engineBridge.applicationRegistrar.messenger())
    methodChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -&gt; Void in
        let method = call.method
        if (method == "input") {
            let args = call.arguments as! [Any]
            // ...iOSでやりたい処理 ...
            result("ok")
        } else {
            result(FlutterMethodNotImplemented)
        }
    })
  }
}
</code></pre>
<h3 id="flutter側">Flutter側</h3>
<pre><code class="language-Dart">import 'dart:async';

import "package:flutter/services.dart";

class Caller {

    static const MethodChannel _methodChannel = const MethodChannel('jp.co.jwork.sample.app/sample');

    Future run(String param) async {
        try {
            return await _methodChannel.invokeMethod("input", [ param ]);
        } on PlatformException {
            // エラーハンドリング
        }
    }
}
</code></pre>

<h2 id="パターン2-iosからイベントをflutter側に流す">パターン2 iOSからイベントをFlutter側に流す</h2>
<p>iOSからFlutterへ渡す場合、EventChannelを使用できます。<br />
Flutter側で入り口（EventChannel）を名前付きで用意しておき、名前を元にiOS側で呼ぶイメージです。<br />
なお、1回きりで良い場合は、同じ流れでパターン1のMethodChannelを使うことでも実現可能です。</p>

<h3 id="ios側-1">iOS側</h3>
<pre><code class="language-Swift">import Flutter

@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {

  static var eventSink: FlutterEventSink?
  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
    ...
    FlutterEventChannel(name: "jp.co.jwork.sample.app/sampleEvent", binaryMessenger: engineBridge.applicationRegistrar.messenger())
            .setStreamHandler(self)
  }

  // 何らかのiOS側処理の値
  private func onInput(_ value: String) {
    AppDelegate.eventSink?(value)
  }
}


extension AppDelegate: FlutterStreamHandler {

  func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -&gt; FlutterError? {
    AppDelegate.eventSink = events
    return nil
  }

  func onCancel(withArguments arguments: Any?) -&gt; FlutterError? {
    AppDelegate.eventSink = nil
    return nil
  }
}

</code></pre>

<h3 id="flutter側-1">Flutter側</h3>
<pre><code class="language-Dart">import 'dart:async';

import "package:flutter/services.dart";

class Receiver {

    static const EventChannel _eventChannel = const EventChannel("jp.co.jwork.sample.app/sampleEvent");
    
    StreamSubscription? _listenEventStreamSubs;
    
    // 引数で関数を受けていますが、単純にStreamSubscriptionを返す作りでも良いと思います。
    Future&lt;void&gt; listen(void Function(String value) onListen) async {
        await _listenEventStreamSubs?.cancel();
        _listenEventStreamSubs = null;
        _listenEventStreamSubs = _eventChannel.receiveBroadcastStream().listen((event) {
            if (event is String) {
                onListen(event);
            }
        }, onError: (error) {
            // エラーハンドリング
        }, cancelOnError: true);
    }

    // 不要になったら閉じます。
    Future&lt;void&gt; cancel() async {
        await _listenEventStreamSubs?.cancel();
        _listenEventStreamSubs = null;
    }
}
</code></pre>

<h2 id="まとめ">まとめ</h2>
<p>MethodChannel、EventChannelで簡単にiOSコードとやりとりが出来ました。<br />
汎用的なものならpluginとして切り出し、使いまわせるようにするのも良いと思います。</p>]]></content><author><name>sh.sato</name></author><category term="app" /><summary type="html"><![CDATA[FlutterのPlatform Channelsを使って、iOSのネイティブコードと連携する流れを紹介します。]]></summary></entry><entry><title type="html">iOSアプリの起動リンクが動作しない時の回避策</title><link href="/articles/ios-app-link-troubleshooting/" rel="alternate" type="text/html" title="iOSアプリの起動リンクが動作しない時の回避策" /><published>2026-08-12T00:00:00+09:00</published><updated>2026-08-12T00:00:00+09:00</updated><id>/articles/ios-app-link-troubleshooting</id><content type="html" xml:base="/articles/ios-app-link-troubleshooting/"><![CDATA[<h2 id="結論">結論</h2>
<p>iOS標準の「メモ」アプリにリンクを貼り付け、リンクを長押しして表示されるメニューからアプリを起動できる場合があります。</p>

<h2 id="概要">概要</h2>
<p>リンクをタップした時にアプリが起動しない、と問い合わせが来ることがたまにあります。<br />
そういった時にご紹介している回避策です。</p>

<h2 id="手順">手順</h2>
<ol>
  <li>リンクをコピー</li>
  <li>コピーしたリンクをiOS標準の「メモ」アプリに貼り付ける</li>
  <li>リンクを長押ししてコンテキストメニューを表示</li>
  <li>メニュー内の”アプリ名”で開くをタップ</li>
</ol>

<p><img src="/assets/images/articles/ios-app-link-troubleshooting/capture.png" alt="メモアプリでリンクを長押しした時のコンテキストメニュー" width="50%" /></p>

<h2 id="原因">原因</h2>
<p>ユニバーサルリンクは、アプリインストール時にApple CDNからAASA（apple-app-site-association）ファイルを取得することで動作しています。<br />
そのため、今回は何らかの理由でAASAファイルを取得できておらず、アプリが起動しない状態になっていると考えています。</p>

<h2 id="参考">参考</h2>
<p><a href="https://developer.apple.com/documentation/xcode/supporting-associated-domains" target="_blank" rel="noopener noreferrer">Apple公式ドキュメント</a></p>]]></content><author><name>sh.sato</name></author><category term="app" /><summary type="html"><![CDATA[Universal Linksが一部のユーザーだけ動作しない時の回避策を説明します。]]></summary></entry><entry><title type="html">GitHub ActionsからOIDCでS3へデプロイ</title><link href="/articles/github-oidc-s3/" rel="alternate" type="text/html" title="GitHub ActionsからOIDCでS3へデプロイ" /><published>2026-08-10T00:00:00+09:00</published><updated>2026-08-10T00:00:00+09:00</updated><id>/articles/github-oidc-s3</id><content type="html" xml:base="/articles/github-oidc-s3/"><![CDATA[<h2 id="概要">概要</h2>
<p>当ブログはAmazon CloudFrontで公開、GitHubで記事を管理する構成です。<br />
ここでは、その際に行ったOIDC設定について説明します。</p>

<h2 id="構成">構成</h2>
<ul>
  <li>GitHub Actions</li>
  <li>GitHub OIDC Provider</li>
  <li>AWS IAM Role</li>
  <li>S3</li>
  <li>CloudFront</li>
</ul>

<h2 id="awsgithub設定">AWS・GitHub設定</h2>
<h3 id="amazon-s3バケットの作成">Amazon S3バケットの作成</h3>
<p>ソースコードを配置します。パブリックアクセスは全てブロックで問題ありません。</p>

<h3 id="amazon-cloudfrontの作成">Amazon CloudFrontの作成</h3>
<p>CloudFrontでディストリビューションを作成します。色々プランがありますが、今回は無料プランにしました。<br />
オリジンタイプを「Amazon S3」とし、オリジンに作成したS3バケットを設定します。OACでS3へアクセスします。</p>

<h3 id="s3バケットポリシーの変更">S3バケットポリシーの変更</h3>
<p>作成したCloudFrontからのアクセスを許可します。</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"Version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2008-10-17"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PolicyForCloudFrontPrivateContent"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Statement"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"Sid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AllowCloudFrontServicePrincipal"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Principal"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"Service"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cloudfront.amazonaws.com"</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="s2">"s3:GetObject"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arn:aws:s3:::[bucket name]/*"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Condition"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"ArnLike"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"AWS:SourceArn"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arn:aws:cloudfront::[aws id]:distribution/[distribution id]"</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="id-プロバイダの作成">ID プロバイダの作成</h2>
<p>AWS IAMのID プロバイダでOpenID Connectのプロバイダを作成します。<br />
プロバイダのURLは<code class="language-plaintext highlighter-rouge">https://token.actions.githubusercontent.com</code>、対象者は<code class="language-plaintext highlighter-rouge">sts.amazonaws.com</code>です。
<img src="/assets/images/articles/github-oidc-s3/capture.png" alt="AWS IAMのOpenID Connectプロバイダ設定画面" width="50%" /></p>

<h3 id="githubリポジトリの作成">GitHubリポジトリの作成</h3>
<p>GitHubのリポジトリが未作成の場合、作成しておきます。</p>

<h3 id="iamロールの作成">IAMロールの作成</h3>
<p>ここまでに作成したS3、CloudFront、GitHubリポジトリのリソースで、IAMロールを作成します。<br />
OIDCの設定を行います。この後使うため、ARN IDをコピーしておくと良いと思います。</p>
<h4 id="許可ポリシーにs3cloudfrontを設定">許可ポリシーにS3、CloudFrontを設定</h4>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"Version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2012-10-17"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Statement"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"Sid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AllowDevBucketList"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                </span><span class="s2">"s3:GetBucketLocation"</span><span class="p">,</span><span class="w">
                </span><span class="s2">"s3:ListBucket"</span><span class="w">
            </span><span class="p">],</span><span class="w">
            </span><span class="nl">"Resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arn:aws:s3:::[bucket name]"</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"Sid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AllowDevBucketSync"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                </span><span class="s2">"s3:PutObject"</span><span class="p">,</span><span class="w">
                </span><span class="s2">"s3:DeleteObject"</span><span class="w">
            </span><span class="p">],</span><span class="w">
            </span><span class="nl">"Resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arn:aws:s3:::[bucket name]/*"</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"Sid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AllowCloudFrontInvalidation"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                </span><span class="s2">"cloudfront:CreateInvalidation"</span><span class="w">
            </span><span class="p">],</span><span class="w">
            </span><span class="nl">"Resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arn:aws:cloudfront::[aws id]:distribution/[distribution ID]"</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h4 id="信頼関係にoidcの設定">信頼関係にOIDCの設定</h4>
<p>subはブランチ名まで指定すると安心です。<br />
ここでは単に組織名、リポジトリ名、ブランチ名を指定していますが、2026年7月15日以降に作られたGitHubリポジトリでは、<code class="language-plaintext highlighter-rouge">repo:org@id/repo@id:...</code>形式とする必要があるようです。<br />
id部分には、各々固有の値が入ります。idはGitHub CLI（gh）を使って取得するのが一番手軽かと思います。</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"Version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2012-10-17"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Statement"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"Sid"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Principal"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"Federated"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arn:aws:iam::[aws id]:oidc-provider/token.actions.githubusercontent.com"</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sts:AssumeRoleWithWebIdentity"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Condition"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"StringEquals"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"token.actions.githubusercontent.com:aud"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sts.amazonaws.com"</span><span class="w">
                </span><span class="p">},</span><span class="w">
                </span><span class="nl">"StringLike"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"token.actions.githubusercontent.com:sub"</span><span class="p">:</span><span class="w"> </span><span class="s2">"repo:[github organization]/[github repository]:ref:refs/heads/[branch name]"</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="github-actionsの設定">GitHub Actionsの設定</h3>
<p>リポジトリに<code class="language-plaintext highlighter-rouge">.github/workflows/deploy.yml</code>を作成、S3へのOIDC設定を記載します。<br />
今回はJekyllを使用しています。<br />
また各種環境値は、GitHub ActionsのRepository Variablesに設定して参照するようにしています。<br />
※ymlファイルの名称は任意です。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Deploy Dev to S3</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">dev</span>
  <span class="na">workflow_dispatch</span><span class="pi">:</span>

<span class="na">permissions</span><span class="pi">:</span>
  <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
  <span class="na">id-token</span><span class="pi">:</span> <span class="s">write</span>

<span class="na">concurrency</span><span class="pi">:</span>
  <span class="na">group</span><span class="pi">:</span> <span class="s">deploy-dev-s3</span>
  <span class="na">cancel-in-progress</span><span class="pi">:</span> <span class="kc">false</span>

<span class="na">env</span><span class="pi">:</span>
  <span class="na">AWS_REGION</span><span class="pi">:</span> <span class="s">$</span>
  <span class="na">AWS_ROLE_ARN</span><span class="pi">:</span> <span class="s">$</span>
  <span class="na">S3_BUCKET</span><span class="pi">:</span> <span class="s">$</span>
  <span class="na">CLOUDFRONT_DISTRIBUTION_ID</span><span class="pi">:</span> <span class="s">$</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">deploy</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Build and Deploy</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Setup Ruby</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">ruby/setup-ruby@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">ruby-version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.3"</span>
          <span class="na">bundler-cache</span><span class="pi">:</span> <span class="kc">true</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Validate deployment settings</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">test -n "${AWS_REGION}" || (echo "AWS_REGION repository variable is required" &amp;&amp; exit 1)</span>
          <span class="s">test -n "${AWS_ROLE_ARN}" || (echo "DEV_AWS_ROLE_ARN repository variable is required" &amp;&amp; exit 1)</span>
          <span class="s">test -n "${S3_BUCKET}" || (echo "DEV_S3_BUCKET repository variable is required" &amp;&amp; exit 1)</span>
          <span class="s">test -n "${CLOUDFRONT_DISTRIBUTION_ID}" || (echo "DEV_CLOUDFRONT_DISTRIBUTION_ID repository variable is required" &amp;&amp; exit 1)</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Validate content</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">bundle exec ruby scripts/validate_content.rb</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build site for dev</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">JEKYLL_ENV=production bundle exec jekyll build --strict_front_matter --baseurl ""</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Configure AWS credentials</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">aws-actions/configure-aws-credentials@v6</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">aws-region</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">role-to-assume</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">role-session-name</span><span class="pi">:</span> <span class="s">sample-deploy</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Sync site to S3</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">aws s3 sync _site/ "s3://${S3_BUCKET}/" --delete</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Invalidate CloudFront</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">aws cloudfront create-invalidation \</span>
            <span class="s">--distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \</span>
            <span class="s">--paths "/*"</span>
</code></pre></div></div>

<h3 id="アップロード">アップロード</h3>
<p>GitHub Actionsの設定が済んだらpushします。<br />
Build and Deployが走り、正常終了すれば成功です。S3に反映されていると思います。</p>

<h2 id="参考">参考</h2>
<ul>
  <li>https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws</li>
  <li>https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html</li>
  <li>https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html</li>
</ul>]]></content><author><name>sh.sato</name></author><category term="web" /><summary type="html"><![CDATA[GitHub ActionsとAWS OIDCを使い、S3へ静的サイトをデプロイする設定を紹介します。]]></summary></entry><entry><title type="html">AndroidのtargetSdkVersion 36対応</title><link href="/articles/android-target-sdk-version-36/" rel="alternate" type="text/html" title="AndroidのtargetSdkVersion 36対応" /><published>2026-08-09T00:00:00+09:00</published><updated>2026-08-09T00:00:00+09:00</updated><id>/articles/android-target-sdk-version-36</id><content type="html" xml:base="/articles/android-target-sdk-version-36/"><![CDATA[<h2 id="概要">概要</h2>
<p>今年もtargetSdkVersionの引き上げ対応が必要になりました。<br />
その際に調査・整理した内容を記載します。<br />
基本的には下記リンクの内容を確認して、1つずつアプリ影響有無をチェックしていく作業です。</p>
<ul>
  <li><a href="https://developer.android.com/about/versions/16/behavior-changes-16" target="_blank" rel="noopener noreferrer">動作の変更点</a></li>
</ul>

<h2 id="エッジ-ツー-エッジのオプトアウトの廃止">エッジ ツー エッジのオプトアウトの廃止</h2>
<p>エッジツーエッジのオプトアウトが出来なくなりました。<br />
予告もされていたので困ることはなかったですが、当時対応するのが少し面倒だったのは覚えています。</p>

<h2 id="予測型戻るには移行またはオプトアウトが必要">予測型「戻る」には移行またはオプトアウトが必要</h2>
<p>戻る操作をした時、戻り先がちらっと見えるようになりました。戻る操作は手が入れられることが多いですね。<br />
これ自体は良いのですが、下記の記載がさらっと書かれています。</p>
<blockquote>
  <p>onBackPressed が呼び出されなくなり、 KeyEvent.KEYCODE_BACK がディスパッチされなくなります。</p>
</blockquote>

<p>長く運用されているアプリだと、どちらもまだ使われているのではないでしょうか。<br />
対応自体は<code class="language-plaintext highlighter-rouge">onBackPressedDispatcher</code>等の新しい仕組みに移行すれば良いので、そこまで大変ではありませんでした。<br />
下記のようにオプトアウトも可能です。数年で廃止される可能性はありますが・・。</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nt">&lt;application</span>
        <span class="err">...</span>
        <span class="na">android:enableOnBackInvokedCallback=</span><span class="s">"false"</span>
        <span class="err">...</span>
        <span class="nt">&gt;</span>
</code></pre></div></div>

<h2 id="elegant-フォント-api-のサポート終了と無効化">Elegant フォント API のサポート終了と無効化</h2>
<p>elegantTextHeight機能が無効化されました。<br />
一部言語の時に自動でフォントが入れ替わる仕組みのようですが・・・、多言語対応をしているアプリでは確認が必要そうです。</p>

<h2 id="固定レートの作業スケジュールの最適化">固定レートの作業スケジュールの最適化</h2>
<p>定期実行系の処理の動作変更のようです。<code class="language-plaintext highlighter-rouge">タスクの実行を逃した場合</code>というのが具体的にどのタイミングを指すのか分かりかねたのですが・・・、これまで不定だった実行回数が最大1回になるようです。</p>

<h2 id="向きサイズ変更の可能性アスペクト比の制限を無視する">向き、サイズ変更の可能性、アスペクト比の制限を無視する</h2>
<p>今回、一番影響が大きい変更点と思っています。<br />
最小幅が 600 dp 以上のディスプレイで画面の向き、サイズ変更、アスペクト比の制限が適用されなくなりました。<br />
特に縦向き固定にしているアプリは多いと思います。ちゃんと対処するならデザインから見直しが必要ですね。<br />
なお、下記のようにオプトアウトは可能なものの、一時的な措置とのことです。</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;application</span> <span class="err">...</span><span class="nt">&gt;</span>
  <span class="nt">&lt;property</span> <span class="na">android:name=</span><span class="s">"android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY"</span> <span class="na">android:value=</span><span class="s">"true"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/application&gt;</span>
</code></pre></div></div>

<p>今後、アプリを構築する際に悩むことが一つ増えてしまいました。</p>

<h2 id="健康とフィットネスの権限">健康とフィットネスの権限</h2>
<p>センサー周りの権限が細分化されたようです。Wear OS系はいつかやってみたいと思いつつ中々機会がありません。</p>

<h2 id="bluetoothの変更">Bluetoothの変更</h2>
<p>Bluetoothのペアリング周りで変更が入ったようです。<br />
Bluetoothを使用しているアプリでは一通り機能の動作確認をした方が良さそうですね。</p>

<h2 id="セキュリティ">セキュリティ</h2>
<p>Intentやローカルネットワーク周りのセキュリティ強化が行われています。<br />
action無しのIntent実装がないか確認した方が良さそうです。<br />
また、自身が受け手側のアプリを作成している場合は、intentMatchingFlagsの導入要否を検討すると良いのではないでしょうか。</p>

<h2 id="まとめ">まとめ</h2>
<p>今回のバージョンは、アプリによって影響度合いが全然違っていたんじゃないかと思います。<br />
幸い？にして、私の担当するアプリは影響軽微でした。</p>

<h2 id="参考">参考</h2>
<ul>
  <li>https://developer.android.com/about/versions/16/behavior-changes-all</li>
  <li>https://www.jssec.org/dl/android_securecoding_20250827/4_using_technology_in_a_safe_way.html#safer-intents</li>
</ul>]]></content><author><name>sh.sato</name></author><category term="app" /><summary type="html"><![CDATA[targetSdkVersion 36対応の内容について説明します。]]></summary></entry><entry><title type="html">CloudFront Functionsで検証環境にBasic認証をかける</title><link href="/articles/cloudfront-basic-auth/" rel="alternate" type="text/html" title="CloudFront Functionsで検証環境にBasic認証をかける" /><published>2026-08-07T00:00:00+09:00</published><updated>2026-08-07T00:00:00+09:00</updated><id>/articles/cloudfront-basic-auth</id><content type="html" xml:base="/articles/cloudfront-basic-auth/"><![CDATA[<h2 id="概要">概要</h2>
<p>CloudFrontで検証サイトを配信する場合、簡易的な認証として、Basic認証をかけたい時があると思います。<br />
ここではその手順について説明します。<br />
なおIaC的な仕組みは使わず、ブラウザ操作で進める場合の内容になっています。</p>

<h2 id="構成">構成</h2>
<ul>
  <li>S3</li>
  <li>CloudFront</li>
  <li>CloudFront Functions</li>
</ul>

<h2 id="basic認証用の文字列を作る">Basic認証用の文字列を作る</h2>
<p>Basic認証に用いるユーザー名/パスワードを準備します。<br />
色々なやり方があると思いますが、コマンドラインで作るなら、次のように作成できます。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="nt">-n</span> <span class="s2">"test:hoge"</span> | <span class="nb">base64</span>
<span class="c"># dGVzdDpob2dl</span>
</code></pre></div></div>

<p>パスワード生成のWebサービス等もありますが、漏洩リスクとなるためあまりお勧めできません。</p>

<h2 id="cloudfront-functionsを作成する">CloudFront Functionsを作成する</h2>
<p>CloudFront Functionsは、AWS CloudFrontサービスの「Functions」ないし「関数」です。<br />
JavaScriptコードを実行することができるので、ここでBasic認証の処理を行います。<br />
「関数」-&gt;「関数を作成」から作成し、コードを貼り付けます。</p>

<pre><code class="language-JavaScript">function handler(event) {
    var request = event.request;

    var authResponse = basicAuth(request);
    if (authResponse) {
      return authResponse;
    }

    return rewriteIndexHtml(request);
}

function basicAuth(request) {
    var expected = 'Basic dGVzdDpob2dl'; // 上で生成したダミー値
    var headers = request.headers;

    if (headers.authorization &amp;&amp; headers.authorization.value === expected) {
      return null;
    }

    return {
      statusCode: 401,
      statusDescription: 'Unauthorized',
      headers: {
        'www-authenticate': { value: 'Basic' },
        'cache-control': { value: 'no-store' }
      },
      body: 'Unauthorized'
    };
}

function rewriteIndexHtml(request) {
    var uri = request.uri;

    if (uri.endsWith('/')) {
      request.uri = uri + 'index.html';
    } else if (!uri.includes('.')) {
      request.uri = uri + '/index.html';
    }

    return request;
}
</code></pre>

<p>Jekyllの構成では、リクエストURIを index.html に書き換える必要があるため、rewriteIndexHtml部分で一手間かけています。</p>

<h2 id="functionをcloudfrontのディストリビューションに関連づける">FunctionをCloudFrontのディストリビューションに関連づける</h2>
<p>あとは「発行」メニューから、ディストリビューションのビューワーリクエストと関連づければ完了です。</p>

<h2 id="動作確認">動作確認</h2>
<p>ブラウザでアクセスし、Basic認証が表示されればOKです。<br />
念の為トップ以外のページが問題なく開けるかもよく確認しておきましょう。</p>]]></content><author><name>sh.sato</name></author><category term="web" /><summary type="html"><![CDATA[CloudFront Functionsを使って、S3 + CloudFront構成の検証環境にBasic認証を追加する方法を紹介します。]]></summary></entry></feed>