1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

docker参照コマンドチートシート

よく使うDockerコマンド

調査系のみ

コンテナの基本情報を絞って表示

docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
NAMES               IMAGE               STATUS
r1                  redis               Up 25 minutes

コンテナのIPアドレス一覧

docker ps -q | xargs docker inspect --format '{{ .Name }} - {{ .NetworkSettings.IPAddress }}'
/r1 - 172.18.0.2

コンテナがマウントしているボリュームを表示

docker run  -v /docker/redis-data:/data --name r1 -d redis
# マウント一覧を表示
docker ps -q   | xargs docker inspect --format '{{ .Name }} - {{range $mount :=.Mounts}}{{ $mount.Source }} <-> {{ $mount.Destination }}{{end}}'
/r1 - /docker/redis-data <-> /data

コンテナの統計情報

docker ps -q | xargs docker stats
CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %
     NET I/O             BLOCK I/O           PIDS
3c1719011282        r1                  0.08%               6.918MiB / 737.6MiB   0.94%     26.3kB / 90B        6.37MB / 0B         4

docker内のネットワーク

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
cc9ab9e4598e        bridge              bridge              local
fa054a9af353        host                host                local
f50397115ef2        none                null                local
$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "cc9ab9e4598e563b04c3c58f93ccdc15d9c1c682abbaf05e44034c63254cde4f",
        "Created": "2018-08-06T16:05:15.115331313Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.18.0.1/24",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "0e4a62e4d4a90d38f9f3d1be868f278807b390cdaba92951448ef64bdedd492e": {
                "Name": "tutorial_web_1",
                "EndpointID": "b396b59a93be30084d902e882eb21e961eac9d8599edbf32e832e1c05d3dc305",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/24",
                "IPv6Address": ""
            },
            "94158270f37a09f5ee28412dc569b048319fec166e1b0cf952fd8efa24c798a7": {
                "Name": "tutorial_redis_1",
                "EndpointID": "a14d820f0659721ebe1293e56871e1e68f94d6e07c5067f85c892e37a86bcfe4",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/24",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

まとめ

コンテナの起動や削除は開発環境(ローカル)はcompose、検証・本番環境はECS/GKEだから使用頻度が高いのは調査系のみだった。

pod(Kubernetes)のlifecycle.prestopの挙動

コンテナ削除時すぐにコンテナをSTOPされると困ることありません?

 

例えば….?

ApacheやNginxなどのWebサーバはSIGTERMが送られても、処理中のコネクションがCloseされないまま終了してしまう。 理想としては、残っている接続済みのコネクションを終了してからコンテナがSTOPして欲しいですよね。

Prestopを設定する

SIGTERMでの挙動は以下のようなもの(https://httpd.apache.org/docs/2.2/ja/stopping.htmlより引用)

TERM あるいは stop シグナルを親プロセスに送ると、即座に子プロセス全てを kill しようとします。 子プロセスを完全に kill し終わるまでに数秒かかるかもしれません。 その後、親プロセス自身が終了します。 処理中のリクエストは全て停止され、もはやリクエストに対する 応答はされません。

これでは、まずいのでPrestopフックを入れます。PrestopフックはPodのシャットダウンプロセスの最初に実行されるものです。Deploymentの.spec.template.spec.containers.lifecycle.preStopに処理を記載していきます。httpGet等でhttpリクエストを引っ掛けることもできますが、基本的にはexecで記述することが多いように思えます。execは記述したコマンドの同一のコンテナで実行します。

Apacheのwebサーバの場合だと、httpd -k graceful-stopをDeploymentの.spec.template.spec.containers.lifecycle.preStop.exec.commandに書いていくような形となるかと思います。Pod preStop フックの実行と Service エンドポイントからの Pod IP アドレス削除は並列して実行されるため、preStop フックの実行が先に実行されてしまいアクセスがまだ来る状態でコネクション作成をやめてしまうことになります。 なので、httpd -k graceful-stopの前に数秒置く必要があります。また、指定したコマンドが非同期の場合には、勝手にpreStopフックが完了されたとみなし、SIGTERMがPodのコンテナに送られてしまいます。そのため、非同期のコマンドのあとにはそのコマンドでかかる時間sleepしておくなどするべきです。

もしapacheのリクエストのタイムアウト時間を30秒としているならば、以下のような設定となるかと思います。 terminationGracePeriodSecondsはpodのシャットダウンプロセスが始まってからSIGKILLが送られるまでの時間です。preStopの設定を入れている場合は、preStop処理が終了後にSIGTERMが送られ、シャットダウンプロセスが始まってからterminationGracePeriodSecondsで指定した秒数が経過してもプロセスが完了していない場合にはSIGLKILLが送られます。デフォルトは30秒となっているはずです。

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
    image: httpd
    lifecycle:
      preStop:
        exec:
          command: ["sh", "-c", "sleep 2; httpd -k graceful-stop; sleep 30"]
  terminationGracePeriodSeconds: 40

kubernetesソースコード上では以下のように、preStopが実装されてます。 preStopの定義がDeploymentにない場合は、すぐにStopContainerが送られてしまうのがわかると思います。

        // Run the pre-stop lifecycle hooks if applicable and if there is enough time to run it
    if containerSpec.Lifecycle != nil && containerSpec.Lifecycle.PreStop != nil && gracePeriod > 0 {
        gracePeriod = gracePeriod - m.executePreStopHook(pod, containerID, containerSpec, gracePeriod)
    }
    // always give containers a minimal shutdown window to avoid unnecessary SIGKILLs
    if gracePeriod < minimumGracePeriodInSeconds {
        gracePeriod = minimumGracePeriodInSeconds
    }
    if gracePeriodOverride != nil {
        gracePeriod = *gracePeriodOverride
        glog.V(3).Infof("Killing container %q, but using %d second grace period override", containerID, gracePeriod)
    }

    err := m.runtimeService.StopContainer(containerID.ID, gracePeriod)
    if err != nil {
        glog.Errorf("Container %q termination failed with gracePeriod %d: %v", containerID.String(), gracePeriod, err)
    } else {
        glog.V(3).Infof("Container %q exited normally", containerID.String())
    }

Prestopフックですが、標準出力などに吐き出してもStackdriverなどでは拾えないので注意です。なお失敗した場合には、kubectl describe <pod>のイベントのところに表示されます。

インフラ初心者がAWS(Amazon ECS)についてまとめてみた

前提となる知識の理解

以下の資料を読んで理解を深めた。
アマゾン ウェブ サービスの概要
20180411 AWS Black Belt Online Seminar Amazon EC2
リージョンとアベイラビリティーゾーン - Amazon Elastic Compute Cloud
AWSにおけるマルチアカウント管理の手法とベストプラクティス
AWS アカウント ID とその別名 - AWS Identity and Access Management
AWS Black Belt Online Seminar AWS Identity and Access Management (AWS…

RegionとAvailability Zone

  • AWS クラウドインフラストラクチャはリージョンとアベイラビリティーゾーン (AZ) を中心として構築される。
  • リージョンは2つ以上のAZ から構成されている(ローカルリージョンを除く)。各リージョンは、他のリージョンと完全に分離されるように設計されている。
  • 各AZは互いに影響にを受けないように、地理的・電源的・ネットワーク的に独立している。AZ間は低遅延の高速専用線で繋がれている。
  • AZ は 1 つ以上の独立したデータセンターで構成される。各データセンターは、冗長性のある電源、ネットワーキング、および接続性を備えており、別々の設備に収容されている。このような AZ によって、お客様は単一のデータセンターでは実現できない高い可用性、耐障害性、および拡張性を備えた本番用のアプリケーションとデータベースを運用できる。
  • AZはAWS上ではリージョンコードとそれに続く文字識別子によって表されます (us-east-1a など)
  • 東京リージョンのリージョンコードはap-northeast-1で大阪ローカルリージョンのリージョンコードはap-northeast-3となっている。

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/images/aws_regions.png

Account

  • AWSアカウントはAWSアカウントIDによって一意に識別される。全てのAWS製品の中で一意であれば、AWS アカウントの別名を作成することもできる(ただし一つまで)
  • AWSアカウントとは、”リソース管理の単位”かつ”セキュリティ上の境界”であり、”AWS課金単位”でもある。
  • 単一でなく、複数のAWSアカウントの使用がAWSゴールドパートナーであるNRIさんの推奨である。
  • 複数のAWSアカウントを用いる理由(メリット)
    • ガバナンスの観点
      • 本番環境の管理コンソール(後述)を分離できる
      • 本番環境と非本番環境を管理するメンバーとで明確な権限の分離
      • 環境間におけるセキュリティ対策の分離が可能
    • 課金の観点
      • 分離したアカウント毎に明確な課金管理を行うことができる。
      • 複数のコストセンターやLOB等に対し、シンプルな課金やチャージバックの運用を行うことができる。
    • 組織の観点
      • 異なるLOBを異なる課金管理と共にサポートすることができ、LOB単位での管理を容易に行うことができる
      • 統制や課金が異なる専用アカウントを用いる個々の顧客に対してサービスプロバイダー型のサービスを提供しやすい
    • 運用の観点
      • 変更による影響範囲を縮小し、リスクアセスメントをシンプルにできる。例)開発環境の変更が本番環境に及ばない
      • AWSアカウントのリソース上限にかかる可能性を緩和できる

サービスにアクセスする方法

AWS IAM (Identity and Access Management)

  • AWS操作をよりセキュアに⾏うための認証・認可の仕組み
  • AWS利⽤者の認証と、アクセスポリシーを管理
    • AWS操作のためのグループ・ユーザー・ロールの作成が可能
    • グループ、ユーザーごとに、実⾏出来る操作を規定できる
    • ユーザーごとに認証情報の設定が可能 f:id:jrywm121:20180505205648p:plain

AWSアカウント(root)ユーザー

  • AWSアカウント作成時のID
  • アカウントの全ての AWS サービスとリソースへの完全なアクセス権限を持つ
  • アカウントの作成に使⽤したメールアドレスとパスワードでサインイン
  • ⽇常的なタスクには、それが管理者タスクであっても、root ユーザーを使⽤しないことを強く推奨
  • AWSのroot権限が必要な操作
    • AWS ルートアカウントのメールアドレスやパスワードの変更
    • IAMユーザーの課⾦情報へのアクセスに関するactivate/deactivate 等々…
  • AWSルートアカウントはIAMで設定するアクセスポリシーが適⽤されない強⼒なアカウントであるため、⼗分に強度の強いパスワードを設定した上、通常は極⼒利⽤しないような運⽤をする。Security CredentialのページからAccess Keyの削除を行う。

IAMユーザー

  • AWS操作⽤のユーザー(1AWSアカウント毎に5000ユーザーまで作成可能)
  • ユーザーごとにユーザー名/パス(オプション)/所属グループ(上限は10)/パーミッション(Json形式で記述)

IAMグループ

  • IAMユーザーをまとめるグループ(1AWSアカウント毎に100グループまで作成可能)
  • グループ毎にグループ名/パス(オプション)/パーミッション

IAMで使⽤する認証情報

  • アクセスキーID/シークレットアクセスキー(2つまで) = REST,Query形式のAPI利⽤時の認証に使⽤
  • X.509 Certificate = SOAP形式のAPIリクエスト⽤
  • AWSマネジメントコンソールへのログインパスワード
  • 多要素認証(MFA)

アクセス条件の記述

f:id:jrywm121:20180505214653p:plain

AWS CloudTrail

  • AWSアカウントで利⽤されたAPI Callを記録し、S3上にログを保存するサービス。
  • AWSのリソースにどのような操作が加えられたか記録に残す機能であり全リージョンでの有効化を推奨。
  • 適切なユーザーが与えられた権限で環境を操作しているかの確認と記録に使⽤。
  • 記録される情報
    • APIを呼び出した⾝元
    • APIを呼び出した時間
    • API呼び出し元のSource IP
    • 呼び出されたAPI 等々…

IAMロール

  • AWSサービスやアプリケーション等、エンティティに対してAWS操作権限を付与するための仕組み
  • IAMユーザーやグループには紐付かない
  • 設定項⽬は、ロール名とIAMポリシー

AWS STS(Security Token Service)

  • ⼀時的に利⽤するトークンを発⾏するサービス
  • ユーザーに対して、以下の3つのキーを発⾏
    • アクセスキー:(ASIAJTNDEWXXXXXXX)
    • シークレットアクセスキー:(HQUdrMFbMpOHJ3d+Y49SOXXXXXXX)
    • セッショントークン(AQoDYXdzEHQ……1FVkXXXXXXXXXXXXXXXXXXXXXXXX)
  • トークンのタイプにより有効期限は様々

AWSを運用する上での常識を身につける

以下の資料を読んで理解を深めた。 (英語のホワイトペーパーは読めてない)
20180403 AWS White Belt Online Seminar AWS利用開始時に最低限おさえておきたい10のこと
AWS Well-Architected Framework
AWS Black Belt Online Seminar 2016 AWS CloudTrail & AWS Config
Amazon RDS 入門

セキュリティ

  • AWSルートアカウントは極力使用しない
    • 十分に強度の強いパスワードを設定した上で多要素認証(MFA)で保護し、通常は極力使用しない運用を行う。
    • Security CredentialのページからAccess Keyの削除を行う。
  • ユーザには最小限の権限を付与する
    • IAMユーザとIAMグループを利用する
    • 最小限のアクセス権を設定し、必要に応じて追加のアクセス権限を付与する。
    • 特権のあるIAMユーザ(機密性の高いリソースやまたはAPIにアクセスが許されているユーザ)に対してはMFAを設定する
    • 認証情報を定期的にローテーションする
  • 認証情報を埋め込まない
    • 認証情報をOSやアプリケーション側に持たせることなく、IAMロールを使用してAWSサービス操作権限を付与する
    • 認証情報はSTS(Security Token Service)で生成し、自動的に認証情報のローテーションが行われる
  • 証跡取得(ログ取得)
    • AWS CloudTrailによって操作ログを取得する。取得したログをCloudWatch Logsに転送し監視することも可能
    • Amazon GuardDutyを利用して疑わしいアクティビティをログから検知する
  • 各レイヤでのセキュリティ対策
    • ネットワークレイヤ(ネットワークACL
      • VPCのサブネット単位で設定するステートレスなファイアーウォール
      • ベースラインとなるポリシーを設定するのに用いる
    • Amazon RDS / EC2などのリソース(セキュリティグループ)
      • インスタンス(グループ)単位に設定するステートフルなファイアーウォール
      • サーバの機能や用途に応じたルール設定に適している
    • AWS WAF
    • AWS Shield

コスト最適化

  • 適切なサイジング
    • AWS CloudWatchでリソース利用状況を把握する
      • AWS上で稼働するシステム監視サービス
      • システム全体のリソース使用率、アプリケーションパフォーマンスを把握
      • 予め設定した閾値を超えたら、メール通知、AutoScalingなどのアクションを起こすこともできる
      • またCloudWatch LogsでOS上やアプリケーションのログも取得可能

データのバックアップ

  • EC2のAMI(Amazon Machine Image)を作成。
  • EBSのスナップショットを取得。作成時はデータ整合性を保つために静止点を設けることを推奨
  • RDSの自動バックアップ機能

障害や不具合への対策

f:id:jrywm121:20180506011824p:plain

  • ELB(Elastic Load Balancing)の活用
  • AutoScalingの活用
  • EC2 AutoRecoveryの活用

Amazon VPC(Virtual Private Cloud)

以下の資料を読んで理解を深めた。
20180418 AWS Black Belt Online Seminar Amazon VPC
AWS Black Belt Online Seminar 2016 AWS CloudFormation

概要

  • AWS クラウドの論理的に分離されたセクションをプロビジョニング(※1)し定義した仮想ネットワーク内の AWS リソースを起動することができる
  • 独自の IP アドレス範囲の選択、サブネットの作成、ルートテーブル、ネットワークゲートウェイの設定など、仮想ネットワーク環境を完全にコントロールできる
  • VPC のネットワーク設定は容易にカスタマイズすることができる。たとえば、インターネットとのアクセスが可能なウェブサーバーのパブリックサブネットを作成し、データベースやアプリケーションサーバーなどのバックエンドシステムをインターネットとのアクセスを許可していないプライベートサブネットに配置できる。
  • 既存のデータセンターと自分の VPC 間にハードウェア仮想プライベートネットワーク (VPN) 接続を作成することができるので、AWS クラウドを既存のデータセンターを拡張するかのように活用することができる。

※1 プロビジョニング:
必要に応じてネットワークやコンピューターの設備などのリソースを提供できるよう予測し、準備しておくこと

VPCをカスタマイズするコンポーネントたちと構成例

f:id:jrywm121:20180506014018p:plain

f:id:jrywm121:20180506014126p:plain

↓あとでそれぞれの項目を補足する
インターネット接続VPCのステップ

  • アドレスレンジの選択
    • 推奨: /16 65,534アドレス
  • AZに於けるサブネットの選択
    • VPCあたりのサブネット作成上限数はデフォルト200個
    • /24に設定すれば、200個作成する上で効率が良い(サブネットあたりのIPアドレス数 251)
    • サブネットで利用できないIPアドレス(/24の例)

      ホストアドレス 用途
      .0 ネットワークアドレス
      .1 VPCルータ
      .2 Amazonが提供するDNSサービス
      .3 AWSで予約
      .255 ブロードキャストアドレス
  • インターネットへの経路を設定
    • ルートテーブルはパケットがどこに向かえば良いかを示すもの
    • PublicサブネットのIPがIGW(Internet GateWay)経由でインターネットまたはAWSクラウドと通信するときにパブリックIPを利用
    • EC2のOSで確認できるのはプライベートIPのみ
  • VPCへのIN/OUTトラフィックを許可 f:id:jrywm121:20180506120927p:plain

    |ネットワークACL |セキュリティグループ| |:--:|:--:| |サブネットレベルで効果|サーバレベルで効果| |Allow/DenyをIN・OUTで指定可能(ブラックリスト型)|AllowのみをIN・OUTで指定可能(ホワイトリスト型)| |ステートレスなので、戻りのトラフィックも明示的に許可設定する|ステートフルなので、戻りのトラフィックを考慮しなくて よい| |番号の順序通りに適用|全てのルールを適用| |サブネット内のすべてのインスタンスACLの管理下に入る|インスタンス管理者がセキュリティグループを適用すればその管理下になる|

VPCとのプライベート接続

  • VPN接続 : バーチャルプライベートゲートウェイを利用したサイト間VPN
    • 1つのVPN接続は2つのIPsecトンネルで冗長化
    • ルーティングは静的(スタティック) / 動的(ダイナミック:BGP)が選択可能
  • 専用線接続 : AWS Direct Connectを利用し、一貫性のあるネットワーク接続を実現する。本番サービス向け。
    • AWSとお客様設備を専用線でネットワーク接続

VPC設計のポイント

  • CIDR(IPアドレス)は既存のVPC、社内のDCやオフィスと被らないアドレス帯をアサイ
  • 複数のアベイラビリティゾーンを利用し、可用性の高いシステムを構築
  • パブリック/プライベートサブネットへのリソースの配置を慎重に検討

VPCと他サービスとのやり取り

  • サービスによってVPC内と外のどちらにリソースやエンドポイントが存在するかが異なる
  • VPC Endpointは、グローバルIPをもつAWSのサービス(例えば、DynamoDB / Lambda / S3 / SQS 等…)に対して、VPC内部から直接アクセスするための出口です。
  • VPC EndpointはGateway型の動作とPrivateLink (Interface型)の動作に分かれる
    • Gateway
    • PrivateLink (Interface型)
      • サブネットにエンドポイント用のプライベートIPアドレスが生成される。
      • VPC内部のDNSがエンドポイント向けの名前解決に対してしてプライベートIPアドレスで回答する。
      • エンドポイント用プライベートIPアドレス向け通信が内部的にサービスに届けられる。

f:id:jrywm121:20180506101243p:plain

NATゲートウェイ

Amazon VPCの実装方法

  • マネジメントコンソール
  • AWS CLI / AWS SDK
  • Ansibleなどのサードパーティツール
  • AWS CloudFormation
    • EC2やELBといったAWSリソースの環境構築を、設定ファイル(テンプレート)を元に自動化できるサービス
    • テンプレートには起動すべきリソースの情報をJSONYAMLフォーマットのテキスト形式で記述する
    • 構築済みの環境からテンプレートを作成するツール(CloudFormer)も有益

VPC Flow Logs

  • ネットワークトラフィックをキャプチャし、CloudWatch LogsへPublishする機能
  • ネットワークインタフェースを送信元/送信先とするトラフィックが対象
  • セキュリティグループとネットワークACLのルールでaccepted/rejectされたトラフィックログを取得
  • 運用例:Elasticsearch Service + kibanaによる可視化

f:id:jrywm121:20180506104749p:plain

Amazon ECS(Elastic Container Service)

以下の資料を読んで理解を深めた。 20180220 AWS Black Belt Online Seminar - Amazon Container Services
20180313 Amazon Container Services アップデート
What is Amazon Elastic Container Service? - Amazon Elastic Container Service
Task Definition Parameters - Amazon Elastic Container Service Amazon EC2 Container Service(ECS)の概念整理 - Qiita

概要

  • Amazon EC2 Container Service (ECS) は、Docker コンテナをサポートするスケーラビリティに優れた高性能なコンテナ管理サービス
  • Amazon EC2 インスタンスのマネージド型クラスターで簡単にアプリケーションを実行できる
  • 簡単な API 呼び出しで、Docker が有効なアプリケーションを起動および停止したり、クラスターの完全な状態を問い合わせたり、セキュリティグループ、Elastic Load Balancing、Amazon Elastic Block Store (Amazon EBS) ボリューム、AWSIdentity and Access Management (IAM) ロールなどの機能にアクセスすることができる
  • Amazon EC2 Container Registry (ECR)
    • 開発者が Docker コンテナイメージを簡単に保存、管理、デプロイできる完全マネージド型の Docker コンテナレジストリ

コンテナを利用した開発に必要な技術要素

  • アプリのステートレス化
    • ステートが必要なものはコンテナの外に置く
  • レジストリ
    • コンテナの起動元となるイメージの置き場所
      • アプリ+実行環境をpush / 実行時にpull→build→run
    • 高い可用性、スケーラビリティが求められる
      • 落ちたらデプロイ不能、同時に大量にpullされることもある
      • 自前で持つとその管理コストがかかるので、Amazon ECRを利用しましょう
  • コントロールプレーン / データプレーン
    • コントロールプレーン = コンテナの管理をする場所
      • どこでコンテナを動すか / 生死の監視 / いつ停止するか / デプロイ時の配置 → Amazon ECS / Amazon EKS
    • データプレーン = 実際にコンテナが稼働する場所

f:id:jrywm121:20180506123718p:plain

Task: コンテナ(群)の実行単位

  • Task Definitionの情報から起動される
    • Task Definitionとは、jsonフォーマットのテキストファイルで、これは1つ以上(最大10)のコンテナーを記述できる
    • 記述するパラメータは以下のようなものがある
      • family : タスク定義の名前で、この名前をベースにシーケンシャルにリビジョンナンバーが付与される
      • taskRoleArn : タスク定義を登録するときに、IAM タスクロールを割り当てて、タスクのコンテナに、関連するポリシーに指定された AWS API を呼び出すためのアクセス権限を付与できる。
      • executionRoleArn : タスク定義を登録するとき、タスクのコンテナがユーザーに代わってコンテナイメージを取得して、CloudWatch にコンテナログを発行するためのタスク実行ロールを提供することができる
      • networkMode : タスクのコンテナで使用する Docker ネットワーキングモード。有効な値は、none、bridge、awsvpc、および host です。Fargate 起動タイプを使用している場合、awsvpc ネットワークモードが必要。
      • containerDefinitions
        • name : コンテナの名前。Docker Remote API の コンテナを作成する セクションの name と docker run の --name オプションにマッピングされる。
        • image : コンテナの開始に使用するイメージ。新しいタスクが開始されると、Amazon ECS コンテナエージェントは、指定されたイメージおよびタグの最新バージョンをプルしてコンテナで使用する。
        • memory : コンテナに適用されるメモリのハード制限    などなど…
  • Task内のコンテナ群は同じホスト上で実行
  • CPUとメモリの上限を指定し、それをもとにスケジュールする。

Service : ロングランニングアプリ用スケジューラ

  • Taskの数を希望数に保つ
  • Task Definitionを新しくするとBlue/Greenデプロイ
  • ELBと連携することも可能
  • メトリクスに応じてTask数のAuto Scalingも可能

IAM Role: Task毎に異なる権限のAWS認証が可能

※ タスクの単位について
例えば、裏にいるAPIをCallしてWEB UIを提供するもので、expressのアプリケーション のコンテナとnginx(expressにプロキシする) のコンテナで構成されるFront Serviceがある場合には、expressコンテナとnginxコンテナはTaskという単位でくくられる。

  • IAM RoleをTask毎に設定可能
  • AWS SDKを利用していれば自動的に認証情報が得られるため、アクセス鍵等の埋め込み不要

コンテナの数をAuto Scalingさせる

  • 何らかのメトリクス(例えば、コンテナのCPU・メモリ使用率 / リクエスト数)に応じて、コンテナの数を自動スケールさせたい
  • コントロールプレーンの課題 : メトリクスの変化に対して、コンテナ数をどの程度変化させるのか
  • データプレーンの課題 : コンテナのスケールに応じて、インスタンス数もスケールが必要

ECSのTarget TrackingとFargateの組合せがオススメ

  • Target Trackingとの連携

    • メトリクスに対してターゲットの値を設定するだけ(例: CPU使用率 50%)
    • その値に近づく様に、Application AutoScalingが自動的にServiceのDesiredCountを調整
  • Fargateを利用したコンテナAuto Scalingの優位性

    • Serviceのスケールに応じて自然にコンテナが起動・終了する
    • コンテナの起動時間に対してのみ課金

dokkuのソースコードでシェルスクリプトを勉強する

5月からの仕事がアプリケーション側でなくインフラ側になったので、急遽シェルスクリプトの勉強。powershellはちょいちょい扱ってはいたけども、Invoke-RestMethod(Linuxでのcurlコマンド)でjson取ってきて加工するみたいなことしかやってこなかったため知識不足甚だしい。
dokkuというbash200行ちょいでdockerベースのPaaS環境を構築できるプロジェクトがあり、せっかくなのでシェルスクリプトを学びつつPaaS構築のことも勉強できたら良いなと思い、dokkuのbootstrap.shのコードを見つつ勉強していきます。

bashスクリプトの先頭

#!/usr/bin/env bash
set -eo pipefail; [[ $TRACE ]] && set -x

シェルスクリプトの一行目に必ず記述する#!で始まる行はshebangと言われる。
ここで#!/bin/bashと指定すれば、絶対パスでのbash指定となります。

上記のように、#!/usr/bin/env bashと指定すれば、$PATH 上の bash が使われます。$PATHはコマンド検索パスを格納している環境変数printenv PATHで確認することができる。
メリットは、例えば $HOME/.opt 配下に最新の bash をインストールするなどした場合、$PATH にさえ入っていればそっちが使われるというのがあります。ちなみに$PATHに指定するときには、export PATH="$PATH:/opt/local/bin"とする。

set -eを宣言しておくとエラーが起きた行で中断するので、予想外のエラーを無視してスクリプトが処理を続行するのを防げます。ただ、パイプの中で起こったエラーは検知できず、右端のコマンドがエラーとなった時のみ有効です。set -o pipefailを指定することで、パイプ内のコマンドのエラーでも中断します。その2つを組み合わせてエラーを無視せず処理を中断するようにしたのが、set -eo pipefail

&&の前にあるコマンドを実行し、もし正常に終了した場合(戻り値が0)に&&の後ろにあるコマンドを実行する。set -xによってシェルのtraceが有効になる。実行したコマンドとその引数がトレース情報として標準エラー出力へ出力される。
[[ $TRACE ]]は調べたが分からなかった。

変数宣言と関数の書き方

main() {
  export DOKKU_DISTRO DOKKU_DISTRO_VERSION
  # shellcheck disable=SC1091
  DOKKU_DISTRO=$(. /etc/os-release && echo "$ID")
  # shellcheck disable=SC1091
  DOKKU_DISTRO_VERSION=$(. /etc/os-release && echo "$VERSION_ID")

  export DEBIAN_FRONTEND=noninteractive
  export DOKKU_REPO=${DOKKU_REPO:-"https://github.com/dokku/dokku.git"}

  ensure-environment
  install-requirements
  install-dokku
}

main "$@"

関数の書き方はfunction main() { ... }だが、functionは省略可能であり、通常省略されるため上記のソースのようになる。関数を呼び出すときはmain "$@"のように関数名を宣言すれば良い。引数として値を渡す場合には、関数名と並べて書いていく。今回の場合は、このスクリプトが呼び出された時の全ての引数をそのまま渡している($@は全ての引数を意味する)。
export 変数名環境変数の宣言となる。定数に関しては、Java命名規則と同じで定数+アンダースコアになる。 export DEBIAN_FRONTEND=noninteractiveのように環境変数の宣言と代入を同時に行うことができる。

変数の代入は変数=値の形となる。注意点としては、DOKKU_DISTRO = $(. /etc/os-release && echo "$ID")のように=の前後に空白を入れてしまうとうまく機能しません。

${変数値:-デフォルト値という書き方で変数を初期化することができる。export DOKKU_REPO=${DOKKU_REPO:-"https://github.com/dokku/dokku.git"}$DOKKU_REPOが存在していなければ、"https://github.com/dokku/dokku.git"で初期化する。

変数スコープとリダイレクト、終了ステータス

ensure-environment() {
  local FREE_MEMORY
  echo "Preparing to install $DOKKU_TAG from $DOKKU_REPO..."

  hostname -f > /dev/null 2>&1 || {
    echo "This installation script requires that you have a hostname set for the instance. Please set a hostname for 127.0.0.1 in your /etc/hosts"
    exit 1
  }

# 後半の処理は次のセクション

}

local FREE_MEMORYのようにlocalキーワードを変数宣言の前につけることによって、変数のスコープを関数内に狭めることができる。

echo は改行つきで値を表示するための命令。""(ダブルクォーテーション)の中では$変数または${変数}が展開されるが、''(シングルクォーテーション)の中では展開されない。

hostname -fでは、ホスト名とドメイン名からなるFQDNを表示する。
>command > /dev/null 2>&1これは標準エラー出力の結果を標準出力にマージして、/dev/nullに捨てることを意味する。つまり、標準出力された物も標準エラー出力された物も捨てるということ。詳しくは右記を参照。( 5分で一通り理解できる!Linuxのリダイレクト 使い方と種類まとめ

&&は左辺のコマンドが成功していたら右辺のコマンドも実行するものだが、||に関しては、左辺のコマンドが失敗していたら右辺のコマンドが実行される。このスクリプトの場合は、hostnameが設定されておらず、hostname -fのコマンドが評価され失敗したときに、「127.0.0.1ループバックアドレス)にhostnameを設定してください」とメッセージを出力し終了ステータス1をセットしてスクリプトを終了している。

コマンド終了時には「終了ステータス (exit-status)」と呼ばれるコマンドの成否を表す数値が特殊変数 $? に自動で設定される。一般的には、コマンド成功時には「0」、失敗時には「1」が設定される。exit コマンドに指定したパラメータ (0 もしくは 1~255 の正の整数値のみ可) が、そのシェルの終了ステータスとなる( bashの&&と||: みズとおかズ )。

パイプとawk、if文

ensure-environment() {
  
# 前半の処理は上のセクション

  FREE_MEMORY=$(grep MemTotal /proc/meminfo | awk '{print $2}')
  if [[ "$FREE_MEMORY" -lt 1003600 ]]; then
    echo "For dokku to build containers, it is strongly suggested that you have 1024 megabytes or more of free memory"
    echo "If necessary, please consult this document to setup swap: http://dokku.viewdocs.io/dokku/advanced-installation/#vms-with-less-than-1gb-of-memory"
  fi
}

grep 正規表現 ファイル名でファイル内の文字を検索し該当する行を抽出できる。/proc/meminfoは、カーネルが内部的に管理している枠組みでのメモリ情報を持っている。MemTotalカーネルが認識している全物理メモリを表す。
|(パイプ)はコマンドの入出力をコマンドへ引き渡す処理で、今回の場合はgrepで抽出した行MemTotal: 8069288 kBawkコマンドへ引き渡している。
awk 'パターン {アクション}' ファイル名で、テキストファイルを1行ずつ読み、パターンに合致した行に対して、アクションで指定された内容を実行する。パターンが指定されていない場合には、全ての行に対して処理を行う。テキストの各行を空白文字で区切って“フィールド”として処理するので、今回は1番目のフィールドがMemTotal:となり$1で表現できる。2番目のフィールドが8069288となり、これをprint(出力)している。

if文は、testを使用するもの、[(シングルブラケット)を使用するもの、[[(ダブルブラケット)を使用するものに分けられる。testを使用するのは直感的でないし他言語からくると若干困惑する。[(シングルブラケット)は、変数展開するときにダブルクォーテーションが必要であっったり、数値比較を行う場合には丸括弧で囲まなくてはいけないなどハマりポイントが多いため、[[(ダブルブラケット)を使用するべき。 test と [ と [[ コマンドの違い - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

-lt比較演算子はless thanを意味しており、今回ではメモリが1003600kBより小さかった場合にメッセージを出力している。
ifの終了はfiキーワードを使用する。またthenキーワードは1行消費してしまい見にくいので条件文に;(セミコロン)をつけて続けて書いている。

case文

install-requirements() {
  echo "--> Ensuring we have the proper dependencies"

  case "$DOKKU_DISTRO" in
    debian|ubuntu)
      apt-get update -qq > /dev/null
      ;;
  esac
}

case文はcase 値 in 値1) 処理 ;; 値2) 処理 ;; esacと記述していく。この場合は、$DOKKU_DISTROdebianもしくはubuntuだった場合に、apt-get updateでパッケージ情報を更新している。-qqオプションを使用することによってエラー以外は表示しない。
パッケージをinstallする際に参照するインデックスファイルが存在しない可能性があるから?単純にパッケージリストを更新したいから?

dokkuのソースコードを追ってみる

install-dokku() {
  if [[ -n $DOKKU_BRANCH ]]; then
    install-dokku-from-source "origin/$DOKKU_BRANCH"
  elif [[ -n $DOKKU_TAG ]]; then
    local DOKKU_SEMVER="${DOKKU_TAG//v}"
    major=$(echo "$DOKKU_SEMVER" | awk '{split($0,a,"."); print a[1]}')
    minor=$(echo "$DOKKU_SEMVER" | awk '{split($0,a,"."); print a[2]}')
    patch=$(echo "$DOKKU_SEMVER" | awk '{split($0,a,"."); print a[3]}')

    use_plugin=false
    # 0.4.0 implemented a `plugin` plugin
    if [[ "$major" -eq "0" ]] && [[ "$minor" -ge "4" ]] && [[ "$patch" -ge "0" ]]; then
      use_plugin=true
    elif [[ "$major" -ge "1" ]]; then
      use_plugin=true
    fi

-n 文字列で文字列の長さが0より大きければ真となる。
sourceからdokkuのupgradeをする際に、DOKKU_BRANCHをmasterに設定することになっている( Dokku - The smallest PaaS implementation you've ever seen )。 設定されていた場合には、install-dokku-from-source関数が実行される。

通常インストールされる際には、以下のようなコマンドでインストールされる。
sudo DOKKU_TAG=v0.12.5 bash bootstrap.sh
この際にDOKKU_TAGに"v0.12.5"という値が入り条件が真となる。
${変数名//置換前文字列/置換後文字列}で置換前文字列に一致した全ての文字列が、置換される。今回の場合は"v"が排除される形となり、DOKKU_SEMVERには"0.12.5"という値が入る。
awkのsplit関数はsplit(分割する文字列,格納先の配列,分割文字列の指定)というような使い方をする。配列は(0, 12, 5)となりmajorには"0"、minorには"12"、patchには"5"の値が入る。

# install-dokku()の続き

    # 0.3.13 was the first version with a debian package
    if [[ "$major" -eq "0" ]] && [[ "$minor" -eq "3" ]] && [[ "$patch" -ge "13" ]]; then
      install-dokku-from-package "$DOKKU_SEMVER"
      echo "--> Running post-install dependency installation"
      dokku plugins-install-dependencies
    elif [[ "$use_plugin" == "true" ]]; then
      install-dokku-from-package "$DOKKU_SEMVER"
      echo "--> Running post-install dependency installation"
      sudo -E dokku plugin:install-dependencies --core
    else
      install-dokku-from-source "$DOKKU_TAG"
    fi
  else
    install-dokku-from-package
    echo "--> Running post-install dependency installation"
    sudo -E dokku plugin:install-dependencies --core
  fi
}

Effective Java 読書メモ 2章

英語版のEffective Java 第3版が出ていたので、日本語版が出るまでの繋ぎに。 もし読み終わっても出る気配が無かったら英語版買ってなんとか解読するしか。

コンストラクタの代わりにstaticファクトリーメソッドを活用する

コンストラクタが複数で、渡す引数によって動きが異なる場合には実装すべきと思う。メソッドの戻り値の型の任意のサブタイプで返せるが、そのサブタイプのコンストラクタはpublicもしくはprotectedでなくてはならない。

pros

  • コンストラクタと違い名前を持てること
  • メソッドが呼び出される毎に新たなオブジェクトを生成する必要なし
  • メソッドの戻り値の型の任意のサブタイプで返せる
  • パラメータ化された方のインスタンス生成の面倒さを低減できる

cons

  • public/protedctedのコンストラクタを持たないサブタイプは作れない
  • 他のstaticメソッドと区別しにくい
// 利用する側
char[] array = new char[] { 'H', 'A', 'T', 'E', 'N', 'A' };
String str1 = new String(array)  // コンストラクタの場合
String str2 = String.valueOf(array)  // staticメソッドの場合

// 利用される側(jdk8)
public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
}
public static String valueOf(char data[]) {
        return new String(data);
}

数多くのコンストラクタパラメータに直面した時にはビルダーを検討する

個人的には利用側がスマートになって素敵だと思った。パラメータが多くなる場合は、まずクラス設計を見直すべきだと思うが、仕方なくパラメータを増やさざるを得ない場合には良いのでは。

// 利用側のコード
public class Main
{
  // arguments are passed using the text field below this editor
  public static void main(String[] args)
  {
    Contact contact = new Contact.Builder("Satoshi", "Matsuda", LocalDateTime.now()).address("東京都千代田区××町3−24").age(27).build();
  }
}

// 利用される側のコード
class Contact
{
  private final String firstName;
  private final String lastName;
  private final String address;
  private final int age;
  private final LocalDateTime contactDate;
  
  static class Builder {
    
    // 必須パラメータ
    private final String firstName;
    private final String lastName;
    private final LocalDateTime contactDate;
    
    // オプションパラメータ
    private String address = ""; // String.emptyってJavaには無いんだね・・・
    private int age = 0;  
    
    public Builder(String firstName,  String lastName, LocalDateTime contactDate){
      this.firstName = firstName;
      this.lastName = lastName;
      this.contactDate = contactDate;
    }
    
    public Builder address(String val){
        address = val;
        return this;
    }
    
    public Builder age(int val){
        age = val;
        return this;
    }
    
    public Contact build(){
        return new Contact(this); // クラス内部ならprivateでも呼べる
    }
  }
  
  private Contact(Builder builder){
      firstName = builder.firstName;
      lastName = builder.lastName;
      address = builder.address;
      age = builder.age;
      contactDate = builder.contactDate;
  }
}

privateのコンストラクタかenum 型でシングルトン特性を強制する

コンストラクタのアクセス修飾子をprivateにすると初期化の時に1回だけ呼ばれる

singleton実装①
public finalによるシングルトン。単純で分かりやすい。

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton obj1 = Singleton.INSTANCE;
    }
}

class Singleton {
    public static final Singleton INSTANCE = new Singleton();
    private Singleton(){ /* process */ }
}

singleton実装②
staticファクトリーメソッドによるシングルトン。結城先生のデザインパターン本にも載っている方法がこれ。

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton obj1 = Singleton.getInstance(); // この時点でSingletonクラスの初期化が行われ、staticフィールドも初期化される
        Singleton obj2 = Singleton.getInstance();
        // Singleton obj3 = Singleton(); エラーが発生する
        
        System.out.println(obj1 == obj2); // output : true
    }
}

class Singleton {
    private static final Singleton SINGLETON = new Singleton();
    private Singleton(){ /* process */ }
    
    public static Singleton getInstance(){
        return SINGLETON;
    }
}

singleton実装③
上記の2つの方法だと、シングルトンのクラスをシリアライズするときには、"implements Serializable"を追加するだけではダメとのこと。全てのインスタンスフィールドをtransientと宣言し、readResolveメソッドを実装する必要がある。そうしない場合に、シリアライズされたインスタンスをディシリアライズするたびに、新たなインスタンスが生成されてしまうため、シングルトンを保証できない。transient修飾子は、シリアライズの対象から外すもの。
また、AccessibleObject.setAccesibleメソッドを利用してリフレクションにより、privateのコンストラクタを呼び出すことができてしまうため、シングルトンを保証できない。
Enumを使うとシリアライズするためにアレコレしなくてよくてスマート。

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton.INSTANCE.saySingleton();  // singleton!!
    }
}

enum Singleton {
    INSTANCE;
    public void saySingleton(){
        System.out.println("singleton!!");
    }
}

privateのコンストラクタでインスタンス化不可能を強制する

コンパイラはデフォルトでpublicで引数なしのコンストラクタを提供しているが、明示的にprivateのコンストラクタを用意することでインスタンス化できないようにすることが可能。

不必要なオブジェクトの生成を避ける

staticファクトリーメソッドとコンストラクタの両方を提供している不変クラスの場合は、staticファクトリーメソッドを使用した方が、不必要なオブジェクトの生成を防げる。可変オブジェクトに関しても、メンバのインスタンス生成が一度でいい場合には、static初期化子を使用することを検討すること。
オートボクシング(auto boxing: プリミティブ型→ラッパー型)はパフォーマンスを悪くするので、注意すること。意図した上で変換するのであれば、明示的に行うこと。

廃れたオブジェクト参照を取り除く

クラスが独自にメモリを管理している場合には、例えば下記のようなEffecive JavaにあるようなStackクラスなどに関しては、メモリリークに注意する必要がある。

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack () {        
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push (Object e) {
        ensureCapacity();
        elements[size++] = e; // 参照先をセット
    }

    public Object pop () {
        if (size == 0) {
            throw new EmptyStackException();
        }
        return elements[size--]; 
  // 参照値を渡してスタックには残って無いように見えるが、実際はまだ参照値を持っている
  // 参照値を持っているが、もう参照されることは無い(=廃れた参照)
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

以下のように明示的にnull参照を突っ込むことによって、Garbage Collectionが動いてくれる。

public Object pop () {
    if (size == 0) {
        throw new EmptyStackException();
    }

    Object result = elements[size--];
    elements[size] = null; // ここ!!
    return result;
}

クラスがメモリを独自管理している場合以外にも、キャッシュやリスナー・コールバックでメモリリークが起きる可能性は高くなる。ヒープの使用量とかデバッグしているときに意識できるといいよね的な感じだろうか。
HashMapを使用している場合には、WeakHashMap(WeakHashMap (Java Platform SE 8))で代替すると、使われることがなくなったキーが自動的に削除されてガベージコレクションの対象となります。

ファイナライザを避ける

C++プログラマJavaに移ってきたときに誤解しがちなデストラクタ=ファイナライザに対して「そんなことないぞ!!」と。try-finallyを使用して(DBとかファイルはtry-resourcesでしたっけ?)、明示的に終了メソッドを組み込めとのこと。安定してファイナライザは動作をしてくれない。

黒べこ本(kotlin)でSpring Boot入門メモ③ (DI・バリデーション)

DIとは

DIはDependency Injectionのことで日本語では依存性の注入と言われる。Springのドキュメントでは基本的には、類似の意味を持つIoC(Inversion of Control)という言葉が使われている。DIは、オブジェクトが依存関係を定義する処理となる。以下に挙げる方法でのみ、オブジェクトが依存する他のオブジェクトの定義を行う。

  • コンストラクタ引数 ex) val regex = Regex("Kotlin")
  • ファクトリメソッドの引数 ex) val mutableList = mutableListOf(1, 2, 3)
  • 返り値のオブジェクトインスタンスに設定されるプロパティ ex) person.age = 25

DIコンテナ

DIコンテナは、Beanを作成するときにそれらの依存関係を注入する。BeanとはSpring IoC コンテナによって管理されるオブジェクトのことです。コンテナによって管理されるオブジェクトとしては以下のものが例としてあげられる。

  • ViewResolverのBean (Controllerの戻り値の文字列を解決してViewを戻す処理を行う)
  • ValidatorのBean
  • DataSourceのBean
  • トランザクションマネージャのBean
  • 型変換のBean
  • Controller・Service・Repositoryなどの業務ロジックに関係したBean

DIコンテナは、ConfigurationにしたがってBeanを作成し、IDを割り振って管理する。インタフェースorg.springframework.context.ApplicationContextがSpring IoCコンテナを表している。

Beanの定義

Configurationの設定の仕方は以下の3種類ある。

<bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
</bean>
  • JavaConfig
@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}

インスタンスをBeanとしたクラスに@Componentもしくは@Componentをベースにしたアノテーションを付与する(@Controller / @Service / @Repository / @Configuration)
Java.configに@ComponentScan(basePackages = {"hoge.service.impl"})を付与する。ここで指定したベースパッケージ配下のアノテーションをチェックする。
@Autowiredをインスタンス変数に付与することでコンテナ内から、インジェクションする対象となる。

@Controller
@RequestMapping("houses")
class HouseController {

    @Autowired
    private lateinit var shs: HouseFactoryImpl

@Autowiredの使い方

@Autowiredを使用したDependency Injectionの種類は以下の3種類有ります。記述が簡易なフィールド・インジェクションが使用されることが多いと思われます。ただフィールド・インジェクションは、Spring FrameworkのDIコンテナに依存してしまう点と設計の妥当性(依存関係が多くなり過ぎていないか、循環依存になっていないか)が分かりづらいということが有り、必須の依存関係でコンストラクタ・インジェクション、オプションの依存関係をセッター・インジェクションを使用する方がアプリが大規模になる場合に有効であると言えます。

  • コンストラクタ・インジェクション
class Foo @Autowired constructor(dependency: MyDependency, service: MyService) {
    //...
}

ただし、Spring4.3以降でコンストラクタが単一の場合は@Autowiredが省略できるため、以下のようになります

class Foo(dependency: MyDependency, service: MyService) {
    //...
}
  • フィールド・インジェクション
    lateinitはプロパティを遅れて初期化することをコンパイラに知らせるものです。 バッキングフィールドを持つプロパティは初期化が必要となるはずですが、lateinitを付けることでプロパティ宣言時の初期化を避けることが出来ます。
class Foo {
   @Autowired
   private lateinit var dependency: MyDependency
   @Autowired
   private lateinit var service: MyService
}
  • セッターインジェクション
    kotlinでは無し? → 調査中

バリデーション

Formのクラスを作成し、そのクラスのフィールドにアノテーションを付与することで、単項目チェックを行うことができる。Formのクラスは、HTMLなどから受け取る送信formに対応するもの。アノテーションは、Bean Validationの実装であるhibernate validatorを利用する。Bean Validationは値検証のためのJavaの仕様の一つで、hibernate validatorはそのリファレンス実装となっている。 相関チェックは、Validatorインターフェースを実装したチェック用のクラスを作成し、Controllerにインジェクションします。

単項目チェック

class TaskCreateForm {

    @NotBlank
    @Size(max = 20)
    var content: String? = null
}

仕様頻度が高そうなアノテーションは以下

  • @Length(min=, max=) //文字列長の検査
  • @Max(value=) //数値の最大値の検査
  • @Min(value=) //数値の最小値の検査
  • @NotNull //値がNullでないか
  • @NotEmpty //文字列がNullまたは空文字で無いか
  • @Pattern(regex="regexp", flag=) //正規表現に一致するか否か

以下のようにメソッドの引数としてとったFormにアノテーション@Validatedをつけ、そのすぐ次の引数にBindingResultを取るようにすると、バリデーションが施され、その結果がBindingResultオブジェクトとして表現される。BindingResultオブジェクトのhaserror()メソッドを使用することで検査の結果を得ることが出来ます。

class TaskController(private val taskRepository: JdbcTaskRepository) {

    @PostMapping("")
    fun create(@Validated form: TaskCreateForm,
               bindingResult: BindingResult): String {
        if(bindingResult.hasErrors())
            return "tasks/new"

        val content = requireNotNull(form.content)
        taskRepository.create(content)
        return "redirect:/tasks"
    }

}

次は例外処理周りか永続化について書きます。


参考記事

SpringでField InjectionよりConstructor Injectionが推奨される理由 - abcdefg.....

[随時更新]SpringBoot(with Thymeleaf)チートシート - Qiita

Validator (Java Platform SE 8)

黒べこ本(kotlin)でSpring Boot入門メモ② (Controller・ざっくりThymeleaf)

黒べこ本では、"Hello World"でREST APIのController・"ToDoアプリ"でMVCのControllerを書くことになっています。 Controllerに着目して書いていきます。また、Viewを返す後者で使用されるテンプレートエンジンであるThymeleafも少しだけ知っておこうと思います。

Spring Boot での Controller

そもそもMVCってなんだっけ

ウェブアプリの設計においてポピュラーな選択肢であるModel View Controller (MVC) のこと。アプリケーションロジックを3つに分割することで再利用性や柔軟性を高めている。

例えば、簡単なショッピングリストアプリを作成すると考えてみる。今週買う必要のある商品の名前・数量・価格のみを表示してくれる。

f:id:jrywm121:20180313004328p:plain

Model

Modelはリストの商品データと既に存在するリストを実装する。

View

Viewはリストをユーザに対してどのように見せるかを定義する。モデルから表示するデータを受け取る。

Controller

アプリからの入力を受け取って、それに応じてModelやViewを更新するロジック。 例えば、ショッピングリストアプリには入力フォームや追加・削除ボタンが有ります。これらのアクションにより、Modelのアップデートが必要となります。入力情報は一度、Controllerに送られます。その後、Modelの操作が行われ、更新されたデータがViewへ送られます。 また、Viewを更新し別のデータ形式(アルファベットを昇順→降順 / 価格順に並び替える)にすることもあるでしょう。この場合は、modelへのupdateを必要とせず、直接Controllerがハンドルします。

※ 内容書いてから気づきましたが、MDNを参照したので若干Webフロントエンドよりの解釈となっています。

実際のJavaアプリーションだと以下のような構成になると思われます。 これから始めるSpringのwebアプリケーションの7スライド目を引用 f:id:jrywm121:20180313012920j:plain

Controller関係のアノテーション

Javaが標準で提供しているWEBアプリ(ex: Servlet, JDBC)はプログラムや設定ファイルの記述が面倒。確かに新人研修の時にやったJava EEServletJSPxmlに記述したり、冗長な記述でDispatchしていたような記憶がかすかに有ります。

SpringはJava標準APIをラップして簡単にしてくれる。Configurationの方法はアノテーションXMLJava Configが有りますが、Controller / Service / Dao はアノテーションという手段を取るのが見通し良くデメリットも小さいらしい。 @Controllerコンポーネントと@RestControllerコンポーネントでは、アノテーションを使用してリクエスマッピング、リクエスト入力、例外処理などを表現できる(引用元)。

黒べこ本ではToDoリストのアプリで以下のようにControllerを記述しました(一部分のみです)。

@Controller
@RequestMapping("tasks")
class TaskController(private val taskRepository: InMemoryTaskRepository) {

    @PostMapping("")
    fun create(@Validated form: TaskCreateForm,
               bindingResult: BindingResult): String {
        if(bindingResult.hasErrors())
            return "tasks/new"

        val content = requireNotNull(form.content)
        taskRepository.create(content)
        return "redirect:/tasks"
    }

    @GetMapping("{id}/edit")
    fun edit(@PathVariable("id") id: Long,
             form: TaskUpdateForm): String {
        val task = taskRepository.findById(id) ?: throw NotFoundException()
        form.content = task.content
        form.done = task.done
        return "tasks/edit"
    }
@Controller

このアノテーションは、@Componentと同じようにコンテナで管理するように指定するもの。Beanとして扱われ、@AutowiredアノテーションがControllerの使用者側に付けられることによってInjectionされる。

@RequestMapping

RequestをController内のメソッドにマッピングするのに使用されます。URL、HTTPメソッド、要求パラメータ、ヘッダー、およびメディアタイプによって一致するさまざまな属性があります。これをクラスレベルで使用して共有マッピングを表現したり、メソッドレベルで特定のエンドポイントマッピングに絞り込むことができます。

@GetMapping @PostMapping @PutMapping @DeleteMapping @PatchMapping

@RequestMappingとHTTPメソッドの組み合わせ。()内で指定するURI正規表現でも書ける。引数を取らない場合は()内にパスの文字列のみを記述すれば良いが、引数を2つ以上取る場合には、パラメータとなる変数名を記述しなくてはいけない。
path: エンドポイント / consumes: Content-Type / produces: Accept / headers: request header / params: request parameter

@PostMapping(path = "/pets", consumes = "application/json", produces = "application/json;charset=UTF-8", headers = "myHeader=myValue",  params = "myParam=myValue")
@PathVariable

pathの中で{ }で定義した変数には、このアノテーションを使用することでアクセス出来ます。URI変数はString変数でない場合には、自動的に適切な型に変換されるか、 もし出来ない場合にはTypeMismatchExceptionが発生します。int, long, Dateはデフォルトでサポートされています。

@Validated

このアノテーションが付与された引数は、標準のBean検査が行われ、もし検査にひっかかってしまった場合にはデフォルトでは400(Bad Request)を返すようにしている。BindingResult型の引数を取ることで、ローカルで処理をすることも可能。

redirect:(Path)

指定されたパスにリダイレクトする。

ハンドルに関するアノテーション
  • @RequestParam : リクエストパラメータの値を取る
  • @RequestHeader : リクエストヘッダーの値を取る
  • @CookieValue : Cookieの値を取る
  • @ModelAttribute : モデル(インスタンス)の値を取る。既にModelメソッドで追加されているモデルから もしくは @SessionAttributes経由のHTTPセッションから もしくは デフォルトのコンストラクタの呼び出しから 等々
  • @SessionAttribute : 既存のセッション属性にアクセスする
  • @RequestAttribute : 作成された既存のリクエスト属性にアクセス
  • @RequestBody : リクエストボディにアクセス
  • @ResponseBody : 関数の戻り値をシリアライズします。@ResponseBody + @Controller = @RestController

戻り値がString型だけど・・・

ビューの論理名からビューの物理名を解決する。 "tasks/new" → "/resources/templates/tasks/new.html" Thymeleaf用のViewResolverクラスがあり、それが名前解決をしてくれる。

Viewに相当するThymeleaf

Thymeleafとは

ThymeleafはJavaのテンプレートエンジンライブラリ。XML/XHTML/HTML5で書かれたテンプレートを変換して、アプリケーションのデータやテキストを表示することができる。JSPの代替技術として近年注目されている

Thymeleafの基本

黒べこ本の中で書くコードは以下となります。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>タスク一覧</title>
    <link rel="stylesheet" href="webjars/bootstrap/4.0.0-2/css/bootstrap.min.css">
</head>
<body>
<a th:href="@{tasks/new}">作成</a>
<p th:if="${tasks.isEmpty()}">タスクがありません</p>
<ul th:unless="${tasks.isEmpty()}">
    <li th:each="task: ${tasks}">
        <a th:href="@{tasks/{id}/edit(id=${task.id})}">
            <span th:unless="${task.done}" th:text="${task.content}"></span>
            <s th:if="${task.done}" th:text="${task.content}"></s>
        </a>
    </li>
</ul>
</body>
</html>
@{...} : リンク式

/で始めると、レスポンスされるhtmlにはコンテキストパスが補完されます。コンテキストパスは絶対パスで記述する際に必要となるもので、Applicationサーバの資源の位置を表す。相対パスで指定する場合は環境によって上手く通らないケースが出てくるため危険。

${...} : 変数式

コンテキストのModelにaddされているインスタンスを指定して展開することが出来ます。事前にController側で以下のコードのように、してあげる必要があります。

    @GetMapping("")
    fun index(model: Model): String {
        val tasks = taskRepository.findAll()
        // 直下の部分。第一引数に取っている名前でView側は参照する。
        model.addAttribute("tasks", tasks)
        return "tasks/index"
    }
th:if=${condition} th:unless=${condition}

conditionが正であれば、if記述のブロックはActiveとなる。 conditionが負であれば、unless記述のブロックはActiveとなる。

th:text

サーバーで実行した時は、th:text属性を指定したタグに挟まれた部分が置き換えられます。htmlで直書きしている内容は上書かれるので、基本的にはモックとして扱われる際のデフォルト値

@{path1/{変数}/path2(変数=代入値}

パスに変数を埋め込む

黒べこ本では先ほど挙げたものの他にこのようなコードも書きます

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>タスク作成</title>
</head>
<body>
<form th:method="post" th:action="@{./}" th:object="${taskCreateForm}">
    <div>
        <label>
            内容:<input type="text" th:field="*{content}">
        </label>
        <p th:if="${#fields.hasErrors('content')}" th:errors="*{content}">エラーメッセージ</p>
    </div>
    <div>
        <input type="submit" value="作成" />
    </div>
</form>
</body>
</html>
*{...} : 選択変数式

上記のコードでは、*{content}は本来${taskCreateForm.content}と記述しなくてはいけないのですが、なんども同じような記述が生まれてしまう状況が起きやすいです。そのため簡略化して書けるよう選択変数式があります。親要素でth:object="${taskCreateForm}"と宣言されていれば、*{プロパティ名}で子要素はコンテキストにアクセスすることが可能です。

#fields

#を先頭に付けたものはユーティリティメソッドと呼ばれるもので他にはdateやcalender、sessionに保管されている値を参照するものなどがある。この#fieldsは、Validate後の各プロパティで発生したエラーメッセージを参照している。${#fields.errors('【プロパティ名】')}でアクセスできる。結果はリストになっているので、ループで回せば各エラーメッセージを取得できる。

次回はDIとValidateについてメモしていこうと思います。


参考にさせていただきましたサイト様

MVC architecture - App Center | MDN

これから始めるSpringのwebアプリケーション

Springを何となく使ってる人が抑えるべきポイント

Web on Servlet Stack

Tutorial: Using Thymeleaf (ja)

必要最小限のサンプルでThymeleafを完全マスター - Java EE 事始め!

Spring Boot で Thymeleaf 使い方メモ - Qiita