なになれ

IT系のことを記録していきます。

Tour of HeroesをReact+Reduxで再実装した

最近React+Reduxを勉強したアウトプットとして簡単なアプリを作りました。
hi1280.hatenablog.com

作ったもの

Tour of HeroesはAngular公式のチュートリアルです。
複数画面があって、WebAPIを呼び出すサンプルになっています。
今回はこのTour of Heroesを題材にして、React+Reduxの理解を深めるためにTour of Heroesを再実装しました。

実装したプログラムはこちらです。

github.com

Angularと比較しつつ、React+Reduxによる実装内容を説明します。

Viewの作成

リスト形式でデータを表示する

javascriptの複数のオブジェクトを繰り返すメソッドを使って、HTMLを生成します。
dashboard.jsでは、lodashのmapメソッドを使用して繰り返し処理をしています。

dashboard.js

// 一部抜粋
return _.map(this.props.heroes, hero => {
  return (
    <StyledLink to={`/detail/${hero.id}`} key={hero.id}>
      <Hero>
        <h4>{hero.name}</h4>
      </Hero>
    </StyledLink>
  );
}).slice(0, 4);

Angularの場合はngForというテンプレート構文があります。
これによりHTML内で繰り返し処理が可能です。

dashboard.component.html

<!-- 一部抜粋 -->
<a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}">
  <div class="module hero">
    <h4>{{hero.name}}</h4>
  </div>
</a>

フォームを作成する

redux-formというフォームをReduxに則って扱うためのモジュールを使っています。
Reduxでプログラム自体は複雑になりますが、データが単方向に流れる設計になることで、フォームの値がシンプルに制御されています。

hero-detail.js

// 一部抜粋
class HeroDetail extends Component {
  componentDidMount(){
    const { id } = this.props.match.params;
    this.props.fetchHero(id);
    const { hero } = this.props;
    this.props.initialize({
      name: hero.name
    });
  }

  onBack() {
    this.props.history.goBack();
  }

  onSave(id, values) {
    this.props.updateHero(id, values);
    this.props.history.goBack();
  }

  render() {
    const { hero, handleSubmit } = this.props;

    if (!hero) {
      return <div>Loading...</div>;
    }

    return (
      <form onSubmit={handleSubmit(this.onSave.bind(this, hero.id))}>
        <h2>{hero.name} Details</h2>
        <div><span>id: </span>{hero.id}</div>
        <div>
          <Label>name:
            <Input name="name" component="input" type="text"/>
          </Label>
        </div>
        <Button onClick={this.onBack.bind(this)} type="button">go back</Button>
        <Button type="submit">save</Button>
      </form>
    );
  }
}

function mapStateToProps({ heroes }, ownProps) {
  return {
    hero: heroes[ownProps.match.params.id]
  };
}

export default reduxForm({
  form : 'HeroDetailForm'
})(
  connect(mapStateToProps, { fetchHero, updateHero })(HeroDetail)
)

Angularではフォームを作るのにリアクティブフォームとテンプレート駆動フォームの2つの方法があります。
Angularのチュートリアルではテンプレート駆動フォームが使われていて、双方向データバインディングでフィールドの値とフォームの値とを同期しています。

hero-detail.component.html

<!-- 一部抜粋 -->
<input [(ngModel)]="hero.name" placeholder="name"/>

hero-detail.component.ts

// 一部抜粋
export class HeroDetailComponent implements OnInit {
  @Input() hero: Hero;
}

WebAPI呼び出し

ReduxのActionと呼ばれるオブジェクトでWebAPIを呼び出します。
HTTP通信の公式なやり方はないので、npmのモジュールやWeb標準APIを使います。
今回はaxiosを使っています。

actions/index.js

// 一部抜粋
export function fetchHeroes() {
  const request = axios.get(`${ROOT_URL}?key=${key}`);
  return {
    type: FETCH_HEROES,
    payload: request
  }
}

ActionからReducerに処理が流れる時に、redux-promiseを使って、PromiseのオブジェクトをJavaScriptのオブジェクトに変換しています。
ReducerではPromiseの値であることを意識せずに処理します。

reducer-heroes.js

// 一部抜粋
export default function(state = {}, action) {
  switch(action.type) {
    case DELETE_HERO:
      return _.omit(state, action.payload);
    case FETCH_HEROES:
      return _.mapKeys(action.payload.data, 'id');
    case CREATE_HERO:
    case FETCH_HERO:
    case UPDATE_HERO:
      return {...state, [action.payload.data.id]: action.payload.data};
    default:
      return state;
  }
}

Angularでは、ReduxのようにAPIの呼び出しからStoreへの値の格納までの決められた設計はありません。
サービスクラスを作り、APIを呼び出すことが推奨されているくらいです。
公式のチュートリアルではサービスクラスを用意して、HttpClientでAPIを呼ぶ実装になっています。

hero.service.ts

// 一部抜粋
getHeroes (): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl)
    .pipe(
      tap(heroes => this.log('fetched heroes')),
      catchError(this.handleError('getHeroes', []))
    );
}

API呼び出しによる値はReduxのStoreのように集中管理するわけではなく、各コンポーネントのフィールドに格納しています。
複雑にAPI呼び出しと画面操作が行われると値の管理が難しくなる危険がありそうです。

dashboard.component.ts

// 一部抜粋
export class DashboardComponent implements OnInit {
  heroes: Hero[] = [];

  constructor(private heroService: HeroService) { }

  ngOnInit() {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes.slice(1, 5));
  }
}

CSS指定

コンポーネント内に限定してCSSを指定するためのやり方はありません。
今回はstyled-componentsを使い、CSS in JSでCSSの指定を行いました。

index.js

// 一部抜粋
const Main = styled.div`
  h3 {
    text-align: center; margin-bottom: 0;
  }
  h1 {
    font-size: 1.2em;
    color: #999;
    margin-bottom: 0;
  }
`

ReactDOM.render(
  <Provider store={createStoreWithMiddleware(reducers)}>
    <BrowserRouter>
      <Main>
        <h1>Tour of Heroes</h1>
        <nav>
          <Link to="/">Dashboard</Link>
          <Link to="/heroes">Heroes</Link>
        </nav>
        <Switch>
          <Route path="/heroes" component={Heroes}/>
          <Route path="/detail/:id" component={HeroDetail}/>
          <Route path="/" component={Dashboard}/>
        </Switch>
      </Main>
    </BrowserRouter>    
  </Provider>
  , document.querySelector('.container'));

Angularでは、コンポーネントクラスに指定したCSSコンポーネントクラスのスコープのみに適用されます。
他のコンポーネントに影響しません。
これは設定によって、グローバルなスコープにすることも可能です。

まとめ

React+ReduxによるコードとAngularのコードはかなり違います。
今までAngularをメインに利用してきたので、React+Reduxのコードにまだ違和感があります。
ただReduxを使うことで実装に迷いがなくなるというメリットがあり、かつ、Reduxを便利にするためのモジュールも充実しているために慣れてくると使い勝手が良いのではという印象です。

UdemyのReactコース「Modern React with Redux」を受講した感想

www.udemy.com

なぜこのコースを選んだのかですが、Reduxを学びたいと思い、Udemyのコースを探してました。
評価の高さとレビュー件数の多さから間違いなさそうだと思い、このコースを選びました。
結果的にはこのコースを選んで満足しています。
以下、感想です。

良い点

ES6の構文から丁寧に説明してくれる

アロー関数やテンプレートリテラルなどの比較的新しいJavaScriptの構文を解説してくれています。
モダンなJavaScriptが分からない人にも対応したコースになっています。

サンプルを作りながら段々と必要なことが学べる

以下のような段階を踏んでいくつかのアプリを作っていきます。

  • Reactだけを使ったアプリ
  • クライアント処理だけのReact+Reduxアプリ
  • サーバ通信を行うReact+Reduxアプリ
  • 複数の画面を持つReact+Reduxアプリ

まずReactだけを使ったアプリを作成することで、なぜReduxが必要なのかを体験的に分かるような流れになっています。
Reduxを使う段階に入ると、サーバ通信、ルーティング、入力画面といったアプリを作る際に必要な要素が登場します。
反復的にReactのコンポーネントやReduxのコードを作成することになります。繰り返すことで理解が深まります。

悪い点

英語が必要

コースの内容は全て英語になります。
Google翻訳による日本語字幕を表示することが可能ですが、翻訳の精度が低いので、あまり役立ちません。

Reactの説明が少ない

Reduxの話が中心です。Reactを深く学びたい人には足りないかもしれません。

まとめ

Udemyは定期的にセールを行っていて、1800円でこのコースを購入しました。
セール期間中を待って、購入すると良いと思います。
React+ReduxでWebアプリを作るやり方が学べる良いコースになっています。

XP祭り2018に参加しての気づき 〜品質とアーキテクチャの関係〜

XP祭り2018に参加しまして、QA to AQ – Being Agile at Quality: Values, Practices, and Patternsというワークショップを体験して思ったことを書きたいと思います。

XP祭り2018:QA to AQ – Being Agile at Quality: Values, Practices, and Patterns(Joseph Yoderさん、鷲崎さん)

以下のような内容でした。

伝統的な品質保証(Quality Assurance)の考え方から、アジャイル品質(Agile Quality)へとチームや組織において変わっていくために必要なパターン集が「QA2AQ」としてまとめられています。

本ワークショップでは、背景にあるアジャイル品質の基本的な考え方や主要なパターンの解説のうえで、品質シナリオ等を中心に幾つかの主要なパターンを体験いただく機会とする予定です。

上記の内容にもある通り、重要なパターンとして品質シナリオというものがあります。
品質シナリオとは、対象とするシステムの品質を可視化するための簡易なドキュメントです。
簡易なフォーマットなので、アジャイルなやり方にもマッチするのではとのこと。
以下のようなフォーマットです。

1. Stimulus (刺激) ※刺激と訳されるが入力といったほうが分かりやすいと思われる
 システムに影響を与えるコンディション
2. Source (発生源)
 入力を生み出した要素
3. Environment (環境)
 入力が発生した状況・環境
4. Artifact (成果物)
 入力を受けた成果物
5. Response (応答)
 入力によって持たらされるべき動作
6. Response measure (応答測定)
 評価可能なシステムの応答単位

今回のワークでは、お題としてIoTのシステムが提示されました。
そのIoTシステムで満たすべき品質を品質シナリオで定義します。可用性やパフォーマンスなどの観点です。

品質シナリオを考えたら、次に品質シナリオを満たすアーキテクチャを考えます。
以下のIoTシステムのためのアーキテクチャを参考にして、これを改良して、満たすべき品質を保証するには?という風に考えました。
冗長構成にするとか、遠隔地にバックアップ環境を用意するなど。

azure.microsoft.com

このような一連のワークを経験して、品質とアーキテクチャは目的と手段の関係のように密接に関係していることが分かります。
品質というとあまり興味がなくても、どのようにシステムを作るのかというアーキテクチャの話には興味があるエンジニアもいると思うので、このようなワークを通して品質を考えるのも役立ちそうと思いました。

つまり、アーキテクチャを学ぶことはイコール品質を学ぶことに繋がっているんだなぁという気づきです。
どういう品質を保証するために、こんなアーキテクチャになっているんだろうと考えたりできます。
アーキテクチャがやりたいことを実現できていたとしても品質を考慮しなかったら全然ダメということにも関係します。

開発現場での品質意識を高めるのにこんな内容を話したらいいのではと思った次第です。

後日、今回のワークの内容が共有されるということなので、またその時に振り返りたいと思います。

Angularにおける状態管理をRxJSで試した

状態管理はフロントエンドで必要な機能としてよく取り上げられています。
ReactにおけるReduxが有名で、Reduxライクな状態管理のライブラリが各種フレームワークで用意されていて、それらを使うのが主流なようです。

AngularでもReduxライクなngrxというライブラリがあります。
ただ自分はReduxの経験がないので、パッと見で複雑そうな印象がありました。
ngrx以外の手段で状態管理を試してみました。

どのように状態管理を行うか

参考にしたのはこちらです。 slides.com coryrylan.com

「RxJSを活用したAngularアプリの状態管理設計」では状態の同期を取るという方法が紹介されています。
これを参考にして、今回はPersistent stateとServer stateを同期しつつ、stateをComponentで扱うことを試します。
stateの同期にはRxJSの機能であるBehaviorSubjectを利用することが紹介されています。
BehaviorSubjectを使えば、任意のタイミングでデータを流すことができてかつ、流れたデータを保持することが可能です。

実装

Angularの公式チュートリアルでおなじみのTour of Heroesを元にしました。
github.com

hero.service.tsを変更して、Server stateと同期するPersistent stateを実装しました。

hero.service.ts

import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, throwError as observableThrowError, BehaviorSubject, of } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators';

import { Hero } from './hero';

@Injectable()
export class HeroService {
  private _heroes: BehaviorSubject<Hero[]>;
  private heroes: Observable<Hero[]>;
  private heroesUrl = 'app/heroes'; // URL to web api

  constructor(private http: HttpClient) {
    this._heroes = new BehaviorSubject([]);
  }

  fetchHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl)
      .pipe(
        switchMap(data => {
          this._heroes.next(data);
          this.heroes = this._heroes.asObservable();
          return this.heroes;
        })
      );
  }

  getHeroes(): Observable<Hero[]> {
    return this.heroes || this.fetchHeroes();
  }

  getHero(id: number): Observable<Hero> {
    return this.getHeroes().pipe(
      map(heroes => heroes.find(hero => hero.id === id))
    );
  }

  save(hero: Hero) {
    if (hero.id) {
      return this.put(hero);
    }
    return this.post(hero);
  }

  delete(hero: Hero) {
    const url = `${this.heroesUrl}/${hero.id}`;

    return this.http.delete<Hero>(url)
      .pipe(
        tap(() => {
          const current = this._heroes.getValue().filter(h => h !== hero);
          this._heroes.next(current);
        }),
        catchError(this.handleError)
      );
  }

  // Add new Hero
  private post(hero: Hero) {
    return this.http
      .post<Hero>(this.heroesUrl, hero)
      .pipe(
        tap((h) => {
          const current = this._heroes.getValue();
          this._heroes.next([...current, h]);
        }),
        catchError(this.handleError)
      );
  }

  // Update existing Hero
  private put(hero: Hero) {
    const url = `${this.heroesUrl}/${hero.id}`;
    return this.http
      .put<Hero>(url, hero)
      .pipe(
        tap(() => {
          const current = this._heroes.getValue()
            .map(h => {
              if (h.id === hero.id) {
                h = hero;
              }
              return h;
            });
          this._heroes.next(current);
        }),
        catchError(this.handleError)
      );
  }

  private handleError(res: HttpErrorResponse | any) {
    console.error(res.error || res.body.error);
    return observableThrowError(res.error || 'Server error');
  }
}

CRUDに当たる各メソッドでBehaviorSubjectのオブジェクトである_heroesプロパティにnextメソッドでデータを流しています。
Serverへリクエストを発行するとともにそのデータを_heroesに保持することで、Server stateとPersistent stateを同期しています。

また、BehaviorSubjectではnextメソッドを実行したタイミングでsubscribeメソッドが呼ばれるので、ComponentからPersistent stateを参照するのも簡単です。

heroes.component.ts

...
  ngOnInit(): void {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService
      .getHeroes()
      .subscribe(
        heroes => this.heroes = heroes,
        error => this.error = error
      )
  }

  deleteHero(hero: Hero, event: any): void {
    event.stopPropagation();
    this.heroService.delete(hero).subscribe(res => {
      if (this.selectedHero === hero) {
        this.selectedHero = null;
      }
    }, error => (this.error = error));
  }
...

BehaviorSubjectのsubscribeメソッドが実行される流れです。

  1. データを削除するためにComponentクラスのdeleteHeroメソッドが呼ばれる
  2. HeroServiceクラスのdeleteメソッドが呼ばれる
  3. BehaviorSubjectのnextメソッドが呼ばれる
  4. Componentクラスで定義したthis.heroService.getHeroes().subscribeの処理が呼ばれる

結果的に削除後のデータがBehaviorSubjectによってComponentクラスのheroesプロパティからも参照されます。

まとめ

BehaviorSubjectが有能でした。
BehaviorSubjectが保持するデータとServerのデータを同期するようにServiceクラスを作り、ComponentクラスはそのBehaviorSubjectからのみデータを取得するように設計すれば、単方向のデータフローが実現できて、シンプルな状態管理を実現できそうです。

OGPページ生成ツールというWebアプリをつくった

最近Twitter等でOGPによるシェアを前提としたWebサービスが注目されています。
ちょっと前だと、Peing(質問箱)が有名で、質問がOGPでシェアされてます。
peing.net

最近だと、ためしがき、bosyuといったサービスですね。リクエスト毎にOGPでコンテンツをシェアしています。
tameshigaki.jp

bosyu.me

個人的にはリクエスト毎にOGPを設定したページを生成できるのかが疑問で、その疑問を解消する過程でWebアプリを作りました。
OGPページを作れるアプリです。
ogp-generator.hi1280.site

アプリの説明、使い方はこちらです。


OGPページ生成ツールの使い方

アプリの仕組み

実装はAngular+Firebaseでこんな形にしています。
f:id:hi1280:20180821210203p:plain

考えてみれば簡単だったんですが、サーバサイドでHTMLを生成すればよかったんですね。
実装当初はHTMLファイルをStorageに保存してみたいな無駄なことをしてました。

細かい作りは以下のようにしています。

  • Firebase HostingとFunctionsを接続する
  • FunctionsでExpressを利用してHTMLを動的に返す
    • Expressの利用は必須ではないですが、HTMLのテンプレートを楽に使えたりするので利用しました

まとめ

最近はサーバサイドでHTMLをレンダリングすることがほとんどなくなってきたので、その発想が真っ先に出てこなかった。
FIrebaseの使い方として、サーバサイドでの動的コンテンツの生成をFunctionsに任せてしまうというのは汎用性がありそうな利用法だと思いました。Expressをそのまま動かせるのは楽です。

MEAN StackをKubernetesで動かす(その2)

前回はHTTPでアクセスするところまでを行いました。
hi1280.hatenablog.com

今回はHTTPS対応を行います。

MEAN Stackのプログラム一式はこちら
github.com

実運用に耐えうることを想定して、以下の内容を含めています。
MongoDBの内容が前回までで、今回はHTTPS対応の話になります。

  • MongoDBは可用性を高めるためにReplica Setで構成する
  • 外部公開を考慮して、HTTPS対応を行う

cert-managerというKubernetes上で自動的にSSL証明書を管理してくれるパッケージを使います。
github.com なお、cert-managerはまだ安定版ではないようなので、今回の内容が無効になる可能性があります。

事前のインストール

cert-managerをインストールするためにHelmというKubernetesのパッケージ管理ツールが必要です。
各環境に応じたHelmのクライアント環境をインストールします。
docs.helm.sh

KubernetesクラスタにHelmとcert-managerをインストールします。

$ kubectl create serviceaccount -n kube-system tiller

$ kubectl create clusterrolebinding tiller-binding \
    --clusterrole=cluster-admin \
    --serviceaccount kube-system:tiller

$ helm init --service-account tiller

$ helm repo update

$ helm install --name cert-manager --namespace kube-system stable/cert-manager

cert-managerにおけるLet's Encryptのセットアップ

自分のemailアドレスを環境変数にセットします。

$ export EMAIL=xxx@yyy.com

証明書の発行者をLet's Encryptにしてリソースを作成します。

$ curl -sSL https://rawgit.com/ahmetb/gke-letsencrypt/master/yaml/letsencrypt-issuer.yaml | \
    sed -e "s/email: ''/email: $EMAIL/g" | \
    kubectl apply -f-

先ほどのemailアドレスをセットしています。

証明書を取得する

これからの手順を行うにあたり、前回割り当てられたIngressIPアドレスドメイン名を登録しておく必要があります。
事前にドメイン登録業者などで設定しておきます。

ドメイン名が有効になったら、証明書を取得します。

certificate.yml

apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: webapp-tls
  namespace: default
spec:
  secretName: webapp-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  commonName: mean-k8s-example.hi1280.site
  dnsNames:
  - mean-k8s-example.hi1280.site
  acme:
    config:
    - http01:
        ingress: webapp
      domains:
      - mean-k8s-example.hi1280.site

issuerRefで証明書の発行者のリソースを指定しています。
ドメイン名はcommonNamednsNamesdomainsに指定します。
ingressで前回作成したIngressのリソース名を指定します。

$ kubectl apply -f certificate.yml

ここで時間がかかりますので、5分から10分ほど待ちます。

webapp-tlsというSecretリソースが表示されれば、正常に完了しています。

$ kubectl get secrets

describeコマンドでリソース作成の進行状況を確認することができます。

$ kubectl describe -f certificate.yaml

HTTPSに対応する

証明書を使うようにIngressを更新します。

ingress-tls.yml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: webapp
  labels:
    app: webapp
spec:
  backend:
    serviceName: svc1
    servicePort: 80
  tls:
  - secretName: webapp-tls
    hosts:
    - mean-k8s-example.hi1280.site

ドメイン名やSecret名をtls追記しています。

$ kubectl apply -f ingress-tls.yml

10分ほどでHTTPSでアクセスできるようになります。

まとめ

実運用に耐えうるコンテナ環境ができることをKubernetesを利用して確認できました。
今回はGKEを利用したので楽ができました。
逆に自前運用だと大変そうだと思いました。

参考にした資料

github.com

MEAN StackをKubernetesで動かす(その1)

前回はMEAN StackをDocker Composeで動かしてみました。
hi1280.hatenablog.com

今回はMEAN StackをKubernetesで動かします。
Dockerに対応したので、Kubernetesでも動くようになっているはずです。試してみます。

なお、今回のKubernetesの環境はGKE(Google Kubernetes Engine)を使用します。
手軽にKubernetes環境が用意できるので、試しに使ってみるには良いと思います。
無料のトライアル期間もあります。

MEAN Stackのプログラム一式はこちら
github.com

Docker Composeの時と似たような構成でAngularとExpressとMongoDBを動かします。
実運用に耐えうることを想定して、以下の内容を含めます。

  • MongoDBは可用性を高めるためにReplica Setで構成する
  • 外部公開を考慮して、HTTPS対応を行う

HTTPS対応の話は次回に行います。今回はHTTPでアクセスするところまでです。

事前準備

GKEを利用するために、GCP(Google Cloud Platform)用のコマンドラインツール(gcloud)をインストールします。
Quickstarts  |  Cloud SDK  |  Google Cloud

kubectlコマンドをインストールします。
Quickstart  |  Kubernetes Engine Documentation  |  Google Cloud

gcloudコマンドでKubernetesクラスタを構築します。

$ gcloud container clusters create mean-example

MongoDBのセットアップ

MongoDBを作成する前にデータを保存するディスクを準備する必要があります。

ディスクの設定

何のディスクを利用するのかを決めるためにStorageClassを作成します。
ここでは、GCE(Google Compute Engine)のディスクを利用するように設定します。

gce-storageclass.yml

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: slow
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-standard
$ kubectl apply -f gce-storageclass.yml

MongoDBの作成

Replica Setで認証を行うためにkeyfileを用意します。
KubernetesのSecretに登録します。

$ TMPFILE=$(mktemp)
$ /usr/bin/openssl rand -base64 741 > $TMPFILE
$ kubectl create secret generic shared-bootstrap-data --from-file=internal-auth-mongodb-keyfile=$TMPFILE
$ rm $TMPFILE

Replica Setにおける認証の参考情報
Internal Authentication — MongoDB Manual

MongoDB用のStatefulSetとServiceを作成します。

mongodb-service.yml

apiVersion: v1
kind: Service
metadata:
  name: mongodb-service
  labels:
    name: mongo
spec:
  ports:
  - port: 27017
    targetPort: 27017
  clusterIP: None
  selector:
    role: mongo
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongod
spec:
  selector:
    matchLabels:
      role: mongo
  serviceName: mongodb-service
  replicas: 3
  template:
    metadata:
      labels:
        role: mongo
        environment: test
        replicaset: MainRepSet
    spec:
      terminationGracePeriodSeconds: 10
      volumes:
        - name: secrets-volume
          secret:
            secretName: shared-bootstrap-data
            defaultMode: 256
      containers:
        - name: mongod-container
          image: mongo:3.4.16-jessie
          command:
            - "mongod"
            - "--bind_ip"
            - "0.0.0.0"
            - "--replSet"
            - "MainRepSet"
            - "--auth"
            - "--clusterAuthMode"
            - "keyFile"
            - "--keyFile"
            - "/etc/secrets-volume/internal-auth-mongodb-keyfile"
            - "--setParameter"
            - "authenticationMechanisms=SCRAM-SHA-1"
          ports:
            - containerPort: 27017
          volumeMounts:
            - name: secrets-volume
              readOnly: true
              mountPath: /etc/secrets-volume
            - name: mongodb-persistent-storage-claim
              mountPath: /data/db
  volumeClaimTemplates:
  - metadata:
      name: mongodb-persistent-storage-claim
      annotations:
        volume.beta.kubernetes.io/storage-class: "slow"
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

ServiceはHeadless Serviceで作成します。
この後にReplica Setの設定を行う際に、PodのIPアドレスを知る必要があるためです。

StatefulSetではMongoDBコンテナが起動する3つのPodを作成します。
3つのPodがReplica Setで利用されます。
mongodコマンドで、事前に作成したSecretを利用して認証を有効にします。 volumeClaimTemplatesで先ほどのStorageClassを指定しています。
Pod毎に10Gのディスクが設定されます。

$ kubectl apply -f mongodb-service.yml

MongoDBの設定

PrimaryになるMongoDBに接続します。

$ kubectl exec -it mongod-0 -c mongod-container bash
$ mongo

Replica Setを設定します。

> rs.initiate({_id: "MainRepSet", version: 1, members: [
       { _id: 0, host : "mongod-0.mongodb-service:27017" },
       { _id: 1, host : "mongod-1.mongodb-service:27017" },
       { _id: 2, host : "mongod-2.mongodb-service:27017" }
  ]})

ユーザを作成します。

> db.getSiblingDB("admin").createUser({
      user : "xxx",
      pwd  : "xxx",
      roles: [ { role: "root", db: "admin" } ]
  })
> use admin
> db.auth("xxx","xxx")
> db.getSiblingDB("my-heroes").createUser({
      user : "xxx",
      pwd : "xxx",
      roles: [{role:"readWrite", db: "my-heroes"}]
  })

ユーザ、パスワードは適宜設定する。

Angular+Expressの作成

Expressを作成します。

webapp-backend-service.yml

apiVersion: v1
kind: Service
metadata:
  name: webapp-backend-service
  labels:
    name: webapp-backend
spec:
  type: ClusterIP
  ports:
    - protocol: 'TCP'
      port: 3000
      targetPort: 3000
  selector:
    app: webapp-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webapp-backend
  template:
    metadata:
      labels:
        app: webapp-backend
    spec:
      containers:
      - name: webapp-backend
        image: asia.gcr.io/arched-photon-204013/mean-example_backend:latest
        ports:
        - containerPort: 3000
        env:
        - name: PORT
          value: '3000'
        - name: MONGODB_URI
          value: 'mongodb://xxx:xxx@mongod-0.mongodb-service:27017,mongod-1.mongodb-service:27017,mongod-2.mongodb-service:27017/my-heroes?replicaSet=MainRepSet'

コンテナレジストリにはGCR(Google Container Registry)を利用しています。
前回のExpress実行用のDockerfileから作成したイメージです。

Replica SetのMongoDBに接続するため、3つのMongoDBをホスト名で指定しています。

$ kubectl apply -f webapp-backend-service.yml

Angularを作成します。

webapp-frontend-service.yml

apiVersion: v1
kind: Service
metadata:
  name: webapp-frontend-service
  labels:
    app: webapp-frontend
spec:
  type: NodePort
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 80
      targetPort: 80
  selector:
    app: webapp-frontend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp-frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webapp-frontend
  template:
    metadata:
      labels:
        app: webapp-frontend
    spec:
      containers:
      - name: webapp-frontend
        image: asia.gcr.io/arched-photon-204013/mean-example_frontend:latest
        ports:
        - containerPort: 80
        env:
        - name: APP_HOST
          value: 'webapp-backend-service'
        - name: APP_PORT
          value: '3000'
        command: ['/bin/sh']
        args: ['-c', "envsubst '$$APP_HOST$$APP_PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"]

HTTPSに対応するためにIngressを利用することになります。Ingressを利用するためには、事前にNodePortのServiceを作成しておく必要があります。

コンテナイメージに関しては、前回のAngularアプリ実行用のDockerfileから作成したイメージです。

$ kubectl apply -f webapp-frontend-service.yml

外部からの疎通が可能なネットワークの設定

Ingressを作成します。

ingress.yml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: webapp-service
  labels:
    app: webapp
spec:
  backend:
    serviceName: webapp-frontend-service
    servicePort: 80

backendでは先ほど作成したNodePortのServiceを指定しています。

$ kubectl apply -f ingress.yml

Ingressを作成することで、GCLB(Google Cloud Load Balancer)が設定されます。

$ kubectl get ingress

NAME             HOSTS     ADDRESS           PORTS     AGE
webapp-service   *         xxx.xxx.xxx.xxx   80        5m

5分ほどで静的IPアドレスが割り当てられて、アクセスできるようになります。

HTTPS対応の話は次回に。

参考にした資料

Kubernetesの基礎情報 thinkit.co.jp

MongoDBのKubernetes対応 pauldone.blogspot.com