DevLOVE Premium「ドメイン駆動設計本格入門」に参加しました

3/22(金)にDevLOVE Premiumとして開催された「ドメイン駆動設計本格入門」に参加してきました。 講師は「現場で役立つシステム設計の原則」の著者でもある、増田亨(@masuda220)さんです。 まだ、聞いた話をすべて消化できたわけではないのですが、参加してから数日たってしまいましたので、一旦今の段階でまとめておこうと思います。

参加のきっかけ

ドメイン駆動設計には関心があるけど、きちんと理解できているとは言い難い状態で、ずっと来ていました。増田さんの本にしても、 Twitterで時々目にしたり、気にはなっていましたが手に取るところまでは至らずという状態でした。

そんな中、DevLOVE Premiumで増田さんがお話しされることを知り、DevLOVEのクラウドファンディングの特典で無料参加できることから、 ちょうどいい機会だし参加してみようと思いました。

アジェンダ

ドメイン駆動設計の考え方

ソフトウェアを何年も成長させていきたかったら、変更が楽で安全にしておかなければならない。 変更が楽で安全であることは、ソフトウェアにとって、あらゆる面で(使いやすさ、ビジネスの要求への対応、開発スピード、、、)プラスに作用する。 逆に、変更がやっかいで危険だと、全てにマイナスに作用し、何か課題があっても対応できないことになる。 ドメイン駆動設計は、ソフトウェアを「変更が楽で安全」に保つための手段だし、むしろ「変化を呼び込む設計」とも言える。

ドメイン駆動設計を理解する三つのキーワード

・・・なんだけど、なかなか難しい。ここが理解しにくいから、ドメイン駆動設計自体が難しく感じられる。

要点を絞り込み、

と考えると、見通しがよくなる。

(ここからが肝なんだけど、まだ十分に消化できていないので、別エントリーで書こうと思う。)

エヴァンス本のススメ

増田さんから、

  • エヴァンス本は通読するような本じゃない(何度も読んでいるから、読み込んで入るけど)
  • 値オブジェクトがビジネスルールの整理と実装の主役。エンティティは脇役だし、ドメインサービスは必要ない。それくらい振り切って読むと理解しやすいかも。
  • ユビキタス言語とかコンテキストといったキーワードがよく取り上げられるが、それが目的ではない(道具・準備の話でしかない)
  • 第1部、第2部は準備で、本質ではない。役に立つには第3部まで読まないとダメ。特に第3部の9章、10章、第4部の14章、15章、16章。

といった話があった。

レガシーに立ち向かえ

ドキュメントもコードもあやしいレガシーシステムにどう立ち向かうか、という話。それまでとはちょっと毛色の違う印象。 どうやってコードに埋もれたビジネスルールをあぶり出していくか、という感じ。 (個人的には10年位前にやってたことを思い出す)

マイクロサービスとドメイン駆動設計

ドメイン駆動設計は、別にマイクロサービスへの分割を目的とはしていない。 ただ、良い設計を目指しているので、結果的にマイクロサービス化がやりやすくなる。

ドメイン駆動設計の最初は、あちこちに手が入るので、その段階でマイクロサービス化に走るのは失敗する可能性が高い。 もっと安定してから対応すべき。

(最初からマイクロサービスとして分割して作るべきか?という議論では、そのほうが良いのでは?と思っていたが、 こういう話を聞くとたしかに最初はモノリスとして作ったほうがよいのでは?という気がする。そもそも、 最初から分割するとして、何を根拠に分割するんだ?という話になるし。)

全体を通して

Stringのような汎用型を使わず、取りうる値の範囲を意識した固有の型を使うという話など、話としては「なるほど」と思うものの、 まだ「本当にそれで最後までやり通せるのか?」に答えが出せていない状況です。

また、ビジネスルールの抽出・設計・実装は、リファクタリングを続けていく中でモデルが安定していくのだと思いますが、 実際にそれがうまくできるのか、実感を持てていないです。

参加した際、増田さんの著書をいただくことができましたので、早速読み始めています。 早いうちに実践して、この辺の感覚を持てるようになりたいと考えています。

スナップショットを作ってAzure VMを複製する

Azureで既存のVMからコピーを作りたいときどうするんだろう、と思って調べてみました。 ざっくり言うと、「既存のマネージドディスクからスナップショットを作成」→「スナップショットから新しいマネージドディスクを作成」→「作成したマネージドディスクからVMを作成」という流れのようです。

基本的には、管理ディスク (Managed Disks) スナップショットより VM をデプロイするという記事に従って実施すればよいはずです。

VM作成は、ポータルからは実施できないようで、Azure CLIPowerShellを使う必要があるようです。

手順は上記記事からもリンクされている既存の VHD を管理ディスク (Managed Disk) に変換し、VM をデプロイするの「2. 既存の "ディスク" リソースを使用し、仮想マシンを作成する。」に記載がありますが、PowerShellでの実施例になります。どちらかというと、Azure CLIの方が好みなので、Azure Managed Disks(管理ディスク) でスナップショットを使ってみたを参考にしつつ、Azure CLIマニュアルを見て実施しました。

一連の流れを実施するスクリプトGitHubに置きましたので、よろしければご利用ください。このリポジトリーのスクリプトを使った場合、作業手順は以下のような感じになります。

$ git clone https://github.com/kazusato/azure-copy-vm.git
$ cd azure-copy-vm
$ cd 00_common
$ cp ZZ_target_info.sh.template nogit/ZZ_target_info.sh
$ vi nogit/ZZ_target_info.sh # ここでコピー元、コピー先の名前等を指定する
$ cd ../01_copy_vm
$ sh 01_create_snapshot.sh
$ sh 02_create_managed_disk.sh
$ sh 03_create_vm.sh

航空会社サイトでの運賃表示にイラッとするので調べてみた

技術ネタではないですが、UI/UXの話ではあります。

航空会社のサイトで運賃検索するとき、最初に目に入る金額が、会社によって「往復の総額」のところと「往復の半額」のところがあります。

で、最初に目に入る金額を「往復の総額」だと思って画面遷移し、実は「往復の半額」だったので総額は倍です、となるとイラッとするわけです。

以前は、割と各社「往復の総額」を表示していた気がするのですが(曖昧な記憶なので本当かは不明)、ぱっと見の金額を安く見せるためか、「往復の半額」を最初に見せるようになってきた印象があります。

一方で、依然として「往復の総額」で表示している会社もあり、同じようなUIで「往復の総額」か「往復の半額」の両パターンがあるのがわかりにくさというかイラッとする原因のように思います。

というわけで、航空会社のサイトで運賃検索してみて、

  • 表示される金額が「往復の総額」か「往復の半額」か
  • 画面遷移の導線上、どのタイミングで表示金額の正体に気づけるか

という観点で各社の比較をしてみました。

調査結果

結果は記事の最後に書いている表の通りなのですが、単純に数で言うと、圧倒的に「往復の半額」を表示する会社が多いです。 しかし、米国系はAA以外「往復の総額」派。一方、ヨーロッパ系はKLだけ「往復の総額」で、それ以外は「往復の半額」派。 アジア系は両者混在という感じで、個人的に目にすることの多いCXやSQは「往復の総額」派だけど、それ以外は「往復の半額」派も結構ある。

各社まちまちなUI

各社サイトを操作してみてわかったのですが、表示されている金額が「往復の総額」か「往復の半額」かを、わかりやすく表示している会社がほとんどありません。UAとHAは明確に金額の並びに「往復」と書いてありますが、それ以外の「往復の総額」表記の会社は明示されていません。

旅程総額を表示する会社

CX、KE、OZは、便ごとに金額が表示されるのではなく、最初に目に入る金額が旅程に対してのもの(最低額あるいは幅で表示)なので、混乱しませんが、DL、SQ、MU、KLは復路便を選択して初めて「往復の総額」だったことに気づく導線になっています。

画面から「往復の総額」か「往復の半額」か区別がつかないことが多い

復路便を選択して初めて「往復の総額」と気づくパターンの画面は、「往復の半額」を表示している画面とぱっと見区別がつかないので、その後の展開(運賃総額)が予測できず、もやもやしたまま便を選ぶことになるので、イラッとするのかなと思いました。

最初に前後3日間の運賃マトリクスが表示される会社もある

なお、AC、CA、CZ、AYは、デフォルト設定での導線で、前後3日間の運賃マトリクスが表示され、そこには旅程総額の日ごとの最安値が表示されるので、便ごとの金額は「往復の半額」ですが運賃総額が頭に入った上で見ることになります。それでも、「往復の総額の最安値」が10万円なのに、「往復の半額」が10万円の便もあったりして、結構混乱しました。

総額が判明するまでの流れ

「往復の半額」で表示している会社でも、総額がわかるまでの流れが会社によって違います。

  1. 明示的に便を選択する前から総額が表示されているパターン(デフォルト選択便の金額あるいは最低額の表示)
  2. 往復とも選択した後で初めて総額がわかるパターン
  3. 往路選択時に一度「往復の半額」が総額として表示されるパターン

1つめのパターンは、まあまあわかりやすいです。最初から総額の目安がわかるので、画面操作時のもやもや感は少ないです。それでも、表示されている総額が10万円なのに、選択肢の中には「往復の半額」が10万円の便もあって、頭がついていかないことも少なからずあります。

2つめのパターンは、往路便を選択するときに「明らかに安い」金額が出てくれば、そこである程度気づけますが、そうでないことも結構あります。往路便に表示された金額が「往復の総額」としても通用しそうな額の場合、復路便の金額を見て「こういう表示になるってことは、この金額は往復の半額表示だな」とうすうす気付き、総額が出て「やっぱり往復の半額表示だったか、結構高いな」と落胆する、という流れになります。

3つめのパターンが個人的には一番嫌で、一瞬往路分の金額が総額のように見えるので、復路便を選んで総額が更新された時に「やっぱりさっきのは往復の半額だったのか」とイラッとする度合いが高かったです。

そもそもの原因

各社UIがバラバラというのが原因ではあるのですが、航空運賃自体、同じ区間・日付でも平気で2倍、3倍くらい金額が動くので、たとえば「10万円」という金額を見たときに「往復の総額」「往復の半額」のどちらでもありえそうで、総額がイメージできずもやもやしたままの操作を強いられることと、「往復の総額」だったらいいなという期待と「往復の半額」だった場合の現実のギャップが、イラッとする背景にあるように思います。

対策

航空会社間で統一したUI/UXになればうれしいですが、なかなかそうはいかないでしょうから、使いそうな会社の特徴を把握しておいて、画面の挙動(検索・操作結果)が予測の範囲に収まるよう慣れていくしかないのかもしれません。

調査結果一覧

日本

航空会社 表示金額 表示の特徴
NH 往復の半額 便選択前から総額表示あり
JL 往復の半額 便選択前から総額表示あり

※日本の国内線は、以前から区間ごとの運賃を表示する文化なので、国際線の話です。

北米

航空会社 表示金額 表示の特徴
UA 往復 運賃表示に「往復」の記載あり
AA 往復の半額 往復選択するまで総額表示なし
DL 往復 明示的な「往復」の記載はなく、次画面遷移して気づく
HA 往復 運賃表示に「往復」の記載あり
AC 往復の半額 導線の最初に出てくる±3日マトリクスでは総額表示

アジア

航空会社 表示金額 表示の特徴
CX 往復 便ごとの表示はなく、往復総額のみの表示
SQ 往復 往路便選択時に明示的な「往復」の記載はなく、復路便が差額表示となるため往復金額だったと気づく
KE 往復 便ごとの表示はなく、往復総額のみの表示
OZ 往復 便ごとの表示はなく、往復総額のみの表示
CI 往復の半額 往復選択するまで総額表示なし
BR 往復の半額 往復選択するまで総額表示なし
CA 往復の半額 導線の最初に出てくる±3日マトリクスでは総額表示
MU 往復 往路便選択時に明示的な「往復」の記載はなく、復路便選択後に往復金額だったと気づく
CZ 往復の半額 導線の最初に出てくる±3日マトリクスでは総額表示
VN 往復の半額 往復選択するまで総額表示なし
TG 往復の半額 往復選択するまで総額表示なし
MH 往復の半額 往復選択するまで総額表示なし

ヨーロッパ

航空会社 表示金額 表示の特徴
BA 往復の半額 往復選択して次画面遷移するまで総額表示なし
LH 往復の半額 往復選択するまで総額表示なし
AF 往復の半額 便選択前から総額表示あり
KL 往復 往路便選択時に明示的な「往復」の記載はなく、復路便が差額表示となるため往復金額だったと気づく
AZ 往復の半額 往復選択して初めて往復の半額だったことに気づく
SK 往復の半額 往復選択して初めて往復の半額だったことに気づく
IB 往復の半額 往復選択するまで総額表示なし
AY 往復の半額 導線の最初に出てくる±3日マトリクスでは総額表示

中東

航空会社 表示金額 表示の特徴
EK 往復の半額 最初の検索結果画面に最低総額の記載あり
QR 往復の半額 往復選択して初めて往復の半額だったことに気づく
EY 往復の半額 往復選択するまで総額表示なし

Spring Boot + Azure Cosmos DBのチュートリアル

半分は必要に迫られて、半分は自分で調べたことの整理のため、「Spring BootとCosmos DBで簡単なアプリケーションを作ろう」というチュートリアルを作ってみました。いろいろ説明が足りない所もあるかと思いますが、手順通りやれば動くところまではいくようにしたつもりです。

github.com

2018/6/5追記

慌てて書いて書き忘れたので、追記します。Spring Bootの部分は、@makingさんの本を大いに参考にさせていただいています。Spring Boot 2.0で動作確認したとか、OpenJDK10にしたとか、Lombokは使ってないとか、差異はあります。

JJUG CCCに行ってきた&オライリーのSafariはいいよ

先週の土曜日(5/26)に、JJUG CCC 2018 Springに行ってきました。 今までは、だいたい夕方に力尽きて退散していたのですが、懇親会LTに申し込んでいたこともあって、初めて最後までいました。

初めてといえば、JJUG CCCでは初めてアンカンファレンスにも参加してみました。 今回のテーマは以下の通りで、全部出てみたかったのですが、ほかに聞きたいセッションがあったことから、3回目の学習の話だけ参加しました。

時間 ハッシュタグ テーマ
14:30-15:15 #ccc_uc1 最近の開発事情 / JDKのバージョン選択について
15:45-16:30 #ccc_uc2 Javaの話(非同期処理、メモリ管理、好きなところ)
16:45-17:30 #ccc_uc3 学習の話(情報収集、教育、欲しい記事)

情報収集どうやってる?

このアンカンファレンスは、情報収集をどうやっている?という話から始まりました。大まかに「Web(SNSを含む)」「書籍」「人(勉強会を含む)」という切り口で、参加者の傾向や意見を聞く、といった感じだったのですが、その中で、「書籍」に関連して、オライリーがやっている英語の技術書読み放題サービスであるSafariのことがあがり、私も少しSafariについて話をしました。

Safariは、オライリーの本に限らず、膨大な技術書(技術書以外もあるけど、技術書の充実度が高い)が読み放題のサービスです。また、各種技術が学べる動画コンテンツがあったり、日本在住者だと時差の関係で利用しにくいですが、Live Online Trainingがあったりと、ソフトウェアエンジニアの学習コンテンツとしては非常に充実しています(英語ですが)。元々、「Safari Books Online」という名前で始まったサービスで、私も書籍を中心に利用してきましたが、サービスの紹介ページを見ると、今はどちらかというと動画やLive Online Trainingの方を推しているように見えます。

私の場合、もう10年近くこのサービスを使っており(改めて調べてみたら2010年にトライアル利用を始め、2011年に正式入会していたようです)、自分の中ではなくてはならないものになっているのですが、アンカンファレンスの場では、割と「そんなサービスあるんだ」という感じで、あまり知られていないんだなと実感しました。

ということで、ちょっとSafariのサービスについて紹介してみようと思います。

Safariのサービス

書籍

私の場合、一番利用しているのは、やはり書籍読み放題です。普通に買うと1冊数千円はする技術書が読み放題ですから、本を継続的に買う人であれば楽に会費の元はとれます。オライリーの本はもちろんのこと、ManningのIn Actionシリーズなど、めぼしい技術書はたいていそろっていると言ってよいと思います。ただし、Manningの新しい本が見つからなかったり、ここでは見つからない本もあります。

ちなみに、今日の段階では、今年の3月に出版された「Unity in Action, Second Edition」は見つかりませんが、 出たばかりの「Redux in Action」(電子版が5月発行、紙は6月)が登録されていたりして、どういう基準で登録されているのかはよくわかりません。

f:id:kazusato77:20180529062612p:plain

気軽に新しい本に手を出せるのがいい

ちょっと気になる技術書を片っ端から買おうと思ったらすぐに1万円を越えてしまうので、ある程度厳選して買うことになると思います。 その点、Safariのような読み放題サービスであれば、仮に外れであっても読むのをやめればいいだけなので、非常に気軽に手が出せます。

ちなみに、私も、全ての本を英語で読むのは辛いので日本語の本も買いますし(コードが多い本は英語でもいいけど、文章が多い本はやはり辛い)、 英語の本であっても紙で持っていたいということでSafariである程度読んだ後で紙の本を買ったこともあります。

Early Release

Safariのサービスの特徴として、「Early Release」という未出版の本が読めるという点があります。たとえば、以下の画面は、8月発行予定の「The Site Reliability Workbook」という本のページになります。

よく、訳書だと翻訳の時間の分だけ情報が遅いという点が指摘されますが、こういったサービスを前提にすると、英語で情報収集するのと日本語でするのでは、さらにタイムラグがあるということになります。

f:id:kazusato77:20180529064236p:plain

動画コンテンツ

Safariは動画コンテンツも充実しています。たとえば、以下の画面は「Automation in AWS with CloudFormation, CLI, and SDKs」という トレーニングコースの動画ですが、全部で11時間に及ぶ長時間のコンテンツです。

もっと短いコースも含め、様々な技術要素のトレーニングコンテンツが用意されています。

また、オライリー主催のカンファレンスの動画も見ることができます。

f:id:kazusato77:20180529065831p:plain

Live Online Training

上にも書きましたが、SafariにはLive Online Trainingもあります。これは、リアルタイムで視聴できるオンライントレーニングコースで、サービスの紹介ページでも、「Get real answers from real experts—in real time.」と、リアルタイムで講師に質問ができることをうたい文句にしています。

ただし、以下の画面にもあるとおり、日本時間では深夜に及ぶため、なかなか受講するのは容易ではありません。私も受講したことはないです。

f:id:kazusato77:20180529070918p:plain

というわけで、Safariのサービスについて簡単にまとめてみました。

Azure Container Registryの認証

Azure Container Registry(ACR)へのアクセスで使用する認証情報の取り扱いについて

ACRにdocker pushする場合と、Kubernetesでpullする場合のそれぞれについて、認証情報をどう扱うかをまとめておきます。

以前同じことをやったことがあったのですが、手順がすぐに思い出せなかったので、こちらにまとめておくことにしました。 なお、以下の実行環境はUbuntu 16.04 LTSで、ACRのレジストリは作成済みの前提です(レジストリ作成手順はチュートリアル参照)。

また、Azure Container Service(AKS)の場合、以下の「k8sへのsecret登録&YAMLへのimagePullSecretsの指定」をしなくても、 ロールの割り当て(az role assignment create・・・)を行うことでpullできるようになるようです。AKSのチュートリアルでも、その手順で記載されています。

docker pushする場合

Azure CLIがインストールされていない場合、まずインストールします。そのうえで、以下のようにコマンドを実行すると、ローカルにあるコンテナイメージをACRにpushできます(レジストリ名=workreg、タグ=kazusato/demoapp:v1の場合です)。

$ az login
$ sudo az acr login -n workreg
$ sudo docker push kazusato/demoapp:v1

az acr loginコマンドを実行すると、~/.docker/config.jsonに認証情報が書き込まれ、docker push時にそれが使われるようです。sudoで実行した場合でも/rootの下ではなく、sudo実行ユーザーのホームディレクトリ配下に作成されました。

詳しく調べていませんが、az acr loginコマンドもsudoなしで実行するとdocker関連のパーミッションでエラー(Got permission denied while trying to connect to the Docker daemon・・・)になりました。

pushできたかの確認

きちんとpushできたかは、

$ az acr respository show-tags -n workreg --repository kazusato/demoapp

を実行すると、

[
  "v1"
]

のようにACRに登録されているイメージのバージョンが返ってきますので、さきほどpushしたものが含まれているかで確認できます。

Kubernetesでpullする場合

Kubernetes(k8s)では、secret(というk8sのリソース)に認証情報を登録しておき、Pod生成に使うコンテナ情報にimagePullSecretsとして指定することで、認証が必要なコンテナレジストリにアクセスできるようになります。

この際、k8sからはpullしかしない(pushすることはない)ため、読み取り専用の権限があれば十分です。そこで、読み取り専用の ユーザー(サービスプリンシパルと呼ぶ←これはAzureの概念)を作り、その認証情報をSecretとして登録するという手順をとります。 この手順については、Azureのドキュメント サービス プリンシパルによる Azure Container Registry 認証 に記載があるのですが、複数のコマンドが入り混じってわかりにくかったので、少し整理してみました。

ACR読み取り専用サービスプリンシパルの作成

まず、

$ az acr show -n workreg --query id --output tsv

を実行し、レジストリIDを取得します(以下のような文字列が返ってきます。なお、「myresgrp」は、このレジストリの所属するリソースグループ名です)。

/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myresgrp/providers/Microsoft.ContainerRegistry/registries/workreg

次に、

az ad sp create-for-rbac --name sp-workreg-reader --scopes /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myresgrp/providers/Microsoft.ContainerRegistry/registries/workreg --role reader

を実行します。すると、

{
  "appId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "displayName": "sp-workreg-reader",
  "name": "http://sp-workreg-reader",
  "password": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "tenant": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

のような文字列が返ってきます。この中の「appId」と「password」を、secret作成に使います。

kubectlでのsecretの作成

登録するsecret名を「workreg-reader-secret」とする場合、

$ kubectl create secret docker-registry workreg-reader-secret \
  --docker-server workreg.azurecr.io \
  --docker-email myname@example.com \
  --docker-username xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
  --docker-password xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

を実行することで、secretの登録ができます。 ここで、「--docker-username」に指定する値は、「az ad sp ・・・」コマンドで返ってきたJSONのうち、 「appId」の値であることに注意してください。名前が紛らわしいですが、「name」や「displayName」では ありません(私はここで勘違いして、しばらくはまりました)。

正常に登録できると、

secret "workreg-reader-secret" created

のように返ってきます。

imagePullSecretsの指定

ここからの手順はAzure固有の要素はなく、 k8s一般のものになります。なお、ここでは直接Podを作成する場合で記載しますが、 Deploymentを作成する場合のテンプレート情報として指定する場合も同様です。

Podのコンテナ情報として、Pod定義のYAMLに以下のように指定します。

apiVersion: v1
kind: Pod
metadata:
  name: demoapp
spec:
  containers:
  - name: demoapp
    image: workreg.azurecr.io/kazusato/demoapp:v1
  imagePullSecrets:
  - name: workreg-reader-secret

このファイルをdemoapp.yamlとした場合、

$ kubectl apply -f demoapp.yaml

を実行すると、Podが登録できます。このとき、さきほど指定したsecretを使ってレジストリにアクセスしています。 Podが正しく登録できると、

$ kubectl get po

に対して、

NAME         READY     STATUS    RESTARTS   AGE
demoapp      1/1       Running   0          11m

のようにステータスが「Running」で返ってきます(kubectl applyした直後は「 ContainerCreating」かと思いますので、コンテナが起動するまでしばらく待ってください)。

(おまけ)secretが正しく登録されていない場合の挙動

どこかで設定を失敗した場合など、正しくコンテナイメージをpullできないと、Podの登録に失敗してエラーになります。 私もはまりましたので、切り分けの材料として、secretが正しく登録されていない場合の挙動を記載しておきます。

imagePullSecretsに指定したsecretが存在しない場合と、secretはあるが認証情報を誤って指定した場合を試してみました。 いずれの場合も、以下のようにステータスが「ErrImagePull」となりました。

NAME         READY     STATUS         RESTARTS   AGE
demoapp      0/1       ErrImagePull   0          4s

より詳細には、

kubectl describe po demoapp

を実行することで、情報が取得できます。表示最下部のEvents欄を見ると、Messageが 「Failed to pull image・・・(中略)・・・unauthorized: authentication required」 となっている行が存在すると思います。