Capistranoでぺらいち未満のWebサイトをデプロイしてみます
今日はRubyで書かれたビルド・デプロイ作業の自動化フレームワークのCapistranoにさわってみます。
Capistranoでできること
最初に、入門 Capistrano 3 ~ 全ての手作業を生まれる前に消し去りたいを読めばいいという話ではあるんですが、Capistranoでできることを手短にまとめておきます。
- ビルド作業で共通で使う設定を定義する
set :xxx, value_of_xxx
と書いておけば、setした後続のtaskの中でfetch :xxx
と書いてその値を参照できる
- ビルドの際に行う作業をtaskとして定義する
- dockerイメージのビルドなど、他のビルド作業でも利用できる作業をライブラリとして切り出せる。切り出されたライブラリの中にはgemなどの形で配布されているものもある
after_task => :before_task
などの記法でタスク間の依存関係が指定できるafter_task => :before_task
で:after_task
を行う前に:before_task
を行う必要がある、依存関係の指定before task_a, task_b
,after task_b, task_a
でtask_b
の前にtask_a
を行う指定(task_bの実行にtask_aが必要といったタスク間に依存関係はない)
今日やること
- nginxで
ホスト名/
にアクセスしたらHTMLのページを返す単純なWebサイトのデプロイ - デプロイ先はEC2
- (たぶん必要ないかもですが)勉強用にデプロイ前後でnginxの再起動を行う
という内容でCapistranoの設定ファイルを書いて動かしてみます。
設定ファイルを動かしているCapistranoのバージョンは 3.11.0
で、書く内容の設定ファイルのイメージはざっくりと以下です。
# config/deploy.rb # ローカルホストで行う設定を書く # HTMLのソースコードを取得 # ソースコードをアップロードのためにzipに固める
# config/deploy/production.rb # デプロイ先のサーバでおこなう設定を書く # EC2サーバの設定 # htmlファイルのzipのアップロード & 解凍 # nginxの停止・起動
TL;DR
下にも書いたんですが、今日やったことのまとめです。
- GitHubのリポジトリからソースコードを取ってくる
- ブランチの指定はまだ
config/deploy/#{environment}.rb
にserver
を書いてデプロイ先のサーバを指定- deploy_to でデプロイ先のサーバ上のどこのディレクトリにアップロードするかを指定
on roles(:role)
でリモートのサーバで実施するコマンド,run_locally
でローカルサーバで実行するタスクを書くtask :a => :b
で:a
を実行するときはその前に:b
を実行するという指定ができる
実際手を動かしたメモ
Capistranoのインストールと設定ファイルの生成
$ gem install capistrano $ cap --version Capistrano Version: 3.11.0 (Rake Version: 12.3.1) $ cap install mkdir -p config/deploy create config/deploy.rb create config/deploy/staging.rb create config/deploy/production.rb mkdir -p lib/capistrano/tasks create Capfile Capified $ tree . . ├── Capfile ├── config │ ├── deploy │ │ ├── production.rb │ │ └── staging.rb │ └── deploy.rb ├── html │ └── index.html# デプロイ対象のHTML └── lib └── capistrano └── tasks
HTMLのソースコードを取得
デプロイ対象のHTML含めて先ほど表示したファイルをGitHubのプライベートリポジトリで管理することにして、デプロイ時はプライベートリポジトリからソースコードを取ってくることにします。
# config/deploy.rb # config valid for current version and patch releases of Capistrano lock "~> 3.11.0" set :application, "capistrano_experiment" set :repo_url, "git@github.com:woshidan/capistranotest.git" task :download do run_locally do # run_locally do; ... end でローカルマシン(今回は自分のPC)上で実行するコマンドを書く. run_locally ブロックと呼ぶ info "downloading master branch source from GitHub." # info, warn... などでログレベルに応じたログを出力できる execute "git checkout master && git pull origin master" # タスク内でコマンドを実行するときは、execute "コマンド" で書くkaku end end
話を単純にするため、作業用のディレクトリではすでにgitのリポジトリをcloneしてきていて、masterブランチのみをdeploy対象とするものとします*2。
この時点ではタスクを定義しているだけで、deployの際にdownloadタスクを実行すると設定していないので cap production deploy
しても何も起こりませんし、 downloading master branch source from GitHub.
のログも log/capistrano.log
に出力されません。
ソースコードをアップロードのためにzipに固める
サーバにアップロードする必要のあるファイル、今回は量はそこまでするほど量はないですが、index.htmlをzipに固めるタスクです。
# config/deploy.rb task :archive => :download do run_locally do info "archive html directory to zip" execute "zip -r html.zip html" end end
:archive => :download
の部分は :archive
タスクは :download
タスクをやった後にしてください、ということですね。
EC2のインスタンスを立ち上げて、nginxを入れる
nginxで静的ファイルを表示するサイトを作る予定と書いたので、適当なEC2インスタンスを立ち上げて以下のコマンドでnginxを入れておく。 セキュリティグループは雑にマイIP*3からのみSSH, HTTP, HTTPSアクセスを許可。
# 参考: http://d.hatena.ne.jp/january/20130819 $ sudo yum install nginx $ which nginx # インストール確認 /usr/sbin/nginx $ sudo nginx # nginx起動 $ ps -ef | grep nginx # masterプロセスとworkerプロセスの起動を確認 root 2759 1 0 09:27 ? 00:00:00 nginx: master process nginx nginx 2760 2759 0 09:27 ? 00:00:00 nginx: worker process ec2-user 2762 2686 0 09:27 pts/0 00:00:00 grep --color=auto nginx
ホストのアドレスにアクセスすると
のようなページが出てnginxが起動していることが確認できる。
/etc/nginx/nginx.conf
をみると、
http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; # Load modular configuration files from the /etc/nginx/conf.d directory. # See http://nginx.org/en/docs/ngx_core_module.html#include # for more information. include /etc/nginx/conf.d/*.conf; index index.html index.htm; server { listen 80 default_server; listen [::]:80 default_server; server_name localhost; root /usr/share/nginx/html;
とあり、
$ less /usr/share/nginx/html/index.html | grep Welcome <h1>Welcome to <strong>nginx</strong> on the Amazon Linux AMI!</h1>
と確かめてみると、と表示されたページのソースコードの所在が確認できたので、今日のところは server
ディレクティブの root
を /var/www
に変更し、 /var/www
以下に index.html
をアップロードすることを目標にする。
# 変更後のconfファイル index index.html index.htm; server { listen 80 default_server; listen [::]:80 default_server; server_name localhost; root /var/www; # ここを変えた # root /usr/share/nginx/html; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { } # redirect server error pages to the static page /40x.html # # error_page 404 /404.html; # 40xなどを/var/wwwに用意するのが面倒なので一旦コメントアウト # location = /40x.html { # } # redirect server error pages to the static page /50x.html # # error_page 500 502 503 504 /50x.html; # location = /50x.html { # }
また、デプロイが完了するまで仮に表示するページを用意しておき
[ec2-user@ip-172-31-27-79 ~]$ sudo mkdir /var/www [ec2-user@ip-172-31-27-79 ~]$ sudo touch /var/www/index.html [ec2-user@ip-172-31-27-79 ~]$ sudo vim /var/www/index.html
再起動して設定が変更されたことを確認しておく。
[ec2-user@ip-172-31-27-79 ~]$ sudo service nginx stop Stopping nginx: [ OK ] [ec2-user@ip-172-31-27-79 ~]$ sudo service nginx start Starting nginx: [ OK ]
EC2サーバの設定を書く
さて、デプロイ先のサーバができたところでCapistranoの設定ファイルにデプロイ先のサーバの設定を書いていく。
# config/deploy/production.rb # server ホスト名(アクセスできるものだったらよいのでIPアドレス) # user ログインユーザ # roles あとでリモートホストで行うタスクを設定するとき on roles(:web) のように使う、デプロイ作業用グループ server "5x.xxx.xxx.x0", user: "ec2-user", roles: %w{web}
そういえば、 config/deploy/production.rb
のようなデプロイ先のステージの設定ファイルを用意すると
$ cap production download 00:00 download downloading master branch source from GitHub. 01 git checkout master && git pull origin master 01 Already on 'master' 01 From https://github.com/woshidan/capistranotest 01 * branch master -> FETCH_HEAD 01 Already up-to-date. ✔ 01 woshidan@localhost 1.083s # task :archive => :download do $ cap production archive 00:00 download downloading master branch source from GitHub. 01 git checkout master && git pull origin master 01 Already on 'master' 01 From https://github.com/woshidan/capistranotest 01 * branch master -> FETCH_HEAD 01 Already up-to-date. ✔ 01 woshidan@localhost 1.097s 00:01 archive archive html directory to zip 01 zip -r html.zip html 01 updating: html/ (stored 0%) 01 updating: html/index.html (deflated 20%) ✔ 01 woshidan@localhost 0.042s
のように、設定したタスクが実行できるようになる。
htmlファイルのzipのアップロード & 解凍
今回のWebサイトについて、具体的なデプロイ作業とは、nginxで表示するhtmlファイルのzipのアップロードと、アップロードしたファイルの解凍・配置となる。その作業を行うタスクを書いていく。
Capistranoでファイルをアップロードする際は upload! ローカルファイルへのパス, アップロード先サーバ上のパス
を利用するが、その設定は
# config/deploy.rb task :deploy => :archive do on roles(:web) do upload! "./html.zip", "/home/ec2-user/deploy_target_dir" # リモートホスト上の /home/ec2-user/deploy_target_dir へ ローカルホストの ./html.zip をアップロード end end
でありますが、そのままだと
00:01 git:wrapper 01 mkdir -p /tmp (Backtrace restricted to imported tasks) cap aborted! SSHKit::Runner::ExecuteError: Exception while executing as ec2-user@5x.xxx.xxx.x0: Authentication failed for user ec2-user@5x.xxx.xxx.x0 Caused by: Net::SSH::AuthenticationFailed: Authentication failed for user ec2-user@5x.xxx.xxx.x0 Tasks: TOP => deploy:check => git:check => git:wrapper (See full trace by running task with --trace) The deploy has failed with an error: Exception while executing as ec2-user@5x.xxx.xxx.x0: Authentication failed for user ec2-user@5x.xxx.xxx.x0 ** DEPLOY FAILED ** Refer to log/capistrano.log for details. Here are the last 20 lines:
のようにエラーになってしまうので、sshのオプションを config/deploy/production.rb
(デプロイ先の設定を書くファイル) に追加します。
# config/deploy/production.rb server "5x.xxx.xxx.x0", user: "ec2-user", roles: %w{web} set :ssh_options, keys: %{./capistrano_test.pem}, auth_methods: %w{publickey}
すると、今度は
DEBUG [0691849d] Command: ( export GIT_ASKPASS="/bin/echo" GIT_SSH="/tmp/git-ssh-capistrano_experiment-production-woshidan.sh" ; /usr/bin/env git ls-remote git@github.com:woshidan/capistranotest.git HEAD ) DEBUG [0691849d] /usr/bin/env: DEBUG [0691849d] git DEBUG [0691849d] : No such file or directory
のようなメッセージで失敗しますが、これはデプロイ先のサーバにgitがない、という意味なのでデプロイ先のサーバに
$ sudo yum install git
のようにgitをインストールします*4。
実は、Capfileを cap install
から何も変更せずに進めた場合、
task :deploy => :archive do on roles(:web) do upload! "./html.zip", "/var/www" # リモートホスト上の /var/www へ ローカルホストの ./html.zip をアップロード end end
の deploy
のタスクを開始した時点で :deploy_to
に指定したディレクトリ、または /var/www/#{application}
のディレクトリで git clone
を行なっているようです。
task :deploy => :archive do end
のように :deploy
タスクの中身を空にしても
00:00 archive archive html directory to zip 01 zip -r html.zip html 01 updating: html/ (stored 0%) 01 updating: html/index.html (deflated 20%) ✔ 01 woshidan@localhost 0.042s ✔ 02 ec2-user@5x.xxx.xxx.x0 0.421s 00:01 git:wrapper 01 mkdir -p /tmp ✔ 01 ec2-user@5x.xxx.xxx.x0 0.092s Uploading /tmp/git-ssh-capistrano_experiment-production-woshidan.sh 100.0% 02 chmod 700 /tmp/git-ssh-capistrano_experiment-production-woshidan.sh ✔ 02 ec2-user@5x.xxx.xxx.x0 0.092s 00:01 git:check 01 git ls-remote git@github.com:woshidan/capistranotest.git HEAD 01 b132c31d1fc37f08848d8b860bf57f20ad4ef635 HEAD ✔ 01 ec2-user@5x.xxx.xxx.x0 1.774s 00:03 deploy:check:directories 01 mkdir -p /home/ec2-user/upload_prepare_dir/shared /home/ec2-user/upload_prepare_dir/releases ✔ 01 ec2-user@5x.xxx.xxx.x0 0.132s 00:04 git:clone The repository mirror is at /home/ec2-user/upload_prepare_dir/repo 00:04 git:update 01 git remote set-url origin git@github.com:woshidan/capistranotest.git ✔ 01 ec2-user@5x.xxx.xxx.x0 0.093s 02 git remote update --prune 02 Fetching origin 02 From github.com:woshidan/capistranotest 02 f4e5e6b..b132c31 master -> master ✔ 02 ec2-user@5x.xxx.xxx.x0 2.188s 00:06 git:create_release 01 mkdir -p /home/ec2-user/upload_prepare_dir/releases/20180913134338 ✔ 01 ec2-user@5x.xxx.xxx.x0 0.100s 02 git archive master | /usr/bin/env tar -x -f - -C /home/ec2-user/upload_prepare_dir/releases/20180913134338 ✔ 02 ec2-user@5x.xxx.xxx.x0 0.096s 00:07 deploy:set_current_revision 01 echo "b132c31d1fc37f08848d8b860bf57f20ad4ef635" > REVISION ✔ 01 ec2-user@5x.xxx.xxx.x0 0.092s 00:07 deploy:symlink:release 01 ln -s /home/ec2-user/upload_prepare_dir/releases/20180913134338 /home/ec2-user/upload_prepare_dir/releases/current ✔ 01 ec2-user@5x.xxx.xxx.x0 0.092s 02 mv /home/ec2-user/upload_prepare_dir/releases/current /home/ec2-user/upload_prepare_dir ✔ 02 ec2-user@5x.xxx.xxx.x0 0.090s 00:07 deploy:cleanup Keeping 5 of 6 deployed releases on 5x.xxx.xxx.x0 01 rm -rf /home/ec2-user/upload_prepare_dir/releases/20180913112304 ✔ 01 ec2-user@5x.xxx.xxx.x0 0.118s 00:07 deploy:log_revision 01 echo "Branch master (at b132c31d1fc37f08848d8b860bf57f20ad4ef635) deployed as release 20180913134338 by woshidan" >> /home/ec2-user/upload_prepare_dir/r… ✔ 01 ec2-user@5x.xxx.xxx.x0 0.098s
git clone
が行われています。
# config/deploy.rb set :deploy_to, "/home/ec2-user/upload_prepare_dir" task :deploy => :archive do on roles(:web) do upload! "./html.zip", "/home/ec2-user/deploy_target_dir" # リモートホスト上の /home/ec2-user/deploy_target_dir へ ローカルホストの ./html.zip をアップロード end end
とした場合、 /home/ec2-user/upload_prepare_dir
にてgit cloneやgit cloneしてきたコードのバージョン管理用のディレクトリを作成して、 upload!
では、そのディレクトリから2つめに指定したディレクトリへファイルをコピーしているようです*5。
[ec2-user@ip-172-31-27-79 ~]$ pwd /home/ec2-user $ tree . . ├── deploy_target_dir │ └── html.zip ├── html │ └── index.html └── upload_prepare_dir ├── current -> /home/ec2-user/upload_prepare_dir/releases/20180913134913 ├── releases │ └── 20180913134913 │ ├── Capfile │ ├── config │ │ ├── deploy │ │ │ ├── production.rb │ │ │ └── staging.rb │ │ └── deploy.rb │ ├── html │ │ └── index.html │ └── REVISION ├── repo │ ├── branches │ ├── config │ ├── description │ ├── FETCH_HEAD │ ├── HEAD │ ├── hooks │ │ ├── applypatch-msg.sample │ │ ├── commit-msg.sample │ │ ├── post-update.sample │ │ ├── pre-applypatch.sample │ │ ├── pre-commit.sample │ │ ├── prepare-commit-msg.sample │ │ ├── pre-push.sample │ │ ├── pre-rebase.sample │ │ ├── pre-receive.sample │ │ └── update.sample │ ├── info │ │ └── exclude │ ├── objects │ │ ├── info │ │ └── pack │ │ ├── pack-455700f6724c559c3d0264e92c2888bf6b191610.idx │ │ └── pack-455700f6724c559c3d0264e92c2888bf6b191610.pack │ ├── packed-refs │ └── refs │ ├── heads │ └── tags ├── revisions.log └── shared 20 directories, 27 files
上は実際にデプロイ先のサーバで tree .
してみたところ。
アップロードした zip ファイルを解凍して /var/www/index.html
に配置するところまで追加*6。
# config/deploy.rb task :deploy => :archive do on roles(:web) do execute "mkdir -p /home/ec2-user/deploy_target_dir" upload! "./html.zip", "/home/ec2-user/deploy_target_dir" # リモートホスト上の /home/ec2-user/deploy_target_dir へ ローカルホストの ./html.zip をアップロード execute "cd /home/ec2-user/deploy_target_dir && unzip -o /home/ec2-user/deploy_target_dir/html.zip" execute "sudo cp /home/ec2-user/deploy_target_dir/html/index.html /var/www/index.html" end end
nginxの停止・再起動
今回のデプロイには変更ないのだけどデプロイ時にアプリケーションを再起動する、というのはよくあることだから、素振りとしてnginxを再起動しておく。
# config/deploy.rb task :deploy => :archive do on roles(:web) do execute "mkdir -p /home/ec2-user/deploy_target_dir" upload! "./html.zip", "/home/ec2-user/deploy_target_dir" # リモートホスト上の /home/ec2-user/deploy_target_dir へ ローカルホストの ./html.zip をアップロード execute "cd /home/ec2-user/deploy_target_dir && unzip -o /home/ec2-user/deploy_target_dir/html.zip" execute "sudo cp /home/ec2-user/deploy_target_dir/html/index.html /var/www/index.html" # 以下を追加 execute "sudo service nginx stop" execute "sudo service nginx start" end end
デプロイした結果を確認できたのでよさそう。
今日はcapistranoを使って
- GitHubのリポジトリからソースコードを取ってくる
- ブランチの指定はまだ
config/deploy/#{environment}.rb
にserver
を書いてデプロイ先のサーバを指定- deploy_to でデプロイ先のサーバ上のどこのディレクトリにアップロードするかを指定
on roles(:role)
でリモートのサーバで実施するコマンド,run_locally
でローカルサーバで実行するタスクを書くtask :a => :b
で:a
を実行するときはその前に:b
を実行するという指定ができる
あたりの復習をして、さぼったなぁと思うことは
- git pushしていれば、いまいるブランチをデプロイできるようにする
- deploy ユーザーを用意して直接デプロイ対象のディレクトリへファイルをアップロードする
- 静的ファイルのアップロードではなくもう少し動作するアプリをdeployする
- ビルドサーバを用意して、ビルドサーバからだけアップロードできるようにする
- プラグインの詳細
ですが、また今度でいいかなと思います。
とりあえず、現場からは以上です。
*1:デプロイプロセス全体としてはアップロードに見える?
*2:ビルドサーバとかで作業を行うならこういうわけにもいきませんが、今日はcapistranoの勉強ということにして、その辺はまた別途やろうと思います
*3:AWSはセキュリティグループのIPアドレスの設定でマイIPを設定するとAWSに接続しているISPが割り振る範囲のIPを入力してくれるみたい
*4:参考: https://qiita.com/himatani/items/87d54752021879e1ec89
*5:Capfileを何もいじっていなかったので https://github.com/capistrano/capistrano/blob/220db8fabab15b9d5cd5c9ab1f2744e0aa346eb0/lib/capistrano/scm/tasks/git.rake#L1-L2 や Capfile 中の install_plugin Capistrano::SCM::Git あたりが原因と思われます...
*6:実際はnginxで公開するディレクトリ /var/www/ にアップロドして、 /var/www/current/index.html あたりをnginxで公開するパスとしたほうがよさそう。そのためのdeployユーザの設定などがあるが今日は時間がないのでこういう感じで
*7:デプロイプロセス全体としてはアップロードに見える?
Terraformで検証用インスタンスを立ち上げるのに使う最低限の作業についてメモ
検証用の環境作るとき、微妙に設定をいじって立て直す、その後複数台で動かしたい、みたいな場合はTerraformでやったりします*1。
そのとき、共通でやる作業についてメモしておきます。
共通
変数設定など
アプリごとにパスフレーズなしの認証鍵を使い捨てで作る
$ ssh-keygen -t rsa Generating public/private rsa key pair. Enter file in which to save the key (/Users/woshidan/.ssh/id_rsa): ./app_secret Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in ./app_secret. Your public key has been saved in ./app_secret.pub.
access_key = "ACCCCCCCCCCCCCCCCCC" secret_key = "/VXXXXXXKXXXXXXXXXXXXXXXXXXXXXXXXXX" region = "ap-northeast-1" ssh_key_path = "./app_secret.pub"
variable access_key {} variable secret_key {} variable region {} # ssh-keygen -t rsa -f secret_key variable ssh_key_path {}
# terraform plan --var-file=tf.vars provider "aws" { access_key = "${var.access_key}" secret_key = "${var.secret_key}" region = "${var.region}" } resource "aws_key_pair" "app_secret" { key_name = "app_secret" public_key = "${file("./app_secret.pub")}" }
ざついセキュリティグループ
resource "aws_security_group" "sg-app-server" { name = "app-server-sg" // 特に指定しなければデフォルトのVPN # SSH access from anywhere ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # HTTP access from anywhere ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # HTTPS access from anywhere ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }
AMI作る時のちょっぱや用
refs: https://www.terraform.io/docs/providers/aws/r/instance.html
resource "aws_instance" "web" { // aws ec2 describe-images --owners amazon --filters 'Name=name,Valailable' | jq -r '.Images | sort_by(.CreationDate) | last(.[]).ImageId' ami = "ami-08847abae18baa040" instance_type = "t2.micro" security_groups = ["${aws_security_group.sg-app-server.name}"] key_name = "${aws_key_pair.app_secret.key_name}" tags { Name = "HelloWorld" } }
ssh -i app_secret ec2-user@ec2-12-345-67-89.ap-northeast-1.compute.amazonaws.com
AMI作成によく使うコマンド集
なんだか割とよくJava 1.8を入れている気がする。
yum install java-1.8.0-openjdk
refs: https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/awscli-install-linux-python.html
# 古いバージョンを探す必要がある場合 http://www.atmarkit.co.jp/flinux/rensai/linuxtips/901instoldver.html yum search python yum install python # ami-08847abae18baa040 の場合 $ python --version Python 2.7.14 # AWSのLinux系インスタンスのEC2にはpythonは2系と3系が入っていてデフォルトでは2系の方が有効になっている # 切り替えるときは https://aws.amazon.com/jp/premiumsupport/knowledge-center/python-boto3-virtualenv/ などが参考になる $ python3 --version Python 3.6.2
もっと色々覚えた気がするけどとりあえず先週分...、現場からは以上です。
*1:計画性がないと言われたらそれまでですが検証がひと段落して終わった後追加で試したいときにも便利...
ecs_deployのecs_auto_scalerにPRを出すときに環境構築をがんばったよ、というお話
先日、 ecs_auto_scaler
にPRを出してマージされてしばらく運用して問題なさそうので今日はちょっと自慢をします。
ecs_auto_scaler
そのものについては前回の記事でふれたのでそちらをご参照ください。
複数のサービスが連携させて動かすためのgemですが、こういったgemの場合、動作確認のための環境構築がなかなか大変です。
ecs_autoscaler
の場合、ロールやセキュリティグループを除いて最低限でも
- ecs_autoscalerでコンテナインスタンス数、タスク数を管理する対象はクラスタ単位ですが、管理される側のクラスタA
- ecs_autoscalerを動かすホスト
- upscale triggerとdownscale triggerのためのCloudWatchのアラート2種
あたりが必要です。毎回1から用意するのがめんどくさいので、先日仕事で動作確認をした時、ちょっと時間はかかりましたがTerraformで立てれるようにしました。
ので、今日はそれを自慢して終わりにします。どやぁ。
なお、セキュリティグループは検証用にインバウンドはHTTP, HTTPS, SSHのIPを全て空け、アウトバウンドは全部空け、と空きまくりなので検証が終わったらすぐに落としてください。現場からは以上です。
関連エントリ
ecs_deployによるECSのオートスケーリングとAWSのECSによるオートスケーリングの違いについて
今日は会社で管理しているOSSの一つ、ecs_deployに関連する話をECSの復習がてらします。
TL;DR
ecs_deploy
gemでは、ECSへのデプロイに関するCapistranoタスクの定義とECSのオートスケーリングを行うスクリプトが入っている- ECSのオートスケーリングはAutoScaling Groupのdesiredの設定でクラスタ内のコンテナインスタンスの数を、ECSのサービスのdesired countの設定でタスクの数を調整する
reproio/ecs_deploy のgemについて
ecs_deploy
の gem は、各言語で作成されているECSへのデプロイを助けるスクリプト群*1の一つで、特徴としてはECSのデプロイをCapistranoのタスクとして記述させるアプローチを取っていることだと思います。
ecs_deploy
のgemの中身はおおまかにいって
- ECSへのデプロイに関するCapistranoタスクの定義
- 上記Capistranoのタスクの中で利用されるRubyのコード
があって、今回自分が仕事で触ったのは ecs_auto_scalerr
の方なので、 ecs_auto_scaler
についてもう少し説明していきます。
また、ややこしいので、この記事ではAWSが提供しているECSのService AutoScalingを「AWSによるECSのオートスケーリング」、ecs_deployに含まれるecs_auto_saclerによるオートスケーリングは 「ecs_auto_scaler のオートスケーリング」と記載することにします。
ECSのオートスケーリングのために、ECS ServiceとAutoScaling Groupの設定をいじる必要がある
ecs_auto_scaler
はひらたくいうと、ECSのAutoScalingを行うスクリプトです。
このスクリプトが書かれたのは2016年1月には、まだAWSによるECSのオートスケーリングがありませんでした*2が、この二種類のオートスケーリングが行なっていることをおおまかにまとめると
ということをやっています。
Amazon Web Services ブログ > Amazon ECSでAuto Scalingによると、AWSによるECSのオートスケーリングの場合、
- ECSのサービスのタスク数の調整には、ECS ServiceのScaling Policy
- ECSのクラスターのコンテナ数の調整には、コンテナインスタンスが属するAutoScaling GroupのScaling Policy
を用いています。一方、ecs_auto_scaler のオートスケーリングは
しています。
何が言いたいかというと、2つのオートスケーリングで利用している設定やAPIに多少違いはありますが、ECSのオートスケーリングは、ECSのサービスとクラスターのAutoScaling Groupの2つのレイヤーの設定を管理して行う必要があるわけです。
AWSによるECSのオートスケーリングとecs_auto_scaler のオートスケーリングの違い
それでは、この2つのオートスケーリングの方法の違いで一体どういう事態が生じるのでしょうか。
じつは、AWSによるECSのオートスケーリングでは、ECS ServiceのScaling PolicyとAutoScaling GroupのScaling Policyがそれぞれ独立して動いていて、AutoScaling GroupのScaling Policyによりまだタスクが動作しているコンテナインスタンスが停止となりエラーが発生することがあります。
この問題に対応するため、 ecs_auto_scaler ではスケールイン時はECSインスタンス上のタスクの状態をしらべ必要なタスクが動いていないことを確認してから、AutoScaling Groupの設定の調整を行うようになっています*5。
AWSによるECSのオートスケーリングを使っている場合でもこの問題の対応は可能ですが*6、ecs_auto_scalerを利用するメリットとしては、ecs_deployの EcsDeploy::EcsAutoScaler
を利用する場合、ECSインスタンス上のプロセスのチェックを含めたオートスケーリングの処理を 管理対象クラスタの外部のホストで行う*7ため、監視される側のインスタンスには特別な設定をしなくていい点でしょうか。
そのかわり、オートスケーリングのためにネットワークを経由してAWSのAPIを叩くので、AWSのAPIの回数制限*8を超えるような規模のクラスタ、たとえば200台くらいのコンテナインスタンスが存在するような大規模なクラスタの管理は難しそうです*9。
書ける、と思ってたら割とかけなくて焦りました。。現場からは以上です。
*1:たとえば、pythonだと https://github.com/fabfuel/ecs-deploy , シェルスクリプト: https://github.com/silinternational/ecs-deploy/blob/develop/ecs-deploy, JSだと https://www.npmjs.com/package/ecs-deploy など
*2:https://aws.amazon.com/jp/blogs/news/automatic-scaling-with-amazon-ecs/ AWSによるECSのオートスケーリングがアナウンスされたのは2016年5月
*3:正確には少し違っていて、後発のターゲット追跡スケーリングポリシーはCloudWatchのアラートではなく、CloudWatchの特定のメトリクスを見て、その値が一定値に近づくようにする https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-autoscaling-targettracking.html
*4:MinやMaxなども増減させていますが、詳しくは https://dev.classmethod.jp/cloud/aws/comprehend-auto-scaling-desired-capacity/
*5:詳しくは https://github.com/reproio/ecs_deploy/blob/master/lib/ecs_deploy/auto_scaler.rb#L350-L356 あたり
*6:https://developers.cyberagent.co.jp/blog/archives/14664/
*7:ことが前提になっている、おそらく。
*8:ECSのAPIは1時間に1000回くらい叩くとエラーを返してくるようになるとかなんとか...
*9:なんとなく、200台超えてきたらインスタンスタイプ変えることの方を先に検討しそうな気もするけどAWS詳しくない...
ssh-agentでsshコマンドで認証鍵の指定を入れなくていいようにしたりログイン先のサーバに認証鍵の情報を転送したりする
TL;DR
ssh-agent bash
でssh-agent
を起動してssh-add /path/to/secret_key
することでssh
コマンドで認証鍵の指定やパスフレーズの入力が省略できるssh-agent
はOpenSSHの認証エージェントで、認証エージェントの「転送」により、ローカルで動いているクライアントが持っている認証情報をログイン先のサーバに転送することができるssh
コマンドの-A
オプションで「転送」を行う旨を指定する- 認証エージェントの転送を利用することで、たくさんの人がアクセスする可能性のあるサーバに秘密鍵をおかなくて済んだりする
SSHでパスワードを使わずにログインする
SSHでパスワードを使わずにログインするようにするには、
ssh-keygen
で秘密鍵と公開鍵のペアを作成する*1- ログイン先のサーバの
~/.ssh/authorized_keys
以下に公開鍵を設置して、所有者・グループ・権限を設定する*2 - 秘密鍵を
-i
オプションで指定してSSHコマンドを叩くssh -i /path/to/secret_key user@exapmle.com
という手順を踏みますが、さらに秘密鍵を指定しているオプションも ssh
コマンドに含まないようにしたいとします。
やり方は
SSH_CONFIG
*3にログイン先のホスト、ログインするユーザー名、そのユーザーがログインするときに使う秘密鍵のパスを指定するssh-agent
を起動して、ssh-agent
に認証に利用する鍵を追加する
といった方法があります。今日は ssh-agent
を使う方法についてもう少しメモします。
ssh-agentを利用してログインをする方法
ssh-agentは公開鍵認証を行うときに利用する秘密鍵の情報をメモリ上に保持するプログラム*4で、sshでログインを行う前に
$ ssh-agent bash
または
$ eval `ssh-agent`
で起動し、 ssh-agent -k
で停止します*5。
起動した後、
$ ssh-add /path/to/secret_key
# パスフレーズが指定された鍵であれば、このタイミングで入力する
でそのセッションのプロセスのメモリ上に秘密鍵の情報を登録し*6、 ssh-add -l
で登録した鍵の一覧を確認できます。
ssh-agent に鍵を登録した状態で ssh
コマンドを打つと、以下のように認証鍵の指定やパスワードの入力を行わずにリモートのサーバへログインすることができます。
$ ssh server.example.com
ssh-agentを利用してログインする場合にできること
さて、 ssh-agent
を使ってパスフレーズや認証鍵の指定なしでログインする方法を紹介しましたが、この手間はパスワード入力をなクスという意味では SSH_CONFIG
にログイン設定をする方法(具体的には
Host example.com HostName example.com IdentityFile ~/.ssh/id_rsa User woshidan
のように設定を書く)とたいして手間は変わりません。
それでは、 ssh-agent
を使うと SSH_CONFIG
を使う場合と比べてなにがちがうのでしょうか。
ssh-agent
は、OpenSSHにおける認証エージェントですが、認証エージェントには、認証エージェントを「転送」するという機能があります。
認証エージェントを「転送」するとはどういうことかというと、ローカルで動いてる認証エージェントが持つ認証鍵を含む情報をログイン先のサーバに通知し、ログイン先のサーバ上の sshd
デーモンにもローカルと同様の情報を持った認証エージェントとして機能してもらうことです。コマンドとしては
$ ssh -A [ユーザ名@]ホスト名
のように ssh
コマンドに -A
オプションをつけることで可能です。
このようにすることで、他のサーバへアクセスする必要のあるサーバに秘密鍵を配置する必要がなくなります。
たとえば、リモートのビルドサーバはビルドに必要なファイルを取得するために他のサーバにアクセスする必要がありますが、その際、秘密鍵をビルドサーバに配置すると、ビルドのデバッグのためにログインしたエンジニアやそれ以外の人(?)はそのファイルにアクセスできたりするわけですが。
管理者だけがアクセスできるようにしたサーバに秘密鍵を置き、そのサーバから ssh-agent
を使ってビルドサーバにログインしてビルド作業を行うようにすることで、秘密鍵にアクセス可能な人数を制限できたりします*7*8。
逆に、認証情報に関するエラーのデバッグのときは、 ssh
のオプションや ssh-add
が行われたタイミングも確認するとよさそうです。
現場からは以上です。
参考
- http://nigohiroki.hatenablog.com/entry/2013/08/18/221434
- https://euske.github.io/openssh-jman/ssh_config.html
- https://euske.github.io/openssh-jman/ssh-agent.html
- https://qiita.com/naoki_mochizuki/items/93ee2643a4c6ab0a20f5
- http://www.unixuser.org/~euske/doc/openssh/book/chap3.html
- http://www.unixuser.org/~euske/doc/openssh/book/chap4.html#make-life-easy-with-ssh-agent
- https://qiita.com/Yarimizu14/items/6a4bab703d67ea766ddc
*1:SSHでパスワードを使わずにログインする手順なので省いていますが、SSHでユーザがログインできるようにする作業全体としては、この前段階にSSHログインできるようにする目的で、ログイン先のサーバに adduser コマンドでユーザを作成し、passwdコマンドでパスワードを作成する必要がある refs: http://nigohiroki.hatenablog.com/entry/2013/08/18/221434
*2:詳細は http://nigohiroki.hatenablog.com/entry/2013/08/18/221434
*3:https://euske.github.io/openssh-jman/ssh_config.html だいたいユーザ用の設定ファイル ~/.ssh/config に書く
*4:https://euske.github.io/openssh-jman/ssh-agent.html
*5:この辺の便利なスクリプトは http://www.unixuser.org/~euske/doc/openssh/book/chap5.html#spread-ssh-agent にまとめっています。圧倒的感謝...!!
*6:他のオプションの利用例などは https://qiita.com/naoki_mochizuki/items/93ee2643a4c6ab0a20f5 が参考になる
*7:もっと、いろいろある気はしたんですが、自分が考えてぱっと出てきた例がこういうのが限界
*8:たとえば、Rundeckには秘密鍵などを管理するための画面があります https://rundeck.org/2.9.1/administration/key-storage.html https://komeda-shinji.blogspot.com/2015/02/rundeck_68.html
CloudWatchにさわってみたときのメモ(カスタムメトリクスのパブリッシュ、アラームの設定)
とりあえず今日はCloudWatchの復習で。
CloudWatchはAWSのモニタリングや監視のためのサービスで、EC2など各種AWSのサービスを動かすと自動的にCPU利用率などのメトリクスを取得してくれる他、AWS CLIなどを利用してカスタムメトリクスを送ることも可能です。
CloudWatchの用語: 名前空間とディメンジョン、メトリクス
それぞれのメトリクスには名前空間とディメンジョン、名前があります。AWSサービスの場合の名前空間はサービス名であることが多いです。
名前空間はそれぞれディメンジョンで区切ることができ、たとえばEC2名前空間に設定されているディメンジョンは
があります。(ディメンジョンが設定されていないデータについては
のように「範囲の定められていないメトリクス」に収められます。)
ディメンジョンをクリックすると出てくるCPU使用率などがメトリクスの名前にあたります。
カスタムメトリックを送信してみる
データのタイムスタンプと個別のデータポイント、あるいは、ある時刻のデータポイントの数、データの数値合計、平均といった集計値を送信することができます。 データのタイムスタンプは現在時刻から2週間前の間に指定する必要があります。
個別のデータポイントのデータを送る
TIMESTAMP=2018-08-08T08:08:00Z aws cloudwatch put-metric-data --metric-name RequestLatency --namespace GetStarted \ --timestamp $TIMESTAMP --value 8 --unit Milliseconds
ある時刻のデータポイントの数、データの数値合計、平均といった集計値を送信する
TIMESTAMP=2018-08-08T08:08:00Z aws cloudwatch put-metric-data --metric-name RequestLatency --namespace GetStarted \ --timestamp $TIMESTAMP --statistic-values Sum=40,Minimum=8,Maximum=8,SampleCount=8 --unit Milliseconds
送ったメトリクスは
のようにグラフで確認可能です。
CloudWatchのアラームを作成する
CloudWatchのグラフを表示している画面から、CloudWatchのアラームを作成することができます。
アラームは、アラームの状態ごとにAmazon SNSと連携させることでSlackやメールで通知、PagerDutyなどインシデント管理サービスのアイテム作成の自動化に利用できます。
アラームの状態変化について
アラームは基本的にアラーム作成画面の
の部分の条件式を満たしていたら OK
、そうでない場合は ALARM
, アラームの間隔内にデータポイントがない場合はINSUFFICIENT DATA
の状態になります。
の記事にあるコマンドを
$ aws cloudwatch set-alarm-state --alarm-name "RequestLatency is High" --state-value ALARM --state-reason "alarm-test"
のように打ち込むと一瞬指定した状態にアラームの状態を変化させることができます。その後、すぐに直近のデータに応じた状態に戻ってしまいますが、アラームの通知連携のテストにはむしろ便利でしょう。
アラームの状態に応じて動作するプログラムなどをテストするときは、欠落データの処理方法を変更するとデータを送っていない間の状態が指定しやすくて便利です。
参考
- https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/PublishMetrics.html
- https://qiita.com/kooohei/items/e9f8692b9ebe90b6a9df
- https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html
今日は短いですが、現場からは以上です。
EC2インスタンスをECSの特定のクラスタに所属させる
今日は、EC2インスタンスをECSの特定のクラスタのコンテナインスタンスとして登録するために必要なものについてまとめておきます。
EC2インスタンスがECSのコンテナインスタンスになる仕組み
ECSのコンテナインスタンスはecs-agent
が動作していて、ecs-agent
がECS APIを叩いてクラスタに登録したコンテナインスタンスなのですが、実は直接的にEC2インスタンスをECSのコンテナインスタンスとして登録するための設定はありません。
EC2インスタンスがECSのコンテナインスタンスとして動作するための条件を満たしていたら勝手にECSのクラスタにコンテナインスタンスとして登録される、という仕組みで初見は結構分かりにくかったです。。
EC2インスタンスがECSのコンテナインスタンスになるために必要なもの
- ECSに最適化されたAMIの利用
- AWSのアカウントに対する認証情報(どこの組織のECSインスタンスなのかわかる情報)が含まれ、ECSがコンテナインスタンスの登録やイメージの取得などに使うアクセス権限が付与されたIAMロール
- IAMロールをEC2インスタンスにアタッチするためのインスタンスプロファイル
- (EC2起動タイプを選択する場合のみ)SSHログインのためのキーペア
- (強く推奨)コンテナインスタンスを起動するVPC
ecs-agent
がECS APIと通信してコンテナインスタンスを登録するためのアウトバウンドの通信が許可されたセキュリティグループ- コンテナインスタンスを登録するクラスタを決定するためのuserdataで指定する環境変数 ECS_CLUSTERの指定
userdata
に指定がなければデフォルトのクラスタに登録する
それぞれ、Terraformで具体的に指定する様子を確認して今日はおしまいにしようと思います。
必要なものを作成するためのTerraformの設定
ECSに最適化されたAMI
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-optimized_AMI.html などを参考に見繕う。
resource "aws_instance" "ecs_container_instance" { ami = "ami-e4657283" // 管理画面を操作しててでてきたのを使った気がする ...
IAMロール
このへんは昨日のエントリと同じですが。。
// 信頼ポリシーをロールに付与 resource "aws_iam_role" "ec2_instance_role" { name = "ec2_instance_role" assume_role_policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Action": "sts:AssumeRole", "Principal": { "Service": "ec2.amazonaws.com" }, "Effect": "Allow", "Sid": "" } ] } EOF }
// アクセス権限ポリシー resource "aws_iam_policy" "ecs_container_instance_policy" { name = "ecs_container_instance_policy" path = "/" policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ecs:CreateCluster", "ecs:DeregisterContainerInstance", "ecs:DiscoverPollEndpoint", "ecs:Poll", "ecs:RegisterContainerInstance", "ecs:StartTelemetrySession", "ecs:UpdateContainerInstancesState", "ecs:Submit*", "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "*" } ] } EOF }
// 信頼ポリシーが付与されているロールにさらにアクセス権限ポリシーを付与 resource "aws_iam_role_policy_attachment" "ecs_container_instance_policy_attachment" { role = "${aws_iam_role.ec2_instance_role.name}" policy_arn = "${aws_iam_policy.ecs_container_instance_policy.arn}" }
インスタンスプロファイル
// EC2インスタンスにはインスタンスプロファイルを利用してロールをアタッチ resource "aws_iam_instance_profile" "ecs_container_instance_profile" { name = "ecs_container_instance_profile" role = "${aws_iam_role.ec2_instance_role.name}" }
resource "aws_instance" "ecs_container_instance" { ... iam_instance_profile = "${aws_iam_instance_profile.ecs_container_instance_profile.name}" }
キーペア
// public_key_pathで指定したパスに ssh-keygen で作成した公開鍵があるとして resource "aws_key_pair" "auth" { key_name = "${var.key_name}" public_key = "${file(var.public_key_path)}" }
VPC
デフォルト*2のものを利用する場合、Terraformでの設定は不要。
セキュリティグループ
ECSインスタンスとして登録するだけなら、アウトバウンドの通信だけを許可すればよいです。実際はALBやAutoScalingGroupと組み合わせて利用するのでそれらのヘルスチェックや、連携して動く他のサービスからのリクエストを受け付けられるようにする必要がありますが。
resource "aws_security_group" "test_security_group" { name = "test_security_group" egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }
userdataで指定する環境変数 ECS_CLUSTERの指定
#!/bin/bash echo ECS_CLUSTER=woshidan-test-cluster >> /etc/ecs/ecs.config
こういうスクリプトを
$ cat userdata.sh | openssl enc -e -base64
resource "aws_instance" "ecs_container_instance" { ... user_data = <<EOF IyEvYmluL2Jhc2gKZWNobyBFQ1NfQ0xVU1RFUj13b3NoaWRhbi10ZXN0LWNsdXN0 ZXIgPj4gL2V0Yy9lY3MvZWNzLmNvbmZpZw== EOF
のように指定します。
現場からは以上です。
参考
- https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/get-set-up-for-amazon-ecs.html
- https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/instance_IAM_role.html
- https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/launch_container_instance.html
- https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-optimized_AMI.html
- https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-agent-config.html
- キーペアの作成について http://kenzo0107.hatenablog.com/entry/2017/03/27/215941