Amazon Aurora(MySQL互換) Auto Scalingで追加されたレプリカ(Reader)インスタンスのバッファキャッシュはどうなる?

…と来れば、やっぱりAuto Scalingで追加されたレプリカ(Reader)インスタンスのバッファキャッシュ(バッファプール・バッファプールキャッシュ)が起動時にウォームアップされるのか、についても知りたいところ(?)。

というわけで、いい加減しつこいですが、検証…というには雑なので、確認をしてみました。

1. Amazon Aurora(MySQL互換) Auto Scalingの設定

いつもの通り、すでにクラスメソッドさんのDevelopers.IOに記事があります。

ありがたやありがたや。

というわけで、私の記事では部分的にスクリーンショットを載せておきます。

※以前、以下の記事を書くときに使ったスナップショットからインスタンスを復元して使いました。

クラスター画面から、Auto Scaling ポリシーを追加しようとすると、
aws_as1.png

新コンソールへのお誘いがあり、
aws_as2.png

先へ進むと見慣れない画面が。
aws_as3.png

Auto Scalingを設定するには、新コンソールでもクラスター画面から。
簡単にスケールするよう、「平均アクティブ接続数」を選択して「3」アクティブ接続を指定します。
aws_as4.png

とりあえずAuto Scalingで作成されたレプリカで確認できればいいので、上限は少な目で。
aws_as5.png

書き忘れましたが、Auto Scaling ポリシーを新規追加する前に、最低1つのレプリカ(Reader)インスタンスを作成しておきます(そうしないと怒られます)。

これで、Auto Scalingの準備ができました。

2. Auto Scalingでレプリカが自動追加されるよう負荷を掛ける

引き続き、クライアントから複数セッションで接続します(とりあえず4つぐらい)。

まずは接続。

クライアント接続
$ mysql -u mkadmin -h test-cluster.cluster-ro-xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.6.10 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

(ここで3つ追加接続)

mysql> SHOW PROCESSLIST;
+----+----------+--------------------+------+---------+------+----------------------+------------------+
| Id | User     | Host               | db   | Command | Time | State                | Info             |
+----+----------+--------------------+------+---------+------+----------------------+------------------+
|  2 | rdsadmin | localhost          | NULL | Sleep   |    2 | delayed send ok done | NULL             |
|  3 | rdsadmin | localhost          | NULL | Sleep   |    2 | cleaned up           | NULL             |
|  4 | rdsadmin | localhost          | NULL | Sleep   |   13 | cleaned up           | NULL             |
|  5 | rdsadmin | localhost          | NULL | Sleep   |  568 | delayed send ok done | NULL             |
|  6 | mkadmin  | 172.31.21.22:43318 | NULL | Query   |    0 | init                 | SHOW PROCESSLIST |
|  7 | mkadmin  | 172.31.21.22:43320 | NULL | Sleep   |   99 | cleaned up           | NULL             |
|  8 | mkadmin  | 172.31.21.22:43322 | NULL | Sleep   |   79 | cleaned up           | NULL             |
|  9 | mkadmin  | 172.31.21.22:43324 | NULL | Sleep   |    9 | cleaned up           | NULL             |
+----+----------+--------------------+------+---------+------+----------------------+------------------+
8 rows in set (0.00 sec)

各セッションで、SQLを実行していきます。

SQL(SELECT)実行
mysql> USE akptest2;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> SELECT s.member_id memb, SUM(s.total_value) tval FROM dept d, member m, sales s WHERE d.dept_id = m.dept_id AND m.member_id = s.member_id AND d.dept_name = '部門015' GROUP BY memb HAVING tval > (SELECT SUM(s2.total_value) * 0.0007 FROM dept d2, member m2, sales s2 WHERE d2.dept_id = m2.dept_id AND m2.member_id = s2.member_id AND d2.dept_name = '部門015') ORDER BY tval DESC;
+-------+---------+
| memb  | tval    |
+-------+---------+
| 28942 | 1530300 |
| 47554 | 1485800 |
(中略)
| 29294 | 1176700 |
| 70092 | 1176300 |
+-------+---------+
41 rows in set (24.33 sec)

(別セッションで)

mysql> SELECT s.member_id memb, SUM(s.total_value) tval FROM dept d, member m, sales s WHERE d.dept_id = m.dept_id AND m.member_id = s.member_id AND d.dept_name = '部門015' GROUP BY memb HAVING tval > (SELECT SUM(s2.total_value) * 0.0007 FROM dept d2, member m2, sales s2 WHERE d2.dept_id = m2.dept_id AND m2.member_id = s2.member_id AND d2.dept_name = '部門002') ORDER BY tval DESC;
(中略)
60 rows in set (0.19 sec)

すると、めでたく(?)レプリカインスタンスが自動的に追加されました!(手動で作成するのと同様、ちょっと時間が掛かりましたが。)
aws_as6.png

※1個目のレプリカインスタンスはAZ-cに作成しましたが、こちらはAZ-aに追加されました。

3. いよいよ確認

そこで、このインスタンスに直接指定で接続してみます。

自動追加されたレプリカインスタンスに接続
$ mysql -u mkadmin -h application-autoscaling-d43255f2-e133-4c84-85a1-45478224fdd2.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 7
Server version: 5.6.10 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> USE akptest2;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show variables like 'aurora_server_id';
+------------------+--------------------------------------------------------------+
| Variable_name    | Value                                                        |
+------------------+--------------------------------------------------------------+
| aurora_server_id | application-autoscaling-d43255f2-e133-4c84-85a1-45478224fdd2 |
+------------------+--------------------------------------------------------------+
1 row in set (0.01 sec)

さあ、いよいよ、バッファキャッシュがどうなっているか、確認です!
最初に発行したものと同じSQLを発行してみます。
バッファキャッシュに載っていれば、1秒未満で実行できるはずですが…。

追加インスタンスで確認
mysql> SELECT s.member_id memb, SUM(s.total_value) tval FROM dept d, member m, sales s WHERE d.dept_id = m.dept_id AND m.member_id = s.member_id AND d.dept_name = '部門015' GROUP BY memb HAVING tval > (SELECT SUM(s2.total_value) * 0.0007 FROM dept d2, member m2, sales s2 WHERE d2.dept_id = m2.dept_id AND m2.member_id = s2.member_id AND d2.dept_name = '部門015') ORDER BY tval DESC;
+-------+---------+
| memb  | tval    |
+-------+---------+
| 28942 | 1530300 |
| 47554 | 1485800 |
(中略)
| 29294 | 1176700 |
| 70092 | 1176300 |
+-------+---------+
41 rows in set (24.71 sec)

残念!!
…まあ、予想通りですね。

接続を切ってしばらく待つと、自動追加されたインスタンスは削除されます。
aws_as7.png

※私が試したときには、設定した時間よりはるかに長い時間が経過してから削除が始まりました。安全を見ているのでしょうか?

さて、プロキシが間に入り、コンテナ(多分)でDBノードが構成されるAmazon Aurora Serverlessでは、どうなるんでしょうか?


続きを読む

新しいマネージドコンテナサービスAWS Fargateの価格は高いか安いか?ECS/Fargateのコスト最適化を考えてみよう

AWS Fargate Advent Calendar 2017 の10日目の記事です。昨日は 俺はSSHをしたくない ~ Fargate vs ECS+SpotFleet のPrice比較 ~ でした。 ネタ被ったァ!

Fargate の価格

1vCPU 1時間あたり\$0.0506、1GBメモリ 1時間あたり\$0.0127

From https://aws.amazon.com/jp/blogs/news/aws-fargate/

これだけです。高いか安いか考察してみましょう。

EC2 インスタンスのオンデマンド価格との比較

us-east-1(N.Virginia)の価格 を見てみましょう。 Fargate価格 欄は上記のFargateの価格で同じvCPUとメモリを確保した場合の価格です。

インスタンスタイプ vCPU メモリ(GB) EC2価格 Fargate価格 倍率
m5.large 2 8 $0.096/h $0.2028/h x2.11
m4.large 2 8 $0.100/h $0.2028/h x2.03
m3.large 2 7.5 $0.133/h $0.1965/h x1.48
c5.large 2 4 $0.085/h $0.152/h x1.79
c4.large 2 3.75 $0.100/h $0.1488/h x1.49
c3.large 2 3.75 $0.105/h $0.1488/h x1.42
r4.large 2 15.25 $0.133/h $0.2949/h x2.22
r3.large 2 15 $0.166/h $0.2917/h x1.76
x1e.xlarge 4 122 $0.834/h $1.7518/h x2.10

m5シリーズであれば価格は性能と線形なので、たとえばm5.24xlargeでも倍率は同じx2.11になるためとりあえず比較しやすい上記に絞りました。

最新世代のm5やr4と比べて、おおむね2倍強の価格です。

Fargate高くない!? と思ったアナタ。ちょっと待ってください。

ECS Cluster AutoScaling構成との比較

多くの場合、ECSではそのメリットを享受するためにAutoScaling構成を取ります。たとえば、

  • MemoryReservation >= 60% で Scale-out
  • MemoryReservation < 30% で Scale-in

と設定するとしましょう1。この場合、実際に確保されるメモリ容量の割合は6割を超えません。
簡単のため、ECSでのリソース利用率はvCPU・メモリともに上記の中央値である45%程度であると仮定しましょう。
Fargateは 割り当てたvCPU・メモリに対する課金 ですので、それに対応するための計算です。

インスタンスタイプ vCPU45% メモリ45% EC2価格 Fargate価格 倍率
m5.large 0.9 3.6 $0.096/h $0.0913/h x0.95
m4.large 0.9 3.6 $0.100/h $0.0913/h x0.91
m3.large 0.9 3.375 $0.133/h $0.0884/h x0.66
c5.large 0.9 1.8 $0.085/h $0.0684/h x0.80
c4.large 0.9 1.6875 $0.100/h $0.0670/h x0.67
c3.large 0.9 1.6875 $0.105/h $0.0670/h x0.64
r4.large 0.9 6.8625 $0.133/h $0.1327/h x1.00
r3.large 0.9 6.75 $0.166/h $0.1313/h x0.79
x1e.xlarge 1.8 54.9 $0.834/h $0.7883/h x0.95

こうして見ると、 Fargateは非常に妥当な値付け であり、少なくとも高くないか やや安い 、と言えそうです。

さいごに

m5/r4シリーズのインスタンスが元々コスパ高いからSpotFleetで使おう

ECS/Fargateを使うときのコスト最適化については、こんな感じになりそうです。

  • SpotFleetを使っている場合にはそのままSpotFleetを使うのが良い
  • オンデマンドインスタンスでAutoScaling Groupを作っているなら、Fargateに乗り換えると良い

オンデマンド価格でECSを使っているならFargateに乗り換えない理由はありませんね!また、エンジニアの稼働を減らすことでコストメリットが出るなら、SpotFleetからFargateに乗り換えるのも十分アリなのでは、と思います2

ECS/Fargateを利用する場合には、他に ALBの利用料金 も発生します。コストを計算する場合にはそちらの考慮もお忘れなく!ALBの数が増えるとその分コストも増えますので、パスベースルーティングやホストベースルーティングを活用してALBの数を抑えるのも効果がありますよ。


  1. なぜこの数値を採用したかというと、実際使っているからです。Scale-outの閾値をあまりに高くしてしまうと、追加のTaskが起動することができずMemoryReservationが上昇できなくてScale-outして欲しいときに正しくScale-outしない、ということが起こりえます。 

  2. SpotFleetを使う場合、SpotFleetリクエストの期限などいくつか気にしないといけないことが増えます。 

続きを読む

AutoScalingGroupのインスタンス起動・停止をSlackに通知する

AutoScalingGroupの作成からSNSとLabmdaと連携してイベントをSlackに通知するところまで。

LaunchConfigurationの作成

AutoScalingGroupingを作るためにはLaunchTemplate, LaunchConfigurationNameまたはInstanceIdが必要なので今回はLaunchConfigurationを作成します。

$ aws autoscaling create-launch-configuration 
  --launch-configuration-name sample-auto-scale-launch-configration 
  --instance-type t2.micro 
  --image-id ami-bf4193c7
  --key-name xxxxx

AutoScalingGroupの作成

先ほど作成したLaunchConfigurationを指定してAutoScalingGroupを作成します。

$ aws autoscaling create-auto-scaling-group 
  --auto-scaling-group-name sample-auto-scale 
  --min-size 1 --max-size 5 
  --launch-configuration-name sample-auto-scale-launch-configration 
  --availability-zones us-west-2a us-west-2b

インスタンスの起動・停止を通知する

AutoScalingGroupからインスタンスのイベントを受け取る方法は LifeCycleHookでもできそうですが、今回はSNSを使った方法でやってみます。

SNSのトピックを作成する

AutoScalingGroupのイベントを通知するためのTopicを作成します。

$ aws sns create-topic --name sample-auto-scale-notification

AutoScalingGroupのイベントをSNSと紐付ける

NotificationConfigurationを追加します。
今回は以下の4つのイベントをSNSで通知するようにしました。

  • EC2_INSTANCE_LAUNCH
  • EC2_INSTANCE_LAUNCH_ERROR
  • EC2_INSTANCE_TERMINATE
  • EC2_INSTANCE_TERMINATE_ERROR
$ aws autoscaling put-notification-configuration 
  --auto-scaling-group-name sample-auto-scale 
  --topic-arn arn:aws:sns:us-west-2:999999999999:sample-auto-scale-notification 
  --notification-types "autoscaling:EC2_INSTANCE_LAUNCH" "autoscaling:EC2_INSTANCE_LAUNCH_ERROR" "autoscaling:EC2_INSTANCE_TERMINATE" "autoscaling:EC2_INSTANCE_TERMINATE_ERROR"

SNSからSlackに通知するLambdaを作成する

pythonでSlackへ通知するLambdaを作成していきます。

sns-to-slack-notificatioin-lambda.png

import urllib.request
import json

def lambda_handler(event, context):
    url = 'https://hooks.slack.com/services/AAAAAAAAA/BBBBBBBBB/CCCCCCCCCCCCCCCCCCCCCCCC'
    method = "POST"
    headers = {"Content-Type" : "application/json"}
    obj = {"text":json.dumps(event)}
    json_data = json.dumps(obj).encode("utf-8")
    request = urllib.request.Request(url, data=json_data, method=method, headers=headers)
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode("utf-8")

    return response_body

LambdaにSNSをsubscribeさせる

$ aws sns subscribe 
  --topic-arn arn:aws:sns:us-west-2:999999999999:sample-auto-scale-notification 
  --protocol lambda 
  --notification-endpoint arn:aws:lambda:us-west-2:999999999999:function:sns-to-slack-notification

SNSがLambdaを起動できるようにpermissionを与えます。

$ aws lambda add-permission 
  --function-name sns-to-slack-notification 
  --statement-id autoscaling-sns-to-lambda 
  --action "lambda:InvokeFunction" 
  --principal sns.amazonaws.com 
  --source-arn arn:aws:sns:us-west-2:999999999999:sample-auto-scale-notification

インスタンスを追加して通知を受け取る

AutoScalingGroup -> SNS -> Lambdaの設定ができたので実際にインスタンスを追加して通知を確認します。
desired-capacityを増やし、インスタンスを1つ起動します。

$ aws autoscaling set-desired-capacity 
  --auto-scaling-group-name sample-auto-scale 
  --desired-capacity 2

インスタンスが起動されSlackに通知がきました。

スクリーンショット 2017-12-10 14.50.32.png

続きを読む

consul-template & supervisorでプロセスの可視化

こちらはフロムスクラッチ Advent Calendar 2017の9日目の記事です。

はじめに

ポプテピピック

もうすぐ、ポプテピピック始まりますね。
どうも、jkkitakitaです。

概要

掲題通り、consul + supervisordで
プロセス監視、管理に関して、可視化した話します。

きっかけ

どうしても、新規サービス構築や保守運用しはじめて
色々なバッチ処理等のdaemon・プロセスが数十個とかに増えてくると
↓のような悩みがでてくるのではないでしょうか。

  1. 一時的に、daemonをstopしたい
  2. daemonがゾンビになってて、再起動したい
  3. daemonが起動しなかった場合の、daemonのログを見る
  4. daemonが動いているのかどうか、ぱっとよくわからない。
  5. ぱっとわからないから、なんか不安。 :scream:

個人的には
5.は、結構感じます。笑
安心したいです。笑

ツールとその特徴・選定理由

簡単に本記事で取り扱うツールのバージョン・特徴と
今回ツールを選んだ選定理由を記載します。

ツール 特徴 選定理由
supervisor
v3.3.1
1. プロセス管理ツール
2. 2004年から使われており、他でよく使われているdaemon化ツール(upstart, systemd)と比較して、十分枯れている。
3. 柔軟な「プロセス管理」ができる。
4. APIを利用して、プロセスのstart/stop/restart…などが他から実行できる。
1.今までupstartを使っていたが、柔軟な「プロセス管理」ができなかったため。

※ upstartは「プロセス管理」よりかは、「起動設定」の印象。

consul
v1.0.1
1. サービスディスカバリ、ヘルスチェック、KVS etc…
2. その他特徴は、他の記事参照。
https://www.slideshare.net/ssuser07ce9c/consul-58146464
1. AutoScalingするサーバー・サービスの死活監視

2. 単純に使ってみたかった。(笑)

3. 本投稿のconsul-templateを利用に必要だったから(サービスディスカバリ)

consul-template
v0.19.4
1. サーバー上で、consul-templateのdaemonを起動して使用
2. consulから値を取得して、設定ファイルの書き換え等を行うためのサービス
ex.) AutoScalingGroupでスケールアウトされたwebサーバーのnginx.confの自動書き換え
1. ansibleのようなpush型の構成管理ツールだと、AutoScalingGroupを使った場合のサーバー内の設定ファイルの書き換えが難しい。

2. user-data/cloud-initを使えば実現できるが、コード/管理が煩雑になる。保守性が低い。

cesi
versionなし
1. supervisordのダッシュボードツール
2. supervisordで管理されているdaemonを画面から一限管理できる
3. 画面から、start/stop/restartができる
4. 簡易的なユーザー管理による権限制御ができる
1. とにかく画面がほしかった。

2. 自前でも作れるが、公式ドキュメントに載っていたから

3. 他にもいくつかOSSダッシュボードあったが、一番UIがすっきりしていたから。(笑)

実際にやってみた

上記ツールを使って
daemonを可視化するために必要な設定をしてみました。
本記事は、全て、ansibleを使って設定していて
基本的なroleは
ansible-galaxyで、juwaiさんのroleを
お借りしています。
https://galaxy.ansible.com/list#/roles?page=1&page_size=10&tags=amazon&users=juwai&autocomplete=consul

supervisor

クライアント側(実際に管理したいdaemonが起動するサーバー)

supervisord.conf
; Sample supervisor config file.
;
; For more information on the config file, please see:
; http://supervisord.org/configuration.html
;
; Notes:
;  - Shell expansion ("~" or "$HOME") is not supported.  Environment
;    variables can be expanded using this syntax: "%(ENV_HOME)s".
;  - Comments must have a leading space: "a=b ;comment" not "a=b;comment".

[unix_http_server]
file=/tmp/supervisor.sock   ; (the path to the socket file)
;chmod=0700                 ; socket file mode (default 0700)
;chown=nobody:nogroup       ; socket file uid:gid owner
;username=user              ; (default is no username (open server))
;password=123               ; (default is no password (open server))

[inet_http_server]         ; inet (TCP) server disabled by default
port=0.0.0.0:9001        ; (ip_address:port specifier, *:port for all iface)
username=hogehoge              ; (default is no username (open server))
password=fugafuga               ; (default is no password (open server))
;セキュリティ観点から、ここのportは絞る必要有。

[supervisord]
logfile=/tmp/supervisord.log        ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB               ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10                  ; (num of main logfile rotation backups;default 10)
loglevel=info                       ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid        ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=1024                         ; (min. avail startup file descriptors;default 1024)
minprocs=200                        ; (min. avail process descriptors;default 200)

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket

[include]
files=/etc/supervisor.d/*.conf

/etc/supervisor.d/配下に
起動するdaemonを設定します。

daemon.conf
[group:daemon]
programs=<daemon-name>
priority=999

[program:<daemon-name>]
command=sudo -u ec2-user -i /bin/bash -c 'cd /opt/<service> && <実行コマンド>'
user=ec2-user
group=ec2-user
directory=/opt/<service>
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stopasgroup=true
stopsignal=QUIT
stdout_logfile=/var/log/<service>/daemon.stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/var/log/<service>/daemon.stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10


[eventlistener:slack_notifier]
command=/usr/bin/process_state_event_listener.py
events=PROCESS_STATE
redirect_stderr=false
stopasgroup=true
stopsignal=QUIT
stdout_logfile=/var/log/<service>/event_listener.stdout.log
stdout_logfile_maxbytes=2MB
stdout_logfile_backups=10
stderr_logfile=/var/log/<service>/event_listener.stderr.log
stderr_logfile_maxbytes=2MB
stderr_logfile_backups=10
environment=SLACK_WEB_HOOK_URL="xxxxxxx"

eventlistener:slack_notifierは、下記投稿を参考に作成。
https://qiita.com/imunew/items/465521e30fae238cf7d0

[root@test02 ~]# supervisorctl status
daemon:<daemon-name>              RUNNING   pid 31513, uptime 13:19:20
slack_notifier                    RUNNING   pid 31511, uptime 13:19:20

server側(daemonの管理画面を表示するwebサーバー)

supervisord.conf
クライアント側と同様

consul

server側

[root@server01 consul_1.0.1]# pwd
/home/consul/consul_1.0.1

[root@server01 consul_1.0.1]# ll
total 16
drwxr-xr-x 2 consul consul 4096 Dec  3 04:49 bin
drwxr-xr-x 2 consul consul 4096 Dec  3 06:06 consul.d
drwxr-xr-x 4 consul consul 4096 Dec  3 04:50 data
drwxr-xr-x 2 consul consul 4096 Dec  3 04:50 logs

[root@server01 consul.d]# pwd
/home/consul/consul_1.0.1/consul.d

[root@server01 consul.d]# ll
total 16
-rw-r--r-- 1 consul consul 382 Dec  3 06:06 common.json
-rw-r--r-- 1 consul consul 117 Dec  3 04:49 connection.json
-rw-r--r-- 1 consul consul  84 Dec  3 04:49 server.json
-rw-r--r-- 1 consul consul 259 Dec  3 04:49 supervisord.json
/home/consul/consul_1.0.1/consul.d/common.json
{
  "datacenter": "dc1",
  "data_dir": "/home/consul/consul_1.0.1/data",
  "encrypt": "xxxxxxxxxxxxxxx", // consul keygenで発行した値を使用。
  "log_level": "info",
  "enable_syslog": true,
  "enable_debug": true,
  "node_name": "server01",
  "leave_on_terminate": false,
  "skip_leave_on_interrupt": true,
  "enable_script_checks": true, // ここtrueでないと、check script実行できない
  "rejoin_after_leave": true
}
/home/consul/consul_1.0.1/consul.d/connection.json
{
  "client_addr": "0.0.0.0",
  "bind_addr": "xxx.xxx.xxx.xxx", // 自身のprivate ip
  "ports": {
    "http": 8500,
    "server": 8300
  }
}
/home/consul/consul_1.0.1/consul.d/server.json
{
  "server": true, // server側なので、true
  "server_name": "server01",
  "bootstrap_expect": 1 // とりあえず、serverは1台クラスタにした
}
/home/consul/consul_1.0.1/consul.d/supervisord.json
{
  "services": [
    {
      "id": "supervisord-server01",
      "name": "supervisord",
      "tags" : [ "common" ],
      "checks": [{
        "script": "/etc/init.d/supervisord status | grep running",
        "interval": "10s"
      }]
    }
  ]
}

consul自体もsupervisordで起動します。

/etc/supervisor.d/consul.conf
[program:consul]
command=/home/consul/consul_1.0.1/bin/consul agent -config-dir=/home/consul/consul_1.0.1/consul.d -ui // -uiをつけて、uiも含めて起動。
user=consul
group=consul
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stdout_logfile=/home/consul/consul_1.0.1/logs/consul.stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/home/consul/consul_1.0.1/logs/consul.stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10

agent側(管理したいdaemonが起動するサーバー側)

/home/consul/consul_1.0.1/consul.d/common.json
{
  "datacenter": "dc1",
  "data_dir": "/home/consul/consul_1.0.1/data",
  "encrypt": "xxxxxxxxxxxxxxx", // server側と同じencrypt
  "log_level": "info",
  "enable_syslog": true,
  "enable_debug": true,
  "node_name": "agent01",
  "leave_on_terminate": false,
  "skip_leave_on_interrupt": true,
  "enable_script_checks": true,
  "rejoin_after_leave": true,
  "retry_join": ["provider=aws tag_key=Service tag_value=consulserver region=us-west-2 access_key_id=xxxxxxxxxxxxxx secret_access_key=xxxxxxxxxxxxxxx"
  // retry joinでserver側と接続。serverのcluster化も考慮して、provider=awsで、tag_keyを指定。
]
  }
/home/consul/consul_1.0.1/consul.d/connection.json
{
  "client_addr": "0.0.0.0",
  "bind_addr": "xxx.xxx.xxx.xxx", // 自身のprivate ip
  "ports": {
    "http": 8500,
    "server": 8300
  }
}
/home/consul/consul_1.0.1/consul.d/daemon.json
{
  "services": [
        {
      "id": "<daemon-name>-agent01",
      "name": "<daemon-name>",
      "tags" : [ "daemon" ],
      "checks": [{
        "script": "supervisorctl status daemon:<daemon-name> | grep RUNNING",
        "interval": "10s"
      }]
    }
  ]
}
/home/consul/consul_1.0.1/consul.d/supervisord.json
{
  "services": [
    {
      "id": "supervisord-agent01",
      "name": "supervisord",
      "tags" : [ "common" ],
      "checks": [{
        "script": "/etc/init.d/supervisord status | grep running",
        "interval": "10s"
      }]
    }
  ]
}

agent側もsupervisordで管理

/etc/supervisor.d/consul.conf
[program:consul]
command=/home/consul/consul_1.0.1/bin/consul agent -config-dir=/home/consul/consul_1.0.1/consul.d // -uiは不要
user=consul
group=consul
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stdout_logfile=/home/consul/consul_1.0.1/logs/consul.stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/home/consul/consul_1.0.1/logs/consul.stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10

cesi

image2.png

こちらのrepoから拝借させていただきました :bow:
基本的な設定は、README.mdに記載されている通り、セットアップします。

/etc/cesi.conf
[node:server01]
username = hogehoge
password = fugafuga
host = xxx.xxx.xxx.xxx // 対象nodeのprivate ip
port = 9001

[node:test01]
username = hogehoge
password = fugafuga
host = xxx.xxx.xxx.xxx // 対象nodeのprivate ip
port = 9001

[cesi]
database = /path/to/cesi-userinfo.db
activity_log = /path/to/cesi-activity.log
host = 0.0.0.0

(ansibleのroleにもしておく。)
cesiのコマンドも簡単にsupervisordで管理する様に設定します。

/etc/supervisor.d/cesi.conf
[program:cesi]
command=python /var/www/cesi/web.py
user=root
group=root
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stopasgroup=true
stopsignal=QUIT
stdout_logfile=/root/cesi.stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/root/cesi.stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10

スクリーンショット 2017-12-10 1.51.12.png

うん、いい感じに画面でてますね。
ただ、この画面の欠点としてnodeが増えるたびに、
都度、 /etc/cesi.confを書き換えては
webサーバーを再起動しなければならない欠点がありました。
なので
今生きているサーバーは何があるのかを把握する必要がありました。
 → まさにサービスディスカバリ。
そこで、設定ファイルの書き方もある一定柔軟にテンプレート化できる
consul-tamplteの登場です。

consul-template

ここも同様にして、ansibleで導入します。
https://github.com/juwai/ansible-role-consul-template
あとは、いい感じに公式ドキュメントをみながら、templateを書けばok。

[root@agent01 config]# ll
total 8
-rwxr-xr-x 1 root   root    220 Dec  4 05:16 consul-template.cfg
/home/consul/consul-template/config/consul-template.cfg
consul = "127.0.0.1:8500"
wait = "10s"

template {
  source = "/home/consul/consul-template/templates/cesi.conf.tmpl"
  destination = "/etc/cesi.conf"
  command = "supervisorctl restart cesi"
  command_timeout = "60s"
}
/home/consul/consul-template/templates/cesi.conf.tmpl
{{range service "supervisord"}}
[node:{{.Node}}]
username = hogehoge
password = fugafuga
host = {{.Address}}
port = 9001

{{end}}

[cesi]
database = /path/to/cesi-userinfo.db
activity_log = /path/to/cesi-activity.log
host = 0.0.0.0

上記のように、consul-tamplateの中で
{{.Node}}という値を入れていれば
consulでsupervisordのnode追加・更新をトリガーとして
consul-templateが起動し

  1. /etc/cesi.confの設定ファイルの更新
  2. cesiのwebserverの再起動

が実現でき、ダッシュボードにて、supervisordが、管理できるようになります。

また
consul-templateは、daemonとして起動しておくものなので
consul-templateもまた、supervisordで管理します。

/etc/supervisor.d/consul-template.conf
[program:consul-template]
command=/home/consul/consul-template/bin/consul-template -config /home/consul/consul-template/config/consul-template.cfg
user=root
group=root
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stdout_logfile=/home/consul/consul-template/logs/stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/home/consul/consul-template/logs/stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10

早速、実際サーバーを立ててみると…

スクリーンショット 2017-12-10 1.48.57.png

うん、いい感じにサーバーの台数が8->9台に増えてますね。
感覚的にも、増えるとほぼ同時に画面側も更新されてるので
結構いい感じです。(減らした時も同じ感じでした。)

めでたしめでたし。

やってみて、感じたこと

Good

  1. 各サーバーのプロセスの可視化できると確かに「なんか」安心する。
  2. サーバー入らずに、プロセスのstart/stop/restartできるのは、運用的にもセキュリティ的にも楽。
  3. supervisordは、探しても記事とかあまりない?気がするが、本当にプロセスを「管理」するのであれば、感覚的には、まぁまぁ使えるんじゃないかと感じた。
  4. consul-templateの柔軟性が高く、consulの設計次第でなんでもできる感じがよい。
  5. 遊び半分で作ってみたが、思ったより評判はよさげだった笑

Not Good

  1. supervisord自体のプロセス監視がうまいことできていない。
  2. まだまだsupervisordの設定周りを理解しきれていない。。。
     ※ ネットワーク/権限/セキュリティ周りのところが今後の課題。。usernameとかなんか一致してなくても、取れちゃってる・・・?笑
  3. consulもまだまだ使えていない。。。
  4. cesiもいい感じだが、挙動不審なところが若干ある。笑
    ※ 他のダッシュボードもレガシー感がすごくて、あまり、、、supervisordのもういい感じの画面がほしいな。
    http://supervisord.org/plugins.html#dashboards-and-tools-for-multiple-supervisor-instances

さいごに

プロセスって結構気づいたら落ちている気がしますが
(「いや、お前のツールに対する理解が浅いだけだろ!」っていうツッコミはやめてください笑)

単純にダッシュボードという形で
「可視化」して、人の目との接触回数が増えるだけでも
保守/運用性は高まる気がするので
やっぱりダッシュボード的なのはいいなと思いました^^

p.s.
色々と設定ファイルを記載していますが
「ん?ここおかしくないか?」というところがあれば
ぜひ、コメントお願いいたします :bow:

続きを読む

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を!!

続きを読む

CloudFormationで、ECSのCI/CD環境を構築した際のハマりどころ 〜CodePipeline,CodeBuild,KMSも添えて〜

Classiアドベントカレンダー4日目です。
本日は、ECSを利用して、AWS上でAWSどっぷりのCI/CD環境を準備したときのお話になります。

今年のre:InventでEKSとFargateがリリースされましたが、東京リージョンに来てなかったり、プレビュー段階だったりで、まだしばらくは参考になる部分はありそうかなと^^;

1.背景

などで、AWS公式でもECS環境下のCloudFormation(以下、CFn)を使ったデプロイ方法が紹介されています。
とはいえ、現実の要件でCFnで実装しようとすると、デフォルト設定だと失敗したり、ドキュメントだけだと、GUIで設定できる部分がCFnでの書き方がわからかったりして、いくつかハマった内容があったので、3種類ぐらいの特徴を抜粋して書いてみようと思います。

2.TL;DR

ECSを使うなら、

  • ALBとECSの動的ポート機能を組み合わせる
  • IAM Role,KMS,SSMパラメータストアを組み合わせる
  • CodePipelineで複数リポジトリからのコード取得を行う

これらの機能を全部CFnでやろうとすると、一部aws-cliなどを使う必要がありますが、
ひとまずDevとOpsでうまく権限を分担したCI/CD環境を構築できるのではないかなと思います。

3.特徴解説

3-1. ALBとECSの動的ポート機能の組み合わせ

qiita_ecs_port.png

EC2へ割り当てるSecurityGroupは、ECSの動的ポート機能を利用するため、インバウンドのTCPポートを開放しておきます。

securitygroup.yml
ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
        VpcId: !Ref VpcId
       GroupName: sample
       GroupDescription: "ALB Serurity Group"
       SecurityGroupIngress:
            -
                CidrIp: 0.0.0.0/0
                IpProtocol: tcp
                FromPort: 443
                ToPort: 443
EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
        VpcId: !Ref VpcId
       GroupName: sample
       GroupDescription: "EC2 Serurity Group"
       SecurityGroupIngress:
            -
                SourceSecurityGroupId: !Ref ALBSecurityGroup
                IpProtocol: tcp
                FromPort: 0
                ToPort: 65535

ECSの動的ポートを有効にするため、PortMappingsの設定でホストのポートを0に設定します。

ecs.yml
ECSTask:
    Type: "AWS::ECS::TaskDefinition"
    Properties:
        Family: sample
        NetworkMode: bridge
        ContainerDefinitions:
            -
                Name: sample
                Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName}:${ImageTag}"
                Cpu: 2
                Memory: 128
                PortMappings:
                    -
                        ContainerPort: 80
                        HostPort: 0
                Essential: true
                Ulimits:
                    -
                        Name: nofile
                        SoftLimit: 65535
                        HardLimit: 65535
                Environment:
                    -
                        Name: TZ
                        Value: Asia/Tokyo
                LogConfiguration:
                    LogDriver: awslogs
                    Options:
                        awslogs-group: sample
                        awslogs-region: !Sub ${AWS::Region}
                        awslogs-stream-prefix: !Ref ImageTag
    Service:
        Type: "AWS::ECS::Service"
        Properties:
            ServiceName: sample
            Cluster: !Ref ECSCluster
            DesiredCount: 1
            TaskDefinition: !Ref ECSTask
            Role: !Ref ECSServiceRole
            PlacementStrategies:
                -
                    Type: spread
                    Field: instanceId
            LoadBalancers:
                -
                    ContainerName: sample
                    ContainerPort: 80
                    TargetGroupArn: !Ref ALBTargetGroup

注意点

複数のEC2でECSを運用するのであれば、PlacementStrategiesの設定を行っておかないと、random配置ECSのタスクが一つのホストだけに偏ってしまったりすることがあります。

3-2. DevとOpsで別gitリポジトリを運用しつつ、CodePipelineのデプロイフェーズでCFnのChangeSetを使う

qiita_codepipeline.png

デプロイにCFnを利用することで、デプロイの実行記録の管理やCFnで記載された部分のインフラ部分のテストを行いつつ、デプロイをすることが可能になります。
また、Sourceフェーズで、CFnの内容やEC2のASGやAMI設定の管理を行うOps管轄リポジトリと、Dockerコンテナ化するアプリロジックが含まれているDev管轄リポジトリを分割することで、
運用フェーズに入ったときにDevとOpsで独立して、デプロイを行うことができます。

codepipeline.yml
CodePipeline:
    Type: "AWS::CodePipeline::Pipeline"
    Properties:
        Name: sample
        ArtifactStore:
            Type: S3
            Location: sample
        RoleArn: !Ref BuildRole
        Stages:
            -
                Name: Source
                Actions:
                    -
                        Name: AppSource
                        RunOrder: 1
                        ActionTypeId:
                            Category: Source
                            Owner: ThirdParty
                            Version: 1
                            Provider: GitHub
                        Configuration:
                            Owner: !Ref GithubOwner
                            Repo: !Ref GithubAppRepo
                            Branch: !Ref GithubAppBranch
                            OAuthToken: !Ref GithubToken
                        OutputArtifacts:
                            - Name: AppSource
                    -
                        Name: InfraSource
                        RunOrder: 1
                        ActionTypeId:
                            Category: Source
                            Owner: ThirdParty
                            Version: 1
                            Provider: GitHub
                        Configuration:
                            Owner: !Ref GithubOwner
                            Repo: !Ref GithubInfraRepo
                            Branch: !Ref GithubInfraBranch
                            OAuthToken: !Ref GithubToken
                        OutputArtifacts:
                            - Name: InfraSource
            -
                Name: Build
                Actions:
                    -
                        Name: CodeBuild
                        RunOrder: 1
                        InputArtifacts:
                            - Name: AppSource
                        ActionTypeId:
                            Category: Build
                            Owner: AWS
                            Version: 1
                            Provider: CodeBuild
                        Configuration:
                            ProjectName: !Ref CodeBuild
                        OutputArtifacts:
                            - Name: Build
            -
                Name: CreateChangeSet
                Actions:
                    -
                        Name: CreateChangeSet
                        RunOrder: 1
                        InputArtifacts:
                            - Name: InfraSource
                            - Name: Build
                        ActionTypeId:
                            Category: Deploy
                            Owner: AWS
                            Version: 1
                            Provider: CloudFormation
                        Configuration:
                            ChangeSetName: Deploy
                            ActionMode: CHANGE_SET_REPLACE
                            StackName: !Sub ${AWS::StackName}
                            Capabilities: CAPABILITY_NAMED_IAM
                            TemplatePath: !Sub "Source::sample.yml"
                            ChangeSetName: !Ref CFnChangeSetName
                            RoleArn: !Ref BuildRole
                            ParameterOverrides: !Sub |
                                {
                                    "ImageTag": { "Fn::GetParam" : [ "Build", "build.json", "tag" ] },
                                    "AppName": "${AppName}",
                                    "OwnerName": "${OwnerName}",
                                    "RoleName": "${RoleName}",
                                    "StageName": "${StageName}",
                                    "VpcId": "${VpcId}"
                                }
            -
                Name: Deploy
                Actions:
                    -
                        Name: Deploy
                        ActionTypeId:
                            Category: Deploy
                            Owner: AWS
                            Version: 1
                            Provider: CloudFormation
                        Configuration:
                            ActionMode: CHANGE_SET_EXECUTE
                            ChangeSetName: !Ref CFnChangeSetName
                            RoleArn: !Ref BuildRole
                            StackName: !Sub ${AWS::StackName}

注意点

  • CodePipelineのキックは、PRがマージされたタイミングなので、(一応、CodePipelineにはTestフェーズもあるが)マージ前のテストなどはCircleCIとかに任せた方がよいかも
  • ParameterOverridesで上書きするパラメータは、CFnのParametersに設定している項目に応じて設定する
  • Sourceフェーズで持ってこれるリポジトリは2つまで。コンテナビルドに持ってくるのがもっとある場合、CodeBuild内でこちらの記事のように、githubから引っ張ってきて、ビルドするなどの対応が必要になりそう

3-3. CodeBuildでDockerイメージを作る際、KMSとSSMパラメータストアを利用する

qiita_codebuild.png

このあたりはAWSの恩恵をフルに受けている部分かなと。
RDSのパスワードや秘密鍵など、gitリポジトリ内で管理したくない情報は、SSMパラメータストアを使って、Dockerイメージを作成するときに環境変数を埋め込みます。

codebuild.yml
CodeBuild:
    Type: AWS::CodeBuild::Project
    Properties:
        Name: sample
        Source:
            Type: CODEPIPELINE
        ServiceRole: !Ref BuildRole
        Artifacts:
            Type: CODEPIPELINE
        Environment:
            Type: LINUX_CONTAINER
            ComputeType: BUILD_GENERAL1_SMALL
            Image: "aws/codebuild/docker:1.12.1"
            EnvironmentVariables:
                -
                    Name: AWS_DEFAULT_REGION
                    Value: !Sub ${AWS::Region}
                -
                    Name: AWS_ACCOUNT_ID
                    Value: !Sub ${AWS::AccountId}
                -
                    Name: IMAGE_REPO_NAME
                    Value: !Ref ECRRepoName

docker buildするときに、--build-argに秘匿情報として環境変数を引き渡し、できあがったイメージをECRにpushする。

buildspec.yml
version: 0.2

phases:
    pre_build:
        commands:
            - $(aws ecr get-login --region $AWS_DEFAULT_REGION)
            - IMAGE_TAG="${CODEBUILD_RESOLVED_SOURCE_VERSION}"
            - DB_PASSWORD=$(aws ssm get-parameters --names rds_pass --with-decryption --query "Parameters[0].Value" --output text)
    build:
        commands:
            - docker build --build-arg DB_PASSWORD="${DB_PASSWORD}" -t "${IMAGE_REPO_NAME}:${IMAGE_TAG}" .
            - docker tag "${IMAGE_REPO_NAME}:${IMAGE_TAG}" "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${IMAGE_TAG}"
    post_build:
        commands:
            - docker push "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${IMAGE_TAG}"
            - printf '{"tag":"%s"}' "${IMAGE_TAG}" > build.json
artifacts:
    files:
        - build.json
    discard-paths: yes
(snip)
ARG DB_PASSWORD
ENV DB_PASSWORD=${DB_PASSWORD}
(snip)

実運用する際は、IAM Roleを使う権限も意識して、KMSのKeyを利用するIAM UserやIAM Roleを設定する。

kms.yml
KMSKey:
    Type: "AWS::KMS::Key"
    Properties:
        Description: sample-key
        KeyPolicy:
            Version: "2012-10-17"
            Id: "key-default-1"
            Statement:
                -
                    Sid: "Allow use of the key"
                    Effect: "Allow"
                    Principal:
                        AWS: !GetAtt BuildRole.Arn
                    Action:
                        - "kms:DescribeKey"
                        - "kms:Decrypt"
                    Resource: "*"

注意点

  • SSMパラメータにおける、SecureString型の値登録
    3-3.でSSMパラメータストアで暗号化する際、SecureString型はCFnに対応していない。
    そのため、aws-cliで設定することにした。TerraformはSecureString型に対応しているので、CFn側でも対応して欲しいところ…
$ aws ssm put-parameter --name rds-pass --value PASSWORD --type SecureString --key-id hogehoge

4. その他の雑多なハマりどころ

4-1. ECSのAMIのデフォルト設定

  • EBSのストレージタイプのデフォルトがHDD
    LaunchConfigurationのBlockDeviceMappingsで、gp2を明示的に指定してあげる。
  • WillReplace用のシグナルを送るcfn-signalが未インストール
    UserDataの中で記載しておく。シグナルを送るタイミングは、どこまでAMIに手を入れるかによって変更する。
LaunchConfig:
    Type: "AWS::AutoScaling::LaunchConfiguration"
    Properties:
        AssociatePublicIpAddress: true
        KeyName: sample
        IamInstanceProfile: sample
        ImageId: ami-e4657283
        SecurityGroups:
            - !Ref SecurityGroup
        InstanceType: t2.micro
        BlockDeviceMappings:
            -
                DeviceName: "/dev/xvda"
                Ebs:
                    VolumeType: gp2
                    VolumeSize: 30
        UserData:
            Fn::Base64: !Sub |
                #!/bin/bash
                echo ECS_CLUSTER=${ECSClusterName} >> /etc/ecs/ecs.config
                sudo yum install -y aws-cfn-bootstrap
                sleep 60
                /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource AutoScalingGroup --region ${AWS::Region}
AutoScalingGroup:
    Type: "AWS::AutoScaling::AutoScalingGroup"
    Properties:
        LaunchConfigurationName: sample
        DesiredCapacity: 2
        MaxSize: 3
        MinSize: 2
        VPCZoneIdentifier:
            - !Ref PublicSubnet1
            - !Ref PublicSubnet2
    CreationPolicy:
        ResourceSignal:
            Count: 1
            Timeout: PT5M
    UpdatePolicy:
        AutoScalingReplacingUpdate:
            WillReplace: true

5.まとめ

もう少しきれいな書き方がありそうだけど、実運用でよくある要件の参考程度になれば幸いです。
EC2のASGまわりの設定は、従来のECSだとこのような形で大分インフラ側を意識しないといけない構成です。今後、re:Inventで発表されたEKSやFargateなどとも比べながら、本環境をアップデートしていければよいなと思います。

続きを読む

KubernetesでAWS ALBを自動作成する〜ついでにRoute53 Record Setも

kube-ingress-aws-controllerを使います。

kube-ingress-aws-controllerとは

Zalandoが公開している、Kubernetes用のIngress Controllerの一つです。

ZalandoはKubernetes界隈では著名な、ヨーロッパでファッションECをやっている企業です。
Kubernetesコミュニティへの様々な形で貢献していて、今回紹介するkube-aws-ingerss-controllerや先日紹介したexternal-dnsもその一つです。

何かできるのか

KubernetesユーザがAWSを全く触らずとも

  • ALBの自動作成
  • ALBに割り当てるTLS証明書(ACM管理)を自動選択

をしてくれます。

使い方

KubernetesのIngressリソースを普段通りつくります。

kubectl create -f myingress.yaml

すると、1~2分ほどでIngressリソースに書いたホスト名でインターネットからアクセスできるようになります。

open https://myingress.exapmle.com

Ingress Controllerとは

KubernetesのIngressはL7ロードバランサのスペックのようなもので、そのスペックから実際にL7ロードバランサをセットアップするのがIngress Controllerの役割です。

coreos/alb-ingress-controllerとの違い

coreos/alb-ingress-controller

  • Ingressリソース一つに対して、1 ALBをつくります

zalando-incubator/kube-ingress-aws-controller

  • ACM証明書のドメイン一つに対して、ALBを割り当てます
  • 同じドメイン名に対するルートを含むIngressリソースは、一つのALBにまとめられます
  • ALBのターゲットグループにEC2インスタンスを割り当てるところまでしかやってくれない!ので、実際にIngressとして利用するためには他のIngress Controllerを併用する必要があります

kube-ingress-aws-controllerのセットアップ手順

Security Groupの作成

kube-ingress-aws-controllerはALBに割り当てるSecurity Groupまでは自動作成してくれないので、AWSコンソール等で作成します。

kube-ingerss-aws-controllerのドキュメントにはCloudFormationを使った手順がかいてあります。

同等のSecurity Groupをawscliで作る場合は以下のようなコマンドを実行します。

CLUSTER_NAME=...
VPC_ID=vpc-...

aws ec2 create-security-group \
  --description ${CLUSTER_NAME}-kube-aws-ingress-controller-alb \
  --group-name ${CLUSTER_NAME}-kube-aws-ingress-controller-alb \
  --vpc-id $VPC_ID | tee sg.json

SG_ID=$(jq -r '.GroupId' sg.json)

aws ec2 create-tags --resources $SG_ID --tags \
  "Key=\"kubernetes.io/cluster/$CLUSTER_NAME\",Value=owned" \
  "Key=\"kubernetes:application\",Value=kube-ingress-aws-controller"

aws ec2 authorize-security-group-ingress \
  --group-id $SG_ID \
  --ip-permissions '[{"IpProtocol": "tcp", "FromPort": 80, "ToPort": 80, "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}, {"IpProtocol": "tcp", "FromPort": 443, "ToPort": 443, "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}]'

aws ec2 describe-security-groups --group-id $SG_ID

# あとで: 不要になったらクラスタやVPCの削除前に以下のように削除
aws ec2 delete-security-group --group-id $SG_ID

IAMポリシーの割当

kube-ingerss-aws-controllerのドキュメントにIAM Policy Statementの一覧がかいてありますが、要約すると

  • CloudFormationスタックのCRUD権限
  • ACM証明書、VPC、RouteTable、Subnet、Security Group、AutoScalingGroup、EC2 InstanceのGet/List/Describe権限
  • ALBのCRUD権限

が必要です。

kube-awsのcluster.yamlの場合は、以下のように書きます。

worker:
  nodePools:
  - iam:
      policy:
        statements:
        - effect: Allow
          actions:
          - "autoscaling:DescribeAutoScalingGroups"
          - "autoscaling:AttachLoadBalancers"
          - "autoscaling:DetachLoadBalancers"
          - "autoscaling:DetachLoadBalancerTargetGroup"
          - "autoscaling:AttachLoadBalancerTargetGroups"
          - "elasticloadbalancing:AddTags"
          - "elasticloadbalancing:DescribeLoadBalancers"
          - "elasticloadbalancing:CreateLoadBalancer"
          - "elasticloadbalancing:DeleteLoadBalancer"
          - "elasticloadbalancing:DescribeListeners"
          - "elasticloadbalancing:CreateListener"
          - "elasticloadbalancing:DeleteListener"
          - "elasticloadbalancing:DescribeTags"
          - "elasticloadbalancing:CreateTargetGroup"
          - "elasticloadbalancing:DeleteTargetGroup"
          - "elasticloadbalancing:DescribeTargetGroups"
          - "elasticloadbalancingv2:DescribeTargetGroups"
          - "elasticloadbalancingv2:DescribeLoadBalancers"
          - "elasticloadbalancingv2:CreateLoadBalancer"
          - "elasticloadbalancingv2:DeleteLoadBalancer"
          - "elasticloadbalancingv2:DescribeListeners"
          - "elasticloadbalancingv2:CreateListener"
          - "elasticloadbalancingv2:DeleteListener"
          - "elasticloadbalancingv2:DescribeTags"
          - "elasticloadbalancingv2:CreateTargetGroup"
          - "elasticloadbalancingv2:DeleteTargetGroup"
          - "ec2:DescribeInstances"
          - "ec2:DescribeSubnets"
          - "ec2:DescribeSecurityGroup"
          - "ec2:DescribeRouteTables"
          - "ec2:DescribeVpcs"
          - "acm:ListCertificates"
          - "acm:DescribeCertificate"
          - "iam:ListServerCertificates"
          - "iam:GetServerCertificate"
          - "cloudformation:Get*"
          - "cloudformation:Describe*"
          - "cloudformation:List*"
          - "cloudformation:Create*"
          - "cloudformation:Delete*"
          resources:
          - "*"

kube-aws-ingress-controllerをデプロイ

$ kubectl apply -f kube-aws-ingress-controller.yaml
kube-aws-ingress-controller.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: kube-ingress-aws-controller
  namespace: kube-system
  labels:
    application: kube-ingress-aws-controller
    component: ingress
spec:
  replicas: 1
  selector:
    matchLabels:
      application: kube-ingress-aws-controller
      component: ingress
  template:
    metadata:
      labels:
        application: kube-ingress-aws-controller
        component: ingress
    spec:
      containers:
      - name: controller
        image: registry.opensource.zalan.do/teapot/kube-ingress-aws-controller:latest
        env:
        - name: AWS_REGION
          value: ap-northeast-1

併用するIngress Controllerをデプロイ

今回はskipperを使ってみます。

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: skipper-ingress
  namespace: kube-system
  labels:
    component: ingress
spec:
  selector:
    matchLabels:
      application: skipper-ingress
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      name: skipper-ingress
      labels:
        component: ingress
        application: skipper-ingress
    spec:
      hostNetwork: true
      containers:
      - name: skipper-ingress
        image: registry.opensource.zalan.do/pathfinder/skipper:latest
        ports:
        - name: ingress-port
          containerPort: 9999
          hostPort: 9999
        args:
          - "skipper"
          - "-kubernetes"
          - "-kubernetes-in-cluster"
          - "-address=:9999"
          - "-proxy-preserve-host"
          - "-serve-host-metrics"
          - "-enable-ratelimits"
          - "-experimental-upgrade"
          - "-metrics-exp-decay-sample"
          - "-kubernetes-https-redirect=true"
        resources:
          limits:
            cpu: 200m
            memory: 200Mi
          requests:
            cpu: 25m
            memory: 25Mi
        readinessProbe:
          httpGet:
            path: /kube-system/healthz
            port: 9999
          initialDelaySeconds: 5
          timeoutSeconds: 5

WorkerノードのSecurity Group設定変更

今回はskipperをつかうことにしたので、kube-ingress-aws-controllerが作成したALBからアクセスする先はskipper(がいるEC2インスタンス)になります。

Security GroupへALBからskipperがいるEC2インスタンスへの通信をブロックしたままだとGateway Timeoutになってしまいます。そうならないように、ALB用につくったSGから、WorkerノードのSGへの9999番ポート(kube-ingress-aws-controllerと組み合わせて使うskipperのhostPortに指定した)の通信を許可しましょう。

ALB側SGのOutboundを絞っていないのであれば、Worker側SGのInboundを追加すればOKです。

Ingressリソースの作成

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: nginx
spec:
  rules:
  - host: nginx-ingress.example.com
    http:
      paths:
      - backend:
          serviceName: nginx
          servicePort: http

---

apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 80
    targetPort: 80
  selector:
    app: nginx

---

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        ports:
        - containerPort: 80

ログの確認

$ k logs kube-ingress-aws-controller-6bbd8f9d6c-bcqph
2017/12/06 12:33:52 starting /bin/kube-ingress-aws-controller
2017/12/06 12:33:54 controller manifest:
2017/12/06 12:33:54     kubernetes API server:
2017/12/06 12:33:54     Cluster ID: k8s3
2017/12/06 12:33:54     vpc id: vpc-12345678
2017/12/06 12:33:54     instance id: i-07e29f841f676ca00
2017/12/06 12:33:54     auto scaling group name: k8s3-Nodepool1-MMF7MXKI9350-Workers-BZWB5IAV7JW8
2017/12/06 12:33:54     security group id: sg-8368a6fa
2017/12/06 12:33:54     private subnet ids: []
2017/12/06 12:33:54     public subnet ids: [subnet-12345678 subnet-23456789]
2017/12/06 12:33:54 Start polling sleep 30s

30秒経過後、以下のようにCloudFormationスタックが作成される。

2017/12/06 12:34:24 Found 1 ingresses
2017/12/06 12:34:24 Found 0 stacks
2017/12/06 12:34:24 Have 1 models
2017/12/06 12:34:24 creating stack for certificate "arn:aws:acm:ap-northeast-1:myawsaccountid:certificate/64f33935-05ac-4e6b-b1ae-58973d556a74" / ingress ["kube-system/nginx"]
2017/12/06 12:34:25 stack "arn:aws:cloudformation:ap-northeast-1:myawsaccountid:stack/k8s3-b9dbfe3/caf1f3a0-da81-11e7-9e21-500c28b97482" for certificate "arn:aws:acm:ap-northeast-1:myawsaccountid:certificate/64f33935-05ac-4e6b-b1ae-58973d556a74" created
2017/12/06 12:34:25 Start polling sleep 30s

作成されたAWSリソースの確認

1〜2分待ってスタックがCREATE_COMPLETE状態になれば成功。

コンソールでCloudFormationスタックのResourcesの内容を見ると、何がつくられたのかがわかる。

image.png

つくられるのは以下の4つ。

  • HTTPListener: 80番ポート用のListener
  • HTTPSListener: 443番ポート用のListener
  • LB: ALB
  • TG: kube-ingress-aws-controllerがデプロイされているノードが登録されたTargetGroup

ALB

リスナー

443番ポート用のListenerには、Ingressリソースに書いたドメインに対応するACM証明書が選択されています。

今回は*.example.com用のワイルドカード証明書を事前に用意しておいたのですが、Ingressにnginx-ingress.example.comというホスト名を設定したところ、ちゃんとワイルドカード証明書を探し出してくれました(かしこい)。

image.png

ターゲットグループ

kube-aws-ingress-controllerがデプロイされたノードのASGをコンソールでみてみると、ターゲットグループに割り当てられていました。EC2インスタンスを直接TargetGroupに登録していくような方法だとインスタンスが落ちた場合などが怖いですが、ちゃんとしてますね。

image.png

Route53 RecordSetの作成

これだけだとALBが作成されただけなので、nginx-ingress.example.comでアクセスできないはずです。

しかし、昨日デプロイしたexternal-dnsがIngressリソースとALBを検知して、勝手にRecordSetをつくってくれていました。

stern_external-dns.log
external-dns-768686fd4c-zpnlx external-dns time="2017-12-06T12:43:53Z" level=info msg="Desired change: CREATE nginx-ingress.example.com A"
external-dns-768686fd4c-zpnlx external-dns time="2017-12-06T12:43:53Z" level=info msg="Desired change: CREATE nginx-ingress.example.com TXT"
external-dns-768686fd4c-zpnlx external-dns time="2017-12-06T12:43:53Z" level=info msg="Record in zone example.com. were successfully updated"

image.png

ちゃんとALBへのA(lias)レコードを作成してくれていますね。

 インターネットからアクセスしてみる

nginx-ingress.example.comにブラウザからアクセスしてみて、以下のようなnginxのウェルカムページが表示されてば成功です。おつかれさまでした。

image.png

まとめ

kube-ingress-aws-controllerを使うと、Kubernetesユーザはkubectl createするだけでALBとRecordSetをよしなにセットアップしてくれます。
ALBの作成・管理やRoute53 RecordSetの作成のためにいちいちインフラエンジニアを呼び出したくない!というようなセルフサービス好きの会社さんでは特に役立つのではないでしょうか?!

トラブルシューティング

unable to get details for instance “i-0346d738155e965d8”

IAMポリシーが足りないときに出るエラーです。

$ k logs kube-ingress-aws-controller-7f7974ff58-6bvv8
2017/12/06 07:11:51 starting /bin/kube-ingress-aws-controller
2017/12/06 07:11:53 unable to get details for instance "i-0346d738155e965d8": NoCredentialProviders: no valid providers in chain. Deprecated.
    For verbose messaging see aws.Config.CredentialsChainVerboseErrors

required security group was not found

Security Groupがないか、またはSecurityGroupのタグが間違っているか、EC2インスタンスにkubernetes.io/cluster/クラスタ名=ownedというタグがついていない場合のエラーです。

$ k logs kube-ingress-aws-controller-7f7974ff58-xqgrq
2017/12/06 08:10:40 starting /bin/kube-ingress-aws-controller
2017/12/06 08:10:41 required security group was not found

CloudFormationで「At least two subnets in two different Availability Zones must be specified」

KubernetesのWorkerノードのASGが単一のAZに割り当てられているときのエラー。ALBの仕様で、最低2つのAZが必要。

kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:06 Start polling sleep 30s
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:36 Found 1 ingresses
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:37 Found 0 stacks
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:37 Have 1 models
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:37 creating stack for certificate "arn:aws:acm:ap-northeast-1:myaccountid:certificate/64f33935-05ac-4e6b-b1ae-58973d556a74" / ingress ["kube-system/nginx"]
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:37 stack "arn:aws:cloudformation:ap-northeast-1:myaccountid:certificate:stack/k8s3-b9dbfe3/360e1830-da7d-11e7-99f7-500c596c228e" for certificate "arn:aws:acm:ap-northeast-1:myaccountid:certificate:certificate/64f33935-05ac-4e6b-b1ae-58973d556a74" created

image.png

instance is missing the “aws:autoscaling:groupName” tag

ASG以外で作ったEC2インスタンスにkube-ingress-aws-controllerがデプロイされてしまったときのエラー。

$ k logs kube-ingress-aws-controller-7f7974ff58-m6ss2
2017/12/06 12:25:59 starting /bin/kube-ingress-aws-controller
2017/12/06 12:25:59 instance is missing the "aws:autoscaling:groupName" tag

kube-aws-ingress-controllerは、デフォルトではASGに設定されたSubnetをALBのSubnetに流用する。
そのためにASGを探すとき、EC2インスタンスについたaws:autoscaling:groupNameというASGが自動的につけてくれるタグをヒントにするため、ASG以外でつくったEC2インスタンスではこのエラーが出てしまう。

Ref: Spot Fleet support · Issue #105 · zalando-incubator/kube-ingress-aws-controller

Issueも出ているが、まだASG以外は対応していない。ワークアラウンドとしては、kube-ingress-aws-controllerのaffinityでASGでつくったノードにだけスケジュールされるようにすることが考えられる。

kube-awsの場合、awsNodeLabels機能をオンにすると、ASGでつくったノードには”kube-aws.coreos.com/autoscalinggroup”というラベルが付与されるので、それを前提にすると以下のようなaffinityをかけばOK。

              affinity:
                nodeAffinity:
                  requiredDuringSchedulingIgnoredDuringExecution:
                    nodeSelectorTerms:
                    - matchExpressions:
                      - key: "kube-aws.coreos.com/autoscalinggroup"
                        operator: "Exists"

504 Gateway Time-out

ALB経由でnginxにアクセスしようとしてこのエラーがかえってきた場合、ALB用につくったセキュリティグループからkube-aws-ingress-controllerが動いているEC2インスタンスへのアクセスを許可できていない可能性があります。

EC2インスタンス側のSGに、ALB用SGからの9999番ポート(kube-ingress-aws-controllerと組み合わせて使うskipperのhostPortに指定した)への通信をを許可するようなInboundルールを追加しましょう。

続きを読む

Lambdaを使ってAWS BatchにjobをSubmitする方法(1)

「 00. はじめに 」

「AWS Lambda」を使って、「AWS Batch」にjobをSubmitするまでの実装 (設定) 手順をまとめたいと思います:point_up:

今回考えている構成は、下記の通り。

overview (2).jpg

処理フロー

  1. 特定のBucketにファイルをアップロード (ユーザー)
  2. ファイルアップロードを検知 (Lambda:トリガー)
  3. AWS Batchにjobを投げる (Lambda: index.js)

この構成を実現するために実装が必要な部分は、下記の通り。

(1) S3バケットの作成 ( 本記事では、割愛 )
(2) AWS Batchの初期設定
(3) Amazon Lambda関数作成
(4) [検証]ファイルアップロード

AWSBatch.jpg 「 01. AWS Batchの初期設定 」

AWS Batchとは?

aws_batch.jpg

「Job definitions(ジョブ定義)」を使って生成した「 Jobs (ジョブ) 」「 Job queues (ジョブキュー) 」に投げると、job queuesに紐づいた「 Compute environments (コンピューティング環境) 」をプロビジョニングし、
job definition内で定義されたECR ( EC2 Container Registory ) または、Docker Hubからコンテナイメージを取得して、
自動でバッチ処理を実行してくれるマネージドサービスです。

(※1) 後述「 Minimum vCPUs 」を 0 に指定しておくことで、実行しているタスクがない場合、
インスタンスを起動してから1時間以内に自動で停止・削除 ( Terminate ) を行ってくれます。


AWS Batchは、バッチ処理を行う実行コマンド、インフラ環境等をあらかじめ設定しておくためのものなので、
この時点では利用料金は無料です。AWS Batchの設定に基づいて起動したECS ( EC2クラスタ ) やその他、
AWS Batchにより起動したサービスにおいて従量課金モデルが適用されるので、デモ等を行った後は、
不要なインスタンスが立ち上がったままになっていないかチェックしてみてください!


AWS Batchに触れたことのない私のようなユーザーは、
まず最初に、下記の2つの記事を読むとサービスのイメージを掴みやすいと思います。

それでは、AWS Batchを扱う上で押さえておくべき3つのコンポーネントについて紹介したいと思います。
(上記記事内でも触れられている内容ですが、さらに初心者にも分かりやすいよう表現したつもりです。)

AWS Batch の3つのコンポーネントについて!

image.png
[ 図 : AWS Batchコンソール画面 ]

1. Job definitions [ ジョブ定義 ]

ジョブとして起動するコンテナイメージ、実行コマンド、環境変数、マウントするボリューム等を
指定することができます。また、エラー発生時のリトライ回数の指定を行うことも可能です。

分かりやすく一言で言うと、ジョブを生成する際のテンプレートです。

2. Job queues [ ジョブキュー ]

AWS Batch用のジョブキューです。 FIFO (First In First Out) に対応しており、
キューには優先順位をつけることができます。

サブミットされたジョブを管理するキュー。
どのコンピューティング環境でジョブを実行するのか紐づけをすることが可能です。

3. Compute environments [ コンピューティング環境 ]

バッチ処理を行うECS(クラスタ)の定義を行います。インスタンスタイプから、key pair、
ロールなどを指定することができます。また、AutoScalingの設定も可能です。

実際に処理を行うコンピューティング環境の設定が可能です。

実践編

なにはともあれ、設定画面を見ずにテキストでできるだけ教えてもらっても意味がないので、
実際の設定を行いながら、解説を進めていきたいと思います。

image.png

さっそく、[ Get started ] をクリックして進めていきます。
はじめてAWS Batchの設定を行う際に表示される「初期設定ウィザード」はそのまま進めていき、
デフォルトのまま作成すると、後で個別の設定を行う際に楽になるので、とにかくデフォルトのまま進めていきます。
(※また、jobが実行されインスタンスが立ち上がりますので、削除しておいてください。)

[ 参考サイト ]
AWS Batchを使って5分以上かかる処理を実行してみる

1. Compute environments ( コンピューティング環境 ) の設定


image.png

  • Compute environment type は、「 Managed 」のままにしておく。
  • 今回は「初回設定ウィザード」を使って作成したコンピューティング環境と区別をしたいので、適当に名前を付ける。
  • 「Desired VCPUs」は起動するインスタンスの希望するvCPUの数なので、1~2に設定しておくのが良いと思います。
  • また、冒頭でも記載しましたが、「Minimun vCPUs」を 0 に設定しておくと、
    起動したインスタンスの実行タスクがなくなってから一時間以内に自動的にterminatedしてくれます。

詳しい設定方法については、下記URLを参照いただければと思う。

実際に、上記条件で、コンピューティング環境を作成してみると、、
ECSのクラスターが自動生成されていることが確認できると思います。
AWS Batchの裏側ではECSが動いているので、AWS Batchでタスクを実行すると、
こちらでもステータスを確認することができます。

image.png

2. job queues の設定


新たにjob queuesを作成します。名前を適当に付けて、「Priority 1」「Select a compute environment 〇〇〇(さきほど作ったコンピューティング環境)」を設定し、Createボタンを押下すれば作業終了です。
image.png

続いて、job definitionsの設定に移ります。

3. job definitions の設定


image.png

「 02. AWS Lambda関数を作成する 」

1. 名前と、Lambda関数を実行するロールを選択します。

image.png
今回は、初期設定ウィザードに従って作成したユーザー(lambda_basic_execution)を使用します。

2. トリガーの設定を行います。

今回の構成では、S3(特定のファイルバケット)へのファイルアップロードをLambda関数のトリガーとして設定します。
image.png

image.png

image.png

image.png
「トリガーの有効化」にチェックを入れておくと、設定後すぐに動作するようになります。

3. 設定を追加します。

2.で指定したイベントが発生すると、「AWS Batch」へ「job」を投げるソースコードを追加します。
今回は、コードをインラインで編集しますが、ZIPファイルS3から読み込むことも可能です。

image.png

index.js
'use strict';

const AWS = require('aws-sdk');
const S3  = new AWS.S3();

// AWS Batch Setting
const BATCH         = new AWS.Batch({apiVersion: '2016-08-10'});
const JOBDEFINITION = 'YOUR_JOBDEFINITION';
const JOBQUEUE      = 'YOUR_JOBQUEUE';

console.log('Loading function');

exports.handler = (event, context, callback) => {

    // どこにファイルがアップロードされたのかを検知します。  
    let bucket = event.Records[0].s3.bucket.name;
    let key = event.Records[0].s3.object.key;
    let filePath = bucket + '/' + key;
    console.log(filePath);

    let params = {
        jobDefinition: JOBDEFINITION, 
        jobName: 'JOB_NAME(任意の値)',
        jobQueue: JOBQUEUE, 
        parameters: { 'path' : filePath } // jobにパラメータを付加することも可能です。
    };

    // Submit the Batch Job
    BATCH.submitJob(params, function (err, data) {
        if (err) console.log(err, err.stack);
        else console.log(data);
        if (err) {
            console.error(err);
            const message = `Error calling SubmitJob for: ${event.jobName}`;
            console.error(message);
            callback(message);
        } else {
            const jobId = data.jobId;
            console.log('jobId:', jobId);
            callback(null, jobId);
        } 
    });
};

jobをサブミットする際に、任意のパラメーターも付与することができますが、
job queues側で事前に設定していないとエラーがでるので、ご注意ください。
ちなみに、上記ソースコードのparameter:pathをjob実行時に使用したい場合は、
下記のように設定することができます。

image.png

詳しくは、下記公式サイトをご参照ください。
http://docs.aws.amazon.com/batch/latest/userguide/job_definition_parameters.html

「 03. 実際に作成したLambda関数を動かしてみる。 」

実際に、S3 (トリガーで設定したバケット) にファイルをアップロードしてみると、
関数が実行されることが分かると思います。AWS Batchのダッシュボードにて job のステータスを確認することが可能です。

image.png

「 04. おわりに 」

実際に手を動かして、作業を行うことで知識が身につくと思うので、
みなさんも是非試してみてください。最後までお読みいただき、ありがとうございました。
記事も随時アップデートしていく予定なので、「いいね」よろしくお願いいたします。

続きを読む

Packer初歩的な利用方法

00. はじめに

HashiCorpさんの『Packer』
コチラの初歩的な使い方を整理してみました。
Packer_logo.png

00-1. 検証環境

・PackerはMulti Platformに対応しており、それが特徴でもありますが、今回はAWS上での確認のみになります。

■検証環境

Packer v1.1.1
Amazon Linux AMI release 2017.09

01. Packerとは

01-1. 特徴

・公式ドキュメントに簡潔に記載されています。
https://www.packer.io/intro/index.html

上記説明をもとに私の理解は下記になります。

1) 各IaaS・仮想化ソフトウェア/コンテナ管理ソフトウェア(※)にて、カスタムしたマシンイメージを作成できるツール
※AWS、Azure、Google Cloud、VMware、VirtualBoxなど多数のプラットフォームに対応

2) 一度の実行で複数の環境へマシンイメージを作成することが可能
例: AWSでAMIを作成、 並行してAzureでイメージを作成する 等

3) マシンイメージのプロビジョニングをコード管理できる
「Infrastructure as Code」の一種

01-2. Use Case

・ユースケースに関しても、公式に簡潔な説明があります。
https://www.packer.io/intro/use-cases.html

・主な用途としてはベースとなるイメージを要件に沿ってプロビジョニング(サーバセットアップ、コードデプロイ等)した上、カスタムイメージを作成することが挙げられるかと思います。

※個人的な話ですが、AMIを作成するツールと聞いていたため、触るまでは単なるバックアップツールの一種と思っていました。しかし、上述の特徴およびユースケースからも分かるように起動中のEC2からAMIを作成する機能は無いようです(バックアップツールではないのでした)。
→ AWSでしか確認していないため、他platformでも同様のことが言えるか確認できておりませんことご了承ください。

01-3. 考慮すべき事項

・例えば、AutoScalingの起動用AMIを継続的に更新したい場合
プロビジョニングのため該当AMIより一旦インスタンスを起動する必要があるため、同一サーバが重複起動することになります。その際の懸念点(例: S3 や Cloudwatch Logsへの自動ログアップデート処理など)を考慮しておく必要がありそうです。具体的にはそういったサービスは実稼働のlaunch時のみ行うようにする等が考えられるかと思います。

02. 実装例の前に

02-1. 導入はかんたん

・Packerの導入方法はとても簡単ですので、公式の説明を参照下さい。
https://www.packer.io/intro/getting-started/install.html

・また各種コマンドオプションもとてもシンプルになっています。
https://www.packer.io/docs/commands/build.html
https://www.packer.io/docs/commands/validate.html
→ 同HashiCorpさんの『Terraform』同様、構文チェックがあるのはありがたいです。

02-2. Tips

■ 日付付与

1) 例えば「ami_name」をランダムにすべく、日付を付与する場合

{ "ami_name": "Packer {{isotime | clean_ami_name}}" }

↓この場合、下記のようなAMI名となります。
「Packer 2016-07-18T13-22-19Z」
https://www.packer.io/docs/templates/engine.html

2) 文字列_YYYYMMDD-HHMM としたい場合は下記のように指定

{"文字列_{{isotime "20060102-0304"}}" }

↓ 下記のような出力結果に
「文字列_20171120-2008」

※format指定時の”(ダブルコーテーション)はエスケープする必要がありました。
※年は西暦後半2桁しか出力されないため、上二桁を足しています。
TimezoneはUTC固定。この方法ではJSTなどに変更することは出来ないとのことです。

▽format Referece

a date Numeric
Year 06
Month 01(or Jan)
Date 02
Hour 03(or 15)
Minute 04
Second 05

03. 実装例

03-1. 基本的なセットアップがされているAMIの作成

■ 概要

・amzn-linuxのAMIを基に、システム関連の設定、ログインユーザの作成など
サーバ構築時によく行われるセットアップを施したAMIを作成してみようと思います。

■ 詳細

1) 公式配布のAmazon Linuxをベースに
2) yum update実施 (amzn-linuxは配布時点で最新になっているので、この場合不要ですが)
3) TimeZoneやlocaleを日本に
4) 第三者のログイン用ユーザ「guest」を作成。公開鍵も配置
5) swap領域も確保

03-2. 事前準備

■ 03-2-a. ファイル

・packer実行サーバ上に該当ユーザの鍵ペアを作成しておきました。

/home/guest/.ssh/authorized_keys
/home/guest/.ssh/guest.pem

■ 03-2-0. シェルスクリプト

・補足
PackerのTemplate(JSON形式)に直接記載することも可能ですが、
JSONの書式規定により正規表現の利用が難しいため、外部シェルスクリプトを呼び出すというシンプルな形をとってみました。

03-2-1. general.sh

・全般的なものを定義。今回はYum updateのみ

#!/bin/sh

sudo yum -y update

注意: 起動したインスタンスがインターネットに出られる環境であること
※起動時にアサインする VPC(NAT、IGW)、Subnet、SecurityGroupに注意
一時的に起動する場所のため本番と同じNW環境でなくとも問題ありません

03-2-2. system_setup.sh

・TimeZoneなどシステム関連の設定

#!/bin/sh

sudo sed -i -e 's/ZONE="UTC"/ZONE="Asia/Tokyo"/g' /etc/sysconfig/clock
sudo cp  -f /usr/share/zoneinfo/Japan /etc/localtime
sudo sed -i -e 's/repo_upgrade: security/repo_upgrade: none/g' /etc/cloud/cloud.cfg
sudo sed -i -e 's/en_US.UTF-8/ja_JP.UTF-8/g' /etc/sysconfig/i18n

03-2-3. create_user.sh

・ユーザ作成

#!/bin/sh

sudo groupadd -g 1000 guest
sudo useradd -u 1000 -g 1000 guest
sudo echo password1234 | sudo passwd --stdin guest
sudo gpasswd -a guest wheel
sudo sed -i -e "s/^#(.*NOPASSWD.*)/1/" /etc/sudoers
sudo mkdir /home/guest/.ssh
sudo chmod 700 /home/guest/.ssh

秘密鍵をイメージ内に残したくないため、ここでは用意していません。

03-2-4. create_swap.sh

・Swap領域定義

#!/bin/sh

sudo dd if=/dev/zero of=/swap bs=1M count=1024
sudo mkswap /swap
sudo chmod 600 /swap
sudo swapon /swap
sudo sed -i -e '$ a /swap       swap        swap    defaults        0   0' /etc/fstab

必要があれば

03-3. テンプレート

basic-setup.json
{
    "builders":
    [
        {
            "type": "amazon-ebs",
            "ami_name": "mitzi_base_{{isotime "20060102-0304"}}",
            "region": "ap-northeast-1",
            "source_ami": "ami-2803ac4e",
            "instance_type": "t2.micro",
            "ssh_username": "ec2-user",

            "security_group_ids": ["sg-962132f1", "sg-d02033b7"],
            "vpc_id": "vpc-505ac034",
            "subnet_id": "subnet-75f7da03",
            "associate_public_ip_address": true,
            "ssh_timeout": "5m",
            "tags": {
              "OS_Version": "Amazon Linux 2017.09.01",
              "BillingType": "RESEARCH"
            }
        }
    ],
    "provisioners": [
      {
        "type": "shell",
        "scripts": [
            "./scripts/general.sh",
            "./scripts/system_setup.sh",
            "./scripts/create_user.sh",
            "./scripts/create_swap.sh"
        ]
      },
      {
         "type": "file",
         "source": "/home/guest/.ssh/authorized_keys",
         "destination": "/tmp/authorized_keys"
      },
      {
        "type": "shell",
        "inline": [
        "sudo mv /tmp/authorized_keys /home/guest/.ssh/authorized_keys",
        "sudo chmod 600 /home/guest/.ssh/authorized_keys",
        "sudo chown -R guest:guest /home/guest"
        ]
      }
    ]
}

03-3-1. ポイント

source_amiから起動したインスタンスへのSSH接続には、Packerが用意する一時的なキーペアが利用されます。

Credential keyをコード上にもたせたくなかったため、packer実行インスタンスにIAM Roleを付与しています。

・事前に用意していた公開鍵を起動したインスタンスに配置しています。
→ 「/home/guest/.ssh/」配下に直接配置しようとしましたが権限無しとエラーになるため、一旦 /tmp 配下に配置しています。 (配置先のpermissionを777にしても駄目でした)

03-4. 実行結果

・実行すると下記のようなログが標準出力されます。
(日本語設定を盛り込んでから一部文字化けするようになりました。)

・構文チェック

[root@ip-10-100-5-195 ~]# packer validate basic-setup.json 
Template validated successfully.

・build実行

[root@ip-10-100-5-195 ~]# packer build basic-setup.json 
amazon-ebs output will be in this color.

==> amazon-ebs: Prevalidating AMI Name: mitzi_base_20171120-0731
    amazon-ebs: Found Image ID: ami-2803ac4e
==> amazon-ebs: Creating temporary keypair: packer_5a1284ed-27b6-4c78-e7aa-9b86380a5e56
==> amazon-ebs: Launching a source AWS instance...
==> amazon-ebs: Adding tags to source instance
    amazon-ebs: Adding tag: "Name": "Packer Builder"
    amazon-ebs: Instance ID: i-01a24d283513a107a
==> amazon-ebs: Waiting for instance (i-01a24d283513a107a) to become ready...
==> amazon-ebs: Waiting for SSH to become available...
==> amazon-ebs: Connected to SSH!
==> amazon-ebs: Provisioning with shell script: ./scripts/general.sh
    amazon-ebs: Loaded plugins: priorities, update-motd, upgrade-helper
    amazon-ebs: No packages marked for update
==> amazon-ebs: Provisioning with shell script: ./scripts/system_setup.sh
==> amazon-ebs: Provisioning with shell script: ./scripts/create_user.sh
    amazon-ebs: 繝ヲ繝シ繧カ繝シ guest 縺ョ繝代せ繝ッ繝シ繝峨r螟画峩縲
    amazon-ebs: passwd: 縺吶∋縺ヲ縺ョ隱崎ィシ繝医□繧ッ繝ウ縺梧ュ」縺励¥譖エ譁ー縺ァ縺阪∪縺励◆縲
    amazon-ebs: Adding user guest to group wheel
==> amazon-ebs: Provisioning with shell script: ./scripts/create_swap.sh
    amazon-ebs: 1024+0 繝ャ繧ウ繝シ繝牙□蜉
    amazon-ebs: 1024+0 繝ャ繧ウ繝シ繝牙□蜉
    amazon-ebs: 1073741824 繝舌う繝 (1.1 GB) 繧ウ繝斐□縺輔l縺セ縺励◆縲 14.094 遘偵 76.2 MB/遘
    amazon-ebs: 繧ケ繝ッ繝□□遨コ髢薙ヰ繝シ繧ク繝ァ繝ウ1繧定ィュ螳壹@縺セ縺吶√し繧、繧コ = 1048572 KiB
    amazon-ebs: 繝ゥ繝吶Ν縺ッ縺ゅj縺セ縺帙s, UUID=7ee19a25-bb14-40da-8115-74eaf0d0dbe5
==> amazon-ebs: Uploading /home/guest/.ssh/authorized_keys => /tmp/authorized_keys
==> amazon-ebs: Provisioning with shell script: /tmp/packer-shell459700274
==> amazon-ebs: Stopping the source instance...
    amazon-ebs: Stopping instance, attempt 1
==> amazon-ebs: Waiting for the instance to stop...
==> amazon-ebs: Creating the AMI: mitzi_base_20171120-0731
    amazon-ebs: AMI: ami-ba2291dc
==> amazon-ebs: Waiting for AMI to become ready...
==> amazon-ebs: Adding tags to AMI (ami-ba2291dc)...
==> amazon-ebs: Tagging snapshot: snap-03cf7a7d44094beb7
==> amazon-ebs: Creating AMI tags
    amazon-ebs: Adding tag: "BillingType": "RESEARCH"
    amazon-ebs: Adding tag: "OS_Version": "Amazon Linux 2017.09.01"
==> amazon-ebs: Creating snapshot tags
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Cleaning up any extra volumes...
==> amazon-ebs: No volumes to clean up, skipping
==> amazon-ebs: Deleting temporary keypair...
Build 'amazon-ebs' finished.

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
ap-northeast-1: ami-ba2291dc

・AMI作成が完了すると、一時的に起動したインスタンス(名称は「Packer Builder」)は自動で削除されます。

03-5. 確認

・意図したとおりのAMIが作成されたか、作成したAMIからインスタンスを起動して確認。

1) guestユーザの秘密鍵でSSH接続出来ることを確認
2) ユーザがsudo権限を持っていることを確認
3) TimeZoneや文字設定が意図したとおりに設定されていることを確認
4) Swapが有効になっていることを確認
5) 作成されたAMIに意図したTagが付与されていることを確認

04. 備考

04-1. 必要なPolicy

・今回はIAM Roleを利用しましたが、Packer実行に最低限必要なAWS権限は下記公式のとおりになります。
https://www.packer.io/docs/builders/amazon.html#using-an-iam-task-or-instance-role

04-2. 独自key pairをアサインしているAMIを利用した場合

・build時にssh接続出来ない問題が発生しました。
同問題は他の方も経験しているようで下記の方が仰るように公開鍵を削除する処理を盛り込みましたが、私の場合はそれでも起動しませんでした。
https://qiita.com/ikuyamada/items/97c286990adfe1770acf

そこで、下記の方のコメント欄のやりとりにあるようにPackerの独自keyを使用せず、
「key pair」及び接続時の「秘密鍵」を指定する方法にて問題解決しています。
https://qiita.com/soeda_jp/items/e9f759e2db63637e8549

おわり

今回はここまでになります。

続きを読む

やってみて分かった AWS CodeDeploy の落とし穴(かすり傷を負う程度)

本記事の内容

AWS CodeDeploy を触ってみて、はまってしまった落とし穴について書いたものです。

appspec.yml の hooks に指定するスクリプトのパスには、アプリケーションモジュールの root からの相対パスを記述すること

アプリケーションモジュールをインストールするディレクトリを files セクションにて指定しますが、hooks でインストール先のパスを指定すると、ファイルが見つからないエラーになります。

CodeDeploy は hooks に指定したファイルを /opt/codedeploy-agent/deployment-root/9368645e-c79f-4304-9356-21a755b9a421/d-BK4MCFZ0P/deployment-archive/ というようなパスの配下から探します。左記のパスは CodeDeploy が EC2 にアプリケーションモジュールをインストールする際に、取得したモジュールを展開する temporary なディレクトリです。
従って、hooks ではアプリケーションモジュールの root からの相対パスを記述する必要があります。

in-place デプロイで ApplicationStop hook に不備があると、次回以降のデプロイが失敗する

デプロイする際、 lifecycle の最初に ApplicationStop が実行されますが、ここでは最後に成功したデプロイのモジュールが使われます。つまり、直前のデプロイが成功していれば、その設定・モジュールが使われます。
従って、直前のデプロイの ApplicationStop に不備がありエラーになると、次回のデプロイがこけます。デプロイ設定でデプロイ失敗時のロールバックを有効にしていると、再度デプロイしても直前の成功デプロイ(ApplicationStop に不備があるデプロイ)の ApplicationStop が使われるため、やはりデプロイがこけます。
回避策として、デプロイ設定でロールバックを無効にすると、デプロイが失敗してもモジュールが更新されるので、再度デプロイすれば更新後の ApplicationStop が実行されます。

codeDeploy エージェントのインストールは userData の最後で行うこと

CodeDeploy エージェントをインストール → userData の処理が完了
の間に、CodeDeploy によるデプロイ処理が走ってしまう可能性があるためです。

AutoScaling グループへの初回デプロイは少し困難が伴う

初回デプロイ成功後は、AutoScaling により新たに作成されたEC2インスタンスには、CodeDeployにより直前の成功デプロイのリビジョンのアプリが自動でデプロイされます。逆にいうと、初回デプロイ成功前は、直前の成功リビジョンが存在しないため、インスタンスを起動しても、CodeDeploy はアプリをデプロイしません(できません)。しかしながら、CodeDeploy でデプロイするには、デプロイ対象のインスタンスが起動されていなければなりません。つまり、卵が先か鶏が先か、という話になります。

AutoScaling グループのヘルスチェックでは通常、 ELB ヘルスチェックを有効にしますが、アプリケーションが稼働しないとエラーになるでしょう。従って、放っておくと新規インスタンスが起動→ヘルスチェックエラーでインスタンスが停止→新規インスタンスが起動 というループが発生してしまいます。これを防ぐためには、AutoScaling グループで最初のインスタンスを作成した後に、速やかに CodeDeploy のデプロイ処理をキックしなければなりません。その際のデプロイ設定は in-place で AllAtOnce とします。

デプロイ設定は初回は in-place、二回目以降は blue/green とするのが良いかな

初回デプロイは blue 環境がないので in-place で行います。二回目以降は blue/green で行います。

ちなみに、blue/green をすると green 環境として新たな AutoScaling グループが立ち上げられます。AutoScaling グループの設定は blue 環境のものを引きつぎ、名前は CodeDeploy_[デプロイ設定名]_[デプロイID] となります。そのため、blue/green をすると、アプリの稼働している AutoScaling グループが変更になるのですが、そうすると初回デプロイで使用した in-place のデプロイ設定は(AutoScaling グループ名が指定されているため)そのままでは使えなくなります。なので、初回デプロイで使用した in-place のデプロイ設定は削除してしまっていいと思います。

blue/green の場合は blue 環境の終了待ち時間を指定すること

blue/green の場合、デプロイ後に元の環境は不要になるので、デプロイ設定で「デプロイの成功後にデプロイグループの置き換え元インスタンスを終了」を選択するのが通常ですが、インスタンス終了までの時間はデフォルトでは 1時間です。これはもっと短くした方がいいと思います。なぜなら、元の環境のインスタンスが終了するまではデプロイのステータスが完了にならないのですが、完了にならないと次のデプロイを開始できない、つまりは元の環境のインスタンスが終了するまでは次のデプロイを開始できない、ためです。

デプロイ設定は blue/green + AllAtOnce でいいのでは

他にする理由があまりないかな。

まとめ

以上。

続きを読む