KubernetesをAWSで運用するときの前提条件を満たす

EKSが発表された今、AWSでのKubernetesを見直したい。

AWSで運用するときの前提条件

KubernetesをAWSで運用しようというとき、比較的忘れがちなのが以下の条件です。

m4よりm3というのは、EBSよりアクセスが安定していて高速なインスタンスストアが利用できるからですね。
インスタンス終了とともに消えてしまうインスタンスストアですが、コンテナは揮発性なので、それが乗っかるホストのディスクも揮発性でいいでしょという…1

また、Dockerのストレージドライバでdevicemapperを使用している際、loop-lvmモードは本番環境で推奨されていません。
http://docs.docker.jp/engine/userguide/storagedriver/device-mapper-driver.html

ループバックデバイスを使っていると、コンテナの展開は遅くなり、コンテナ内のファイル読み書き速度も劣化するはずですが、RHEL系のOSだと多くの場合、デフォルトの設定がdevicemapperloop-lvmです2

https://www.school.ctc-g.co.jp/columns/nakai/nakai68.html

そういうわけで、この記事では、

  • インスタンスストアのあるm3.largeでKubernetesクラスタを構築し、
  • インスタンスストアにDockerストレージを置きdevicemapperdirect-lvmモードを使う

という条件でKubernetesのクラスタを構築してみます。クラスタ構築にはkubeadmを使用します。

デフォルトのストレージドライバ

とはいえ、よく使われている構成ツールの場合はDockerストレージをループバックデバイスに作ることはないようです。

kubeadmや手作業でクラスタを構築する場合、dockerインストールは自分でやる必要があるので、RHEL系で普通にセットアップするとdocker infoの結果が以下のようになります。

# CentOSに普通にdockerをyumでインストールした場合

$ sudo docker info
Storage Driver: devicemapper
 Pool Name: docker-202:1-4210412-pool
 Pool Blocksize: 65.54 kB
 ...
 Deferred Deleted Device Count: 0
 Data loop file: /var/lib/docker/devicemapper/devicemapper/data
 WARNING: Usage of loopback devices is strongly discouraged for production use. Use `--storage-opt dm.thinpooldev` to specify a custom block storage device.

WARNINGで怒られてますね。これを正しくセットアップしようというのがこの記事の主旨になります。

一方、メジャーな構成ツールであるkopsでk8sクラスタを構築すると以下の通りです。

# kopsで構成した場合のDocker Storage

Server Version: 1.13.1
Storage Driver: overlay
 Backing Filesystem: xfs

kube-awsはContainer Linuxを使用していますが、以下の様にContainer LinuxはデフォルトでDockerストレージが正しく設定されているので大丈夫そうです。

# Container Linux

Server Version: 1.12.6
Storage Driver: overlay
 Backing Filesystem: extfs

kubesprayも、インストールプロセスでエラーが出たので実際の挙動は見てませんが、コード上だとdocker-storge-setupというDockerストレージのセットアップ実行コードがあったので大丈夫そうですね。

プロダクション運用を考えた際overlay2overlayを使用するのが本当は良さそうですが、overlay2にカーネルv4.0以降が必要なことやCentOSでdevicemapperが推奨されていること、Dockerの公式ページであまりoverlayを推奨してないことから、今回はdevicemapperdirect-lvmをセットアップします。

https://docs.docker.com/engine/userguide/storagedriver/overlayfs-driver/
https://docs.docker.com/engine/userguide/storagedriver/selectadriver/#docker-ce

(あと、別のディスクにDockerストレージをセットアップすることで、Dockerストレージの使用率が増えてきたときもOS、Kubernetesの動作に与える影響が少なく、安定することが期待できそう)

m3系のインスタンスを作成し、インスタンスストアをアタッチする

まず、AWSでインスタンスを作成して、インスタンスストアをアタッチします。
今回はmaster(&etcd)1台、worker1台で構成するので2つインスタンスを立ち上げます。
パブリックサブネットに、通常通りm3.largeのインスタンスを立ち上げますが、以下のようにインスタンスストアもアタッチしておきます。

スクリーンショット 2017-12-13 12.04.33.png

セキュリティグループは、面倒だったので今回はインスタンス間全通しで行きます。
スクリーンショット 2017-12-13 12.10.53.png

Dockerのセットアップ

kubeadmは、実行前にクラスタのノードにdockerkubeletをインストールしておく必要があります。

https://kubernetes.io/docs/setup/independent/install-kubeadm/

ここでdockerをインストールした際に、Dockerストレージのセットアップも実行しておきます。

$ ssh centos@k8s-master

$ sudo su -

$ yum install -y docker

awsのインスタンスストアは以下のようにマウント済みなので、そのままDockerストレージとしてセットアップしようとすると失敗します。
そのため、前もってアンマウントします。

$ lsblk

NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda    202:0    0   8G  0 disk 
└─xvda1 202:1    0   8G  0 part /
xvdb    202:16   0  30G  0 disk /mnt

$ umount /mnt
$ lsblk

NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda    202:0    0   8G  0 disk 
└─xvda1 202:1    0   8G  0 part /
xvdb    202:16   0  30G  0 disk 

インスタンスストアの/dev/xvdbデバイスをDockerストレージとしてセットアップします。

# 設定ファイルの作成
$ cat << EOT > /etc/sysconfig/docker-storage-setup
DEVS=/dev/xvdb
VG=docker-vg
WIPE_SIGNATURES=true
EOT

# 以下コマンドでDockerストレージをセットアップ
$ docker-storage-setup

INFO: Volume group backing root filesystem could not be determined
INFO: Wipe Signatures is set to true. Any signatures on /dev/xvdb will be wiped.
/dev/xvdb: 2 bytes were erased at offset 0x00000438 (ext3): 53 ef
INFO: Device node /dev/xvdb1 exists.
  Physical volume "/dev/xvdb1" successfully created.
  Volume group "docker-vg" successfully created
  Using default stripesize 64.00 KiB.
  Rounding up size to full physical extent 32.00 MiB
  Thin pool volume with chunk size 512.00 KiB can address at most 126.50 TiB of data.
  Logical volume "docker-pool" created.
  Logical volume docker-vg/docker-pool changed.

$ lsblk

NAME                              MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda                              202:0    0   8G  0 disk 
└─xvda1                           202:1    0   8G  0 part /
xvdb                              202:16   0  30G  0 disk 
└─xvdb1                           202:17   0  30G  0 part 
  ├─docker--vg-docker--pool_tmeta 253:0    0  32M  0 lvm  
  │ └─docker--vg-docker--pool     253:2    0  12G  0 lvm  
  └─docker--vg-docker--pool_tdata 253:1    0  12G  0 lvm  
    └─docker--vg-docker--pool     253:2    0  12G  0 lvm  

Dockerストレージがセットアップできました。dockerサービスを起動します。

$ systemctl enable docker && systemctl start docker
$ docker info

Storage Driver: devicemapper
 Pool Name: docker--vg-docker--pool
 Pool Blocksize: 524.3 kB
 Base Device Size: 10.74 GB
 Backing Filesystem: xfs
 Data file: 
 Metadata file: 
 Data Space Used: 19.92 MB
 Data Space Total: 12.81 GB
 Data Space Available: 12.79 GB
 Metadata Space Used: 45.06 kB
 Metadata Space Total: 33.55 MB
 Metadata Space Available: 33.51 MB
 Thin Pool Minimum Free Space: 1.281 GB
 Udev Sync Supported: true
 Deferred Removal Enabled: true
 Deferred Deletion Enabled: true
 Deferred Deleted Device Count: 0
 Library Version: 1.02.140-RHEL7 (2017-05-03)

このように、ループバックデバイスに関するWARNINGが消えていることが確認できました。

同様の処理をworkerに対しても実施します。

kubeadmによるクラスタの初期化

https://kubernetes.io/docs/setup/independent/install-kubeadm/

ドキュメントを参考に、kubeadmでクラスタを初期化します。

kubeadmはまだβ段階で、プロダクション運用にはまだ早いですが、簡単にクラスタを構築できるのでここではkubeadmでクラスタを構築してみます。

kubernetesのmasterを初期化

kubeadm initコマンドでmasterを初期化します。
なお、kubeadm initコマンドが成功した際に表示される、

kubeadm join --token xxxx

というコマンドは後でWorkerを追加する際に必要になるので控えておきます。

# yumリポジトリの追加
$ cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF

# k8s関連パッケージの追加
yum install -y kubelet kubeadm kubectl

# kubeletの開始
systemctl enable kubelet && systemctl start kubelet

# kubeadmのセットアップはselinuxを無効化する必要があるようです
$ setenforce 0

# kubeadmでmasterを初期化
$ kubeadm init --pod-network-cidr=10.244.0.0/16

# 以下コマンドが表示されるので控えておく(Worker追加の際に使用する)
# kubeadm join --token xxx

# kubectlをAPIサーバーへアクセスできるよう設定
$ mkdir -p $HOME/.kube
$ cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ chown $(id -u):$(id -g) $HOME/.kube/config

# podネットワーク用のadd-onを追加(今回はflannelを使用)
$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/v0.9.1/Documentation/kube-flannel.yml

# masterがReadyになるのを待つ
watch kubectl get nodes

kubernetesのworkerを追加

worker用に作成したec2インスタンスにsshログインし、上記で初期化したクラスタにJOINさせます。

$ ssh centos@k8s-worker
$ sudo su -

# yumリポジトリの追加
$ cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF

# kubeadmのセットアップはselinuxを無効化する必要があるようです
$ setenforce 0

# k8s関連パッケージの追加
yum install -y kubelet kubeadm kubectl

# kubeletの開始
systemctl enable kubelet && systemctl start kubelet

# kubeadmでWorkerを追加する。
# kubeadm initコマンドの完了時に表示されたコマンドをworkerインスタンスで実行する
$ kubeadm join --token xxxx 10.0.157.43:6443 --discovery-token-ca-cert-hash xxx

kubeadm join --token 2f80ec.3db07de32fde6f62 10.0.157.43:6443 --discovery-token-ca-cert-hash sha256:5db1f5a2fb1694bd9ad986a8367351632499d9668f30373797d1b74d85456b40
[kubeadm] WARNING: kubeadm is in beta, please do not use it for production clusters.
[preflight] Running pre-flight checks
[preflight] WARNING: kubelet service is not enabled, please run 'systemctl enable kubelet.service'
[preflight] Starting the kubelet service
[discovery] Trying to connect to API Server "10.0.157.43:6443"
[discovery] Created cluster-info discovery client, requesting info from "https://10.0.157.43:6443"
[discovery] Requesting info from "https://10.0.157.43:6443" again to validate TLS against the pinned public key
[discovery] Cluster info signature and contents are valid and TLS certificate validates against pinned roots, will use API Server "10.0.157.43:6443"
[discovery] Successfully established connection with API Server "10.0.157.43:6443"
[bootstrap] Detected server version: v1.8.5
[bootstrap] The server supports the Certificates API (certificates.k8s.io/v1beta1)

Node join complete:
* Certificate signing request sent to master and response
  received.
* Kubelet informed of new secure connection details.

Run 'kubectl get nodes' on the master to see this machine join.

master側で、workerがjoinしたことを確認します。

$ ssh centos@k8s-master
$ sudo su -

$ watch kubectl get nodes

NAME                                  STATUS    ROLES     AGE   VERSION
ip-x.ap-northeast-1.compute.internal  Ready     <none>    45m   v1.8.5
ip-x.ap-northeast-1.compute.internal  Ready     master    1h    v1.8.5

nginxでも走らせてみますか。

# masterで
$ kubectl run my-nginx --image=nginx --replicas=2 --port=80
$ kubectl expose deployment my-nginx --type=NodePort
$ kubectl describe services my-nginx

Name:                     my-nginx
Namespace:                default
Labels:                   run=my-nginx
Annotations:              <none>
Selector:                 run=my-nginx
Type:                     NodePort
IP:                       10.99.129.58
Port:                     <unset>  80/TCP
TargetPort:               80/TCP
NodePort:                 <unset>  32017/TCP
Endpoints:                10.244.1.2:80,10.244.1.3:80
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

# NodePortでアクセスして表示されることを確認
$ curl http://$(curl -s http://169.254.169.254/latest/meta-data/local-ipv4):32017

というわけで、Dockerストレージをdirect-lvmモードで動かしてkubernetesクラスタを動かすことができました。

まとめ

Kubernetesを使用していて、コンテナのデプロイが遅いとか、コンテナ内ファイルアクセスが遅いとか、そういった問題を感じるようでしたら一度Dockerストレージの設定を見直してみてはいかがでしょうか。


  1. ただ、以前は上記のようなことがkubernetesのドキュメントに書いてましたが今は消えてるので、インスタンスストアにこだわる必要性はあまりないのかもしれません。既にm5インスタンスが利用可能になっている時代ですし… 

  2. Docker v1.13.1からデフォルトのストレージドライバがoverlayになっているので、最新のDockerをインストールしている場合はoverlayドライバを使用していると思います。普通にyum installでDockerを入れた場合1.12.6がインストールされました。 

続きを読む

はじめてのServerless ✕ Webpack ✕ TypeScript


このエントリーはaratana Advent Calendar 201712日目のエントリーです。

こんばんは!最近Google Home MiniAmazon echo dotを購入したはいいが置き場所に困っている蔭山です。
みなさんはどのような場所に置かれているのでしょうか。。。

前日は新卒エンジニアには決して見えない安定感をお持ちの猿渡くんの「NoSQLについて何か。」という記事でした!
NoSQL?あぁ、聞いたことはある。
みたいな僕でもわかりやすい記事でした!
最近AWSに興味が出始めたところでしたので、ぜひDynamoDBを使って軽い画像投稿サービスでも作ってみます!

さて今回はServerless ✕ Webpack ✕ TypeScriptの組み合わせで使えるように手順をまとめてみたいと思います!

動作環境

今回の動作環境は以下になります!

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G1114

$ node -v
v8.2.1

$ serverless -v
1.24.0

$ webpack -v
3.5.6

環境準備

nodeやserverlessのインストールについては下記記事を参考に。。。

とりあえずやってみる

まずはServerlessのプロジェクトを作ってみましょう

$ serverless create -t aws-nodejs -p hogehoge
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/fugafuga/hogehoge"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.24.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"
$ cd ./hogehoge

JSer御用達npmを使って必要なパッケージをダウンロードしましょう。

$ npm init
$ npm install --save-dev serverless-webpack serverless-offline ts-loader typescript webpack

インストールが終わり次第、各種設定を行います。
今回はTypescript -> ES2015へのコンパイルを目的に設定させていただきます。
細かい設定内容に関しては割愛します。

./serverless.yml
service: hogehoge

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1

plugins:
- serverless-webpack
- serverless-offline

functions:
  hello:
    handler: handler.hello
    events:
     - http:
         path: hello
         method: get
./webpack.config.js
module.exports = {
  entry: './handler.ts',
  target: 'node',
  module: {
    loaders: [{
      test: /\.ts$/,
      loader: 'ts-loader'
    }]
  },
  resolve: {
    extensions: ['.ts']
  },
  output: {
    libraryTarget: 'commonjs',
    path: __dirname + '/.webpack',
    filename: 'handler.js'
  }
};
./tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs"
  },
  "exclude": [
    "node_modules"
  ]
}

準備はできたので、次はTypeScriptでコーディングしてみましょう!

./handler.ts
export * from './src/ts/functions/hello';
./src/ts/functions/hello.ts
export function hello(event, context, callback): void {
  const response = {
    statusCode: 200,

    headers: {
    },

    body: JSON.stringify({
      "message": "Hello Serverless x Webpack x TypeScript!!"
    })
  };

  callback(null, response);
};

コードが書けたらローカル環境で動作確認

$ sls offline
・・・・・・・・・・・・・・・・
途中は割愛。m(__)m
・・・・・・・・・・・・・・・・
Serverless: Routes for hello:
Serverless: GET /hello

Serverless: Offline listening on http://localhost:3000

きちんと動作するか確認。

$ curl -X GET http://localhost:3000/hello
{"message":"Hello Serverless x Webpack x TypeScript!!"}

動作が問題なければ、早速デプロイしてみましょう!

$ sls deploy
・・・・・・・・・・・・・・・・
途中は割愛。m(__)m
・・・・・・・・・・・・・・・・
api keys:
  None
endpoints:
  GET - https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/dev/hello
functions:
  hello: hogehoge-dev-hello

デプロイが完了したようです。
では早速動作確認。

$ curl -X GET https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/dev/hello
{"message":"Hello Serverless x Webpack x TypeScript!!"}

ちゃんと動きましたね!

最後に

無理やりTypeScriptを使った感が凄まじいですね。。。申し訳ありません><
僕個人がTypeScriptを使ったことがなかったため使ってみたかったんです

明日は新卒田村くんの「Ctagsで自由な翼を得たVimについて」です!
お楽しみに!

参考

主にこちらの記事を参考にさせて頂きました!ありがとうございますm(__)m

続きを読む

ALB Ingress Controller を使う

この記事では、 ALB Ingress Controller について書きます。

zalando-incubator/kube-ingress-aws-controller については、 Kubernetes2 Advent Calendar 2017 7日目 @mumoshu 先生の記事で、 書かれていますので、そちらを参照して下さい :bow:

WHY

Kubernetes on AWS で運用している場合、 Kubernetes の Service を作成すると、 AWS の Classic Load Balancer が作成されます。 Classic Load Balancer 以外に Application Load Balancer を利用したい場合が以下のような時にあります。

  • http2 を利用したい
  • /blog などリソース毎に向き先を区切る

Kubernetes on AWS を利用する方は既に AWS を使いだおしている方が大半だと思います。既存のアプリケーションを Kubernetes へ移行しようとした際に、 既に ALB(Application Load Balancer) を利用していたのが、 Kubernetes へ移行したら ELB (Classic Load Balancer) になって http2 無くなりましたというのはパフォーマンスにも影響を与えます。

そこで ALB Ingress Controller を利用することで、 ALB が使えます。

ALB Ingress Controller

The ALB Ingress Controller satisfies Kubernetes ingress resources by provisioning Application Load Balancers.

ALB Ingress Controller は、 Kubernetes の ingress を作成したタイミングで、 Application Load Balancer を作成します。

Design

image.png

The following diagram details the AWS components this controller creates. It also demonstrates the route ingress traffic takes from the ALB to the Kubernetes cluster.

Design に ALB が作られるまでの流れと、トラフィックの流れが書かれています。

Ingress Creation

Kubernetes 上に ingress を一つ作った時の流れ

[1]: The controller watches for ingress events from the API server. When it finds ingress resources that satisfy its requirements, it begins the creation of AWS resources.

[1] ALB Ingress Controller は、 Kubernetes の API Server からの Event を監視し、該当の Event を検知したら AWS のリソースを作成し始める。

[2]: An ALB (ELBv2) is created in AWS for the new ingress resource. This ALB can be internet-facing or internal. You can also specify the subnets its created in using annotations.

[2] ALB を作成する。 annotation を指定することで、サブネットやインターネット向けか内部向けかも決めることができる。

[3]: Target Groups are created in AWS for each unique Kubernetes service described in the ingress resource.

[3] ALB の向き先となるターゲットグループは、 ingress に記述された Service ごとに AWS で作成。

[4]: Listeners are created for every port detailed in your ingress resource annotations. When no port is specified, sensible defaults (80 or 443) are used. Certificates may also be attached via annotations.

[4] リスナは、 ingress の annotation で指定したポート用に作成されます。ポートが指定されていない場合、80または443を使用。 ACM も使用することもできる。

[5]: Rules are created for each path specified in your ingress resource. This ensures traffic to a specific path is routed to the correct Kubernetes Service.

[5] 入力リソースで指定された各パスに対してルールが作成され、特定のパスへのトラフィックが正しい Kubernetes の Service にルーティングされる。

Ingress Traffic

This section details how traffic reaches the cluster.

As seen above, the ingress traffic for controller-managed resources starts at the ALB and reaches the Kubernetes nodes through each service’s NodePort. This means that services referenced from ingress resource must be exposed on a node port in order to be reached by the ALB.

ALB から始まり、各サービスの NodePort を通じて Kubernetes ノードに到達するようになっている。 ALB を使ったサービスを公開するためには、 ingress と NodePort を使った Service の二つが必要になる。

How it Works

  • alb-ingress-controller 用の IAM を作成
  • ALB 作る際に、 sg と subnet を自動でアサインされるように、 subnet にタグの設定
  • AWS の IAM 情報と CLUSTER_NAME を secrets に入れる
  • default サーバーという一旦 target group アサインできるテンポラリのサービスを建てる
  • alb-ingress-controller を deploy する

alb-ingress-controller 用の IAM を作成

Role Permissions

AWS を操作するため、専用の IAM が必要になります。必要になるリソースは例と以下に記載されています。

IAM Policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "acm:DescribeCertificate",
                "acm:ListCertificates"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:CreateSecurityGroup",
                "ec2:CreateTags",
                "ec2:DeleteSecurityGroup",
                "ec2:DescribeInstances",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeSubnets",
                "ec2:DescribeTags",
                "ec2:ModifyInstanceAttribute",
                "ec2:RevokeSecurityGroupIngress"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:AddTags",
                "elasticloadbalancing:CreateListener",
                "elasticloadbalancing:CreateLoadBalancer",
                "elasticloadbalancing:CreateRule",
                "elasticloadbalancing:CreateTargetGroup",
                "elasticloadbalancing:DeleteListener",
                "elasticloadbalancing:DeleteLoadBalancer",
                "elasticloadbalancing:DeleteRule",
                "elasticloadbalancing:DeleteTargetGroup",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:DescribeLoadBalancers",
                "elasticloadbalancing:DescribeRules",
                "elasticloadbalancing:DescribeTags",
                "elasticloadbalancing:DescribeTargetGroups",
                "elasticloadbalancing:DescribeTargetHealth",
                "elasticloadbalancing:ModifyListener",
                "elasticloadbalancing:ModifyLoadBalancerAttributes",
                "elasticloadbalancing:ModifyRule",
                "elasticloadbalancing:ModifyTargetGroup",
                "elasticloadbalancing:RegisterTargets",
                "elasticloadbalancing:RemoveTags",
                "elasticloadbalancing:SetSecurityGroups",
                "elasticloadbalancing:SetSubnets"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:GetServerCertificate",
                "iam:ListServerCertificates"
            ],
            "Resource": "*"
        }
    ]
}

ALB 作る際に、 sg と subnet を自動でアサインされるように、 subnet にタグの設定

Subnet Selection

ingress の annotation か auto-detection で、 各ALBを作成するサブネットを決定。

  • annotation: alb.ingress.kubernetes.io/subnets に、 subnet ID または NAME タグを使用して指定
  • auto-detection: annotation の指定はなく、自動検出で ALB を作成

auto-detection を有効にするためには、以下の tag を追加します。 ALB を作る際に subnet が二つ必要なため、二つ tag をつける。

  • kubernetes.io/role/alb-ingress=
  • kubernetes.io/cluster/$CLUSTER_NAME=shared

    • $CLUSTER_NAMEalb-ingress-controller.yamlCLUSTER_NAME 環境変数と一致させる必要がある

設定例

image

AWS の IAM 情報と CLUSTER_NAME を Secrets に入れる

namespace name key description
kube-system alb-ingress-controller AWS_ACCESS_KEY_ID credentials of IAM user for alb-ingress-controller
kube-system alb-ingress-controller AWS_SECRET_ACCESS_KEY credentials of IAM user for alb-ingress-controller
kube-system alb-ingress-controller CLUSTER_NAME cluster name
  • 登録方法

k8sec を使って Sercrets に登録します。

$ k8sec set alb-ingress-controller KEY=VALUE -n kube-system
  • 確認
$ k8sec list alb-ingress-controller -n kube-system
NAME            TYPE    KEY         VALUE
alb-ingress-controller  Opaque  AWS_ACCESS_KEY_ID   "hoge"
alb-ingress-controller  Opaque  AWS_SECRET_ACCESS_KEY   "fuga"
alb-ingress-controller  Opaque  CLUSTER_NAME        "Ooops"

ingress に必要になる Default backend サービスを建てる

kubectl Deployments

alb-ingress-controller を使うために必要になる Default backend サービスを建てる。 alb-ingress-controller を利用する ingress は、全て Default backend を指す。

$ kubectl create -f https://raw.githubusercontent.com/coreos/alb-ingress-controller/master/examples/default-backend.yaml

alb-ingress-controller を deploy する

  • alb-ingress-controller manifest ファイルをダウンロードする
$ wget https://raw.githubusercontent.com/coreos/alb-ingress-controller/master/examples/alb-ingress-controller.yaml
  • Secrets に追加したものを manifest file に反映する
        envFrom:
        - secretRef:
            name: alb-ingress-controller
  • AWS_REGION を設定する
- name: AWS_REGION
  value: ap-northeast-1
  • Deploy alb-ingress-controller
$ kubectl apply -f alb-ingress-controller.yaml  
  • log で起動できているか確認できる。
$ kubectl logs -n kube-system 
    $(kubectl get po -n kube-system | 
    egrep -o alb-ingress[a-zA-Z0-9-]+) | 
    egrep -o '[ALB-INGRESS.*$'
[ALB-INGRESS] [controller] [INFO]: Log level read as "", defaulting to INFO. To change, set LOG_LEVEL environment variable to WARN, ERROR, or DEBUG.
[ALB-INGRESS] [controller] [INFO]: Ingress class set to alb
[ALB-INGRESS] [ingresses] [INFO]: Build up list of existing ingresses
[ALB-INGRESS] [ingresses] [INFO]: Assembled 0 ingresses from existing AWS resources

上手く動かない場合ははここを true にすると良い。 AWS の制限で止められている可能性もありえる。

        - name: AWS_DEBUG
          value: "false"

これで ALB Ingress Controller の準備は完了

実際に ALB 作成してみる

alb-ingress-controller にある echo server を元にやってみる。基本的に以下、二点を抑えるだけで ALB
を利用できる。

  • ingress と NodePort を使った Service
  • ingress の annotation の設定

echoservice

alb-ingress-controller にある sample を元に echoserver を建ててみる。

$ kubectl apply -f https://raw.githubusercontent.com/coreos/alb-ingress-controller/master/examples/echoservice/echoserver-namespace.yaml &&
kubectl apply -f https://raw.githubusercontent.com/coreos/alb-ingress-controller/master/examples/echoservice/echoserver-service.yaml &&
kubectl apply -f https://raw.githubusercontent.com/coreos/alb-ingress-controller/master/examples/echoservice/echoserver-deployment.yaml

Namespace を切って、 NodePort で開放する Service と Deployment が作られる。

$ kubectl get all -n echoserver
NAME                             READY     STATUS    RESTARTS   AGE
po/echoserver-2241665424-xm1rt   1/1       Running   0          10m

NAME             TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
svc/echoserver   NodePort   100.65.13.23   <none>        80:31509/TCP   10m

NAME                DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/echoserver   1         1         1            1           10m

NAME                       DESIRED   CURRENT   READY     AGE
rs/echoserver-2241665424   1         1         1         10m
  • ingress file を取得する
wget https://raw.githubusercontent.com/coreos/alb-ingress-controller/master/examples/echoservice/echoserver-ingress.yaml
  • annotation の設定(オプション)

Annotations に全部使える ALB の option が書いてある。

alb.ingress.kubernetes.io/scheme: internet-facing # or 'internal'
alb.ingress.kubernetes.io/connection-idle-timeout: # Defauflt 60
alb.ingress.kubernetes.io/subnets: # subnet ID か Name 
alb.ingress.kubernetes.io/security-groups: # sg ID か Name Default 0.0.0.0/0 
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80,"HTTPS": 443}]' # Default 80
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:hoge:certificate/UUID # ACM 利用する場合
alb.ingress.kubernetes.io/healthcheck-path: # Default "/"
alb.ingress.kubernetes.io/healthcheck-port: # Default Traffic port
alb.ingress.kubernetes.io/healthcheck-interval-seconds: # Default 15
alb.ingress.kubernetes.io/healthcheck-protocol: # Default HTTP
alb.ingress.kubernetes.io/healthcheck-timeout-seconds: # Default 5
alb.ingress.kubernetes.io/healthy-threshold-count: # Default 2
alb.ingress.kubernetes.io/unhealthy-threshold-count: # Default 2
alb.ingress.kubernetes.io/successCodes: # Default 200
alb.ingress.kubernetes.io/tags:  # Tag を入れる
  • ingress を建てる
$ kubectl apply -f echoserver-ingress.yaml

とりあえず default のままでいい場合は下記のコマンド

$ kubectl apply -f https://raw.githubusercontent.com/coreos/alb-ingress-controller/master/examples/echoservice/echoserver-ingress.yaml
  • ALB が作成された log を確認して見る
$ kubectl logs -n kube-system 
  $(kubectl get po -n kube-system | 
  egrep -o alb-ingress[a-zA-Z0-9-]+) | 
  egrep -o '[ALB-INGRESS.*$' | 
  grep 'echoserver/echoserver'
[ALB-INGRESS] [echoserver/echoserver] [INFO]: Start ELBV2 (ALB) creation.
[ALB-INGRESS] [echoserver/echoserver] [INFO]: Completed ELBV2 (ALB) creation. Name: hogefuga-echoserver-ech-2ad7 | ARN: arn:aws:elasticloadbalancing:ap-northeast-1:0000:loadbalancer/app/hogefuga-echoserver-ech-2ad7/17fd1481cb40fcc2
[ALB-INGRESS] [echoserver/echoserver] [INFO]: Start TargetGroup creation.
[ALB-INGRESS] [echoserver/echoserver] [INFO]: Succeeded TargetGroup creation. ARN: arn:aws:elasticloadbalancing:ap-northeast-1:0000:targetgroup/hogefuga-31509-HTTP-c3a0606/9914a217042c4006 | Name: hogefuga-31509-HTTP-c3a0606.
[ALB-INGRESS] [echoserver/echoserver] [INFO]: Start Listener creation.
[ALB-INGRESS] [echoserver/echoserver] [INFO]: Completed Listener creation. ARN: arn:aws:elasticloadbalancing:ap-northeast-1:0000:listener/app/hogefuga-echoserver-ech-2ad7/17fd1481cb40fcc2/0fe42e9436e45013 | Port: 80 | Proto: HTTP.
[ALB-INGRESS] [echoserver/echoserver] [INFO]: Start Rule creation.
[ALB-INGRESS] [echoserver/echoserver] [INFO]: Completed Rule creation. Rule Priority: "1" | Condition: [{    Field: "host-header",    Values: ["echoserver.example.com"]  },{    Field: "path-pattern",    Values: ["/"]  }]
[ALB-INGRESS] [echoserver/echoserver] [INFO]: Fetching Targets for Target Group arn:aws:elasticloadbalancing:ap-northeast-1:0000:targetgroup/hogefuga-31509-HTTP-c3a0606/9914a217042c4006
[ALB-INGRESS] [echoserver/echoserver] [INFO]: Fetching Rules for Listener arn:aws:elasticloadbalancing:ap-northeast-1:0000:listener/app/hogefuga-echoserver-ech-2ad7/17fd1481cb40fcc2/0fe42e9436e45013
[ALB-INGRESS] [echoserver/echoserver] [INFO]: Ingress rebuilt from existing ALB in AWS
  • URL を確認
$ kubectl describe ing -n echoserver echoserver
Name:             echoserver
Namespace:        echoserver
Address:          hogefuga-echoserver-ech-2ad7-126540505.ap-northeast-1.elb.amazonaws.com
Default backend:  default-http-backend:80 (100.96.27.7:8080)
Rules:
  Host                    Path  Backends
  ----                    ----  --------
  echoserver.example.com  
                          /   echoserver:80 (<none>)
Annotations:
Events:
  Type    Reason  Age   From                Message
  ----    ------  ----  ----                -------
  Normal  CREATE  2m    ingress-controller  Ingress echoserver/echoserver
  Normal  CREATE  2m    ingress-controller  hogefuga-echoserver-ech-2ad7 created
  Normal  CREATE  2m    ingress-controller  hogefuga-31509-HTTP-c3a0606 target group created
  Normal  CREATE  2m    ingress-controller  80 listener created
  Normal  CREATE  2m    ingress-controller  1 rule created
  Normal  UPDATE  2m    ingress-controller  Ingress echoserver/echoserver

ここからさらに踏み込んで external DNS の設定がありますが今回は、ALB までで閉じます。
最後に cURL で確認して終了です。

$ curl hogefuga-echoserver-ech-2ad7-126540505.ap-northeast-1.elb.amazonaws.com
CLIENT VALUES:
client_address=10.1.93.88
command=GET
real path=/
query=nil
request_version=1.1
request_uri=http://hogefuga-echoserver-ech-2ad7-126540505.ap-northeast-1.elb.amazonaws.com:8080/

SERVER VALUES:
server_version=nginx: 1.10.0 - lua: 10001

HEADERS RECEIVED:
accept=*/*
host=hogefuga-echoserver-ech-2ad7-126540505.ap-northeast-1.elb.amazonaws.com
user-agent=curl/7.43.0
x-amzn-trace-id=Root=1-5a2d4e2f-5545b75b74003cd80e5134bb
x-forwarded-for=192.168.100.10
x-forwarded-port=80
x-forwarded-proto=http
BODY:
-no body in request-
  • 最後は、削除
$ kubectl delete ns echoserver
namespace "echoserver" deleted

ALB も削除される。

$ curl hogefuga-echoserver-ech-2ad7-126540505.ap-northeast-1.elb.amazonaws.com
curl: (6) Could not resolve host: hogefuga-echoserver-ech-2ad7-126540505.ap-northeast-1.elb.amazonaws.com

続きを読む

Route53 で S3 バケットへ alias レコードを作った際のリダイレクトのふるまい

 Amazon S3 は、全てのリクエストをリダイレクトや、パスに応じたリダイレクトを設定できる機能を持っています。これは Static Web Hosting という設定を入れることにより実現可能です。実際の技術詳細については以下の公式ドキュメントを参照してください。

リダイレクト設定

gochiusa.53ningen.com に対して gochiusa.com へのリダイレクト設定作業は以下の 3 STEP です

  1. S3 バケットを gochiusa.53ningen.com という名前で作成する
  2. Static Web Hosting 機能を有効にして、すべてのリクエストを gochiusa.com にリダイレクトする設定を入れる
  3. Route 53 に gochiusa.53ningen.com に対して先ほど作ったバケットへのエイリアスを張る A レコードを作成する

動作確認

作業後、設定を dig で確認します

% dig gochiusa.53ningen.com @ns-771.awsdns-32.net.

; <<>> DiG 9.8.3-P1 <<>> gochiusa.53ningen.com @ns-771.awsdns-32.net.
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 23376
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;gochiusa.53ningen.com.     IN  A

;; ANSWER SECTION:
gochiusa.53ningen.com.  5   IN  A   52.219.68.26

;; AUTHORITY SECTION:
53ningen.com.       172800  IN  NS  ns-1431.awsdns-50.org.
53ningen.com.       172800  IN  NS  ns-2010.awsdns-59.co.uk.
53ningen.com.       172800  IN  NS  ns-393.awsdns-49.com.
53ningen.com.       172800  IN  NS  ns-771.awsdns-32.net.

;; Query time: 59 msec
;; SERVER: 205.251.195.3#53(205.251.195.3)
;; WHEN: Sat Dec  2 02:45:43 2017
;; MSG SIZE  rcvd: 192

解決される 52.219.68.26 は S3 の静的配信サーバーの IP の模様で、振る舞い的には Host ヘッダと同じ名前を持つ S3 バケットへリダイレクトをかけるという形のようです。それを確認するために次のようなリクエストを送ってみます。

% curl -I 52.219.68.26
HTTP/1.1 301 Moved Permanently
x-amz-error-code: WebsiteRedirect
x-amz-error-message: Request does not contain a bucket name.
x-amz-request-id: C6B9BBFE84DAC0E0
x-amz-id-2: L0Hv8iRFv2kIRyLUUlhre6cqJpGKfdPo+LYF4EfjgcIaA3W4L+TOzVhs+U+H5W/J3NRp4Jfnn2A=
Location: https://aws.amazon.com/s3/
Transfer-Encoding: chunked
Date: Fri, 01 Dec 2017 17:50:44 GMT
Server: AmazonS3

x-amz-error-message: Request does not contain a bucket name. というメッセージからわかるようにバケットネームの指定がないというエラーで AWS S3 のトップページに飛ばされます。次に Host ヘッダをつけるとどうなるか試してみましょう。

% curl -I -H "Host: gochiusa.com" 52.219.68.26
HTTP/1.1 404 Not Found
x-amz-error-code: NoSuchBucket
x-amz-error-message: The specified bucket does not exist
x-amz-error-detail-BucketName: gochiusa.com
x-amz-request-id: DF0A27572D2A69C2
x-amz-id-2: 6PAUKw9BxMEQjZ7ELLQELCOfXdCAMYpQEuH9fN8pdvK3HVCHwUTU3DIZTAaC+vJTevpEsi+jo4I=
Transfer-Encoding: chunked
Date: Fri, 01 Dec 2017 17:53:07 GMT
Server: AmazonS3

The specified bucket does not exist というエラーメッセージに変わりました。ではそろそろ真面目に先ほど作った gochiusa.53ningen.com という値を Host ヘッダにつけて叩いてみましょう。

% curl -I 52.219.68.26 -H "Host: gochiusa.53ningen.com"
HTTP/1.1 301 Moved Permanently
x-amz-id-2: JIFBN5bduooIXFt6ps6DKsGca1jEWKmojrNJqTx+7MPXEBeHUK3A/BMehFQlsjyYRvzgPcZ2U4w=
x-amz-request-id: 53093E7405B86943
Date: Fri, 01 Dec 2017 17:54:49 GMT
Location: http://gochiusa.com/
Content-Length: 0
Server: AmazonS3

正常に 301 で http://gochiusa.com/ にリダイレクトされていることが確認できました。

実験からわかること

  1. S3 バケットへの alias レコードセットの制約の理由が推測できる

    • Route53 で S3 static web hosting しているバケットに alias を張る際は、公式ドキュメントに記載されているように Record Set の Name と S3 バケット名が一致している必要がある
    • これは Host ヘッダをみて、対象の S3 静的配信コンテンツへのリダイレクトを行なっている仕組みから生まれる制約だろうということが、上記の実験から分かる
  2. S3 バケットへの alias を張っているドメインを指す CNAME を張ると正常に動作しない
    • 実際に gochiusa2.53ningen.com に対して gochiusa.53ningen.com を指すような CNAME レコードセットを作ると以下のような実験結果になる
% dig gochiusa2.53ningen.com @ns-771.awsdns-32.net.

; <<>> DiG 9.8.3-P1 <<>> gochiusa2.53ningen.com @ns-771.awsdns-32.net.
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2232
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 4, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;gochiusa2.53ningen.com.        IN  A

;; ANSWER SECTION:
gochiusa2.53ningen.com. 300 IN  CNAME   gochiusa.53ningen.com.
gochiusa.53ningen.com.  5   IN  A   52.219.0.26

;; AUTHORITY SECTION:
53ningen.com.       172800  IN  NS  ns-1431.awsdns-50.org.
53ningen.com.       172800  IN  NS  ns-2010.awsdns-59.co.uk.
53ningen.com.       172800  IN  NS  ns-393.awsdns-49.com.
53ningen.com.       172800  IN  NS  ns-771.awsdns-32.net.

;; Query time: 120 msec
;; SERVER: 2600:9000:5303:300::1#53(2600:9000:5303:300::1)
;; WHEN: Sat Dec  2 03:10:55 2017
;; MSG SIZE  rcvd: 216

% curl -I 52.219.68.26 -H "Host: gochiusa2.53ningen.com"
HTTP/1.1 404 Not Found
x-amz-error-code: NoSuchBucket
x-amz-error-message: The specified bucket does not exist
x-amz-error-detail-BucketName: gochiusa2.53ningen.com
x-amz-request-id: 294AEC84E239A2AC
x-amz-id-2: VAWMR0xq0Gw1cJOdipIgJBRsx6RMYaaP679GD/hKomU3yOFJ6m7/n62h9wraTQoirBYKofnO2WM=
Transfer-Encoding: chunked
Date: Fri, 01 Dec 2017 18:04:22 GMT
Server: AmazonS3

続きを読む

AWS Cloud9 のPHP/MySQL を 7.1/5.7 にしてみる

PHP Advent Calendar 2017 の9日目です。

Docker を絡めた内容にすると予告してましたが、がらっと変更してしまいました・・・
新しく選んだテーマは、「AWS Cloud9」です。

AWS Cloud9 とは

「AWS Cloud9」とは、今年の11月末から12月頭にかけて開催された「AWS re:Invent 2017」で発表された新しいサービスです。

Cloud9 自体は以前からサービスされていたもので、2016年7月に Amazon に買収されて、とうとう AWS に統合されたという流れです。

Cloud9 は、ブラウザ上で動作する IDE で、複数の開発言語に対応し、共同作業が可能という特徴があるサービスです。それが、AWSに統合されたということで、IAMベースのユーザ管理や、ネットワークの制御等もできるので、より細かい管理ができるという形になります。

セットアップしてみる

とりあえずは、AWSのアカウントが必要なので、もし持っていない場合は作成する必要があります。アカウントの作成が終われば、「AWS Cloud9」の環境構築となります。

「AWS Cloud9」は現在以下のリージョンのみで提供されています。

  • EU(アイルランド)
  • アジアパシフィック(シンガポール)
  • 米国東部(バージニア北部)
  • 米国東部(オハイオ)
  • 米国西部(オレゴン)

残念ながら、東京リージョンには来ていないので、今回は「米国東部(バージニア北部)」(us-east-1)で試してみます。

welcome 画面

Welcome to AWS Cloud9.png

まずは、「AWS Cloud9」のサービストップの「Create environment」をクリックします。

Step1 Name environment

step1.png

Step1として、環境名(Name)と説明文(Description)を入力して、Step2へ行きます。

Step2 Configure settings

step2.png

Step2では、 作業するための環境設定を行います。

Environment Type としては、以下の2つを選ぶことになります。

  • 新しい EC2 インスタンスをこの環境用に起動する
  • 既存のサーバーに SSH 接続して作業をする

今回は、新しいインスタンスを立てますが、既存サーバーへの接続での共同編集というのも面白そうですね。

Environment Type で新しいインスタンスを使うことを選択した場合は、EC2 のインスタンスタイプを選択します。

また、コストを抑えるための設定があります。デフォルトでは、IDEを閉じてから30分後にインスタンスが停止され、再度IDEを開くとインスタンスが再起動するというものです。

それ以外の設定として、使用する IAM role と ネットワークの設定が行なえます。特に設定しなければ、Cloud9しか制御できない IAM role で、新しい VPC ネットワーク が設定されます。

ちなみに、既存のサーバーに SSH 接続する方を選択すると以下の選択肢になります。

step2-ssh.png

Step3 Review

step3.png

確認画面です。内容に問題がなければ、「Create environment」ボタンを押して、環境作成を開始します。

この画面では、以下のような注意が表示されます。

step3-info.png

「Create environment」ボタンを押すと、環境作成中画面ということで次のような画面になります。環境は、だいたい 2 〜 3 分くらいで作成されました。

build-cloud9.png

「AWS Cloud9」 IDE 画面

cloud9.png

IDE の画面としては、オーソドックスな画面で、左側にソースツリー、右側の上部のメインとなる部分にソース等の表示がありますが、右側の下部がターミナルになっているというのが面白いですね。

ここで、起動したインスタンスの情報を見てみるとこんな感じでした。

uname.png

さらに、起動したインスタンスの PHP と MySQL のバージョンをみてみると・・・

default_php_mysql_version.png

PHP はともかく、MySQLが 5.5 というのがちょっとつらい。

というわけで、アップグレードするためのシェルを準備しました。以下のものをターミナルから実行するとPHPとMySQLがバージョンアップできます。

sh -c "$(curl -fsSL https://gist.githubusercontent.com/kunit/c2cc88d18d4ce9ad972bab2bdc3b6f3f/raw/27f538fe5d21d024f72a6dfbee7563dc7247ad46/aws-cloud9-php71-mysql57.sh)"

実行する sh の内容を貼っておくと以下のような感じです。

(2017/12/10 10:12 追記) 最初書いていたスクリプトは、わざわざ PHP 5.6を削除してましたが、 alternatives の機能を使えば、PHPの切り替えができたので、7.1をインストールして、 alternatives で切り替えるものにしました

https://gist.github.com/kunit/c2cc88d18d4ce9ad972bab2bdc3b6f3f

#!/bin/sh

sudo service mysqld stop
sudo yum -y erase mysql-config mysql55-server mysql55-libs mysql55
sudo yum -y install mysql57-server mysql57
sudo service mysqld start

sudo yum -y install php71 php71-cli php71-common php71-devel php71-mysqlnd php71-pdo php71-xml php71-gd php71-intl php71-mbstring php71-mcrypt php71-opcache php71-pecl-apcu php71-pecl-imagick php71-pecl-memcached php71-pecl-redis php71-pecl-xdebug
sudo alternatives --set php /usr/bin/php-7.1 
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/bin/composer

この sh を実行すると、以下のように、PHP 7.1.11 および MySQL 5.7.20 の環境になります。

upgrade_php_mysql_version.png

では、実際のコードを編集および動作させてみよう

環境を作っただけで満足してしまいそうですが、実際のコードを動かしてみたいと思います。

サンプルとして使用させていただいたのは、CakePHP Advent Calendar 2017 2日目の @tenkoma さんの記事、CakePHP 3 のチュートリアルにユニットテストを追加する (1) のコードです。

AWS Cloud9 のターミナルから、以下のコマンドを実行し、ソースコードの取得及び compose install を行います。(us-east-1 で起動しているので、composer install もさくっと終わります)

git clone https://github.com/tenkoma/cakephp_cms.git
cd cakephp_cms
composer install

そして、サンプルを動かすために、MySQLにテスト用のデータベースを作らないと行けないので、以下のコマンドを実行します。

mysql -u root -e 'CREATE DATABASE test_cake_cms CHARACTER SET utf8mb4;GRANT ALL  ON test_cake_cms.* TO cakephp@localhost IDENTIFIED BY "AngelF00dC4k3~";FLUSH PRIVILEGES;'

あとは、Cloud9 の IDE 上から、 cakephp_cms/config/app.php のテストデータベースの設定部分を編集して、ターミナル上から phpunit を実行すると、次のようになります。

edit_app_and_phpunit.png

ターミナル部分を拡大するとこんな感じです。

phpunit.png

キーバインド

IDEの設定項目がありますが、最初に変更したのがこちら。

keybinding.png

プルダウンの種類的には、以下の4つでした。

  • default
  • Vim
  • Emacs
  • Sublime

ダッシュボード

IDEを開いたタブを閉じても、ダッシュボードに行けば再度 IDE を開き直せます。

dashboard.png

何もせずに、環境構築時に設定した時間が経過したらインスタンスは自動的に停止され、次にIDEを開いたときに再起動されます。

AWS Cloud9 の料金

AWS Cloud9 自体は無料で、使用する EC2 インスタンスに対する課金のみとなります。そういった意味で、IDEを閉じたら、30分後にEC2を自動停止してくれるというのは結構ありがたいですね。

最後に

AWS Cloud9 ですが、ちょっとした共同作業の環境として使えるだけではなく、使いようによってはおもしろい使い方ができるかもと思っています。

普段 PhpStorm という超強力な IDE を使っているので、それとくらべて IDE としての使い勝手はどうなんだということも今後いろいろと試してみたいなと思ったりもしています。

AWS アカウントさえあれば、本当に簡単に起動できるので、みなさんも試してみるのはいかがでしょうか?

明日の担当は、@hanhan1978 さんです。

続きを読む

AWS Lambda で Angular アプリを Server Side Rendering してみる

AWS Lambda Advent Calendar 2017 の8日目です。

前書き

Server Side Rendering (SSR) は、いわゆる SPA (Single Page Application) において、ブラウザー上(クライアントサイド)で動的に生成される DOM と同等の内容を持つ HTML をサーバーサイドで出力するための仕組みです。

React、Vue 等のモダンなフロントエンドフレームワークに軒並み搭載されつつあるこの機能ですが、 Angular にもバージョン2以降 Universal という SSR の仕組みがあります。

この仕組みを Lambda の上で動かして、 API Gateway 経由で見れるようにしてみる記事です。

SSR すると何がうれしいの?

以下のようなメリットがあるとされています

  • Web クローラーへの対応(SEO)

    • 検索エンジンのクローラーは HTML の内容を解釈してその内容をインデックスしますが、クライアントサイドで動的に変更された状態までは再現できません。そのため SSR に対応していない SPA では、コンテンツをクローラーに読み込ませるのが困難です。検索エンジンではないですが、 OGPTwitter Card もクローラーで読み込んだ情報を元に表示していますね
  • モバイル、低スペックデバイスでのパフォーマンス改善
    • いくつかのデバイスは JavaScript の実行パフォーマンスが非常に低かったり、そもそも実行できなかったりします。 SSR された HTML があれば、そのようなデバイスでもコンテンツを全く見られないという事態を避けられます
  • 最初のページを素早く表示する

どうやって Lambda で SSR するか

Angular Universal は各 Web サーバーフレームワーク用のミドルウェアとして実装されており、現時点では Express, Hapi, ASP.NET 用のエンジンがリリースされています。

他方 Lambda には AWS が開発した、既存の Express アプリを Lambda 上で動かすための aws-serverless-express があります。

今回は Angular Express Engineaws-serverless-express を組み合わせて Lambda 上で Angular アプリを SSR してみます。

やってみる

公式の Universal チュートリアルをなぞりつつ、 Lambda 対応に必要な部分をフォローしていきます。

Router を使っていないと面白くないので、公式の Angular チュートリアルである Tour of Heroes の完成段階 をいじって SSR 対応にしてみましょう。

チュートリアルのコードをまず動かす

コードを DL して展開、 yarn で依存物をインストールします。

ついでに git init して、 .gitignore も追加しておきましょう。

curl -LO https://angular.io/generated/zips/toh-pt6/toh-pt6.zip
unzip toh-pt6 -d toh-pt6-ssr-lambda
cd toh-pt6-ssr-lambda
yarn

ng serve で動くことを確認しておきます。

yarn run ng serve --open

SSR に必要なものをインストール

yarn add @angular/platform-server @nguniversal/express-engine @nguniversal/module-map-ngfactory-loader express
yarn add --dev @types/express

ルートモジュールを SSR 用に改変

src/app/app.module.ts
import { NgModule, Inject, PLATFORM_ID, APP_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

// ...

@NgModule({
  imports: [
    BrowserModule.withServerTransition({appId: 'toh-pt6-ssr-lambda'}),

// ...

export class AppModule {
  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
    @Inject(APP_ID) private appId: string,
  ) {
    const platform = isPlatformBrowser(platformId) ? 'browser' : 'server';

    console.log({platform, appId});
  }
}

サーバー用ルートモジュールを追加

yarn run ng generate module app-server --flat true
src/app/app-server.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    CommonModule,
    AppModule,
    ServerModule,
    ModuleMapLoaderModule,
  ],
  declarations: [],
  bootstrap: [AppComponent],
})
export class AppServerModule {}

サーバー用ブートストラップローダーを追加

src/main.server.ts
export { AppServerModule } from './app/app.server.module';

サーバーのコードを実装

server/index.ts
import 'zone.js/dist/zone-node';
import 'reflect-metadata';

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

import * as express from 'express';
import {join} from 'path';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
export const app = express();

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('../dist/serverApp/main.bundle');

// Express Engine
import {ngExpressEngine} from '@nguniversal/express-engine';
// Import module map for lazy loading
import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';

app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,

  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ],
}));

app.set('view engine', 'html');
app.set('views', join(process.cwd(), 'dist', 'browserApp'));

// Server static files from /browser
app.get('*.*', express.static(join(process.cwd(), 'dist', 'browserApp')));

// All regular routes use the Universal engine
app.get('*', (req, res) => {
  res.render(join(process.cwd(), 'dist', 'browserApp', 'index.html'), {req});
});
server/start.ts
import {app} from '.';

const PORT = process.env.PORT || 4000;

app.listen(PORT, () => {
  console.log(`Node server listening on http://localhost:${PORT}`);
});

チュートリアルからの変更点: あとで Lambda で使い回すのでこのコードでは app.listen() せずに単に export しておき、スタート用のファイルは別に用意します。ファイルの置き場所も若干変更しています。

サーバー用のビルド設定を追加

.angular-cli.json
// ...
  "apps": [
    {
      "platform": "browser",
      "root": "src",
      "outDir": "dist/browser",
// ...
    {
      "platform": "server",
      "root": "src",
      "outDir": "dist/server",
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.server.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.server.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "styles.css"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
src/tsconfig.server.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "baseUrl": "./",
    "module": "commonjs",
    "types": []
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "app/app-server.module#AppServerModule"
  }
}

.angular-cli.json にはクライアント用のビルド設定しか書かれていないので、ここにサーバーサイド用アプリのビルド設定を追加します。 outDir が被らないように変えておきます。

ng build では「サーバーサイド用の Angular アプリのビルド」までは面倒を見てくれますが、サーバー自体のコードのビルドは自力でやる必要があるので、もろもろ追加します。

yarn add --dev awesome-typescript-loader webpack copy-webpack-plugin concurrently
server/webpack.config.js
const {join} = require('path');
const {ContextReplacementPlugin} = require('webpack');
const CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = {
  entry: {
    server: join(__dirname, 'index.ts'),
    start: join(__dirname, 'start.ts'),
  },

  resolve: { extensions: ['.js', '.ts'] },
  target: 'node',
  externals: [/(node_modules|main\..*\.js)/],
  output: {
    path: join(__dirname, '..', 'dist', 'server'),
    filename: '[name].js'
  },
  module: {
    rules: [{ test: /\.ts$/, loader: 'awesome-typescript-loader' }]
  },
  plugins: [
    new CopyWebpackPlugin([
      {from: "dist/browserApp/**/*"},
      {from: "dist/serverApp/**/*"},
    ]),

    // Temporary Fix for issue: https://github.com/angular/angular/issues/11580
    // for 'WARNING Critical dependency: the request of a dependency is an expression'
    new ContextReplacementPlugin(
      /(.+)?angular(\\|\/)core(.+)?/,
      join(__dirname, '..', 'src'), // location of your src
      {} // a map of your routes
    ),
    new ContextReplacementPlugin(
      /(.+)?express(\\|\/)(.+)?/,
      join(__dirname, '..', 'src'),
      {}
    )
  ]
};

サーバーの実装上の都合で (クライアント用の Angular ビルド + サーバー用の Angular ビルド) → サーバーのビルド という手順を踏む必要があるので、一連のビルド用スクリプトを追加します。

package.json
    "build:prod": "concurrently --names 'browser,server' 'ng build --prod --progress false --base-href /prod/ --app 0' 'ng build --prod --progress false --base-href /prod/ --app 1 --output-hashing false'",
    "prebuild:server": "npm run build:prod",
    "build:server": "webpack --config server/webpack.config.js",
    "start:server": "node dist/server/start.js"

--base-href を指定しているのは、後で API Gateway の仕様との整合性をとるためです。

ローカルでサーバーを動かしてみる

yarn run build:server
yarn run start:server

http://localhost:4000/prod/detail/14 あたりにアクセスして、 SSR されているか確認してみます。

Tour_of_Heroes.png

最初のリクエストに対するレスポンスで、 Angular が生成した HTML が返ってきていることがわかります。チュートリアル第1章で pipe を使って大文字にしたところもちゃんと再現されていますね。

普通に ng serve した場合はこんな感じのはずです。

Tour_of_Heroes.png

Lambda にデプロイする

おなじみの Serverless Framework を使います。いつもありがとうございます。

ここで aws-serverless-express も追加しましょう。

yarn add aws-serverless-express
yarn add --dev @types/aws-serverless-express serverless serverless-webpack

Lambda 用のエントリーポイントを追加します。

lambda/index.ts

import {createServer, proxy} from 'aws-serverless-express';

import {app} from '../server';

export default (event, context) => proxy(createServer(app), event, context);

Lambda (serverless-webpack) 用のビルド設定を追加します。さっきのやつとほぼ同じですが、 libraryTarget: "commonjs" がないと動かないようです。

lambda/webpack.config.js
const {join} = require('path');
const {ContextReplacementPlugin} = require('webpack');
const CopyWebpackPlugin = require("copy-webpack-plugin");
const slsw = require('serverless-webpack');

module.exports = {
  entry: slsw.lib.entries,
  resolve: { extensions: ['.js', '.ts'] },
  target: 'node',
  externals: [/(node_modules|main\..*\.js)/],
  output: {
    libraryTarget: "commonjs",
    path: join(__dirname, '..', 'dist', 'lambda'),
    filename: '[name].js'
  },
  module: {
    rules: [{ test: /\.ts$/, loader: 'awesome-typescript-loader' }]
  },
  plugins: [
    new CopyWebpackPlugin([
      {from: "dist/browserApp/**/*"},
      {from: "dist/serverApp/**/*"},
    ]),

    // Temporary Fix for issue: https://github.com/angular/angular/issues/11580
    // for 'WARNING Critical dependency: the request of a dependency is an expression'
    new ContextReplacementPlugin(
      /(.+)?angular(\\|\/)core(.+)?/,
      join(__dirname, '..', 'src'), // location of your src
      {} // a map of your routes
    ),
    new ContextReplacementPlugin(
      /(.+)?express(\\|\/)(.+)?/,
      join(__dirname, '..', 'src'),
      {}
    )
  ]
};

Serverless の設定ファイルを追加します。あらゆるパスへの GET を Lambda にルーティングするように設定します。

serverless.yml
service: toh-pt6-ssr-lambda
provider:
  name: aws
  runtime: nodejs6.10
  region: ${env:AWS_REGION}
  memorySize: 128
plugins:
- serverless-webpack
custom:
  webpack: lambda/webpack.config.js
  webpackIncludeModules: true
functions:
  main:
    handler: lambda/index.default
    events:
    - http:
        path: /
        method: get
    - http:
        path: "{proxy+}"
        method: get

Serverless をインストールした状態でビルドしようとすると以下のようなエラーが出るので、とりあえず tsconfig.json を変更して回避しておきます。

ERROR in [at-loader] ./node_modules/@types/graphql/subscription/subscribe.d.ts:17:4
    TS2304: Cannot find name 'AsyncIterator'.

ERROR in [at-loader] ./node_modules/@types/graphql/subscription/subscribe.d.ts:29:4
    TS2304: Cannot find name 'AsyncIterable'.
tsconfig.json
    "lib": [
      "es2017",
      "dom",
      "esnext.asynciterable"
    ]

Lambda 用のデプロイスクリプトを追加します。

package.json
    "predeploy": "npm run build:prod",
    "deploy": "serverless deploy"

で、ようやく Lambda のデプロイです。

yarn run deploy

うまくいけば Serverless によって各種 AWS リソースが作られ、 API Gateway のエンドポイントが出力されるはずです。

API Gateway にアクセスして SSR されているか見てみる

先程と同様に確認してみます。

Tour_of_Heroes.png

よさげ。

今後の課題

Lambda + API Gateway で SSR することができました。しかし、いろいろと課題はまだありそうです。

HTTP ステータスコード問題

現状のコードでは固定的に 200 を返しているため、ありえないパスにリクエストが来てもクローラー的には OK と解釈されてしまいます。本来なら 404 などを適切に返すべきです。

HTML 以外をどうやって動的に返すか

一般的なサイトでは sitemap.xml など動的な内容かつ HTML ではないものも返す必要があります。これを Angular でできるかどうか、調べる必要がありそうです。

Express なくせるんじゃね?

今回は Angular -> Universal + Express Engine -> Express -> aws-serverless-express -> Lambda (API Gateway Proxy Integration) という繋ぎ方をしました。

これを Angular -> Universal + [何か] -> Lambda (API Gateway Proxy Integration) にできたら構成要素が減らせていい感じですよね。

renderModuleFactory などを自力で叩く実装ができれば、これが実現できるのかもしれません。

今回作ったコード

参考

続きを読む

AWS LambdaをTest Runnerとして使ってみる

はじめに

Serverless Advent Calendar 2017 の6日目です。

サーバーレスの技術を使うとWeb APIの開発が非常にお手軽になりました。
ところで質問。

ちゃんとテスト書いてますか??

この記事では、サーバーレスで作ったWeb API (別にサーバーレスで作ってなくてもいいけど) のAPIテストをサーバーレスでやっちゃおう、という小ネタです。 実運用ができるかどうかはやってみないとわかりません。

Web APIのテストって?

デプロイ後のWeb API、皆さんはどのようにテストされていますか?

  • curlなどを使ってShellScrpitでテスト組む?
  • いわゆるxUnit系のテストフレームワークを使ってテストする?
  • 何かしらのSaaSを使ってテスト?
  • Rest Client (Postmanなど) を使って手動でテスト^^;

などでしょうか?

ここで提案です。

サーバーレスでやればいいんじゃね??

AWS Lambdaをテストランナーとして使う

概要

Nodeでやります。

JavaScriptのテストフレームワークは、MochaやJasmine、Jestなどが有名です。最近だとAVAが流行っていますかね。 これらは基本的にはLocal環境でテストしたり、CI/CD環境でテストをまわすのに利用されます。

が、今回は上記のようなテストフレームワークは一切使わず、Lambdaでテストを書いてみます。

テスト対象のAPI

すごーくシンプルな、下記のようなCRUDなAPIを想定します。

API description
POST /users ユーザリソース作成
GET /users/{userId} ユーザリソース取得
PUT /users/{userId} ユーザリソース更新
DELETE /users/{userId} ユーザリソース削除

テストを書く!

テストランナーはLambdaにするとしても、Web APIをたたいたり、Assertionなどは外部パッケージに頼ります。
とりあえず定番の supertestshould を使います。

コードの内容としては以下のようなものになっています。

  1. POST /users で34歳のTaroというユーザを作成し、
  2. GET /users/{userId} でユーザ情報を参照し、
  3. PUT /usert/{userId} でTaro -> Hanakoに改名し、
  4. DELETE /users/{userId} でユーザを削除する
const Supertest = require('supertest');
const should = require('should');

const agent = Supertest.agent('https://your-api-host/v1');

function testShouldPostUserSucceeds() {
  const user = {
    name: 'Taro',
    age: 34
  };

  return new Promise((resolve) => {
    agent.post('/users')
      .set('Accept', 'application/json')
      .send(user)
      .expect((res) => {
        res.status.should.equal(200);
        res.body.should.hasOwnProperty('userId');
        res.body.name.should.equal('Taro');
        res.body.age.should.equal(34);
      }).end((err, res) => {
        if (err) {
          console.log('[Failed] testShouldPostUserSucceeds');
          throw err;
        }
        console.log('[Passed] testShouldPostUserSucceeds');
        resolve(res.body.userId);
      });
  });
}

function testShouldGetUserSucceeds(userId) {
  return new Promise((resolve) => {
    agent.get(`/users/${userId}`)
      .set('Accept', 'application/json')
      .expect((res) => {
        res.status.should.equal(200);
        res.body.userId.should.equal(userId);
        res.body.name.should.equal('Taro');
        res.body.age.should.equal(34);
      }).end((err, res) => {
        if (err) {
          console.log('[Failed] testShouldGetUserSucceeds');
          throw err;
        }
        console.log('[Passed] testShouldGetUserSucceeds');
        resolve(res.body.userId);
      });
  });
}

function testShouldPutUserSucceeds(userId) {
  const user = {
    name: 'Hanako',
    age: 34
  }

  return new Promise((resolve) => {
    agent.put(`/users/${userId}`)
      .set('Accept', 'application/json')
      .send(user)
      .expect((res) => {
        res.status.should.equal(200);
        res.body.userId.should.equal(userId);
        res.body.name.should.equal('Hanako');
        res.body.age.should.equal(34);
      })
      .end((err, res) => {
        if (err) {
          console.log('[Failed] testShouldPutUserSucceeds');
          throw err;
        }
        console.log('[Passed] testShouldPutUserSucceeds');
        resolve(res.body.userId);
      });
  });
}

function testShouldDeleteUserSucceeds(userId) {
  return new Promise((resolve) => {
    agent.delete(`/users/${userId}`)
      .set('Accept', 'application/json')
      .expect((res) => {
        res.status.should.equal(200);
        res.body.userId.should.equal(userId);
      }).end((err, res) => {
        if (err) {
          console.log('[Failed] testShouldDeleteUserSucceeds');
          throw err;
        }
        console.log('[Passed] testShouldDeleteUserSucceeds');
        resolve();
      });
  });
}

exports.handler = (event, context) => testShouldPostUserSucceeds()
  .then(userId => testShouldGetUserSucceeds(userId))
  .then(userId => testShouldPutUserSucceeds(userId))
  .then(userId => testShouldDeleteUserSucceeds(userId))
  .then(() => {
    context.succeed('Test Passed');
  }).catch(() => {
    context.fail('Test Failed');
  });
  • Node 6.x でTranspile無しで動くように書いています。
  • ESLintにはめちゃくちゃ怒られる書き方になっているのでご注意を。
  • Supertestなどの説明は省いているので他の記事をご参照ください。

これをLambdaで実行するとどうなるか?

テスト成功時

上記のコードをLambdaで実行し、テストに全部成功すると、例えばAWSのConsoleで確認すると以下の図のようになります。

1-ok-min.png

テスト失敗時

一方で、 testShouldPutUserSucceeds をちょっといじって、Taro -> Hanakoの改名に失敗するような、Failするテストに変更してみると、

  res.status.should.equal(200);
  res.body.userId.should.equal(userId);
//  res.body.name.should.equal('Hanako');
  res.body.name.should.equal('Taro');
  res.body.age.should.equal(34);

下記の図のように、失敗したテストがLambdaの実行ログでわかるのです!!

2-ng-min.png

result.png

所感

これはいったい誰トクなのか?

たぶん下記のようなメリットがあるはず。

  • CloudWatch Eventsなどから定期実行できる
  • 定期的に動かせば、APIの死活監視にも使える
  • Lambdaの実行結果をとりあえずSNSにでも飛ばしておけば、メールだったりWebhookでどっかにポストしたり、通知の柔軟性が高い

ただ、冒頭に書いた通り、本当にこんなものが必要なのかはよくわからない。
もし気が向いたら (需要がありそうなら) フレームワーク化してOSS化を検討したいところ。

続きを読む

Chaliceを使ってみた

AWS+Lambda+Pythonでサーバレス開発をするためのAWS謹製フレームワーク

インストール

pip install chalice
chalice new-project helloworld
cd helloworld

ローカルで起動

chalie local

リロードさせたい

npm install -g nodemon
nodemon --exec "chalice local" --watch *.py

sqlalchemyでMySQLにつなぐ

pip install SQLAlchemy
pip install PyMySQL
app.py
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
import os

app = Chalice(app_name='helloworld')
app.debug = True
Base = declarative_base()

#Userモデル
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(40))

#session取得
def getSession():
    #環境変数から接続文字列を取得
    engine = create_engine(os.environ["CONNECTION_STRING"], echo=True)
    session = sessionmaker(bind=engine)()
    return session

@app.route('/')
def index():
    session = getSession()
    user = session.query(User).one()
    return {'hello': user.name}

.chalice/config.json へ環境変数を定義
http://chalice.readthedocs.io/en/latest/topics/configfile.html

chalice/config.json

  "stages": {
    "dev": {
      "api_gateway_stage": "api",
      "environment_variables": {
        "CONNECTION_STRING": "mysql+pymysql://userid:passowrd@host/database?charset=utf8",
        "OTHER_CONFIG": "dev-value"
      }
    }

#デプロイしてみる
pip freeze > requirements.txt
chalice deploy

#依存関係をvendorに入れとけって怒られる
Could not install dependencies:
itsdangerous==0.24
typing==3.5.3.0
MarkupSafe==1.0
SQLAlchemy==1.1.15
You will have to build these yourself and vendor them in
the chalice vendor folder.

#入れる
pip install -U itsdangerous==0.24 -t ./vendor/
pip install -U SQLAlchemy==1.1.15 -t ./vendor/
pip install -U MarkupSafe==1.0 -t ./vendor/
pip install -U typing==3.5.3.0 -t ./vendor/

chalice deploy

#接続を試す・・・成功!
curl https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/
{"hello": "hirao"}

続きを読む

GitLabのgitlab-runner autoscalingをaws上でdocker-machineしてみる

概要

GitLab Runner 1.1 with Autoscalingによると、gitlab-runner自身もスケールできるよ、と。

runnerの環境にいろいろ入れるのは嫌だし、runnnerの環境にDockerを入れてそこで動かすにしても、
メモリやCPUも常時そんなに必要なわけじゃないから、runnner自体は安いインスタンスにしたい。
そう思うのは人情です。

そこで、今回はgitlab-runnerはt2.microの小さいインスタンスで動かし、
実際のビルドは、そこからdocker-machineで作成された先のインスタンス内でやろうと考えたのです。

AmazonLinuxで1からgitlab-runnerを入れ、ビルドできるところまでをステップごとに紹介します。
公式ドキュメントをコピペしていると気づかないつまづきポイントつき!

やってみた結果、思ったこと

メリット

  • gitlab-runner自身は、ビルドを行わないので、t2.microレベルで良い。やすい。
  • 複数のリクエストがきても、EC2インスタンスが新規に生成されるので、スケールしやすい。

デメリット

  • EC2インスタンスの起動から行われるため、その分ビルドに時間がかかる

aws内にDockerのレジストリ(ECR)があったり、S3にビルド用の資材が一式入ってます!みたいな人には、aws上にgitlab-runnnerを入れるメリットがありそうです。

EC2インスタンスの作成

まず、gitlab-runnerを動かすインスタンスを作成します。t2.microにしておきます。
作成する際のイメージはAmazonLinux(2017/12/06時点で ami-da9e2cbc)を指定します。

 aws ec2 run-instances 
    --image-id ami-da9e2cbc 
    --count 1 
    --instance-type t2.micro 
    --key-name ${KEY_NAME} 
    --security-group-ids ${SECURITY_GROUP_ID} 
    --subnet-id ${SUBNET_ID}

key-nameやセキュリティグループID、サブネットのIDは作成する環境にあわせて設定しておきます。

docker-machineのインストール

docker-machineを先の手順でたてた環境にインストールします。

公式の手順は、https://docs.docker.com/machine/install-machine/#install-machine-directly にあります。

curl -L https://github.com/docker/machine/releases/download/v0.13.0/docker-machine-`uname -s`-`uname -m` >/tmp/docker-machine &&
chmod +x /tmp/docker-machine &&
sudo cp /tmp/docker-machine /usr/bin/docker-machine

つまづきポイント1

公式の手順では、docker-machineを/usr/local/bin/docker-machineにコピーしますが、
これだとインストールした自分は使えるけども、gitlab-runnerユーザには使えないことになるので、
/usr/bin/docker-machine とします。

もし、/usr/bin/local以下に入れてしまっていたら、ビルド実行時にこんなエラーになります。

ERROR: Preparation failed: exec: “docker-machine”: executable file not found in $PATH

build test failed.png

gitlab-runnerのインストール

公式の入れ方はhttps://docs.gitlab.com/runner/install/linux-repository.htmlにあります。

こんな感じでyumで入れてしまいます。

curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash
sudo yum install gitlab-runner -y

gitlab-runnnerの登録

GitLabにrunnerを登録します。
予めGitLabでトークンの確認と、docker-machineで使うオプション値を確認しておきましょう。

dockerを用いたビルドを想定する場合は、docker-privilegedオプションをつけておくのが良いです。

例はこちら。
docker-machineのオプションは、実際にビルドをするマシンとして過不足ないものを選ぶといいでしょう。

sudo gitlab-runner register --non-interactive 
  --url https://gitlab.com/ 
  --registration-token XXXXXXXXXXXXXXXXXXXXXXX 
  --executor "docker+machine" 
  --name "gitlab-ci-auto-scaling" 
  --docker-image "ubuntu" 
  --docker-privileged 
  --machine-machine-driver "amazonec2" 
  --machine-machine-name "gitlab-ci-%s" 
  --machine-machine-options "amazonec2-access-key=ACCESS_KEY" 
  --machine-machine-options "amazonec2-secret-key=SECRET_KEY" 
  --machine-machine-options "amazonec2-region=ap-northeast-1" 
  --machine-machine-options "amazonec2-root-size=30" 
  --machine-machine-options "amazonec2-instance-type=m4.large" 
  --machine-machine-options "amazonec2-vpc-id=vpc-0123456" 
  --machine-machine-options "amazonec2-subnet-id=subnet-1234567" 
  --tag-list "ec2-auto-scale,docker"

つまづきポイント2

machine-machine-optionsで指定する内容は、“KEY=VALUE”の形で、イコールでつなぐようにします。
“KEY VALUE”のようにしておくと、registerそのものは成功しますが、動作しないことになります。

つまづきポイント3

もし、docker-priviledgedがない状態(false)で、dockerコマンドを実行するようなビルドが走ったときは、こうなります。

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
ERROR: Job failed: exit code 1

a.png

あとはCIするだけ

あとは、gitlab-ci.ymlを用意してビルドするだけ!

image: docker:latest

services:
  - docker:dind

build-test:
  stage: build
  script:
    - echo sekai no yasuda and aoki.
    - docker version
  tags:
    - ec2-auto-scale

tagsには、忘れずに自分で登録したrunnnerのタグにしておきましょう。

インスタンスが作成され、ビルドが走り、そしてインスタンスが削除される。
terminatedがたくさんAWSのコンソールで見えても気にしない!
じゃんじゃんバリバリ、CIがまわせるようになりますね。

では、Happy GitLab Lifeを!!

続きを読む

本番環境をGCP/AWSで何個か作り、インフラについて少しわかったこと【GCP環境構築編】

Hakusan Mafiaアドベントカレンダー5日目を余語 [Qiita|facebook|github] が担当します!

本番環境をGCP/AWSで何個か作り、インフラについて少しわかったこと【SSL証明書編】
本番環境をGCP/AWSで何個か作り、インフラについて少しわかったこと【GCP環境構築編】 ←今これ
本番環境をGCP/AWSで何個か作り、インフラについて少しわかったこと【パフォーマンス改善編】

はじめに

以前はAWSでインフラ環境を整えるのがベストプラクティスかと思っていました。というのも、EC2は直感的に触って起動できるし、S3・RDS・ DynamoDMなどの記事も相当Webに溢れていたので簡単でした。ただ、プレスリリースを売ったりメディアに出したりする案件があった際にわざわざAmazonへ申請を出さなければならないといけないらしいということを聞いて少し疑問に感じた時もありました。

そんな時に、AWSと同等のものを用意してくれている。且つ、自動スケーリングが凄い(詳しくいうと、ロードバランサがGoogle検索と同じらしい)という記事を見た時に、少々衝撃を覚えました。

今回の記事では、Google Cloud Platform(GCP)を利用して本番環境を構築する方法を記述します。
基本的には、公式ドキュメント通りにやると全てうまくいくのですが、自分のターミナルから色々いじりたい人用に書いてます。

10分でGCP環境構築

1. ログイン、プロジェクト作成

https://cloud.google.com/?hl=ja の右上でログインし、その後「コンソール」へ入る
プロジェクト作成から、「プロジェクト名」を入力して、準備完了

2. GCEを選択し、VMインスタンス作成

https://gyazo.com/0581708139112c11db672d5cdcfb7ea6

名前は適当で、
- ゾーンを「asia-northeast1-a」
- ブートディスクに今回は、「CentOS」
- ファイアウォールの設定は二つともチェックを入れましょう。

https://gyazo.com/5cc259ec583d4635073f1876cbbd28e6

3. Terminalにてgcloudログイン

今回は、ミドルウェア、とりわけWebサーバーをNginx、ApサーバーをUnicornで実装します。
OSは先ほどCentOSを利用すると選択したので、webにあるDebianでの記事と比較しながらやると勉強になると思います。
(RoRアプリケーションを想定しています。)

ミドルウェアの設定は次の通りです。

ミドルウェア 項目
nginx conn./worker 1024
unicorn worker processes/cpu 2 or 3

インスタンス作成後に、そのVMインスタンスの「接続」から「glcloud コマンドを表示」という箇所でコマンドをコピーして、ローカルで接続して見てください。

local
$ gcloud compute --project "xxxxx-yyy-1111111" ssh --zone "asia-northeast1-a" "xxx"
Last login: Sun Dec  3 08:13:59 2017 from softbankxxxx.bbtec.net

server
[xxxx@yyyy ~]$ #こんな感じでログインできる

先ほどGoogle Compute Engineで作成したVMインスタンスにログインできる。
(ここで弾かれたら、permission関連なのでgithubのSSH and GPG keysという箇所に公開鍵を貼り、ログインしてください。)

gcloudがないと言われたら、下記でインストール&ログイン

local
$ curl https://sdk.cloud.google.com | bash
$ gcloud init

4. 権限管理・ミドルウェアのインストール

4.1 新規ユーザー作成

server

[user_name| ~ ]$ sudo adduser [新規ユーザー名]
[user_name| ~ ]$ sudo passwd [新規ユーザー名]
[user_name| ~ ]$ sudo visudo
----------------------------
root ALL=(ALL) ALL
[新規ユーザー名] ALL=(ALL) ALL # この行を追加
# ↑後々 wheelか何かで管理できるとなお良い
----------------------------
[user_name| ~ ]$ sudo su - [新規ユーザー名]

4.2 VMインスタンス内の環境構築

yum -> node.js -> rbenv/ruby-build -> rubyの順

4.2.1 yum (mysql関連含む)

server
[user_name| ~ ]$ sudo yum install 
                 git make gcc-c++ patch 
                 libyaml-devel libffi-devel libicu-devel 
                 zlib-devel readline-devel mysql-server mysql-devel  -y

4.2.2 node.js

server
[user_name| ~ ]$ sudo curl -sL https://rpm.nodesource.com/setup_6.x | sudo bash -
[user_name| ~ ]$ sudo yum install -y nodejs

4.2.3 rbenv/ruby-build

server
[user_name| ~ ]$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
[user_name| ~ ]$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
[user_name| ~ ]$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
[user_name| ~ ]$ source ~/.bash_profile
[user_name| ~ ]$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
[user_name| ~ ]$ rbenv rehash

4.2.4 ruby

server
# centOSを選択した場合
[user_name| ~ ]$ sudo yum -y install bzip2

[user_name| ~ ]$ rbenv install -v 2.4.1
[user_name| ~ ]$ rbenv global 2.4.1
[user_name| ~ ]$ rbenv rehash
[user_name| ~ ]$ ruby -v
ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-linux]

4.3 Gitとインスタンスの紐ずけ

ssh接続 → git clone

server
[user_name@vm_name .ssh]$ ssh-keygen -t rsa
[user_name@vm_name .ssh]$ cat config 
Host github
  Hostname github.com
  User git
  IdentityFile ~/.ssh/id_rsa #<- 追加
[user_name@vm_name .ssh]$ ssh -T github
# root権限だとgit cloneできないため
[user_name| ~ ]$ sudo chown user_name www/
[user_name@vm_name www]$ pwd
/var/www
[user_name@vm_name www]$ git clone git@github.com:~~~~

4.4 Nginxを積む

server
[ユーザー名|~]$ sudo yum install nginx
[ユーザー名|~]$ cd /etc/nginx/conf.d/
[ユーザー名|~]$ sudo vi <your_app_name>.conf (小文字でもok)
# default.conf/ssl.confなどと分けるとなお良い。(細分化)

Nginxの設定は最小限です。

/etc/nginx/conf.d/your_app_name.conf
upstream unicorn {
    server  unix:/var/www/<your_app_name>/tmp/sockets/unicorn.sock;
}

server {
    listen       80;
    server_name  <ip address and/or domain name>;

    access_log  /var/log/nginx/access.log;
    error_log   /var/log/nginx/error.log;

    root /var/www/<your_app_name>/public;

    client_max_body_size 100m;
    error_page  404              /404.html;
    error_page  500 502 503 504  /500.html;
    try_files   $uri/index.html $uri @unicorn;

    location @unicorn {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_pass http://unicorn;
    }
}
server
[user_name|~]$ cd /var/lib
[user_name|lib]$ sudo chmod -R 775 nginx #パーミッション調整

4.5 Unicornの導入/設定

server
[user_name@vm_name app_name]$  vi Gemfile
---------------------
group :production do
    gem 'unicorn'
end
---------------------
# gem: command not found
[user_name@vm_name app_name]$ gem install bundler
[user_name@vm_name app_name]$ bundle install
[user_name@vm_name app_name]$ vim config/unicorn.conf.rb
/var/www/app_name/config/unicorn.conf.rb
$worker = 2
$timeout = 30
$app_dir = '/var/www/<App_name>'
$listen  = File.expand_path 'tmp/sockets/unicorn.sock', $app_dir
$pid     = File.expand_path 'tmp/pids/unicorn.pid', $app_dir
$std_log = File.expand_path 'log/unicorn.log', $app_dir
worker_processes  $worker
working_directory $app_dir
stderr_path $std_log
stdout_path $std_log
timeout $timeout
listen  $listen
pid $pid
preload_app true

before_fork do |server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.connection.disconnect!
  old_pid = "#{server.config[:pid]}.oldbin"
  if old_pid != server.pid
    begin
      Process.kill 'QUIT', File.read(old_pid).to_i
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end
end
after_fork do |_server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end

4.6 CSS/JSをコンパイル

server
[user_name@vm_name app_name]$ bundle exec rake assets:precompile RAILS_ENV=production

4.7 Unicorn起動

server
[user_name@vm_name app_name]$ bundle exec unicorn_rails -c /var/www/<your_app_name>/config/unicorn.conf.rb -D -E production

4.7 Nginx再起動

server
[user_name@vm_name app_name]$ sudo service nginx reload

これで本番環境が構築できたかと思います。
エラーが出た際は、下記を参考にデバッグをすれば問題ないので、各自調べてください。

nginx   なら /var/log/nginx/error.log
unicornなら  /log/unicorn.log

AWSやVPSで普段環境構築してる人からしたら、ほとんど難しいところはないかと思います。
今後

GKE・k8sによるオートスケールの設定
Dockerによる環境開発改善・デプロイ改善
Cloud Storageによる外部ファイルサーバ利用

などをする際に、便利さを身にしみて感じるのではと思います。
では最後の投稿を楽しみにしていてください。

本番環境をGCP/AWSで何個か作り、インフラについて少しわかったこと【パフォーマンス改善編】

続きを読む