Amazon EKS on AWS Fargate + ALB Ingress Controllerを試す

はじめに

f:id:hiroki-sawano:20201025192920p:plain

KubernetesのマネージドサービスであるAmazon EKSを試用します。
ポッドの実行環境としてはEC2とAWS Fargateが選択できますが、EC2インスタンスの管理が不要なFargateを使います。

実行環境

macOS Catalina Version 10.15.5

eksctlでEKSクラスタを作成

以下のリンク先を参考に進めていきます。

Getting started with eksctl - Amazon EKS

前提

  • AWS CLIのインストール
❯ curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
❯ sudo installer -pkg AWSCLIV2.pkg -target /

AWS CLIeksctlAWSとやり取りするために必要な設定を行います。

❯ aws configure
AWS Access Key ID [None]: <your-access-key>
AWS Secret Access Key [None]: <your-secret-access-key>
Default region name [None]: <region-code>
Default output format [None]: <json>
  • eksctl(とkubectl)のインストール

Homebrewで eksctl をインストールすると kubectl も一緒にインストールしてくれます。

❯ brew tap weaveworks/tap
❯ brew install weaveworks/tap/eksctl

EKSクラスタの作成

eksctl create clusterクラスタを作成します。
バージョンは公式マニュアルに従い 1.18 、リージョンは東京( ap-northeast-1 )を指定しました。
Fargateを使うので --fargate オプションが必要です。

❯ eksctl create cluster \
    --name <your-cluster-name> \
    --version 1.18 \
    --region ap-northeast-1 \
    --fargate

...
[✔]  EKS cluster "..." in "ap-northeast-1" region is ready

f:id:hiroki-sawano:20201022022101p:plain

この時点ではCoreDNSのポッド2つのみが存在します。

❯ kubectl get pods -A
NAMESPACE     NAME                                      READY   STATUS        RESTARTS   AGE
kube-system   coredns-84c76989c7-8s58g                  1/1     Running       0          4d8h
kube-system   coredns-84c76989c7-pb5cb                  1/1     Running       0          4d8h

サンプルアプリのデプロイ

続けてサンプルアプリをデプロイします。

Deploy a sample Linux application - Amazon EKS

名前空間は標準で作成される default を指定しました。

apiVersion: v1
kind: Service
metadata:
  name: my-service
  namespace: default
  labels:
    app: my-app
spec:
  selector:
    app: my-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  namespace: default
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: beta.kubernetes.io/arch
                operator: In
                values:
                - amd64
                - arm64
      containers:
      - name: nginx
        image: nginx:1.19.2
        ports:
        - containerPort: 80

作成したマニフェストを指定し、 kubectl apply でアプリケーションをデプロイします。

❯ kubectl apply -f sample-pod.yml
service/my-service created
deployment.apps/my-deployment created

kubectl get all で作成されたリソースを確認します。
Deploymentspec.replicas に3を設定したので3つのポッドが作成されました。

❯ kubectl get all
NAME                                 READY   STATUS    RESTARTS   AGE
pod/my-deployment-68f6fff96f-ccdkn   1/1     Running   0          2m21s
pod/my-deployment-68f6fff96f-gdv8t   1/1     Running   0          2m21s
pod/my-deployment-68f6fff96f-lxp54   1/1     Running   0          2m21s

NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.100.0.1       <none>        443/TCP   22h
service/my-service   ClusterIP   10.100.149.193   <none>        80/TCP    2m21s

NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/my-deployment   3/3     3            3           2m21s

NAME                                       DESIRED   CURRENT   READY   AGE
replicaset.apps/my-deployment-68f6fff96f   3         3         3       2m21s

kubectl describe でリソースの詳細を確認できます。

❯ kubectl -n default describe service my-service
Name:              my-service
Namespace:         default
Labels:            app=my-app
...

❯ kubectl -n default describe pod my-deployment-68f6fff96f-ccdkn
Name:                 my-deployment-68f6fff96f-ccdkn
Namespace:            default
Priority:             2000001000
...

kubectl exec -it <ポッドの名前> -n <名前空間> -- /bin/bash でポッド(コンテナ)の中に入れます。

❯ kubectl exec -it my-deployment-68f6fff96f-ccdkn -n default -- /bin/bash
root@my-deployment-68f6fff96f-ccdkn:/#

ロードバランサの設定

この時点では外部からアクセスできないので、ALB Ingress ControllerをEKSクラスターにデプロイします。
(EKS on Fargateで使用できるロードバランサはALBのみ。)
ALB Ingress ControllerはIngressリソースの作成を契機にALBを作成してくれます。

ALB Ingress Controller on Amazon EKS - Amazon EKS

IAM OIDCプロバイダーの作成

❯ eksctl utils associate-iam-oidc-provider --region ap-northeast-1 --cluster <your-cluster-name> --approve
[ℹ]  eksctl version 0.30.0
[ℹ]  using region ap-northeast-1
[ℹ]  will create IAM Open ID Connect provider for cluster "<your-cluster-name>" in "ap-northeast-1"
[✔]  created IAM Open ID Connect provider for cluster "<your-cluster-name>" in "ap-northeast-1"

ALB Ingress Controllerポッド用のIAMポリシーを作成

ALB Ingress ControllerがAWSAPIを呼び出すために必要となるIAMポリシー <ALBIngressControllerIAMPolicy> を作成します。
ポリシーを作成した際に出力されるARNをメモしておきます。

❯ curl -o iam-policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.8/docs/examples/iam-policy.json
❯ aws iam create-policy --policy-name ALBIngressControllerIAMPolicy --policy-document file://iam-policy.json
{
    "Policy": {
        "PolicyName": "ALBIngressControllerIAMPolicy",
        ...
    }
}
❯ rm iam-policy.json

alb-ingress-controllerサービスアカウント、ClusterRole、ClusterRoleBindingの作成

❯ kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.8/docs/examples/rbac-role.yaml

clusterrole.rbac.authorization.k8s.io/alb-ingress-controller created
clusterrolebinding.rbac.authorization.k8s.io/alb-ingress-controller created
serviceaccount/alb-ingress-controller created

ALB Ingress ControllerのIAMロールを作成し、ロールをサービスアカウントに割り当て

--attach-policy-arn にはALB Ingress Controllerポッド用のIAMポリシーを作成で確認したARNを指定します。

❯ eksctl create iamserviceaccount \
    --region ap-northeast-1 \
    --name alb-ingress-controller \
    --namespace kube-system \
    --cluster <your-cluster-name> \
    --attach-policy-arn arn:aws:iam::<111122223333>:policy/<ALBIngressControllerIAMPolicy> \
    --override-existing-serviceaccounts \
    --approve

[ℹ]  eksctl version 0.30.0
[ℹ]  using region ap-northeast-1
[ℹ]  2 iamserviceaccounts (kube-system/alb-ingress-controller, kube-system/aws-node) were included (based on the include/exclude rules)
...
[ℹ]  updated serviceaccount "kube-system/aws-node"

ALB Ingress Controllerをデプロイ

❯ kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.8/docs/examples/alb-ingress-controller.yaml
deployment.apps/alb-ingress-controller created

ALB Ingress Controllerデプロイメントのマニフェストを編集

❯ kubectl edit deployment.apps/alb-ingress-controller -n kube-system

spec.containers.args--cluster-name--aws-vpc-id--aws-region を設定します。

    spec:
      containers:
      - args:
        - --ingress-class=alb
        - --cluster-name=<your-cluster-name>
        - --aws-vpc-id=<your-vpc-id>
        - --aws-region=ap-northeast-1

ALB Ingress Controllerのポッドを確認

kube-systemalb-ingress-controller のポッドが出来上がりました。

❯ kubectl get pods -n kube-system
NAME                                      READY   STATUS    RESTARTS   AGE
alb-ingress-controller-65fcfcd9d4-nzpdr   1/1     Running   1          2m15s
coredns-84c76989c7-8s58g                  1/1     Running   0          23h
coredns-84c76989c7-pb5cb                  1/1     Running   0          23h

Ingressリソースを作成

Ingressリソースを作成し、サンプルアプリのService my-service に接続するALBを用意します。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: "ingress"
  namespace: "default"
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
  labels:
    app: ingress
spec:
  rules:
    - http:
        paths:
          - path: /*
            backend:
              serviceName: my-service
              servicePort: 80
❯ kubectl apply -f ingress.yaml
ingress.extensions/ingress created

動作確認

kubectl get ingress で取得したアドレスにアクセスできることを確認します。

❯ kubectl get ingress
NAME      CLASS    HOSTS   ADDRESS                                                                    PORTS   AGE
ingress   <none>   *       ...   80      2m6s

f:id:hiroki-sawano:20201022023633p:plain

後片付け

最後に kubectl delete でデプロイしたサンプルアプリを削除します。

❯ kubectl delete -f sample-pod.yml
service "my-service" deleted
deployment.apps "my-deployment" deleted

MacでNeovimをソースからインストール

はじめに

以下の投稿で述べたNeovimの不具合がこのPRで直りそうなんですが、なかなかリリースされないので修正されたソースからNeovimをビルドします。

hiroki-sawano.hatenablog.com

実行環境

macOS Catalina Version 10.15.5

前提

macOSなので以下の前提を事前に満たしておきます。

Building Neovim · neovim/neovim Wiki · GitHub

ソースの取得

今回は上記PRの修正を試したいので、マージ元のブランチをチェックアウトします。

$ git clone https://github.com/erw7/neovim.git
$ cd neovim/
$ git checkout fix-paste

ビルド・インストール

あとはREADMEに記載の手順でインストールします。

$ make CMAKE_BUILD_TYPE=RelWithDebInfo

すると以下のエラーが発生してしまいました。

FAILED: build/src/libvterm-stamp/libvterm-configure 
cd /Users/hiroki_sawano/.ghq/github.com/erw7/neovim/.deps/build/src/libvterm && "" "" && /usr/local/Cellar/cmake/3.18.2/bin/cmake -E touch /Users/hiroki_sawano/.ghq/github.com/erw7/neovim/.deps/build/src/libvterm-stamp/libvterm-configure
/bin/sh: : command not found

どうやらmacOS 10.15とcmake 3.18だと発生するエラーだったようです。Neovim本体では修正済みの不具合なので同様の修正を取り込みました。

github.com

では改めてビルド、インストールします。

$ make CMAKE_BUILD_TYPE=RelWithDebInfo
$ sudo make install

これでソースからビルドしたNeovimをインストールできました。

$ nvim --version
NVIM v0.5.0-3075-g23463dd07-dirty

さいごに

今回インストールした版のNeovimでこの不具合が再現しないことを期待し、当面試用してみます。

DjangoのFixtureで投入するユーザ・グループに権限を設定

はじめに

DjangoFixturedjango.contrib.authUserGroupPermissionを設定する方法を確認しました。

パーミッションの作成

まずは適当なパーミッションModel.Meta.permissionsで作成します。
myapp アプリの MyPermission モデルに perm1perm2 を用意しました。

class MyPermission(models.Model):
    class Meta:
        managed = False
        default_permissions = ()
        permissions = (
            ('perm1', '権限1'),
            ('perm2', '権限2'),
        )

マイグレーションすると、以下の通りPermissionモデルに perm1perm2 が追加されています。

$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py dbshell
sqlite> .header on
sqlite> select * from auth_permission where codename == 'perm1' or codename == 'perm2';
id|content_type_id|codename|name
33|9|perm1|権限1
34|9|perm2|権限2

ユーザの作成

以下のように modelauth.userpk に1からインクリメントした数値、 fields にユーザの情報を設定すればユーザを登録できます。

[
   {
      "model":"auth.user",
      "pk":1,
      "fields":{
         "last_name":"テスト",
         "first_name":"ユーザ",
         "email":"foo@example.com",
         "username":"foo@example.com"
      }
   }
]

では権限をどのようにして割り当てるかというと、user_permissionsフィールドに Permissionオブジェクトのキーを指定することになります。

"fields":{
   ...
   "user_permissions": [...]
}

しかしPermissionオブジェクトの id 値はマイグレーション時に動的に決定されるため推測することは困難です。 例えば上記で作成した権限 perm1perm2id はそれぞれ 3334 です。

ではどうするかというと、 id 値の代わりにナチュラルキーを使用します。
まさにこのような場面を想定し、Djangoはモデルのシリアライズナチュラルキーを使用する仕組みを実装しています。

docs.djangoproject.com

PermissionManagerには get_by_natural_key が実装されているため、静的な情報である codenameapp_label および model でオブジェクトを特定することができます。

class PermissionManager(models.Manager):
    use_in_migrations = True

    def get_by_natural_key(self, codename, app_label, model):
        return self.get(
            codename=codename,
            content_type=ContentType.objects.db_manager(self.db).get_by_natural_key(app_label, model),
        )

例えば perm1 の権限を与える場合には、 user_permissions["perm1", "myapp", "mypermission"] を指定すれば良いわけです。なお model は小文字で指定する必要があります。

[
   {
      "model":"auth.user",
      "pk":1,
      "fields":{
         "last_name":"テスト",
         "first_name":"ユーザ",
         "email":"foo@example.com",
         "username":"foo@example.com",
         "user_permissions": [
            ["perm1", "myapp", "mypermission"],
            ["perm2", "myapp", "mypermission"]
         ]
      }
   }
]

これで、権限を割り当てたユーザを作成することができました。

$ python manage.py loaddata auth_user.json
Installed 1 object(s) from 1 fixture(s)

$ python manage.py dbshell
sqlite> .header on
sqlite> select * from auth_user_user_permissions where user_id = 1;
id|user_id|permission_id
1|1|33
2|1|34

グループの作成

グループの場合は権限のフィールド名がpermissionsに変わるだけです。

[
   {
      "model":"auth.group",
      "pk":1,
      "fields":{
         "permissions":[
            ["perm1", "myapp", "mypermission"],
            ["perm2", "myapp", "mypermission"]
         ],
         "name":"テストグループ"
      }
   }
]

SAMLレスポンスをブラウザで取得・Base64デコード

はじめに

SimpleSAMLphpSAML認証に対応したSPで、IdP認証後のSAMLレスポンス検証がうまくいかなかったため、SAMLレスポンスのデバッグ方法を確認しました。
便利なもので、以下のリンク先にある通りChromeFirefox等で簡単に取得できます。

docs.aws.amazon.com

ChromeSAMLレスポンスを取得

以下の手順を実行します。

  1. F12 キーを押して開発者コンソールを開始します。

  2. [Network (ネットワーク)] タブを選択し、[Preserve log (ログの保持)] を選択します。

  3. 問題を再現します。

  4. 開発者コンソールペインで [SAML Post] を探します。その行を選択し、次に下部の [Headers (ヘッダー)] タブを表示します。エンコードされたリクエストを含む [SAMLResponse] 属性を探します。

SAMLレスポンスを確認したい画面まで移動し、 Headers から SAMLResponse を見つけることができました。

f:id:hiroki-sawano:20200829031749p:plain

SAMLレスポンスをデコード

取得したレスポンスはBase64エンコードされているので以下のコマンドでデコードします。
これでXML形式のレスポンスが手に入りました。

$ echo 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVV...' | base64 -D
<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response....

Jupyter Notebook+Ansible(ansible-jupyter-kernel)で実行可能な手順書を作成

はじめに

先日以下のエントリでDjangoアプリのデプロイをAnsibleで自動化しました。

hiroki-sawano.hatenablog.com

今回はansible-jupyter-kernelを使用し、同等の作業をJupyter Notebook上から実行してみます。
ノートブックにAnsibleのプレイブックを記述することで、実行結果の保存、Markdownセルを用いた手順のドキュメント化など、Jupyter Notebookの利点を享受できます。

github.com

実行環境

$ python --version
Python 3.7.4

ansible-jupyter-kernelのインストール

適当な仮想環境を用意し、 ansible-kernel をインストールします。

$ python -m venv venv
$ . ./venv/bin/activate
$ pip install ansible-kernel
$ python -m ansible_kernel.install

Jupyter Notebookの起動

以下のコマンドを実行するとhttp://localhost:8888/が開き、カレントディレクトリのファイル一覧が表示されます。

$ jupyter notebook

f:id:hiroki-sawano:20200729205920p:plain

ノートブックの作成

New ▼ からAnsibleのノートブックを作成します。

f:id:hiroki-sawano:20200729210044p:plain

プレイブックの記述

こちらの説明に従い、 #inventory#play および #task のセルを作成していきます。

inventory

#inventory から始まるセルにインベントリの情報を記述します。ansible_ssh_private_key_fileansible-playbook--private-key に対応する設定です。

f:id:hiroki-sawano:20200824172322p:plain

play

#play から始まるセルにプレイ部分を記述します。

f:id:hiroki-sawano:20200824172418p:plain

task

あとはタスクをひたすら書いていきます。
先頭に #task を書くことでタスクのセルであることを明示的できますが、省略可能です。

f:id:hiroki-sawano:20200824172634p:plain

ノートブックの実行

これでセルを実行すればインベントリに指定したサーバにDjangoアプリをデプロイできるようになりました。
以下のように実行結果がセルごとに残るため、変更内容を確認することができ、また環境構築作業のエビデンスを残すことができるため大変便利です。

f:id:hiroki-sawano:20200824173153p:plain

ノートブックの実行後、デプロイされたアプリに問題なくアクセスできることを確認しました。

f:id:hiroki-sawano:20200824172944p:plain

SQL Serverの変更データキャプチャ機能(CDC)

はじめに

SQL Serverの変更データキャプチャ機能(以降、CDC)を使用し、データ変更の履歴を取得します。
CDCを使えば自前でトリガーを仕込まなくてもトランザクションに紐づくデータ変更の履歴を取得することができるので、CRUD図の存在しないシステムの調査や監査証跡等で活用できると思います。

環境構築

SQL ServerSQL Server Agentのインストール

以下の記事を参考に、Docker環境を用意します。

dbafromthecold.com

$ docker pull dbafromthecold/sqlserverlinuxagent:latest
$ docker run -d -p 15789:1433 --env ACCEPT_EULA=Y --env SA_PASSWORD=lis6ot9I --name sqlserver linuxwithagent
$ docker exec -it sqlserver bash

sqlcmdのインストール

SQLの発行にsqlcmdを使うためmssql-toolsをインストールします。

# curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
# curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list | tee /etc/apt/sources.list.d/msprod.list
# apt-get update
# apt-get install -y mssql-tools unixodbc-dev

クエリの発行と変更データのキャプチャ

DBサーバに接続

# /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P lis6ot9I

DBの作成

1> CREATE DATABASE CDC_TEST;
2> GO
1> USE CDC_TEST;
2> GO
Changed database context to 'CDC_TEST'.

テーブルの作成

履歴の取得対象となるテーブルを用意します。

1> CREATE TABLE TestTable (
2>     ColumnA  INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
3>     ColumnB  INT NOT NULL,
4>     ColumnC  TINYINT NOT NULL,
5>     ColumnD  NVARCHAR(20) NOT NULL,
6>     ColumnE  CHAR(1) NOT NULL,
7>     ColumnF  BIT NOT NULL DEFAULT(0)
8> );
9> GO

初期データの挿入

1> INSERT INTO TestTable VALUES
2>    (1, 2, 'a', 'b', 1),
3>    (1, 2, 'a', 'b', 1),
4>    (1, 2, 'a', 'b', 1),
5>    (1, 2, 'a', 'b', 1)
6> GO

(4 rows affected)

CDC機能の有効化

履歴取得の対象となるデータベースとテーブルに対してCDC機能を有効化します。

1> EXECUTE sys.sp_cdc_enable_db;
2> GO
1> EXECUTE sys.sp_cdc_enable_table
2>     @source_schema = N'dbo'
3>   , @source_name = N'TestTable'
4>   , @role_name = N'cdc_Admin';
5> GO
Job 'cdc.CDC_TEST_capture' started successfully.
Job 'cdc.CDC_TEST_cleanup' started successfully.

これでCDCを有効にしたテーブル TestTable に対して更新操作(INSERT、UPDATE、DELETE)が行われると、 cdc.スキーマ名_テーブル名_CT に変更履歴が出力されます。

1> SELECT COUNT(*) FROM cdc.dbo_TestTable_CT;
2> GO
           
-----------
          0

(1 rows affected)

クエリの発行と履歴の確認

更新履歴(UPDATE)

変更前のデータは以下の通りです。

1> SELECT * FROM TestTable;
2> GO
ColumnA     ColumnB     ColumnC ColumnD              ColumnE ColumnF
----------- ----------- ------- -------------------- ------- -------
          1           1       2 a                    b             1
          2           1       2 a                    b             1
          3           1       2 a                    b             1
          4           1       2 a                    b             1

(4 rows affected)

ColumnA が3の行を対象に、 ColumnBColumnD を更新してみます。

1> UPDATE TestTable SET ColumnB = 5, ColumnD = 'AAAA' WHERE ColumnA = 3;
2> GO

(1 rows affected)

1> SELECT * FROM TestTable;
2> GO
ColumnA     ColumnB     ColumnC ColumnD              ColumnE ColumnF
----------- ----------- ------- -------------------- ------- -------
          1           1       2 a                    b             1
          2           1       2 a                    b             1
          3           5       2 AAAA                 b             1
          4           1       2 a                    b             1

(4 rows affected)

cdc.dbo_TestTable_CT を確認すると、以下の通り更新前後の履歴が出力されていることがわかります。
__$start_lsn__$end_lsn ではLSN(Log Sequence Number:ログ順序番号)によって、更新時刻(や順序)を管理しています。
LSNはトランザクション単位に一意となるため、同一トランザクションで影響を受けたデータを特定することができます。
__$operation には履歴の種別(1:削除、2:挿入、3:更新前、4:更新後)が格納されます。

1> SELECT * FROM cdc.dbo_TestTable_CT;
2> GO
__$start_lsn           __$end_lsn             __$seqval              __$operation __$update_mask                                                                                                                                                                                                                                                     ColumnA     ColumnB     ColumnC ColumnD              ColumnE ColumnF __$command_id
---------------------- ---------------------- ---------------------- ------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ----------- ----------- ------- -------------------- ------- ------- -------------
0x00000025000004A00003 NULL                   0x00000025000004A00002            3 0x0A                                                                                                                                                                                                                                                                         3           1       2 a                    b             1             1
0x00000025000004A00003 NULL                   0x00000025000004A00002            4 0x0A                                                                                                                                                                                                                                                                         3           5       2 AAAA                 b             1             1

(2 rows affected)

挿入の履歴(INSERT)

適当なデータを1件挿入します。

1> INSERT INTO TestTable VALUES
2>   (3, 9, 'C', 'E', 0)
3> GO
(1 rows affected)

挿入の履歴が追加されました。

1> SELECT * FROM cdc.dbo_TestTable_CT;
2> GO
__$start_lsn           __$end_lsn             __$seqval              __$operation __$update_mask                                                                                                                                                                                                                                                     ColumnA     ColumnB     ColumnC ColumnD              ColumnE ColumnF __$command_id
---------------------- ---------------------- ---------------------- ------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ----------- ----------- ------- -------------------- ------- ------- -------------
...
0x00000025000007280004 NULL                   0x00000025000007280003            2 0x3F                                                                                                                                                                                                                                                                         5           3       9 C                    E             0             1

(3 rows affected)

削除の履歴(DELETE)

ColumnB が1のデータ3件を削除します。

1> SELECT * FROM TestTable;
2> GO
ColumnA     ColumnB     ColumnC ColumnD              ColumnE ColumnF
----------- ----------- ------- -------------------- ------- -------
          1           1       2 a                    b             1
          2           1       2 a                    b             1
          3           5       2 AAAA                 b             1
          4           1       2 a                    b             1
          5           3       9 C                    E             0

(5 rows affected)

1> DELETE FROM TestTable WHERE ColumnB = 1;
2> GO

(3 rows affected)

削除の履歴が3件追加されました。

1> SELECT * FROM cdc.dbo_TestTable_CT;
2> GO
__$start_lsn           __$end_lsn             __$seqval              __$operation __$update_mask                                                                                                                                                                                                                                                     ColumnA     ColumnB     ColumnC ColumnD              ColumnE ColumnF __$command_id
---------------------- ---------------------- ---------------------- ------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ----------- ----------- ------- -------------------- ------- ------- -------------
...
0x00000025000009300007 NULL                   0x00000025000009300002            1 0x3F                                                                                                                                                                                                                                                                         1           1       2 a                    b             1             1
0x00000025000009300007 NULL                   0x00000025000009300005            1 0x3F                                                                                                                                                                                                                                                                         2           1       2 a                    b             1             2
0x00000025000009300007 NULL                   0x00000025000009300006            1 0x3F                                                                                                                                                                                                                                                                         4           1       2 a                    b             1             3

(6 rows affected)

履歴の取得時刻を確認する方法

sys.fn_cdc_map_lsn_to_time にLSNを渡すと取得できます。
例えば __$start_lsn0x00000025000004A00003 の履歴の取得時刻は以下の通りです。

1> SELECT sys.fn_cdc_map_lsn_to_time(0x00000025000004A00003);
2> GO
                       
-----------------------
2020-08-15 03:08:17.613

(1 rows affected)

履歴情報の整形

上記の履歴はわかりにくいので例えば以下のようなSQLで加工すると良いと思います。

1>  SELECT sys.fn_cdc_map_lsn_to_time(__$start_lsn) AS 'TIME',
2>      CASE WHEN __$operation = 1 THEN 'Delete'
3>          WHEN __$operation = 2 THEN 'INSERT'
4>          WHEN __$operation = 3 THEN 'BEFORE UPDATE'
5>          WHEN __$operation = 4 THEN 'AFTER UPDATE'
6>      END AS OPERATION,
7>      ColumnA,
8>      ColumnB,
9>      ColumnC,
10>     ColumnD,
11>     ColumnE,
12>     ColumnF
13> FROM cdc.dbo_TestTable_CT
14> ORDER BY __$start_lsn;
15> GO

TIME                    OPERATION     ColumnA     ColumnB     ColumnC ColumnD              ColumnE ColumnF
----------------------- ------------- ----------- ----------- ------- -------------------- ------- -------
2020-08-15 03:08:17.613 BEFORE UPDATE           3           1       2 a                    b             1
2020-08-15 03:08:17.613 AFTER UPDATE            3           5       2 AAAA                 b             1
2020-08-15 03:11:45.350 INSERT                  5           3       9 C                    E             0
2020-08-15 03:16:47.667 Delete                  1           1       2 a                    b             1
2020-08-15 03:16:47.667 Delete                  2           1       2 a                    b             1
2020-08-15 03:16:47.667 Delete                  4           1       2 a                    b             1

(6 rows affected)

日時で絞り込みたい場合にはWHERE句を追加します。

1>  SELECT sys.fn_cdc_map_lsn_to_time(__$start_lsn) AS 'TIME',
2>      CASE WHEN __$operation = 1 THEN 'Delete'
3>          WHEN __$operation = 2 THEN 'INSERT'
4>          WHEN __$operation = 3 THEN 'BEFORE UPDATE'
5>          WHEN __$operation = 4 THEN 'AFTER UPDATE'
6>      END AS OPERATION,
7>      ColumnA,
8>      ColumnB,
9>      ColumnC,
10>     ColumnD,
11>     ColumnE,
12>     ColumnF
13> FROM cdc.dbo_TestTable_CT
14> WHERE sys.fn_cdc_map_lsn_to_time(__$start_lsn) BETWEEN '2020-08-15 03:11:00' AND '2020-08-15 03:12:00'
15> ORDER BY __$start_lsn;
16> GO

TIME                    OPERATION     ColumnA     ColumnB     ColumnC ColumnD              ColumnE ColumnF
----------------------- ------------- ----------- ----------- ------- -------------------- ------- -------
2020-08-15 03:11:45.350 INSERT                  5           3       9 C                    E             0

(1 rows affected)

同一トランザクションで複数回同一行を更新した場合の履歴

一つのトランザクションで同じ行を更新する場合、以下のように __$start_lsn で同一トランザクションの履歴であることがわかります。

1> SELECT * FROM TestTable;
2> GO
ColumnA     ColumnB     ColumnC ColumnD              ColumnE ColumnF
----------- ----------- ------- -------------------- ------- -------
          3           5       2 AAAA                 b             1
          5           3       9 C                    E             0

(2 rows affected)

1> BEGIN TRANSACTION;
2> UPDATE TestTable SET ColumnB=1 WHERE ColumnA = 3;
3> UPDATE TestTable SET ColumnB=2 WHERE ColumnA = 3;
4> COMMIT TRANSACTION;
5> GO

(1 rows affected)

(1 rows affected)

1> SELECT * FROM TestTable;
2> GO
ColumnA     ColumnB     ColumnC ColumnD              ColumnE ColumnF
----------- ----------- ------- -------------------- ------- -------
          3           2       2 AAAA                 b             1
          5           3       9 C                    E             0

(2 rows affected)

1> SELECT * FROM cdc.dbo_TestTable_CT;
2> GO
__$start_lsn           __$end_lsn             __$seqval              __$operation __$update_mask                                                                                                                                                                                                                                                     ColumnA     ColumnB     ColumnC ColumnD              ColumnE ColumnF __$command_id
---------------------- ---------------------- ---------------------- ------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ----------- ----------- ------- -------------------- ------- ------- -------------
...
0x00000026000008100004 NULL                   0x00000026000008100002            3 0x02                                                                                                                                                                                                                                                                         3           5       2 AAAA                 b             1             1
0x00000026000008100004 NULL                   0x00000026000008100002            4 0x02                                                                                                                                                                                                                                                                         3           1       2 AAAA                 b             1             1
0x00000026000008100004 NULL                   0x00000026000008100003            3 0x02                                                                                                                                                                                                                                                                         3           1       2 AAAA                 b             1             2
0x00000026000008100004 NULL                   0x00000026000008100003            4 0x02                                                                                                                                                                                                                                                                         3           2       2 AAAA                 b             1             2

(10 rows affected)

同一トランザクションで複数のテーブルを更新した場合の履歴

同一トランザクションで複数のテーブルを更新対象とする場合も同様に $__start_lsn が同じになります。

1> CREATE TABLE TestTable2 (
2>     ColumnA  INT NOT NULL
3> );
4> GO
1> EXECUTE sys.sp_cdc_enable_table
2>     @source_schema = N'dbo'
3>   , @source_name = N'TestTable2'
4>   , @role_name = N'cdc_Admin';
5> GO

1> BEGIN TRANSACTION;
2> UPDATE TestTable SET ColumnB = 3 WHERE ColumnA = 3;
3> INSERT INTO TestTable2 VALUES
4>     (4),
5>     (5)
6> COMMIT TRANSACTION;
7> GO

(1 rows affected)

(2 rows affected)

1> SELECT * FROM cdc.dbo_TestTable_CT;
2> SELECT * FROM cdc.dbo_TestTable2_CT;
3> GO
__$start_lsn           __$end_lsn             __$seqval              __$operation __$update_mask                                                                                                                                                                                                                                                     ColumnA     ColumnB     ColumnC ColumnD              ColumnE ColumnF __$command_id
---------------------- ---------------------- ---------------------- ------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ----------- ----------- ------- -------------------- ------- ------- -------------
...
0x0000002700000630001F NULL                   0x00000027000006300002            3 0x02                                                                                                                                                                                                                                                                         3           2       2 AAAA                 b             1             1
0x0000002700000630001F NULL                   0x00000027000006300002            4 0x02                                                                                                                                                                                                                                                                         3           3       2 AAAA                 b             1             1

(12 rows affected)

__$start_lsn           __$end_lsn             __$seqval              __$operation __$update_mask                                                                                                                                                                                                                                                     ColumnA     __$command_id
---------------------- ---------------------- ---------------------- ------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ----------- -------------
0x0000002700000630001F NULL                   0x0000002700000630001C            2 0x01                                                                                                                                                                                                                                                                         4             2
0x0000002700000630001F NULL                   0x0000002700000630001E            2 0x01                                                                                                                                                                                                                                                                         5             3

(2 rows affected)

Angular10入門(環境構築〜REST APIアクセス)

はじめに

近いうちにAngularを使いそうなので学習の過程をメモしていきます。
今回は環境構築から簡単なコンポーネントの作成、REST APIアクセスまでを実践していきます。

事前学習

とりあえずYouTubeangular と検索し、トップに出てきた以下の動画で学習しました。
TypeScriptの基本からAngularの仕組みについてわかりやすく説明されているので、英語が苦でなければおすすめします。

www.youtube.com

本エントリで実施する範囲はほぼこの動画でカバーできると思いますが、APIアクセス周りについては以下の記事を参考にしました。

環境構築

Node.jsのインストール

実行環境としてNodeが必要なのでインストールします。
バージョンは2020/8/10時点で最新の v14.7.0 を用意しました。

❯ nvm install v14.7.0

Angularのインストール

npmで @angular/cli をインストールします。

❯ npm install -g @angular/cli
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
npm WARN deprecated har-validator@5.1.5: this library is no longer supported
/Users/hiroki_sawano/.nvm/versions/node/v14.7.0/bin/ng -> /Users/hiroki_sawano/.nvm/versions/node/v14.7.0/lib/node_modules/@angular/cli/bin/ng

> @angular/cli@10.0.5 postinstall /Users/hiroki_sawano/.nvm/versions/node/v14.7.0/lib/node_modules/@angular/cli
> node ./bin/postinstall/script.js

? Would you like to share anonymous usage data with the Angular Team at Google under
Google’s Privacy Policy at https://policies.google.com/privacy? For more details and
how to change this setting, see http://angular.io/analytics. No
+ @angular/cli@10.0.5
added 280 packages from 206 contributors in 53.464s

バージョンは 10.0.5 です。

❯ ng --version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 10.0.5
Node: 14.7.0
OS: darwin x64

Angular: 
... 
Ivy Workspace: 

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.1000.5
@angular-devkit/core         10.0.5
@angular-devkit/schematics   10.0.5
@schematics/angular          10.0.5
@schematics/update           0.1000.5
rxjs                         6.5.5

新規アプリケーションの作成

ng newで新規アプリを作成します。
プロンプトでAngular routingはあり、スタイルシートにはSCSSを選択しました。Angular routingは本エントリで扱いませんが、次回以降で使用していく予定です。

❯ ng new my-app
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS   [ https:
//sass-lang.com/documentation/syntax#scss                ]
CREATE my-app/README.md (1023 bytes)
CREATE my-app/.editorconfig (274 bytes)
CREATE my-app/.gitignore (631 bytes)
CREATE my-app/angular.json (3654 bytes)
CREATE my-app/package.json (1249 bytes)
CREATE my-app/tsconfig.base.json (458 bytes)
CREATE my-app/tsconfig.json (426 bytes)
CREATE my-app/tslint.json (3184 bytes)
CREATE my-app/.browserslistrc (853 bytes)
CREATE my-app/karma.conf.js (1018 bytes)
CREATE my-app/tsconfig.app.json (292 bytes)
CREATE my-app/tsconfig.spec.json (338 bytes)
CREATE my-app/src/favicon.ico (948 bytes)
CREATE my-app/src/index.html (291 bytes)
CREATE my-app/src/main.ts (372 bytes)
CREATE my-app/src/polyfills.ts (2835 bytes)
CREATE my-app/src/styles.scss (80 bytes)
CREATE my-app/src/test.ts (753 bytes)
CREATE my-app/src/assets/.gitkeep (0 bytes)
CREATE my-app/src/environments/environment.prod.ts (51 bytes)
CREATE my-app/src/environments/environment.ts (662 bytes)
CREATE my-app/src/app/app-routing.module.ts (245 bytes)
CREATE my-app/src/app/app.module.ts (393 bytes)
CREATE my-app/src/app/app.component.scss (0 bytes)
CREATE my-app/src/app/app.component.html (25757 bytes)
CREATE my-app/src/app/app.component.spec.ts (1059 bytes)
CREATE my-app/src/app/app.component.ts (211 bytes)
CREATE my-app/e2e/protractor.conf.js (869 bytes)
CREATE my-app/e2e/tsconfig.json (299 bytes)
CREATE my-app/e2e/src/app.e2e-spec.ts (639 bytes)
CREATE my-app/e2e/src/app.po.ts (301 bytes)
✔ Packages installed successfully.
    Successfully initialized git.

作成された my-app ディレクトリに移動しておきます。

❯ cd my-app/

アプリケーションのビルド・起動

ng serveでアプリをビルド、起動します。 --open オプションをつけると自動でブラウザが開きます。
ng serve はファイルの変更を検知し、自動でリビルドしてくれるので、以降の作業では起動しっぱなしにしておきます。

❯ ng serve --open
Compiling @angular/core : es2015 as esm2015
Compiling @angular/animations : es2015 as esm2015
Compiling @angular/compiler/testing : es2015 as esm2015
Compiling @angular/core/testing : es2015 as esm2015
Compiling @angular/animations/browser : es2015 as esm2015
Compiling @angular/common : es2015 as esm2015
Compiling @angular/platform-browser : es2015 as esm2015
Compiling @angular/common/http : es2015 as esm2015
Compiling @angular/common/testing : es2015 as esm2015
Compiling @angular/platform-browser-dynamic : es2015 as esm2015
Compiling @angular/platform-browser/testing : es2015 as esm2015
Compiling @angular/router : es2015 as esm2015
Compiling @angular/animations/browser/testing : es2015 as esm2015
Compiling @angular/common/http/testing : es2015 as esm2015
Compiling @angular/forms : es2015 as esm2015
Compiling @angular/platform-browser/animations : es2015 as esm2015
Compiling @angular/platform-browser-dynamic/testing : es2015 as esm2015
Compiling @angular/router/testing : es2015 as esm2015

chunk {main} main.js, main.js.map (main) 60.6 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 141 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.15 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 12.9 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 2.65 MB [initial] [rendered]
Date: 2020-08-09T13:42:30.673Z - Hash: e069bd65d556c23a289b - Time: 18402ms
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
: Compiled successfully.

http://localhost:4200/ が開き、以下の初期画面が表示されました。

f:id:hiroki-sawano:20200809224454p:plain

生成されたAppコンポーネントの確認と編集

初期画面の実装は src/app/app.component.html です。このファイルを以下のように書き換えます。

<h1>{{ title }}</h1>

保存すると、自動的にコンパイルされ、変更後のページが読み込まれます。

f:id:hiroki-sawano:20200809224921p:plain

このHTMLは app.component.ts@Componentデコレータに指定されたtemplateUrlで設定されています。
{{ title }} はこのモジュールでエクスポートしてあるクラス AppComponent のプロパティ title の値を参照しています。

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'my-app';
}

@ComponentselectorコンポーネントをHTML上に埋め込む際に使用するセレクタです。
src/index.html を確認するとbody部に <app-root> が記述され、Appコンポーネントが組み込まれていることがわかります。

<body>
  <app-root></app-root>
</body>

selectorCSSセレクタなので、id属性で指定する場合は selector: '#app-root' 、 class属性で指定する場合は selector: '.app-root' となります。その時、テンプレートはそれぞれ <div id="app-root"></div><div class="app-root"></div> のようになります。

上記のHTMLをChromeデベロッパーツールで確認するとこんな感じです。

f:id:hiroki-sawano:20200809225705p:plain

スタイルは@ComponentstyleUrlsで指定されています。配列なので複数指定可能です。
AppComponent に設定されたスタイルシート app.component.scss を変更してみます。

h1 {
  color: red;
}

スタイルが適用され、フォントの色が変わりました。

f:id:hiroki-sawano:20200809230507p:plain

コンポーネントの作成

ユーザの一覧を表示するコンポーネント user-listng generateで作成します。

❯ ng g c user-list
CREATE src/app/user-list/user-list.component.scss (0 bytes)
CREATE src/app/user-list/user-list.component.html (24 bytes)
CREATE src/app/user-list/user-list.component.spec.ts (643 bytes)
CREATE src/app/user-list/user-list.component.ts (287 bytes)
UPDATE src/app/app.module.ts (485 bytes)

作成したコンポーネントAppModule@NgModuledeclarationsに自動的に追加されます。

// ...
import { UserListComponent } from './user-list/user-list.component'; // 追加

@NgModule({
  declarations: [
    AppComponent,
    UserListComponent //追加
  ],
// ...

生成されたテンプレートでは user-list works! とだけ表示されます。

<p>user-list works!</p>

コンポーネントの実装は以下の通りです。

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.scss']
})
export class UserListComponent implements OnInit {

  constructor() { }

  ngOnInit(): void {
  }

}

UserListコンポーネントの表示

まずは自動生成されたコンポーネントをそのまま表示してみます。
AppComponent のテンプレートに <app-user-list> を追記します。

<h1>{{ title }}</h1>
<app-user-list></app-user-list>

user-list.component.html の内容が表示されました。

f:id:hiroki-sawano:20200809233004p:plain

ユーザ一覧の表示

Userクラスの作成

ユーザを表現するクラス User を作成します。

❯ ng g class user --type=model
CREATE src/app/user.model.spec.ts (152 bytes)
CREATE src/app/user.model.ts (22 bytes)

user.model.tsUser クラスでコンストラクタに必要なプロパティを定義します。
username のみ必須で後は任意な項目としました。

export class User {
  constructor(private username: string, private firstName?: string, private lastName?:string, private email?: string) { }
}

ユーザ一覧を返すプロパティを実装

User クラスをインポートして、適当な User オブジェクトを users で返します。

import { User } from '../user.model';
// ...

export class UserListComponent implements OnInit {
  // ...
  get users() {
    let user1 = new User('foo');
    let user2 = new User('bar', 'John', 'Doe', 'foo@example.com');
    return [user1, user2]
  }
}

Userオブジェクトをループして表示

テンプレートで*ngForを使ってユーザ一覧を表示します。

<table>
  <thead>
    <tr>
      <th>Username</th>
      <th>First Name</th>
      <th>Last Name</th>
      <th>Email</th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let user of users">
      <td>{{ user.username }}</td>
      <td>{{ user.firstName }}</td>
      <td>{{ user.lastName }}</td>
      <td>{{ user.email }}</td>
    </tr>
  </tbody>
</table>

スタイルシートでは枠線を設定しておきます。

table, th, td {
  border: 1px solid black;
  border-collapse: collapse;
}

以下のように User オブジェクトの一覧が表示できました。

f:id:hiroki-sawano:20200810001857p:plain

REST APIでユーザ一覧を取得

次にユーザの一覧をREST APIで取得するように変更します。

APIの実装

APIのモックをjson-serverで用意します。

❯ npm install -g json-server

/users で2件のユーザ情報を返すように設定します。

{
  "users": [
    { "username": "foo"},
    { "username": "bar", "firstName": "John", "lastName": "Doe", "email": "foo@example.com"}
  ]
}

サーバを起動します。

❯ json-server --watch db.json 

これで、 http://localhost:3000/users にGETリクエストを投げると db.json に記述したJSONを返すようになりました。

❯ curl http://localhost:3000/users
[
  {
    "username": "foo"
  },
  {
    "username": "bar",
    "firstName": "John",
    "lastName": "Doe",
    "email": "foo@example.com"
  }
]

HttpClientModuleのインポート

HTTPリクエストを扱うため、HttpClientModuleをインポートします。

import { HttpClientModule } from '@angular/common/http'; // 追加
// ...

@NgModule({
  // ...
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule // 追加
  ],
  // ...
})
export class AppModule { }

サービスの作成

APIを叩くサービスクラスを作成します。

❯ ng g service api
CREATE src/app/api.service.spec.ts (342 bytes)
CREATE src/app/api.service.ts (132 bytes)

生成された api.service.ts に以下のコードを追加します。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; // 追加

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  constructor(private httpClient: HttpClient) { } // HttpClientをインジェクト
  fetch(){ // 追加
    return this.httpClient.get('http://localhost:3000/users');
  }
}

UserListコンポーネントからAPIを実行

最後に UserListComponent のコンストラクタで ApiService をインジェクトし、ngOnInitusers プロパティにAPIから取得したJSONを設定します。
User クラスを用いた実装は不要なので削除しました。

import { Component, OnInit } from '@angular/core';
import { ApiService } from '../api.service'; // 追加

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.scss']
})
export class UserListComponent implements OnInit {
  users = [] // 追加
  constructor(private apiService: ApiService) { } // サービスをインジェクト
  ngOnInit(): void {
    this.apiService.fetch().subscribe((data: any[])=>{ // 追加
      this.users = data;
    })
  }
}

これでAPIから取得したユーザ一覧を表示することができました。

f:id:hiroki-sawano:20200810173302p:plain

さいごに

ここまでの実装は以下のリポジトリに格納しておきました。

github.com

次回以降ではルーティングの設定や、マテリアルデザインの導入、Firebaseとの連携等を試していく予定です。