SignalベースのAngular HttpResourceでAPIを叩いてみた

世間ではすっかりSignalが定着し、状態管理やFormなどの領域にもSignalベースの手法が模索されるようになりましたが、自分はまだ基本的なSignalの使い方しか知らなかったのでちょっと練習してみました。
以前Angular AI Tutorで書いた料理レシピ取得アプリケーションをベースに、今回はHttpResourceを導入しました。

HttpResourceの導入

HttpResourceは、Angular v19.2から実験的に導入されたHTTP Clientで、API経由の非同期的なデータ取得をラップし、Signalとして扱えるようになります。

angular.jp

今までのObservableの世界のHttpClientとは異なるやり方ができそうで、興味を持って導入してみました。

RecipeService

以下の2つのAPIを叩いて結果を返す RecipeService を実装してみました。 - /api/recipes - /api/recipes/{recipeId} の2つのエンドポイントを利用しています。

(※ Angular v21.0.0で実装しています。HttpResourceはまだexperimentalなので、今後APIが変わって実装が動かなくなる可能性もあることに留意してください)

@Injectable({ providedIn: 'root' })
export class RecipeService {
  private readonly httpClient = inject(HttpClient);
  private readonly recipeId = signal('');

  private readonly listRecipesResource = httpResource<RecipeModel[]>(() => '/api/recipes', {
    defaultValue: [],
  });
  
  private readonly getRecipeResource = httpResource<RecipeModel>(() => {
    const recipeId = this.recipeId();
    if (recipeId === '') {
      return undefined;
    }
    return `/api/recipes/${recipeId}`;
  });

  public listRecipes(): Signal<RecipeModel[]> {
    return computed(() => this.listRecipesResource.value());
  }

  public getRecipe(recipeId: string): Signal<RecipeModel | undefined> {
    this.recipeId.set(recipeId);
    return computed(() => {
      return this.getRecipeResource.value();
    })
  }

  constructor() { 
    effect(() => {
      if (this.listRecipesResource.status() === 'error') {
        this.handleError(this.listRecipesResource.error() as HttpErrorResponse);
      }
    });
    effect(() => {
      if (this.getRecipeResource.status() === 'error') {
        this.handleError(this.getRecipeResource.error() as HttpErrorResponse);
      }
    });
  }

  private handleError(error: HttpErrorResponse) {
    if (error.status === 0) {
      console.log('An error occurred:', error.error.message);
    } else {
      console.log(`Backend returned code ${error.status}, reason: ${error.error.message}`);
    }
    return throwError(() => new Error('Something bad happened; please try again later.'));
  }
}

HttpResourceをそのまま露出させてもよかったのですが、Component側では単純なSignalだけを扱うようにしたいと思い、メソッドで隠蔽しています。(この辺はベストプラクティスがまだよくわかっていません...)

初期値問題

今回つきまとったのが、流れてくる初期値にどう対応するかです。何も意識しないと、HttpResourceの初期値としてundefinedが流れてきます。
List APIの場合は簡単で、 defaultValue を設定することでundefinedからおさらばできます。普通のSignalでいう initialValue に相当します。

private readonly listRecipesResource = httpResource<RecipeModel[]>(() => '/api/recipes', {
  defaultValue: [],
});

問題はGet APIです。今回は呼び出し元から受け取った recipeId をSignalにsetし、それをトリガーにAPIを叩くようにしているので、下手に recipeId の初期値を設定すると不要なリクエストが飛びかねないと思っていました。
しかしHttpResourceでは、URLのstringを導出する関数でnullやundefinedを返せば、リクエストが実行されないようです。ここで初期値であればリクエストを飛ばさないようにすることができました。

private readonly recipeId = signal('');

private readonly getRecipeResource = httpResource<RecipeModel>(() => {
  const recipeId = this.recipeId();
  if (recipeId === '') {
    return undefined;
  }
  return `/api/recipes/${recipeId}`;
});

effectでのエラーハンドリング

HttpResourceを叩くときのエラーハンドリングをどこでやるのか迷いましたが、これはエラーメッセージを返すような副作用を伴うので、 effectの中で行っています。
HttpResourceでは error() を叩くことでエラーレスポンスを受け取れるのですが、エラーがないときに叩くとこれもundefinedを返してくるのでまた面倒なことになります。そのため、 status() を叩いて今の状態を取得し、errorであればエラーハンドリング用のメソッドに飛ばす方法を取りました。

constructor() { 
    effect(() => {
      if (this.listRecipesResource.status() === 'error') {
        this.handleError(this.listRecipesResource.error() as HttpErrorResponse);
      }
    });
    effect(() => {
      if (this.getRecipeResource.status() === 'error') {
        this.handleError(this.getRecipeResource.error() as HttpErrorResponse);
      }
    });
  }

Componentからの呼び出し

List APIはレシピ一覧画面のComponentから呼んでいます。ここでは取得したレシピ全件に対するキーワード検索機能をつけているので、 computedを利用してServiceからのSignalの戻り値を加工しています。
computedは読み取り専用であり、副作用のない純粋関数的な処理を想定して作られているため、本来はcomputedの中で副作用を起こすことはできません。しかし、HttpResourceが内部での非同期的なAPIリクエストを隠蔽し、Signalとしてアクセスできるようにしてくれています。

@Component({
  selector: 'app-recipe-list',
  imports: [FormsModule, RouterLink],
  templateUrl: './recipe-list.component.html',
  styleUrls: ['./recipe-list.component.css'],
})
export class RecipeList {
  private readonly recipeService = inject(RecipeService);
  private readonly recipes = this.recipeService.listRecipes();

  protected readonly keyword = signal('')

  protected readonly filteredRecipes = computed(() => {
    return this.recipes().filter(
      recipe => recipe.name.toLowerCase().includes(this.keyword().toLowerCase())
    );
  });
}

これに対し、Get APIはレシピ詳細画面のComponentから利用し、effectの中からServiceを叩いています。

@Component({
  selector: 'app-recipe-detail',
  imports: [],
  templateUrl: './recipe-detail.component.html',
  styleUrls: ['./recipe-detail.component.css'],
})
export class RecipeDetail {
  private readonly recipeService = inject(RecipeService);
  protected readonly recipeId = input.required<string>()
  protected readonly recipe = signal<RecipeModel | null>(null);

  constructor() {
    effect(() => {
      const recipe = this.recipeService.getRecipe(this.recipeId());
      this.recipe.set(recipe() || null);
    });
  }
}

なぜGet APIではeffectを使っているのかというと、Service側の getRecipe() の実装で recipeId Signalに値をsetしているためです。getRecipe() をcomputedの中で呼び出すと Writing to signals is not allowed in a computed と怒られます。
「それなら recipeId Signalを使うのをやめて、リクエストのたびにHttpResourceにrecipeIdを渡せばいいのでは?」と思ったのですが、現時点(Angular v21.0.0)でHttpResource内のurlコールバック関数に外から引数は渡せません。IDなどを渡したければSignalを経由させるしかなさそうです。

// url関数に引数を渡せない
httpResource<RecipeModel>(url: () => string | undefined, options?: HttpResourceOptions<RecipeModel, unknown> | undefined): 

なかなか使用例を見つけられず苦労しましたが、なんとか動作させられました。自分で試行錯誤して理解していくのも楽しいものですね。

Kotlin Fest 2025に参加しました

11/1に行われたKotlin Fest 2025に参加してきました。

大規模なテックカンファレンスに参加するのは久しぶりでした。大きなのぼりが立っていたり、コーヒーやペットボトルが無料だったり、懇親会でカレーが出たりととても豪華で、スタッフの皆様がどれほど準備に尽力されたのかと思うと感謝の気持ちでいっぱいです。

セッションには少しだけ出るつもりだったのですが、面白そうなものばかりで気づいたらいくつも参加していました。

Kotlin言語仕様書への招待 〜コードの「なぜ」を読み解く〜

Kotlinのドキュメントは普段よく見ているのですが、言語仕様書までは読んだことがありませんでした。いろいろな文法が仕様としてEBNF表記で定義されていて面白いです。
trailing lambdaやinfixなど他のKotlin文法の仕様も気になっているので、とりあえずNotebookLMに仕様書を食わせていろいろ見てみることにします。

デッドコード消せてますか? - 構文解析とGradleプラグイン開発で始めるコードベース改善

構文解析ができれば自動でのコード削除や生成に大きな力を発揮しそうですね。Gradleプラグイン開発にも興味があったのですが、スライドを見た限りでは思った以上に簡単に完成させていた印象を受けて驚いています。

文字列操作の達人になる ~ Kotlinの文字列の便利な世界 ~

知らなかったStringメソッドやIDEの機能が多くありました。特にmulti-line stringのシンタックスハイライトは、まさにテストコードの入出力をJSONでやるときにいらついたことがあるのでぜひ使ってみたいです。

「動く」サンプルでスムーズなコミュニケーションを 〜CMP時代のKotlinPlayground活用最前線〜

Kotlin Playgroundが予想以上に高機能であることを知りました。自分はサーバサイドKotlinしか書いていませんが、マルチプラットフォーム開発でちょっとサンプルコードを動かして画面を出したいときにも役立つんですね。
あとKotlin Playgroundには補完機能がなくて不便だなと思っていたのですが、ちゃんとあることがわかって不覚でした。(とはいえ自分の端末では他のショートカットキーとかぶってしまっていますが...)

Functional Calisthenics in Kotlin: Kotlinで「関数型エクササイズ」を実践しよう

とても共感できることが多いセッションでした。Kotlinの好きなところはJavaの要素を踏襲しながらも関数型プログラミングの要素を取り入れやすいところだと思っています。関数型の言語は以前挑戦してみて難しかった印象があるのですが、改めて試してみたいです。

Kotlin 2.2が切り拓く:コンテキストパラメータで書く関数型DSLと新しい依存管理のかたち

コンテキストパラメータの話は全く知らなかったので濃密なセッションでした... DIがアプリケーションロジックの依存を記述する手法とすれば、コンテキストパラメータはロギングや認証など実行時に利用するコンテキストを記述できる手法、という理解をしていますがあまり自信がありません。
Spring Bootなどのフレームワークでは実行時コンテキストがどう設定されているのかがなかなか見えづらい側面があるので、新しい書き方へのアプローチとしてとても興味深いです。

2025.kotlinfest.dev

ノベルティ: Loglass Tech Blog Sprint Review 2025

ログラスさんのブースでいただいた技術記事の冊子です。ログラスさんはZennにKotlin関連の記事をたくさん投稿されていて、何度も参考にしてきたので大変ありがたいです...!じっくり読みます。

Kotlin Festのテーマである「Kotlinを愛でる」ができた日になったと思います。業務とか勉強とかの文脈に関係なく、個人として単純に技術を好きでいることの大切さを改めて感じました。
開催いただきありがとうございました!

【Spring Boot + Kotlin】 REST APIサーバとして認証機能を実装する

最近はSpring Bootを使った個人用のWebアプリケーションを作っているが、認証機能で思った以上に苦戦中。フロントエンドはSpring Bootのtemplateを使っていないため、単純なREST APIとしてSpring Bootを使う場合どう認証をすればいいのか理解が大変だった。 今回はユーザ名とパスワードによる基本的なログイン処理を作る。JWTなどのトークンは導入せず、サーバ側でセッション管理できるようにする。

まずspring-securityを入れる。

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-security")
}

次にConfigurationを設定する。Spring Securityでは、実際の認証処理(ユーザ名・パスワードの照合)は AuthenticationProvider で行われる。ユーザ名・パスワード認証では DaOAuthenticationProvider で行われ、その中ではサーバに保持されているユーザの情報を 受け取るための UserDetailService と、入力されたパスワードをEncodeする PasswordEncoder が使われる。これらを定義して渡してやると認証ができるようになる。

@Configuration
@EnableWebSecurity
class WebSecurityConfig {
    @Bean
    fun filterChain(http: HttpSecurity, authenticationManager: AuthenticationManager): SecurityFilterChain {
        http {
            // /api/loginには認証なしでアクセス可、それ以外は要認証
            authorizeHttpRequests {
                authorize("/api/login", permitAll)
                authorize(anyRequest, authenticated)
                HttpMethod.PUT
                HttpMethod.DELETE
            }
            csrf { disable() } // APIサーバなのでdisableしておく
        }

        return http.build()
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return BCryptPasswordEncoder(8)
    }

    @Bean
    fun authenticationManager(
        userDetailsService: UserDetailsService,
        passwordEncoder: PasswordEncoder,
    ): AuthenticationManager {
        val authenticationProvider = DaoAuthenticationProvider()
        authenticationProvider.setUserDetailsService(userDetailsService)
        authenticationProvider.setPasswordEncoder(passwordEncoder)

        return ProviderManager(authenticationProvider)
    }
}

UserDetailService に実装を加える。今回はデータベースの users テーブルから、ユーザ名を使ってユーザ情報を取得できるようにする。

@Service
@Transactional
class UserDetailServiceImpl(
    private val userRepository: UserRepository,
) : UserDetailsService {
    override fun loadUserByUsername(username: String): UserDetails {
        val loginUser = userRepository.findByUsername(username)
            ?: throw UsernameNotFoundException("User not found")

        return LoginUserDetails(loginUser)
    }
}

次にDBからユーザ情報を取得する部分を実装する。以下の資料がとても参考になった。

最新の6.0で学ぶ!初めてのひとのためのSpring Security | ドクセル

UserRepositoryは以下。今回はO/R mapperとしてExposedを使っている。 UserTable の定義はテーブルのスキーマ定義があればほぼそのまま書けるので省略。

@Repository
class UserRepository {
    fun findByUsername(username: String): LoginUser? {
        return UserTable.select { UserTable.username eq username }
            .map { LoginUser(it[UserTable.username], it[UserTable.password]) }
            .firstOrNull()
    }
}

Repositoryで返している LoginUser というのは自作の型。認証のためには UserDetails を返さなければいけないが、フィールドが多くてRepositoryが知るべき情報ではないので、Repositoryの段階では簡易的な LoginUser 型を返し、それをもとにUserDetails の実装である LoginUserDetails を生成する形にする。

data class LoginUser(
    val username: String,
    val password: String,
)

class LoginUserDetails(private val loginUser: LoginUser) : UserDetails {
    override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
        return mutableListOf()
    }

    override fun getPassword(): String {
        return loginUser.password
    }

    override fun getUsername(): String {
        return loginUser.username
    }

    override fun isAccountNonExpired(): Boolean {
        return true
    }

    override fun isAccountNonLocked(): Boolean {
        return true
    }

    override fun isCredentialsNonExpired(): Boolean {
        return true
    }

    override fun isEnabled(): Boolean {
        return true
    }
}

最後にログイン時に使うControllerを作る。Springのドキュメントの例を参考にする。

spring.pleiades.io

注意点として、今回のようにREST APIサーバとして認証を行うときは、その後にセッションを保持する処理は自分で明示的に書かなければいけない。 Spring Bootからテンプレートを使ってログインフォームを配信する場合は自動で行ってくれるらしいが、上のドキュメントには以下のような注意書きがある。

この例では、必要に応じて、認証されたユーザーを SecurityContextRepository に保存するのはユーザーの責任です。例: HttpSession を使用してリクエスト間で SecurityContext を永続化する場合は、HttpSessionSecurityContextRepository を使用できます。

セッション保持の処理はこちらのドキュメントを参考にする。

spring.pleiades.io

authenticationManager.authenticate() が終わった後の処理は、セッション保持のためのもの。

data class LoginRequestParams(
    val username: String,
    val password: String,
)

@RestController
class LoginController(private val authenticationManager: AuthenticationManager) {
    private val securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy()
    private val securityContextRepository: SecurityContextRepository = HttpSessionSecurityContextRepository()

    @PostMapping("/api/login")
    fun login(
        @RequestBody loginRequestParams: LoginRequestParams,
        request: HttpServletRequest,
        response: HttpServletResponse,
    ) {
        val authenticationToken =
            UsernamePasswordAuthenticationToken(loginRequestParams.username, loginRequestParams.password)
        val authentication = authenticationManager.authenticate(authenticationToken)

        val securityContext = securityContextHolderStrategy.createEmptyContext()
        securityContext.authentication = authentication
        securityContextHolderStrategy.context = securityContext
        securityContextRepository.saveContext(securityContext, request, response)
    }
}

これでログイン認証はできたので、APIでアクセスを試してみる。新規登録導線は作っていないので、DBに手動でユーザ名とパスワード ( PasswordEncoder で指定したのと同じ形式でハッシュ化したもの) を入れておく。

curl -X POST -H "Content-Type: application/json" -d '{"username": "Udomomo", "password": "<password>"}' http://localhost:8080/api/login -i -c cookie.txt
HTTP/1.1 200
Set-Cookie: JSESSIONID=<cookie value>; Path=/; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Sat, 25 Nov 2023 10:45:44 GMT

無事に200が返ってきて、 JSESSIONIDcookieにセットされる。

【Alfred Workflow】Script Filterで自作コマンドを開発する

せっかくAlfred Powerpackに入っているのでもっと使いこなしたいと思い、Workflowを使ってツールを作ってみた。
作ったのはこんな感じのモールス信号解読機。趣味でパズルを解いたりしていると、ふとしたときに意外と出てくるので、爆速で立ち上がるAlfredで解読したいと思っていた。

Alfred Workflowとは

Alfred Workflowを使うと、Alfredを拡張し、独自のキーワードで様々な機能を開発することができる。ただし有料ライセンスであるPowerpackを購入していることが条件。
全体の流れはGUIで作成するが、ビジネスロジックの部分はスクリプトで書くことができる。対応言語もBash, PHP, Ruby, Pythonなどいろいろある。

Script Filterを設定する

今回のツールでは、

  • モールス信号を入力している間に結果がリアルタイムで下に表示され、更新されていく
  • Enterキーを押すと結果がクリップボードにコピーされる

という挙動にしたかったので、Script Filterを使用した。
Script Filterはもともとは検索機能のために用意されているもので、スクリプトの実行中にAlfredに結果を表示できる。また、どのような書式で結果を表示するかもカスタマイズできる。

www.alfredapp.com

Script Filterを設定する

Alfred Preferenceの画面からWorkflowsを選ぶと、新しいWorkflowを開発できる。ちなみにAlfredの入力画面で alfred と入力すれば、Alfred Preferenceに飛べる。
右クリックで Inputs以下にある Script Filter を選択すると、新しいScript Filterを作成できる。また、最終結果をクリップボードに渡したいので、Outputとして Copy to Clipboardを選び、Script Filterから線をドラッグしてつないでおく。

f:id:Udomomo:20220206110325p:plain

Script Filterを作成すると、以下のような設定画面になる。 Keyword の欄には、このworkflowを開始するためにAlfred上で入力するキーワードを指定する。引数はqueryとargvのどちらでも良いが、エスケープの心配がいらない点やパフォーマンス面から、Alfredではargvを推奨している。

f:id:Udomomo:20220206110440p:plain

Run Behaviourからは、入力中のスクリプトの挙動を設定できる。文字をどんどん入力していったら前の段階での結果はいらなくなるので、Queue ModeTerminate previous script にする。また、結果のリアルタイム表示といっても、入力が一段落した時点でスクリプトを実行すれば十分なので、 Queue DelayAutomatic delay after last character typed にする。

f:id:Udomomo:20220206111256p:plain

スクリプトを作成する

今回はPythonで作成した。日本語と英語の解読ができれば良かったので、モールス信号の辞書をそのまま貼ったのだが、その部分は長いので省略している。

# coding: utf-8

import json
import sys

MORSE_CODE_DICT_EN = {
  'a': '.-', 'b': '-...', 'c':'-.-.', 'd':'-..', 'e':'.', 'f':'..-.', 
  ...
}

MORSE_CODE_DICT_JA = {
  'あ': '--.--', 'い': '.-', 'う': '..-', 'え': '-.---', 'お': '.-...',
 ...
}

def decrypt(lang, input):
  if lang == 'ja':
    return _morse_to_letters(input, MORSE_CODE_DICT_JA)
  elif lang == 'en':
    return _morse_to_letters(input, MORSE_CODE_DICT_EN)
  else:
    return 'Invalid argument. Usage: <lang (`ja` or `en`)> <morse code (`.` or `-`, split by space)>)'
  

def _morse_to_letters(input, morse_dict):
  if not input:
    return 'No morse code input.'
  reversed_dict = {value:key for key,value in morse_dict.items()}
  result = ''
  for m in input:
    result += reversed_dict.get(m, '#')
  return result

if __name__ == '__main__':
  args = sys.argv[1].split()
  lang, input = args[0], args[1:]
  result = {
    'items': [{
      'uid': 'morse',
      'title': decrypt(lang, input),
      'arg': decrypt(lang, input)
    }]
  }
  sys.stdout.write(json.dumps(result, ensure_ascii=False))

注意すべきポイントは3つある。まず、通常のPythonスクリプトsys.argvとは異なり、Script Filterではキーワードの後に入力した引数全てがsys.argv[1] に入る。例えば morse ja .-- --.-- と入力する場合、ja .-- --.--sys.argv[1]の値になる。最終的なargsの数を予測できないので当然といえば当然だが、自分はここでかなりハマった。
2つ目は、Alfred WorkflowはPython 2のみに対応していること。これはWorkflowがMacのビルトイン環境のみで動くことを重視しているためらしい。今回の場合、コード内でひらがなを使っているため、冒頭に # coding: utf-8 をつけないとエラーになった。
最後に、Script FilterからAlfredに結果を渡すためには、専用のJSONまたはXMLフォーマットに従う必要がある。JSONフォーマットは以下のページに記載されている。

www.alfredapp.com

複雑なWorkflowになると、専用のライブラリ(例: alfred-workflow )を使うことも多いが、今回は単純なスクリプトなので手動でJSONを作った。

{
    'items': [{
       'uid': 'morse',
       'title': decrypt(lang, input),
       'arg': decrypt(lang, input)
    }]
}

uidはAlfredが結果のitemを識別するためのID(必須ではない)。titleの部分に解読結果を載せている。また、 args に指定された値は、Enterキーを押した時点でOutputに渡される。今回の場合、argsにも解読結果を指定することで、最終的な結果をクリップボードに渡すことができる。
この他にも任意のフィールドがたくさんあり、アイコンの指定などもできる。

これでいつものAlfredウィンドウから、morseコマンドが使えるようになった。
なお、Alfred Workflowにはデバッグツールも用意されているので、Script Filterが思うように動かない場合はログを確認してみよう。

www.alfredapp.com

AWS SysOps Administrator Associateに合格しました

少し前になってしまうが、3月末にAWS SysOps Administrator Associate試験を受験して合格した。1月末から試験勉強を始めたのだが、幸い1回で合格できた。

aws.amazon.com

受験の経緯

会社のシステムがAWS上で動いており、運用のためには体系的な知識が必要だと感じたため。社内ではSolution Architect Associateの方を受験する人もいたが、自分の興味がアプリケーションというよりもミドルウェア・インフラ寄りだったため、SysOpsを選択した。

学習法

まずUdemyのコースで一通りの範囲を学習した。

www.udemy.com

このコースは実際にAWSの各サービスを利用するハンズオンが充実しており、単なる試験対策にとどまらない実践的な知識を得ることができる。ただし量が多めで、1月末から観始めて3月上旬くらいまでかかった。自分はEC2やS3・VPC等についての基本的な知識がある状態で勉強を開始したので、何も知識がない場合はもっとかかるかもしれない。

このコースには最後にmock examがついているが、実際の試験よりも難しめで細かな内容が多い。そのため、できなくてもあまり自信を失う必要はない。そのかわり、各コースの最後についている小テストは基本的な内容なので、ここは満点を取れるまで復習した。

また、模擬試験には以下のサービスを利用した。

aws.koiwaclub.com

このサービスは無料会員でも#1~#5までの5回分の模擬試験を受けられるが、有料会員になるのがおすすめ。なぜかというと、後ろの模擬試験になるほど現在の本番試験に即した問題になっているからである。自分は#86から解きはじめ、#60くらいまでを2周したのだが、本番で全く同じ問題が何問か出てきて驚いた。

本番について

試験はオンラインで自宅から受験した。予約はAWS認定の公式サイトから行う。
受験システムはPSIかピアソンを選ぶことができるが、ピアソンの場合Karabiner-Elementsが入っているPCではうまく動作しないという情報があったため、PSIを選択した。

yoshitaku-jp.hatenablog.com

当日は特に問題なく受験できたが、PSIは試験官とのやり取りが英語のみであるため、英語ができない人はピアソンの方が良いかもしれない。

また、他のオンライン試験同様、机の周りはあらかじめ片付けておく必要がある。自分は机の上の物はもちろん、壁のカレンダーや掲示物等もすべて剥がしたうえで臨んだのだが、机の隣に本棚があったため、試験官から場所を移動するよう命じられた(他にPCを置ける場所がなかったので、無理やり本棚を動かしてなんとかした)。机の周りに物や家具がある人は、試験当日にあらかじめ移動させておくか、試験を受けられるどこか別の場所を探しておいた方が良い。

【Kustomize】patchesJson6902の使い方と利用シーン

Kustomizeを使うとき、 patches でファイルを指定してbaseファイルを部分的に変更することが多いと思う。しかし先日、 patches ではなく patchesJson6902 という指定をしているファイルを見かけたので、どう使うのか調べてみた。

patchesJson6902とは

patchesJson6902は、名前の通りRFC6902に沿った方法でPatch処理を行うことができる。RFC6902ではJSON Patchについて定義されている。

tools.ietf.org

Kustomizeは公式ドキュメントをたどりづらいが、Kubernetesのドキュメントの中に詳しい使い方が記載されている。 kubernetes.io

patchesStrategicMergepatchesJson6902 の違い

Kustomizeでは、patchを行う際に patchesStrategicMergepatchesJson6902 の2つの方式に対応している。 patch フィールドは渡したファイルまたはPatchをもとに、どちらの方式かを自動で判定して適用してくれる。
この2つの方式がどう違うのかというと、 patchesStrategicMerge はマージの挙動をKubernetesの設定ファイルに最適化されるように変更している。例えば、以下のようなdeployment.yamlがあるとする。

# base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.1
        ports:
        - containerPort: 80
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
      - name: nginx
        image: nginx:1.14.3
        ports:
        - containerPort: 80

# base/kustomization.yaml
resources:
  - deployment.yaml

overlaysを以下のようにする。

# overlays/test/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  template:
    spec:
      containers:
        - name: busybox
          image: busybox:1.33
          ports:
            - containerPort: 80

# overlays/test/kustomization.yaml
bases:
  - ../../base

patchesStrategicMerge:
  - deployment.yaml

このとき、buildした結果は以下のようになる。overlaysで指定したコンテナが、 containers に追加されていることがわかる。

$ kustomize build . 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: busybox:1.33
        name: busybox
        ports:
        - containerPort: 80
      - image: nginx:1.14.1
        name: nginx
        ports:
        - containerPort: 80
      - image: nginx:1.14.2
        name: nginx
        ports:
        - containerPort: 80
      - image: nginx:1.14.3
        name: nginx
        ports:
        - containerPort: 80

一方で、これと同じことを patchesJson6902 で行おうとすると、overlaysを以下のようにしなければならない。

# overlays/test/kustomization.yaml
bases:
  - ../../base

patchesJson6902:
  - target:
      group: apps
      version: v1
      kind: Deployment
      name: nginx-deployment
    patch: |- # patchの内容は別ファイルに指定しても良い
      - op: add
        path: /spec/template/spec/containers/0
        value: {"image": "busibox:1.33", "name": "busybox", "ports": [{"containerPort": 80}]}

patch 内の pathJSON pathを指定する必要があり、ここで配列のインデックス番号を指定したうえで、配列に入れる新たな要素を定義する必要がある。 patchesStrategicMerge の場合と同じようにしてしまうと、 containers が丸々入れ替わって busybox のみになってしまう。

patchesJson6902 を使うべきシーン

逆に「配列の中の特定の一要素を入れ替えたい場合」は patchesJson6902 の方を使うべき。

# overlays/test/kustomization.yaml
bases:
  - ../../base

patchesJson6902:
  - target:
      group: apps
      version: v1
      kind: Deployment
      name: nginx-deployment
    patch: |-
      - op: replace
        path: /spec/template/spec/containers/0
        value: {"image": "busibox:1.33", "name": "busybox", "ports": [{"containerPort": 80}]}

opreplace に変更したことで、 containers 配列の0番目の要素のみを入れ替えることができる。結果は以下の通り。

$ kustomize build .  
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: busibox:1.33
        name: busybox
        ports:
        - containerPort: 80
      - image: nginx:1.14.2
        name: nginx
        ports:
        - containerPort: 80
      - image: nginx:1.14.3
        name: nginx
        ports:
        - containerPort: 80

これに対して patchesStrategicMerge の場合、 $patch: replace を指定することで containers 配列全体を入れ替えることはできるが、特定の要素を指定してそこだけ入れ替える機能はない。baseの containers 配列全体をoverlaysに書いてそこに新しい要素を加えるという方法はあるが、配列の要素が多い場合は非効率である。

各Patch方式の詳細な挙動は、以下が参考になる。

community/strategic-merge-patch.md at master · kubernetes/community · GitHub kustomize/inlinePatch.md at master · kubernetes-sigs/kustomize · GitHub

「レガシーコードからの脱却」を読んだ感想

昨年の10-12月まで、社内の輪読会で「レガシーコードからの脱却」を読んだ。(最初間違えて「レガシーコード改善ガイド」を買おうとしたのは内緒だ)

www.oreilly.co.jp

感想など

タイトルの通り、レガシーコードが増えていく企業ソフトウェアの開発ノウハウについて書かれた本。前半はアジャイル開発の方法論がメインで、正直この辺はより詳しいアジャイル本を読んだ方が良いと思う。しかし後半になると、単なるリファクタリングにとどまらない本質的な内容がたくさんあった。

ペアプログラミングの恐怖を乗り越える

過去に何度かペアプログラミングを行ったことがあるが、正直あまり良い印象を持っていなかった。会議室に一日中際限なく拘束されるうえに一挙手一投足を監視され、場の空気が非常に重くなりチームとしての生産性も非常に低かったからだ。
しかし8章を読んで、ペアプログラミングに対する誤解に気付かされた。良いペアプログラミングを行う前提として、「全員が問題を理解していること」そして「全員がメンターでありメンティーであること」が重要となる。ジュニアであっても時には問題解決のためにシニアに助言や意見を言うべきであり、全員が同時に問題解決にあたりディスカッションが自然と起こるような場作りが重要になる。ただ長く集まったり、交代でキーボードを打てば良いというものではない。
それでもペアプログラミングの気が進まないという人のために、本書では代替案も提示している。例えばある小さな問題を調査するために時間を区切って集まるスパイクや、1時間だけ2人が集まりお互いのその日のコードをレビューするバディプログラミングなどがある。大事なのは孤独な作業に徹することではなく、どのような形式であれチームメンバーと協力しあうこと・助けを求めることだ。

CLEANコードの意義

CLEAN原則についてはいろいろな本で目にしており頭では知っていたが、本書の9章を読むことで心で理解できた気がする。5つの原則(凝集性・疎結合カプセル化・断定的・非冗長)は根元では全てつながり合っており、その意義は「結合すべきものは結合し、分離すべきものは分離している状態」を作り出すことにある。こうすることで、レガシーコードを最小限に抑え、ソフトウェアの保守・拡張を容易にすることができる。
また、むやみやたらに責務の分離をさせるのも良くない。12章に書かれているが、「銀行に行く」というTODOを分解して「電車の切符を買う」というような細かすぎるタスクを作る人はいない。個々のクラスの責務が過不足なく意味が通るレベルにすることで、コード全体の抽象度が揃い、責務がわかりやすくなる。

創発設計

テストファーストで実装し、常にレガシーコードにならないように気を配りながら書いていくと、責務の分割や名前などに課題が見つかっていく。これを繰り返していくと、書いている途中でより良い設計が浮かぶ。最初にすべての設計を決めてからそのとおりに実装するよりはるかにましになる。この流れを本書では「創発設計」と呼んでいる。
このやり方は初心者には厳しいものがあるかもしれないが、アジャイル開発を行っている際に仕様や実装が大きくひっくり返る経験は何度もしたことがあるので、それが言語化されてとても共感できた。

既存のアーキテクチャに無慈悲になる

最終章に少しだけ書かれていた言葉だが、非常に刺さった。自分はどうしても「既存の設計を学んで理解する」ことに意識を向けがちだが、そこで終わらず「既存の設計の問題点を理解する」ところまで行かなければ、そもそも改善しようという発想に至らない。レガシーコードを改善するためには、まず目の前にあるものがレガシーコードではないかと認識する必要がある。
改善案も既存の設計をベースにした小さなものばかりではなく、時には全く新しい設計にする方が優れたコードになるかもしれない。そのような改善には新しい知識や経験が必要になることもあり、そのために自分たちは業務内だけでなく業務外でもより多くの技術を身につける必要がある。ここは自分の大きな弱点なので、少しずつ考え方を変えていきたい。

まとめ

真新しい概念は出てこないが、今まで知っていた概念とその意義を腹落ちさせてくれる良書だった。輪読会という形式で1章ずつ精読していったことが功を奏したのかもしれない。時間がかかってもじっくり読みたい本だと思う。