WebViewの高さをローディング時に動的に変える方法

はじめに

2021年度アドベントカレンダーの2日目の担当の高野です。メディア事業部のアプリではWebViewを使うことが多く、その備忘録として残します。

動作バージョン

Flutter 2.5.1
Dart 2.14.0
webview_flutter 2.1.1

実装

今回はwebview_flutterを使用していますが、他のWebViewライブラリでもおそらく同じだと思います。
まず初めに動的にWebViewの高さを変更するためにはSizedBoxまたはContainerでWebViewを包みます。

Container(
  height: controller.webViewHeight.value,
  child: WebView(
    navigationDelegate: (request) => controller.onRequestNavigation(request: request),
    javascriptMode: JavascriptMode.unrestricted,
    onPageFinished: (_) => controller.onPageFinished(),
    onWebViewCreated: (webViewController) => controller.onWebViewCreated(controller: webViewController),
  ),
);

ここのheightにWebViewの高さを取得した後に入れることで動的に高さを変えることができます。
このWebViewのonPageFinishedの発火時に以下コードを発火させることによって取得できます。Javascriptを使用するので JavascriptMode.unrestrictedjavascriptMode に指定しておいてください。

Future<double> _getWebViewHeight() async {
  try {
    final rawWebviewHeight = await controller!.evaluateJavascript('document.body.offsetHeight;');
    final webviewHeight = double.parse(rawWebviewHeight);
    return webviewHeight;
  } catch (e) {
    Logger.error(e);
    return 1000;
  }
}

これであとはこの関数の返り値をContainerやSizedBoxのheightに指定してあげれば取得時にbuild()が走るので更新が行われます。

まとめ

どこが一番重要かと言うと

await controller!.evaluateJavascript('document.body.offsetHeight;');

ここですね、これによってロードしたページ(body)の高さを取得することができます。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。
https://www.wantedly.com/companies/excite

Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

SpringBoot x Gradleマルチプロジェクトで環境構築をする

エキサイト株式会社佐々木です。アドベントカレンダー2021の2ページ目の1日目になります。メディア事業部では、SpringBoot x Gradleマルチプロジェクトを使用して、モジュラーモノリスのような構成で各メディアのリビルドの開発を進めています。既存のリポジトリは1メディアで30〜50リポジトリが存在し、リポジトリの行き来で見通しが悪くなっているので、リビルドではモノリポ x マルチプロジェクト形式にしています。

Gradleマルチプロジェクトとは

Gradleマルチプロジェクトは、1つのメインプロジェクト内にいくつものサブプロジェクトが存在する構成になります。

設定

設定には、build.gradleとsettings.gradleを使用します。

settings.gradle

マルチプロジェクトで運用するには、settings.gradleにプロジェクト名を追記します。

仮に、web,batch,api,usecase,service,repositoryのように各層ごとに分けるようにすると下記のようになります。

rootProject.name = 'demo'

include "web"
include "batch"
include "api"
include "service"
include "repository"
include "domain"

build.gradle

settings.gradleで分割したモジュールをbuild.gradleに反映していきます。

plugins {
    id 'org.springframework.boot' version '2.6.1'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

subprojects {   // ★1

    apply plugins: "java"    // ★2
    apply plugin: "org.springframework.boot"
    apply plugin: "io.spring.dependency-management"

    group = 'com.example'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '11'

    repositories {
        mavenCentral()
    }

    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }

    dependencies {
        compileOnly 'org.projectlombok:lombok'
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
        annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }

    test {
        useJUnitPlatform()
    }

}


project(":web") {    // ★3

    bootJar {    // ★4
        enabled = true
    }

    dependencies {    // ★5
        implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-webflux'
        implementation project(":service")   // ★6
        implementation project(":domain")   // ★6
        testImplementation 'io.projectreactor:reactor-test'
    }
}

project(":api") {

    bootJar {
        enabled = true
    }

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-webflux'
        implementation project(":service")
        testImplementation 'io.projectreactor:reactor-test'
    }
}

project(":service") {

    bootJar {
        enabled = false
    }

    dependencies {
        implementation project(":repository")
    }
}

project(":repository") {

    bootJar {
        enabled = false
    }

    dependencies {
        implementation project(":domain")
    }
}

project(":domain") {
    bootJar {
        enabled = false
    }
}

上記のような設定になります。★ごとに解説します。

★1

Gradleマルチプロジェクト構成にすると、プロジェクト直下以外は、すべてサプブロジェクトとなります。この時に、サブプロジェクト共通で読み込みたいようなライブラリや設定があるときに、 subprojects {} の中に書いておくと全てのサブプロジェクトに適用できます。ほぼすべてのプロジェクトで必要そうなものを記述しておきます。(LombokやSpringBootのAnnotationまわりの設定等)

★2

Gradleのプラグインの設定はサブプロジェクトごとにプラグインを使うか使わないかを決められます。逆に設定しないと使えないです。ここでよくハマっているのをみます。

★3

各サブプロジェクトごとの設定は、 project(":プロジェクト名") {} の内側に記述します。ここには、プロジェクト固有のものを設定します。例では、project(":web"){...} では、テンプレートエンジンのThymeleafとSpringBootのWeb系のライブラリを読み込んでいます。

★4

SpringBootはコンパイルすると実行可能なjarの生成が可能です。ですが、serviceプロジェクトやrepositoryプロジェクトでは、実行形式にする必要がありません(エンドポイントがないので)。そういうときには、bootJar{} を使用して、実行可能なjarを生成する・しないの設定を追加できます。webapiプロジェクトは、エンドポイントがそれぞれあるので、bootJar{enabled=true}を指定して、実行可能なjarを生成するようにします。

★5

サブプロジェクトごとに依存関係を定義できます。ここで定義したものは、そのサブプロジェクト内でしか使えないことに注意してください。

★6

ここで、サブプロジェクト内で別のサブプロジェクトを依存関係に含めています。これを行うことで、サブプロジェクト内のコードを使えるようになります。例ではwebプロジェクトは、serviceサブプロジェクトとdomainサブプロジェクトのコードにアクセスができますが、repositoryサブプロジェクトのコードにはアクセスできないことになります。

Gradleマルチプロジェクトのメリット

Gradleマルチプロジェクトのメリットは、アクセス制御が可能になことです。依存関係をサブプロジェクト単位で定義できることによって、モジュラーモノリスやミニサービスの構成を簡単に実現できます。依存関係に定義していないサブプロジェクトやライブラリを使おうとしてもコンパイルエラーになりますので、管理が楽になります。

Gradleマルチプロジェクトのような機能がない状態で、厳密に依存関係を制御しようとすると、コードレビューをするまたはチェック用のコードを書く等のことをしなければならないですが、Gradleの依存関係の設定のみでそれが可能になります。JavaにもArchUnitというライブラリはありますが、テストコードを記述する必要があります。

最後に

アドベントカレンダー1日目を書かせていただきました。他社同様エキサイトHDでもアドベントカレンダーをやっていますので、ご覧いただけると幸いです。(https://qiita.com/advent-calendar/2021/excite-hd)

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

Spring Bootで、AWSパラメータストアから簡単にDBのパスワードを取得する方法

こんにちは。 エキサイト株式会社の三浦です。

いよいよ今年も12月になりました。 「師走」の文字通り、忙しくしている方も多いのではないでしょうか。

この「12月」ですが、我々エンジニアにとっては別の意味も持っています。 すなわち、アドベントカレンダーの時期です!

というわけで、エキサイトホールディングスの今年のアドベントカレンダーの第一回目のブログを作成させていただくことになりました!

第一回目となる今回は、Spring Bootにおける、DBのパスワード管理問題のAWSパラメータストアを使用した簡単な解決方法について書かせていただきます。

DBのパスワード管理問題

アプリケーションでは、特に一定以上の規模になってくれば、多くの場合DBからデータを取得したり書き込んだりする必要が出てくるでしょう。 この時問題になってくるのが、「DBのパスワードをどうやって管理するか」です。

方法自体は以下のようにいくつか考えられますが、DBのデータの改竄はアプリケーションにとって致命的な問題となりうるので、パスワードが漏れてしまうことは絶対に避ける必要があります。

Github等で、その他のコードと一緒にバージョン管理する

Github等でバージョン管理してしまえば管理自体は簡単ですが、何かしらの事情でパスワードが漏れてしまうリスクが増加してしまいます。 可能であれば避けるべきでしょう。

デプロイサーバ等で管理し、デプロイのたびに書き込む形式にする

例えばデプロイサーバの環境変数やどこかのファイルに保存しておき、デプロイ時にアプリケーションコードにそのパスワードを書き込む方法です。 もちろんこれでも可能ですが、何かしらの理由でデプロイサーバのデータが失われたときに、パスワードのデータが消えてしまう可能性があります。 また、デプロイサーバやデプロイ方法が変更となった場合に、移行しなければならないでしょう。

AWS RDSの場合)IAMを使用した認証方法にする

AWS RDSでは、パスワードではなくIAMを使って接続をすることができます。 これができればそもそもパスワード自体が不要ではありますが、RDSでしか使用できないことや、IAMを使用した認証方法だと接続数によっては追加オーバーヘッドが発生してしまう場合があるという問題があります。

AWSパラメータストアを使用する

AWSには、パラメータストアというサービスがあります。 これは、AWS上で様々なデータを保存しておくことが出来るものですが、

  1. パラメータストアにパスワードを置いておく
  2. それをデプロイ時に取得し、アプリケーションコードに書き込む

とすることで、安全にパスワードを管理することができます。

問題点として、パラメータストアからデータを取得するのが面倒なこと、取得したデータをアプリケーションコードに書き込むのが面倒なことが上げられますが、実はSpring Bootであれば、それらを劇的に簡単にする方法があります。

そこで今回は、最後の「AWSパラメータストアを使用」してDBのパスワードを管理する方法について、Spring Bootで簡単に行うやり方について説明します。

Spring Cloudを使ったパラメータストアとの統合

Spring Bootには、Spring Cloudというプロジェクトが存在します。 ここではAWSとの統合を容易にする様々なライブラリが提供されているのですが、その中にパラメータストアとの統合を容易にするものも存在しているのです。

以下の設定だけで、パラメータストアからパスワードを取得出来るようになります。

アプリケーションコードの設定

アプリケーションコードの設定を行います。 必要なのは、 build.gradleapplication.yml のみです。

build.gradle

バージョンは適宜変更してください。

implementation 'io.awspring.cloud:spring-cloud-starter-aws-parameter-store-config:2.3.2'

application.yml

以下以外の設定は通常通りで大丈夫です。

spring:
  application:
    name: sample.application # パラメータストアのキーに使用します
  config:
    import: 'aws-parameterstore:'
  datasource:
    password: xxxx # ここにパスワードが入ります。上書きされるので、何を書いていても大丈夫です。

aws:
  paramstore:
    region: ap-northeast-1 # 使用したいパラメータストアのregionを設定してください
    prefix: /config # パラメータストアのキーのprefixに使用します

コードとしては、なんと実質これだけで「パラメータストアからのパスワードの取得」と「取得したパスワードのアプリケーションコードへの書き込み」ができたことになります!

AWSと接続するための認証

コードとしては上記で完成ですが、パラメータストアからデータを取得するためには、AWSと接続するための認証の設定が必要となります。 方法例として、以下のものが挙げられます。

環境変数で設定する

AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY という名前の環境変数を作成し、それぞれにパラメータストアにアクセスできる権限を持つIAMユーザのアクセストークン・シークレットトークンを入力します。

クレデンシャルファイルを使用する

AWS CLIaws configure コマンド等を使って、パラメータストアにアクセスできる権限を持つIAMユーザのクレデンシャルファイルを作成します。

ECSやEC2インスタンスプロファイルのクレデンシャルを使用する

実行環境のECSやEC2インスタンスプロファイルに、パラメータストアからデータを取得できるポリシーを追加します。

詳しくはこちらを御覧ください。

とりあえずローカルで確認するだけなら、環境変数かクレデンシャルファイルを使用すると良いでしょう。

パスワードの保存

ここまででパラメータストアからデータを取得する準備は整いました! 最後に、パラメータストアにデータを保存します。

キー名は、

{$aws.paramstore.prefix}/{$spring.application.name}_{$プロファイル}/spring.datasource.password

となります。 もし実行時に local というプロファイルを使用するのであれば、今回の設定の場合は

/config/sample.application_local/spring.datasource.password

というキーの値にDBのパスワードを入れれば大丈夫です。 念の為、「安全な文字列」として保存しておくと良いでしょう。

最後に

DBのパスワードは、バージョン管理する管理方法が他の方法と比べてあまりに簡単すぎるので、まだそうしてしまっているアプリケーションも多いのではないでしょうか。 保存しているGithubリポジトリをprivateにしておけばとりあえずは安全にも見えますが、例えば何かしらのヒューマンエラーでpublicにしてしまって、その間に見られてしまう可能性も考えられます。 そして、それによってDBのデータが改竄されてしまった場合、損害は計り知れないものとなってしまう場合もありうるでしょう。

幸いSpring Bootであれば、上記のように比較的簡単にセキュアな方法で管理ができるので、ぜひやってみてはいかがでしょうか。


さて、私の今回のブログは以上ですが、アドベントカレンダーはまだまだ続きます! ぜひ明日以降も御覧ください!

qiita.com

PHPを使って形態素解析と文章の類似度を出してみる

ご無沙汰しております。
taanatsuです。

今回は珍しくPHPの記事を書いていこうと思います。
ExciteといえばPHPですからね!しらんけど。

形態素解析

皆さんは「形態素解析」という言葉を耳にしたことがありますでしょうか?
機械学習だ!AIだ!と騒がれる昨今、文章の解析で使われる手法の一つがこの形態素解析です。
私は漢字が4つ以上並ぶと読めなくなるので、形態素解析という言葉が苦手ではあります。

形態素解析とは、文章を「形態素」、いわゆる名詞・動詞・形容詞・副詞のような、日本語の最小単位の単語に分割する処理のことを言います。

形態素解析器「MeCab

形態素解析を行ってくれるツールです。
今回はよく使われる「MeCab」を利用していきたいと思います。

で、Windowsの方はすいません。。。
会社のPCがMacなので、この記事はMac用になります。
私個人はWindows機を利用していて、できることは知っていますので、Windowsの方は頑張ってください!
(確かバイナリをダウンロードして、ダブルクリックしてインストールするだけだったはず…!)

MeCabのインストール

Homebrewを使えば一発です。

$ brew install mecab

以上!
……と言いたいところですが、文章を単語に分割するための「辞書」が必要になります。
辞書も入れましょう。

$ brew install mecab-ipadic

以上です。
ターミナル上でmecabとタイプしてEnterを押してみてください。
入力待ちになります。

この状態で「すもももももももものうち」と入力してEnterを押してみましょう。

$ mecab            
すもももももももものうち
すもも   名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
うち  名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ

きれいに分割されていますね!
これが形態素解析です。

PHPからMeCabを使う

さて、こういう分野ではPythonブイブイ言わせていますが、弊社はPHPが盛んな会社ですので、PHPで使っていきましょう。 PHP7.1くらいまでは php-mecabというPHPMeCabを使えるようにするバインディングツールがあったのですが、どうやらPHP8には対応していなさそう…
なので、ちょっと強引ですが、PHPexecを使っていきます。
参考

<?php
$result = [];

$text = 'すもももももももものうち';
exec('echo ' . $text . ' | mecab', $result);

var_dump($result);

array(8) {
  [0] =>
  string(61) "すもも    名詞,一般,*,*,*,*,すもも,スモモ,スモモ"
  [1] =>
  string(40) "も        助詞,係助詞,*,*,*,*,も,モ,モ"
  [2] =>
  string(49) "もも      名詞,一般,*,*,*,*,もも,モモ,モモ"
  [3] =>
  string(40) "も        助詞,係助詞,*,*,*,*,も,モ,モ"
  [4] =>
  string(49) "もも      名詞,一般,*,*,*,*,もも,モモ,モモ"
  [5] =>
  string(40) "の        助詞,連体化,*,*,*,*,の,ノ,ノ"
  [6] =>
  string(63) "うち      名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ"
  [7] =>
  string(3) "EOS"
}

形態素解析を使った文章の類似度

それでは形態素解析を使って文章の類似度を出してみましょう。
2つの文章に登場した形態素に対して、文章がその形態素を持っていれば1、持っていなければ0として計算してみましょう。

<?php
$text1 = '新宿は豪雨';
$text2 = '渋谷は豪雨';

echo cosineSimilarity($text1, $text2);

/**
 * 分かち書きのリストを作成します
 * 
 * @param string $text 分かち書きを作成したい文章
 */
function getWakachiList(string $text): array {
    $result = '';
    exec('echo ' . $text . ' | mecab -Owakati', $result);

    if (!is_array($result) || count($result) !== 1) {
        return [];
    }

    return explode(' ', $result[0]);
}


/**
 * 文章の類似度をコサイン類似度を用いて求めます
 * 
 * @param string $text1 文章1つ目
 * @param string $text2 文章2つ目
 */
function cosineSimilarity(string $text1, string $text2): float {
    // 文章を形態素に分解
    $text1Corpus = getWakachiList($text1);
    $text2Corpus = getWakachiList($text2);

    // 2つの文章の形態素を抽出
    $allCorpus = array_unique(array_merge($text1Corpus, $text2Corpus));

    // コサイン類似度の計算に必要な分子分母の変数
    $c = 0;
    $m1 = 0;
    $m2 = 0;

    foreach ($allCorpus as $word) {
        // 文章1に対象の形態素があるかどうか(あれば1、なければ0)
        $n1 = (array_search($word, $text1Corpus) !== false) ? 1 : 0;
        // 文章2に対象の形態素があるかどうか(あれば1、なければ0)
        $n2 = (array_search($word, $text2Corpus) !== false) ? 1 : 0;

        // コサイン類似度に利用する分子分母の数値を計算
        $c += ($n1 * $n2);
        $m1 +=  $n1 * $n1;
        $m2 += $n2 * $n2;
    }

    // コサイン類似度の計算
    if ($m1 === 0 || $m2 === 0) {
        return 0;
    }

    return $c / (sqrt($m1) * sqrt($m2));
}

新宿は豪雨
渋谷は豪雨

以上のテキストから形態素を抽出します。
また各文章にその形態素があるかないかも確認します。

新宿 豪雨 渋谷
テキスト1 1(形態素を持っている) 1 1 0(形態素を持っていない)
テキスト2 0 1 1 1

上記の1と0を使って、コサイン類似度を算出します。

このように形態素解析を使うと、単語単位で分解できるため、精度良く文章の類似度を出すことができます。
それでは今回はこのあたりで!

業種交流LT会【広告チーム編】を開催しました📃

iXITの小長谷です。

11月19日に、2回目の開催となる業種交流LT会を行いました🎉

今回は広告チーム編というテーマで、普段広告に関わる業務を行っている方々に登壇していただきました!

今年8月には業種交流LT会【クリエイティブ編】も開催しています。そちらも以下からぜひご覧ください。 

tech.excite.co.jp

発表内容

メディア事業部から2名、ヘルスケア事業部から2名の、計4名に普段の業務内容などを発表していただきました。

メディア事業部の広告について

メディア事業部の2名からは、それぞれ編集担当、営業担当として行っていること、その中で工夫している点・苦労している点などを発表していただきました。

営業では、案件獲得のために代理店・広告主となる企業へのアプローチを行っていること、編集部では、営業の方と一緒に案件獲得から始まり、記事制作、公開、継続提案までを行っていることをお話しいただきました。

苦労している点として

  • コロナ禍でオフラインによる新規担当者との出会いが減ってしまったこと

  • 確定案件を制作しながら、自主提案企画も併行して進めていること

  • 並行して担当している案件が多く、スケジュール管理が大変であること

などがあり、工夫している点として

  • クライアントと、社内の編集部の方との齟齬が無いように連携を大切にすること

  • 自身の経験や周囲の体験を企画に落とし込むこと

  • 営業と編集で常に情報共有、意見交換を活発に行うこと

などがあるとのことでした。

ヘルスケア事業部の広告について

ヘルスケア事業部の2名からは、広告計測のこと、担当サービスの新規課金を促すための広告運用について発表していただきました。

広告の計測については、間接コンバージョンと直接コンバージョンの説明や、異なる広告媒体でも平等に計測することができる「アドコード」の設定などについてお話しいただきました。

広告運用では、リスティング広告についての説明から、広告の表示順位に関わる「広告ランク」に関してのことと、広告ランクを上げるために取り組んでいる「広告とランディングページの品質」などについてお話しいただきました。

まとめ

他業種のことを知り、交流するきっかけになることを目的とした業種交流LT会の第2回目として、今回は「広告」というテーマで開催しました。

普段なかなか知る機会がない業務内容や、そこで働く方々の知見を得ることができ、とても良い機会になったと感じています!

今後のLT会レポートもお楽しみに!

ECSで複数のターゲットグループを紐付けるときの注意点

こんにちは。 エキサイト株式会社の三浦です。

AWSをインフラとしている場合、アプリケーションを動かす環境としてECSは選択肢の大きな一つです。 それがWebアプリケーションであれば、ECSとALBのターゲットグループを紐付けることが多いでしょう。

実はECSは、2つ以上のターゲットグループと紐付けることができます。 ですが、1つのみ紐付ける場合と比べると注意点があります。

今回は、その注意点について説明していきます。

なおこちら、2021/11/22現在の情報であり、将来的には変わっている可能性があるのでご注意ください。

ECSとターゲットグループ

ECSは、AWSが提供しているコンテナ稼働のマネージドサービスです。 公式ページには、以下のように説明されています。

Amazon ECS は、フルマネージドコンテナオーケストレーションサービスであり、コンテナ化されたアプリケーションを簡単にデプロイ、管理、およびスケーリングできます。

マネージドであり管理が非常に容易なため、AWSをインフラとしていてアプリケーションをコンテナでデプロイしたい場合は、ECSを使うことが選択肢の大きな一つとなります。 また、提供したいアプリケーションがWebサービスである場合、ECSとALBのターゲットグループを紐付けることによりALB経由でアクセスを流すことができます。

多くの場合、ECSとターゲットグループは 1 : 1 で紐付けると思います。 ただ、例えばinternal(VPC内のみ)とexternal(internet経由)の両方のALBを使用する必要があるなど、2つ以上のターゲットグループをECSに紐付けたいこともありえます。

実はそうした場合、1つのECSに対して2つ以上のターゲットグループを紐付けることができます。

f:id:excite-takayuki-miura:20211122113324p:plain
ターゲットグループが1つの場合

f:id:excite-takayuki-miura:20211122114121p:plain
ターゲットグループが2つの場合

非常に便利な機能なのですが、1つ注意点があります。

2つ以上のターゲットグループを紐付ける注意点

ECSには「オートスケーリング」という機能があります。 これは文字通り、コンテナへのアクセス量や負荷に応じて自動的にコンテナ数を増減してくれる機能であり、例えば時間や情勢によってアクセス量が変わりうるWebサービスなどで非常に有用な機能です。

このオートスケーリングでは、基本的に3つの指標をもとにコンテナの増減数を決定します。

ECSServiceAverageCPUUtilization

平均のCPU使用率が指定した値を超えたらコンテナ数を増やす

ECSServiceAverageMemoryUtilization

平均のメモリ使用率が指定した値を超えたらコンテナ数を増やす

ALBRequestCountPerTarget

ALBからの1コンテナ平均リクエスト数が指定した値を超えたらコンテナ数を増やす

これらはそれぞれサービスの特性に応じて使い分ければよいのですが、実は2つ以上のターゲットグループに紐付けたコンテナでは、最後の ALBRequestCountPerTarget を使用するのは危険です。

f:id:excite-takayuki-miura:20211122114906p:plain
ターゲットグループが1つの場合

f:id:excite-takayuki-miura:20211122125321p:plain
ターゲットグループが2つの場合

選択自体はできますが、実はこちら、片方のターゲットグループしか計算されていません。

計算されている方のターゲットグループにアクセスがあった場合

f:id:excite-takayuki-miura:20211122131831p:plain
計算されている方のターゲットグループにアクセスがあった場合のグラフ

f:id:excite-takayuki-miura:20211122131924p:plain
計算されている方のターゲットグループにアクセスがあった場合のコンテナ数

計算されていない方のターゲットグループにアクセスがあった場合

f:id:excite-takayuki-miura:20211122150139p:plain
計算されていない方のターゲットグループにアクセスがあった場合のグラフ

f:id:excite-takayuki-miura:20211122150210p:plain
計算されていない方のターゲットグループにアクセスがあった場合のコンテナ数

このように、片方のターゲットグループしか計算されないので、基本的に2つ以上のターゲットグループを紐付けたECSでは、 ALBRequestCountPerTarget は使わないほうが良いでしょう。

最後に

こちら、実は昔はそもそも2つ以上のターゲットグループを紐付けたECSでは ALBRequestCountPerTarget を選択すること自体が不可能でした。 それが選べるようになったということは、徐々に改修がされていっているのかもしれません。

こちらは2021/11/22現在の情報ですが、将来的には適切に処理がなされるようになるかもしれないので、気になる方は注意してみると良いでしょう。

SpringBootで対話的インターフェースをSpring Shellで実装する

エキサイト株式会社エンジニアの佐々木です。SpringBootは、Webアプリケーションフレームワークというのが一般的な認識可と思いますが、結構なんでもできたります。今回はマイナーな、インタラクティブCUIアプリケーションを作る、SpringShellの機能についてご紹介します。(https://docs.spring.io/spring-shell/docs/2.0.1.RELEASE/reference/htmlsingle/)

はじめに

運用フェーズで極稀に操作することとかあるとおもいます(あまりしたくはないですが)。そういうときに、Shellが役に立つことがあります。

設定

build.gradle

依存関係を解消するのに、下記のように修正します。

依存関係の解決と、 ./gradlew bootRun で実行したときに標準入力を有効にするオプションを追加します。

dependencies {
    ....
    implementation 'org.springframework.shell:spring-shell-starter:2.0.1.RELEASE'   // これを追加する
    ....
}

bootRun {
    standardInput = System.in   // ./gradlew bootRun で実行したときに、標準入力を有効にする為
}

application.properties

下記を入れないと循環参照エラーで起動しないのを回避するのに追加します。

....
spring.main.allow-circular-references=true
....

コード

ここからはWebアプリケーションと同じようなエンドポイントのようなものを書いていきます。

package com.example.batch.controller;

import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;

@ShellComponent
public class DemoShellController {

    @ShellMethod(value = "足し算をする", key = "sampleAdd" , group = "calc")
    public Integer add(int a, @Max(10) int b, @ShellOption(value = "--optional_c" , defaultValue = "1") int c){
        return a + b + c;
    }
}

このような感じで記述できます。@ShellMedthodでShellのエンドポイントを定義できます。valueアトリビュートは必須設定ですが、その他は任意になります。コマンド名はデフォルトメソッド名ですが、keyをつけることにより変更できます。@MaxでValidationも付与でき、@ShellOptionで引数名変更やデフォルト値を入れることも可能です。デフォルト値を入れないと、必須パラメータ になるので、注意してください。

実行

動作結果としてはこのような感じになります。 補完が効くのも便利です。

shell:>sampleAdd 1 10
12
shell:>sampleAdd 1 11
The following constraints were not met:
        --b int : 10 以下の値にしてください (You passed '11')
shell:>sampleAdd 1 11 10
The following constraints were not met:
        --b int : 10 以下の値にしてください (You passed '11')
shell:>sampleAdd 1 10 10
21
shell:>

f:id:earu:20211122100917g:plain

まとめ

Shellアプリケーションが簡単に実装できたかとおもいます。Java + SpringBoot + Gradleではマルチプロジェクト、モジュラーモノリス構成が簡単に構築できますので、内部の実装はWebアプリケーションで開発した実装を使用することが可能です。SpringBoot依存にはなってしまいますが、サービスが大きくなってきたらマイクロサービスに切り出すでいいかと思っております。

今回は、インタラクティブ(対話的)なShellアプリケーションでしたが、同じソースコードバッチ処理も可能ですので、次回ご紹介いたします。

おわりに

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

以上となります。

DNSの名前衝突に気を付けよう

こんにちは。インフラストラクチャーグループの宝田です。

DNS 運用で気を付けるべき名前衝突問題に出くわしたので、事象の発生経緯と今後の対策を記しておきます。DNS の名前衝突問題とは、組織が内部的に使う Top Level Domain(TLD)と インターネットで利用できる TLD が重複してしまうことにより、DNSの動作が期待するものとは違った動作になることを指します。

経緯

サービスで使用中のとある AWS アカウントで AWS ClientVPN(以降 VPN と記載します) を構築し利用していたところ、VPN を接続している時に Slack の通話ができないという現象が発生しました。他の通話アプリ、例えば Google Meet や Zoom 等は問題なく使用できるが Slack だけ通話機能が一切使えないという状態でした。
とりあえず VPN を接続した状態で Slack チャンネルで /call --test と打って通話機能のテストを試したところ、以下のように音声接続がうまくいっていないことが分かりました。

f:id:excite-takarada:20211118101710p:plain
コールテスト画面

Slack 通話のどのフェーズで失敗しているか把握したいので Slack のサポートに問い合わせてみると下記の方法で通話ログを取得できることを教えてもらいました。

  1. Slack アプリの上部のメニューバーで、「ヘルプ」>「トラブルシューティング」>「再起動してネットワークログを取得」を選択します(Windows 10 の場合は、左上の 3 本の横線をクリックして、「ヘルプ」>「トラブルシューティング」>「再起動してネットワークログを取得」を選択します)

  2. 再起動を求めるポップアップが表示されます。「Slack を再起動する」 をクリックします

  3. デスクトップアプリが自動的に再起動し、「ログの進行中です」というポップアップが表示されます

  4. 調査したい内容を再現します

  5. ロギングを 2 分間実行して、問題がキャプチャされたことを確認してから、ポップアップにある「ロギングを停止」をクリックします

  6. Slack のダウンロード先のフォルダに表示される .zip ファイルを解凍した中にある net.log がネットログになります

ただし、通話ログの見方は公開されていないようなので自ら解析するなら雰囲気で理解する必要があります。今回はサポートにお願いして通話ログの中身を解析してもらいました。
解析してもらったところ、Slack 通話で発生する signal.m1.an1.app.chime.awsnearest-media-region.l.chime.aws の名前解決が失敗していることが分かりました。余談にはなりますが、上記の URL からも分かる通り Slack は Amazon Chime SDK を使っているようです。

aws.amazon.com

ここまでくると、aws ドメインの名前解決が失敗する理由はすぐ分かりました。原因は、VPNDNS 設定でプライベートホストゾーンの名前解決を可能にしており、そのプライベートホストゾーンに aws というホストゾーンが存在しているからでした。つまり、 VPN を繋いだ状態で aws ドメインを名前解決するとプライベートホストゾーンを参照してしまい、そのプライベートホストゾーンには signal.m1.an1.app.chime.awsnearest-media-region.l.chime.aws はレコードとして当然登録されていないので名前解決が失敗していたということです。

上記の事象は、AWS が管理している aws ドメインを内部向けドメインに使用してしまったという名前衝突が根本的な原因となります。

対策

今回は弊社の内部向けで使っているドメインが他組織で管理しているドメインと重複してしまい、名前解決に不具合が起きた結果、外部のサービスが利用できなくなる現象がおきました。

名前衝突の対策としては JPNIC の記事にある通り、ドメインを取得してそのドメインを内部向け DNSドメインとして利用することにしました。これで他組織で管理されているドメインと重複することが原理的になくなります。

内部利用向けのTLDに関しては、インターネットで利用できるグローバルなドメイン名を使用する1

まとめ

DNS の名前衝突の事例について紹介しました。内部向けドメインは組織で取得したものを利用しましょう。
最後に補足として、DNS のサーチリスト2と呼ばれるドメイン名の補完機能を使うと、ローカルネットワークのドメイン名を名前解決するつもりがインターネット上の TLD に対して名前解決してしまうという今回の事例とは反対の現象も起こり得ますので、サーチリストも極力使わないということも合わせて押さえておくといいと思います。

JJUG CCC 2021 Fallに弊社社員が登壇します!

弊社社員がJJUG CCC 2021 Fallで登壇いたします。 お時間のある方はぜひご覧ください!

登壇情報

登壇者: s-nakao@エキサイト株式会社 (@openjdk17) | Twitter

日時: 2021/11/21 10:00〜

場所:Track D

タイトル:エキサイトブログ刷新に向けて序章 - APIを一つに

プロポーザルURL: fortee.jp

JJUG CCC 2021 Fall

開催日時:2021/11/21

開催場所:オンライン開催

公式サイト:

jjug.doorkeeper.jp

公式TwitterJJUG (@JJUG) | Twitter

JavaのRestTemplateでタイムアウトを設定する方法

こんにちは。 エキサイト株式会社の三浦です。

APIにアクセスする際、一定時間までにレスポンスが返ってこなかったらエラーとして処理したい、というのはよくある要望かと思います。 そのために使用するタイムアウト設定について、JavaのRestTemplateで設定する方法を説明します。

タイムアウト

APIなどにネットワーク経由でアクセスする場合、必ずしもレスポンスがすぐに返ってくるとは限りません。 API自体に不具合があったり、アクセスが急増していたり、ネットワークに問題があったりなど、様々な理由でレスポンスが返ってくるまでに時間がかかってしまう可能性があります。

バッチなど、多少時間がかかっても問題がないアプリケーションであれば大丈夫かもしれませんが、Webページ用のサーバであればそうも言っていられないこともあるでしょう。 そのような場合、「指定した一定時間でレスポンスが返ってこなかったらエラーとして処理する」というタイムアウト設定をするのが一般的です。

RestTemplateにおけるタイムアウト設定

RestTemplateでは、以下のようにすればタイムアウト設定ができます。

build.gradle

// バージョンは適宜変更してください
implementation 'org.apache.httpcomponents:httpclient:4.5.13'

タイムアウト設定

import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;

@Configuration
public class RestConfig {

    @Bean
    public RestTemplate restTemplate() {
        var clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(HttpClientBuilder.create().build());

        // ここでタイムアウトを設定

        // 接続が確立するまでのタイムアウト
        clientHttpRequestFactory.setConnectTimeout(500);

        // コネクションマネージャーからの接続要求のタイムアウト
        clientHttpRequestFactory.setConnectionRequestTimeout(500);

        // ソケットのタイムアウト(パケット間のタイムアウト)
        clientHttpRequestFactory.setReadTimeout(1000);

        return new RestTemplate(clientHttpRequestFactory);
    }
}

終わりに

タイムアウトを設定していないと、思わぬところでWebページの応答速度が大きくなってしまう可能性があります。 Webページの要件ごとにタイムアウト時間は異なってくると思うので、適切な設定をしましょう。

外部公開勉強会を開催しました!

技術的な知見を共有しあうことを目的に、外部公開勉強会を開催しました。

第1回のテーマは「前へ進むための新しい取り組み」でした。

長年サービス運営をしてきたエキサイト社員とiXIT社員が、どのようにして技術的負債に向き合いサービスを成長させていったのか、その成功と失敗について話していただきました。

今後も外部公開勉強会を開催していきますので、見に来ていただけると幸いです。

excite.connpass.com

発表資料

クラウドネイティブ化への第一歩

speakerdeck.com

僕たちがすべきことはリファクタリングなのか

www.slideshare.net

約10年続くスマホゲームアプリのDB負荷と向き合う話

www.slideshare.net

エキサイトブログ刷新に向けて 序章 - APIを一つに

www.slideshare.net

JacksonでXMLをJavaオブジェクトに変換する

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 既存サービスのリビルドするにあたり、外部APIを呼び出してXMLを取得してJavaオブジェトに変換する処理を書きました。 Jacksonを使用したXMLJavaオブジェクト変換についての記事は少なかったため本記事にて紹介します。

導入

本記事で使用するサンプルのXMLとそれに対応するJavaクラスです。

サンプルで使用したXML

<Response>
    <ItemList>
        <Item id="a123" name="orange"/>
        <Item id="b234" name="lemon"/>
        <Item id="c345" name="apple"/>
    </ItemList>
    
    <ResultList>
        <Result num="1">オレンジ</Result>
        <Result num="2">レモン</Result>
        <Result num="3">リンゴ</Result>
    </ResultList>
</Response>

XMLに対応するJavaのクラス

package com.example.demo.model;

import java.util.List;

import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
import lombok.Data;

@Data
@JacksonXmlRootElement(localName = "Response")
public class Basket {
    @JacksonXmlProperty(localName = "ItemList")
    @JacksonXmlElementWrapper(useWrapping = false)
    private List<Item> items;

    @JacksonXmlProperty(localName = "ResultList")
    @JacksonXmlElementWrapper(useWrapping = false)
    private List<Result> results;

    @Data
    public static class Item {
        @JacksonXmlProperty(isAttribute = true)
        private String id;
        @JacksonXmlProperty(isAttribute = true)
        private String name;
    }

    @Data
    public static class Result {
        @JacksonXmlProperty(isAttribute = true)
        private Integer num;
        @JacksonXmlText
        private String text;
    }
}

XMLのルートの下からマッピングする

XMLのルートの下からマッピングするには、@JacksonXmlRootElementを付与する必要があります。

@JacksonXmlRootElement(localName = "Response")
public class Basket { ... }

XMLのリストをマッピングする

XMLのリストをマッピングするには、下記2つのアノテーションが必要です。

@JacksonXmlProperty(localName = "ResultList")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Result> results;

@JacksonXmlElementWrapperは、useWrappingの他にlocalNameも設定できます。 このとき、@JacksonXmlPropertyを消して、@JacksonXmlElementWrapper(useWrapping = false, localName = "ResultList") と書くと動作するように見えますが、UnrecognizedPropertyExceptionとなってしまいマッピングすることができませんでした。 そのため、上記のように2つのアノテーションを記述しなくてはなりません。

RestTemplateでXMLJavaオブジェクトの変換をする

最後に、下記のように記述することで、外部APIから取得したXMLJavaオブジェクトの変換を実現することができます。

final ResponseEntity<Basket> response = restTemplate
    .exchange(
        "http://example.com",
        HttpMethod.GET,
        httpEntity,
        Basket.class
);

おわりに

Jacksonを使用したXMLJavaオブジェクト変換についてまとめました。 JSONJavaオブジェクトの記事は数多くありますが、XMLJavaオブジェクトの記事はあまり見当たらなかったので、 XMLの変換で困っている人の助けになれば幸いです!

エキサイトは「DroidKaigi2021」に協賛しました!

エキサイトは「DroidKaigi2021」にSUPPORTERSとして協賛しました。今回は惜しくも弊社から登壇者はいませんでしたが、グループ会社であるRadiotalkから牧山さんが登壇しました。 来年は弊社から登壇者が出るように技術力を切磋琢磨し、向上させていきたいです。

登壇内容

登壇者:牧山

タイトル:Androidエンジニアが1人という不安と向き合う

内容:Androidエンジニア不足が叫ばれる昨今、1つのAndroidアプリに対して1人のエンジニアという状況が増えてきているように感じています。このような状況下では、スキル面・キャリア面での不安を持つ方も多いのではないでしょうか。私もそうでした。私のAndroidエンジニアキャリアもほとんどがAndroidエンジニアが1人での開発を経験しています。  本セッションでは、私の経験を踏まえ、どのようにAndroidエンジニアとしての技術を学び、どのように不安と向き合っているのかを紹介します。

  • Androidエンジニアとしての技術の学び方

 ・キャッチアップの方法

 ・ 優れたソースコードを読む

  • 不安と向き合うために

 ・スコープを広げてソフトウェア開発を学ぶ

 ・サービスへの理解を深めソフトウェア開発に還元する

 ・サービスの未来を見据えた設計/開発

 ・エンジニアリングでサービスを前にすすめる

アーカイブURL:https://www.youtube.com/watch?v=5M0xJLdAcAE

DroidKaigi2021

公式サイト:https://droidkaigi.jp/2021/

公式チャンネル:https://www.youtube.com/c/DroidKaigi

Javaでリトライをする方法

こんにちは。 エキサイト株式会社の三浦です。

アプリケーションコードを書く上で、特定の処理をリトライさせたい場合があります。 今回は、Java / Spring Bootでリトライする方法について説明していきます。

リトライ

アプリケーションコードを書く上で、どうしても失敗する可能性が消しきれない処理というものが存在します。 代表的なものとして「インターネットを介したAPIリクエスト」があり、以下のようにリクエスト元である我々ではどうしようもないエラーが発生し得ます。

  • リクエスト先APIに何かしら不具合が発生した
  • 通信をするためのネットワーク周りで何かしら障害が発生した

特にインターネットはベストエフォートなので、そもそも「100%常にリクエストが成功する」ことは保証していません。 そのため、上記のような場合でも我々が管理するアプリケーションは正しく動作することを考える必要があります。

これを解決する方法の一つが「リトライ」です。 すなわち、1回失敗しても「失敗した」判定はせず、規定回数に達するか成功するまで何度も処理を実行する、というものです。

Java / Spring Bootでのリトライ方法

このリトライ処理は、Java / Spring Boot環境では spring-retry というライブラリを使うことで簡単に実装できます。

build.gradle

必要なライブラリを設定します。 spring-retry だけでなく、 spring-boot-starter-aop も必要なので注意してください。

// バージョンは適宜変更してください
implementation 'org.springframework.retry:spring-retry:1.3.1'

// 設定にはこちらも必要になります
implementation 'org.springframework.boot:spring-boot-starter-aop'

設定ファイル

spring-retry の設定ファイルです。 ここで spring-retry を使えるようにします。

@Configuration
@EnableRetry
public class RetryConfig {}

リトライしたいメソッド

準備が整ったら、後はリトライしたいメソッドに @Retryable というアノテーションをつければリトライするようになります。 この時、 value に投げられたらリトライする例外クラスを、 backoff にリトライする際の挙動を書きます。

今回のサンプルコードでは、

  • SampleExceptionが投げられたらリトライする
  • リトライする際は、500ms空けて実行する
  • それ以外はデフォルトの動作

となっていますが、設定次第で挙動をカスタムできます。

@Retryable(value = {SampleException.class}, backoff = @Backoff(delay = 500))
public String sample() {
    return "Sample";
}

最後に

リトライは、アプリケーションが大きくなっていくと必要になる場面が増えてきます。 もちろん何でもかんでもリトライすればいいわけではないですが、適切に使用することでアプリケーションの信頼性は向上するはずです。

ぜひ使ってみてください。

KUROTEN.の技術スタック

エキサイト株式会社でKUROTEN.の開発している森脇です。

今回はKUROTEN.の技術スタックについてご紹介いたします。

KUROTEN.とは

KUROTEN.(クロテン)は経営管理業務のDXを行い、スピード経営を実現するプラットフォームです。必要なデータを一元管理、経営判断が可能な管理会計のあるべき姿を構築し、経営の意思決定をサポートするSaaSのサービスです。

lp.kuroten.jp

KUROTEN.開発の歴史

KUROTEN.は 2020年3月くらいから開発を開始し2021年6月にβ版のリリースをいたしました、その間いろいろ仕様も変わりシステム構成も変わり、なかなかチャレンジングな開発を行ってきました。

ようやくリリースし構成も決まってきたので、ご紹介したいと思います。

サーバーサイドの技術スタック

項目 ソリューション
言語 Go
フレームワーク Iris
ORマッパー gorp
テストフレームワーク testing
データベース Aurora
ドキュメント生成 swaggo
アーキテクチャ クリーンアーキテクチャ

Go

エキサイトではあまり使っていませんでしたが、サービスの特性上、静的型付け言語がよかったので選定しています、 エンジニア的にも業務で使ってみたかったところも大きいです。

Iris

サーバーサイドは紆余曲折あって、最終的にIrisを選定して使っています。

最初はLambdaで動かしていました。エンドポイントは100を超え、メンテナンス性が悪くパフォーマンスもよくなかったので、ECSで動かすことにしました。
ECSで動かすならフレームワークが必要ということで、選んだのがIrisでした、echoやginも候補でしたが、最速ということもありIrisを選んでいます。

ORマッパー

複雑なSQLを書いているため、ORマッパーの恩恵はあまり受けていません。 select文を書いた時に名前でバインドしたかったので、gorpを選んでいます、今はgormでもできるので、そっちが一般的ですね

swaggo

ドキュメントの生成と、postmanに食わせるswagger.jsonを吐き出してくれるとても便利なやつです。
導入前はpostman用のjsonを手動で書き、apiの追加・変更があるたびに修正しないと行けなかったのですごく大変でした

クリーンアーキテクチャ

当初はLambdaで作っていたこともあり、あまりアーキテクチャを意識した作りになっていませんでした。
Irisに移行した時にmvcにしましたが、機能が増えていくことで複雑化し、メンテナンス性や循環参照の問題も出てきたため、クリーンアーキテクチャの導入を行いました、コードもみやすく役割がはっきりと分かれているので、レビューもしやすくなりました、現在はまだ移行中です

フロントエンドの技術スタック

項目 ソリューション
言語 javascript
フレームワーク nuxtjs
デザインフレームワーク vuetify
テストフレームワーク jest

javascript

typescriptにすればよかったんですが、javascriptで書いてます。vue3に上げるタイミングでtypescriptに移行しようと思っています。

jest

当初はavaで行っていましたが、今はjestに変更しています、カバレッジも100を目指して日々テストコードを書いています。

バッチの技術スタック

項目 ソリューション
言語 python
ORマッパー sqlalchemy
テスト pytest

python

特にフレームワークも使わずに好きに書いています、Lambdaで動かすので、関数を呼ぶシンプルなコードになっています

インフラ

主にAWSを使っています、使っているサービスは一般的なものを中心です。

AWS 説明
ECS frontとapiが動いています
RDS Aurora MySQL
S3 一時的なデータ保存場所で使用しています
AppSync 非同期通信を行うために使用しています
Lambda 主にバッチを動かしています
StepFunction LambdaをStep毎に実行するために使用しています
QuickSight サービス分析で使用しています
WAF セキュリティ関連
Codepipline 検証環境や本番へのデプロイで使用しています。
CodeBuild
CodeDeploy
CloudWatch 監視
terraform インフラコードは全てterraformで書いています
cloudformation Lambda関連はsamを使っているので、cloudformationも使っているます

その他

項目 ソリューション
認証 Auth0
spreadjs

最後に

KUROTEN.はいろんな技術を使って構築されています。
今後も新しい技術をどんどん使い、良いサービス提供したいと思います。