長生村本郷Engineers'Blog

千葉県長生村本郷育ちのエンジニアが書いているブログ

EC2 Instance Connect で AWS EC2 への ssh 管理を IAM User or Group で簡単に♪

f:id:kenzo0107:20190628154100p:plain

概要

2019-06-28 に EC2 Instance Conncet が発表されました!

これによって、セキュリティグループと IAM 権限で ssh アクセス許可が可能になります。

例えば、
会社の IP からのみ、特定の IAM User Group に所属している IAM User に ssh アクセス権限を付与、
別のプロジェクトへ異動した、退職した場合は、その IAM User Group から削除で ssh アクセス権限を剥奪できます。

試験環境

macOS 10.14.3 で試しました。

事前準備

$ pip install -U awscli

$ aws s3api get-object --bucket ec2-instance-connect --key cli/ec2instanceconnectcli-latest.tar.gz ec2instanceconnectcli-latest.tar.gz

$ sudo pip install ec2instanceconnectcli-latest.tar.gz

発行した IAM User のパーミッション権限に以下を追加

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "EC2InstanceConnect",
            "Action": [
                "ec2:DescribeInstances",
                "ec2-instance-connect:SendSSHPublicKey"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}

この辺りは terraform 管理案件ですね。

EC2 Instance Conncect 対応 OS

  • Ubuntu>=16.04
  • AmazonLinux2>=2.0.20190618

ssh ログインする EC2側の設定

Ubuntu>=16.04

ec2-instance-connect をインストールしておく必要があります。

$ sudo apt-get update && sudo apt-get install ec2-instance-connect
$ dpkg -l | grep ec2-instance-connect

ii  ec2-instance-connect           1.1.9-0ubuntu3~18.04.1            all          Configures ssh daemon to accept EC2 Instance Connect ssh keys

AmazonLinux2>=2.0.20190618

ec2-instance-connect は設定済みです。

セキュリティグループ

ssh ログイン先となる EC2 インスタンスのセキュリティグループはアクセス元から ssh (22 port) を開けておく必要があります。

ssh ログインしてみる

local%$ mssh ubuntu@i-0f123456abcdefg --profile <profile> --region ap-northeast-1

一見、誰しもが ubuntu でログインしていて監査が不安になりますが、 CloudTrail はちゃんと誰がログインしたか見ています。

CloudTrail

f:id:kenzo0107:20190628160255p:plain
CloudTrail

以下イベントでログが残っています。

  • SendSSHPublicKey
  • DescribeInstances

SendSSHPublicKey の「イベントの表示」ボタンクリックで JSON が表示されますが、その中で、アクセス元 IP, IAM User Arn、アクセス先 インスタンスIDがわかります。

{
    "eventVersion": "1.05",
    "userIdentity": {
        "type": "IAMUser",
        "principalId": "ABCDEFGHIJK....",
        "arn": "arn:aws:iam::123456789012:user/hogehoge",
        "accountId": "123456789012",
        "accessKeyId": "AKIxxxxxxxxxxxxxxxx",
        "userName": "hogehoge",
        "sessionContext": {
            "attributes": {
                "mfaAuthenticated": "false",
                "creationDate": "2019-06-28T06:18:50Z"
            }
        }
    },
    "eventTime": "2019-06-28T06:18:51Z",
    "eventSource": "ec2-instance-connect.amazonaws.com",
    "eventName": "SendSSHPublicKey",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "xx.xxx.xxx.xxx",
    "userAgent": "aws-ec2-instance-connect-cli/1.0.0 Python/2.7.16 Darwin/18.2.0 Botocore/1.12.179",
    "requestParameters": {
        "instanceId": "i-0f.......",
        "osUser": "ubuntu",
        "SSHKey": {
            "publicKey": "ssh-rsa AAAAB....rHb"
        }
    },
    "responseElements": null,
    "requestID": "01234567-890a-1234-5b6d-......",
    "eventID": "f51...",
    "eventType": "AwsApiCall",
    "recipientAccountId": "123456789012"
}

こちらで EC2 インスタンスのアクセス履歴等はわかります。

まとめ

これまで ssh アカウント管理は手間でしたが、IAM 権限での管理によって非常に楽になりました♪

CloudTrail で監査もバッチリ!

Nginx IP 直アクセス不許可 & LB ヘルスチェック設定

f:id:kenzo0107:20190424103347p:plain

よく設定している Nginx の configure file のアクセス元によっての振り分け方をまとめました。

LB → Nginx → Rails

Nginx 設定

  • conf.d/default.conf
# cannot allow ip direct
server {
  listen       80;
  server_name  _;
  return       444;
}

# healthcheck from LB
server {
  listen 80 default_server;
  listen [::]:80 default_server;
  root /work/app/public;

  location = /healthcheck.html {
    access_log  off;
    proxy_pass http://puma;
  }
}

server {
  listen  80;
  server_name example.com;
  ...

IP 直アクセス禁止

server_name _ とすることで、ip 直アクセスをターゲットにしています。

server {
  listen       80;
  server_name  _;
  return       444;
}

LB からのヘルスチェック

LB からヘルスチェックを向ける先を default_sever 設定することで、この server ディレクティブを参照します。

server {
  listen 80 default_server;
  listen [::]:80 default_server;
  root /work/app/public;

  location = /healthcheck.html {
    access_log  off;
    proxy_pass http://puma;
  }
}

上記 config file は、AWS ALB のヘルスチェックパスを /healthcheck.html とし、その向け先を Rails puma にしています。

Rails 側で以下の様に gem 'ok_computer' に向けるのも良し、独自にレスポンス返すも良しです。

get 'healthcheck.html', to: 'ok_computer/ok_computer#index'

ドメイン指定

example.com でアクセスされた際にこちらの server ディレクティブを参照します。

server {
  listen  80;
  server_name example.com;
  ...

ドメイン指定の悪い例

以前は以下の様に指定し、ip 直アクセス、ヘルスチェック対応していました。

server {
    listen 80;
    server_name example.com;

    if ($host != "example.com") {
        return 444;
    }

    location = /healthcheck.html {
      access_log  off;
      proxy_pass http://puma;
    }
    ...
}

勿論これでも動作します。ですが、やや可読性が悪いです。

マルチドメインでの IP 直アクセス不許可に対応をする際にも、この if 文がどんどん長くなります。

その為、向け先の意図毎に server {} を小まめに分ける運用の方が可読性が高く、実運用していてメンテナンサビリティが高いと感じました。

以上 参考になれば幸いです。

Terraform 運用ベストプラクティス 2019 ~workspace をやめてみた等諸々~

f:id:kenzo0107:20190417103456p:plain

以前 terraform で workspace 毎に tfstate 管理する方法を執筆しましたが、実運用上いくつかの問題がありました。

結論、現在は workspace 運用をやめています。

kenzo0107.hatenablog.com

workspace 運用例

まずは実際の運用例です。

もっとうまいことやってるぞ!という話はあろうかと思いますが、まずはありがちなケースを紹介します。

例) セキュリティグループ作成

以下の要件を実現するセキュリティグループを作成するとします。

要件

  • stg では、社内で Wifi の ip からのみアクセス可
  • prd では、ip 制限なくアクセス可

サンプルコード

  • variables.tf
variable "ips" {
  type = "map"
  default = {
    stg.cidrs    = "12.145.67.89/32,22.145.67.89/32"
    prod.cidrs   = "0.0.0.0/0"
  }
}
  • security_group.tf
resource "aws_security_group" "hoge" {
  name        = "${terraform.workspace}-hoge-sg"
  vpc_id      = "${aws_vpc.vpc_main.id}"
}

resource "aws_security_group_rule" "https" {
  security_group_id = "${aws_security_group.hoge.id}"
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  cidr_blocks       = ["${split(",", lookup(var.ips, "${terraform.workspace}.cidrs"))}"]
}

resource "aws_security_group_rule" "https" {
  security_group_id = "${aws_security_group.hoge.id}"
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
}

実際に terraform を plan/apply する前にまずは terraform workspace を定義する必要があります。

terraform workspace new stg // 既に作成されていたらエラーとなります。
terraform workspace select stg

// terraform workspace = stg とした場合の tfstate をローカルのメモリ上で管理します。
terraform init 

上記のような処理があって、初めて、 variable "ips"stg.cidrs, prd.cidrs が利用できるようになります。

こちらを運用しようとしてみると以下の様な問題にぶつかりました。

実運用との相性が悪い

ステージングのみに反映させたい、という時にどう運用したら良いでしょうか。

ステージング用、本番用に設定して、プルリクエストが通って、サンプルのコードを master にマージしていたらどうでしょう?

本番にもデプロイして良さそうに見えます。

いや、むしろ反映されていなければ、混乱します。

その後に master にマージして、本番に反映させたいコードがあった時に、サンプルコードの部分は反映させたくない!と言っても反映されてしまいます。

かといって、以下の様なコードを複数リソースに入れていくのは、余計なステップ数も増え、脳内でリソースが消費されます。レビューするのも辛いです。

count = "${terraform.workspace == "stg" ? 1: 0}"

では、本番用は設定しなければいいじゃないか!と言って設定しないと、本番用はエラーを出す様になり、その他の反映が何もできなくなります。

これはステージングも本番も同じファイルを参照している為に発生しています。

また、 workspace を利用していると以下の様な問題もありました。

stg, prd 以外に新たに workspace を追加したい場合

以下要望があった場合にどうでしょうか。

  • 負荷試験をする為に本番同様の環境を用意してください」
  • 「外部 API との連携試験をしたいので環境を別途増やして欲しいです!」

例えば、 負荷試験環境を用意しようとすると、 loadtst という workspace を用意するとしたら variables.tf を以下のように修正が必要です。

variable "ips" {
  type = "map"
  default = {
    loadtst.cidrs = "12.145.67.89/32,22.145.67.89/32" // 追加
    stg.cidrs     = "12.145.67.89/32,22.145.67.89/32"
    prod.cidrs    = "0.0.0.0/0"
  }
}

上記例ですと variable "ips" に 1行加えただけで良いですが、実際は あらゆる変数に loadtst.*** = *** というコードを追加していく必要があります。

workspace が増える毎に step 数が増え、ファイルの見通しが悪くなります。

また、以下の様なコードがあると、こちらも脳内リソースを消費し、疲弊します。

lookup(var.ips, "${terraform.workspace}.cidrs")
"${terraform.workspace == "stg" ? hoge: moge}"

workspace 運用をまとめると

workspace の利用はリソースを複数環境で共有する ことで運用する想定の為に、可読性の悪化、実運用との乖離がありました。

  1. 新たに workspace 追加する際に、全ての変数 map に追加しなければならない。
    → コードの見通しが悪くなる。
    → 新規環境の構築難易度が上がる。

  2. ステージングのみに反映という時の実運用が困難
    → ステージングも本番も同じファイルを参照している為、ファイルの中でステージングの場合は?と処理を分ける必要が出てきてしまう。

  3. 今、どの workspace なのかがわかりずらく、 terraform apply する際にかなり躊躇してしまう。
    → 実際 terraform apply 実行前に terraform workspace show で workspace 確認しても、実行中で少し時間が経つと、「あれ?どっちだっけ?」と不安になり、 Terminal を遡って確認することがあったりしました。

ではどうすると良いか?

徹底的に workspace をやめます。

= DRY な設計しよう!

これに尽きます。

実際にどうしたか以下まとめました。

ディレクトリ構成は以下のようにしました。

modules/common ... stg, prd どちらの環境でも共通して同構成で作成するリソースを置きます。

modules/stg,prd ... 個々に異なる構成となるリソースを置きます。*1

.
├── README.md
├──envs/
│   ├── prd
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   ├── provider.tf
│   │   ├── region.tf
│   │   ├── templates
│   │   │   └── user-data.tpl
│   │   └── variable.tf
│   └──stg/
│       ├── backend.tf
│       ├── main.tf
│       ├── provider.tf
│       ├── region.tf
│       ├── templates
│       │   └── user-data.tpl
│       └── variable.tf
│
└──modules
    ├── common
    │   ├── bastion.tf
    │   ├── bucket_logs.tf
    │   ├── bucket_static.tf
    │   ├── certificate.tf
    │   ├── cloudfront.tf
    │   ├── cloudwatch.tf
    │   ├── codebuild.tf
    │   ├── codepipeline.tf
    │   ├── network.tf
    │   ├── output.tf
    │   ├── rds.tf
    │   ├── redis.tf
    │   ├── security_group.tf
    │   └── variable.tf
    ├── prd
    │   ├── admin.tf
    │   ├── admin_autoscaling_policy.tf
    │   ├── api.tf
    │   ├── app.tf
    │   ├── ecr.tf
    │   ├── iam_ecs.tf
    │   ├── output.tf
    │   ├── variable.tf
    │   └── waf.tf
    └── stg
        ├── admin.tf
        ├── api.tf
        ├── app.tf
        ├── ecr.tf
        ├── iam_ecs.tf
        ├── output.tf
        ├── variable.tf
        └── waf.tf

前出のセキュリティグループの作成を例にするとどうなるか

以下の様になります。

  • envs/prd/variables.tf
variable "cidrs" {
  default = [
    "0.0.0.0/0",
  ]
}
  • envs/stg/variables.tf
variable "cidrs" {
  default = [
    "12.145.67.89/32",
    "22.145.67.89/32",
  ]
}
  • envs/common/security_group.tf
resource "aws_security_group" "hoge" {
  name        = "${terraform.workspace}-hoge-sg"
  vpc_id      = "${aws_vpc.vpc_main.id}"
}

resource "aws_security_group_rule" "https" {
  security_group_id = "${aws_security_group.hoge.id}"
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  cidr_blocks       = ["${var.cidrs"))}"]
}

resource "aws_security_group_rule" "https" {
  security_group_id = "${aws_security_group.hoge.id}"
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
}

もし stg だけに反映させたいセキュリティグループであれば、 envs/stg/security_group.tf に作成したいセキュリティグループを記述します。

これで stg だけ反映という実運用をカバーできます。

また、負荷試験環境 ( loadtst ) という環境を用意したい場合は、以下の様にコピーし、変数を修正すれば良いです。

  • envs/prdenvs/loadtst
  • modules/prdmodules/loadtst

多少構成に変更があろうとも、 loadtst 関連のリソースが prd, stg に影響することはない様に作成できます。

terraform コーディングルール

以下のような workspace の切り替えを利用したコードを利用しないことです。

lookup(var.ips, "${terraform.workspace}.cidrs")
"${terraform.workspace == "stg" ? hoge: moge}"

また、以下も NG とします。 stg だけ異なるのであれば、 modules/stg,prd と分けるべきです。

"${var.env == "stg" ? hoge: moge}"

terraform 実行手順

stg, prd 各環境構築は envs/stg, envs/prd ディレクトリに移動し、 以下実行します。

terraform init
terraform get -update
terraform plan
terraform apply

AWS credentials の扱い

stg, prd で同じ AWS Account を利用する場合、プロジェクトの root に direnv 等、 .envrc を置いて、運用するのが良いと思います。

stg, prd で異なる AWS Account を利用する場合、 envs/(stg,prd) 以下に .envrc をそれぞれ配置し、上記 terraform 実行手順 を実行すれば良いです。

プロジェクト毎の terraform バージョンの違いの対応

tfenv で対応します。

macOS%$ brew install tfenv

以前の執筆記事では terraform を one-off container で実行しバージョン差異を吸収する様にしていましたが、コマンドが長くなり、管理も煩雑になるので、tfenv が望ましいです。

こちらも運用してみての実感です。

その他

これはしといた方がオススメ?レベルですが、 provider で バージョン固定外した方が良かったです。

provider aws {
  version = "1.54.0"
  region  = "ap-northeast-1"
}

固定されていて、最新のリソースが利用できない時があります。*2

その時は、バージョン固定でなく、アップデートしていく方向で修正した方が、最新に追従できます。

総評

実運用をしてみて、 workspace はやめておいた方がいいかなと感じたことをまとめました。

勿論、 workspace の良さを知り尽くしてないからこういう意見になっているとも思いますので、一概に否定する意図はありません。

リポジトリの整理がついたら現段階で公開できるところをしていこうと思います!

以上 Terraform 運用されてる方の知見になりましたら幸いです。

Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス

Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス

*1:ECS + RDS + Redis 構成で CodePipeline からデプロイするサンプル terraform です。

*2:Aurora MySQL が作れない!と思ったら、バージョン固定してた為だったことがありました。

ProxySQL で DB の Read/Write Endpoint スイッチング

docker-compose 上で ProxySQL で primary DB と secondary DB への SQL 毎にアクセス先をスイッチングする環境を構築し、試験してみました。

github.com

ProxySQL とは?

ProxySQL はハイパフォーマンスな MySQLSQL プロキシです。

MySQLのフォークである Percona Server や MariaDB だけでなく、Galera Cluster にも対応しています。

今回やってみようと思ったのは

今回注目したのは ProxySQL の SQL プロキシの機能です。

ProxySQL は SQL によって、Read/Write エンドポイントをスイッチングしてくれます。

  • SELECT なら Read エンドポイントへ
  • INSERT, UPDATE, DELETE なら Write エンドポイントへ

という感じです。

使おうと思った経緯

Rails に関わらず、アプリケーション側の問題で、Read/Write のスイッチングができない場合があります。

  • Rails で特定の gem に依存して switch_point が効かないところがあるとか。。
  • 独自フレームワークで DB 側の処理が複雑すぎて手が出せないとか。。

なまじっか、サービスが成長していくと、アプリケーション側で DB のスイッチングができないことが、直接的に DB のボトルネックへ繋がることになりかねません。

この解決の為に ProxySQL を利用しようと思いました。

実際に試してみる。

冒頭のリポジトリgit clone して docker-compose up して頂ければ、起動します。

README の通りに実施してみてください。

  • UPDATE で primary DB へ
  • SELECT で secondary DB へ

アクセスしているのがわかります。

TODO

むしろここが肝心ですね。すでにお調べいただいている方、ご教示くださいましたら幸いです。

以上 参考になれば幸いです。

Rails (gem 'sendgrid-ruby') × SendGrid の Event Notification で API Key ごとの独自メタ情報を設定する

f:id:kenzo0107:20190313234822p:plain

SendGrid の Event Notification の使い所

SendGrid には Event Notification という Webhook を設定することでメールの送信状態をイベント情報として取得することができます。

メールを SendGrid が受信した、送信先に届いた、等の情報です。

sendgrid.kke.co.jp

例えば、未達だったメールの情報を取得したい場合等に、この Webhook を利用し、イベント情報を保存することで調査や集計が可能です。

AWS API Gateway + Lambda で構築したエンドポイントに投げ、S3 に保存し、送信失敗件数を Athena で検索集計する、ということができます。

何か問題でも?

SendGrid は 1アカウントで複数のプロジェクト毎の API Key を発行することができます。

ですが、 イベント情報にはどの API Key を利用してメール送信したか、の記録がありません。

複数の API Key がある場合に、どのプロジェクトのどの環境で送信したのか、調査や集計ができません。

これを解決する手段として、メール送信時にメタ情報を登録する方法があります。

Rails 5.2 で試してみました。

まずはセットアップ

  • Gemfile
gem 'sendgrid-ruby'
  • config/initializers/sendgrid.rb
sendgrid_api_key = Rails.application.credentials.dig(Rails.env.to_sym, :sendgrid_api_key)
ActionMailer::Base.add_delivery_method :sendgrid, Mail::SendGrid, api_key: sendgrid_api_key

api_key は credentials に設定し、そこから取得。*1

  • lib/mail/send_grid.rb
# frozen_string_literal: true

class Mail::SendGrid
  def initialize(settings)
    @settings = settings
  end

  def deliver!(mail)
    sg_mail = SendGrid::Mail.new
    sg_mail.from = SendGrid::Email.new(email: mail.from.first)
    sg_mail.subject = mail.subject
    personalization = SendGrid::Personalization.new
    personalization.add_to(SendGrid::Email.new(email: mail.to.first))
    personalization.subject = mail.subject
    sg_mail.add_personalization(personalization)
    sg_mail.add_content(SendGrid::Content.new(type: 'text/plain', value: mail.body.raw_source))

    // ここでカテゴリー情報として登録
    sg_mail.add_category(SendGrid::Category.new(name: "#{Rails.env}-#{Settings.project_name}"))

    sg = SendGrid::API.new(api_key: @settings[:api_key])
    response = sg.client.mail._('send').post(request_body: sg_mail.to_json)
    Rails.logger.info response.status_code
  end
end

#{Rails.env}-#{Settings.project_name} の部分は適宜変更してください。

メール送信してみる

rails c して

ActionMailer::Base.mail(to: "nakayama.kinnikunn@hogehoge.jp", from: "info@<sender authentication で認証したドメイン>", subject: "メールタイトル", body: "すいません、テスト送信です").deliver_now

すると、Event Notification では以下の様なイベント情報が取得できます。

{"email":"nakayama.kinnikunn@hogehoge.jp","timestamp":1551964210,"ip":"12.345.67.89","sg_event_id":"xxxxxxxxxxxxxxxx","sg_message_id":"xxxxxxxxxxxxxxxxxxxx.yyyyyyyyyyyyyyyyyyyyyy","category":["staging-kenkoboys"],"useragent":"Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0 (via ggpht.com GoogleImageProxy)","event":"open"}

注目すべきは category":["staging-kenkoboys"] です。

add_category したカテゴリ情報が取得でき、これで どの API Key のイベント情報であるかの紐付けができます。

以上 参考になれば幸いです。

現場で使える Ruby on Rails 5速習実践ガイド

現場で使える Ruby on Rails 5速習実践ガイド

*1:RAILS_ENV=development でお試し可なので直に設定でも可。そこは自己責任で

Rails に reCAPTCHA v3 導入して bot 対策

概要

Rails で構築した Webサービスbot 攻撃を定期的に受けた為、問い合わせフォームに reCAPTCHA v3 を導入しました。

何故 v2 でなく、reCAPTCHA v3 ?

v2 は I'm not a robot チェックボックスにチェックを入れた後に画像選択させる仕様があります。

例えば、看板が写ってるのはどれ?と選ばせる問いが出てきた場合、
「どこまでが看板としたらいいの?」と心理的負担も高く、ユーザが離脱する可能性もあります。

f:id:kenzo0107:20190216210737p:plain

v3 だと嬉しいことは何?

v3 *1 は設置したページのユーザ行動をスコア化し bot か判断します。

アクセスが増えるとより精度が高まってくる、という仕様です。

bot ユーザへの負担は全くなく、 bot を遮断できる様になるという、世の中進んでるなぁ感満載です。

gigazine.net

gem ある?

今回 gem は使用しませんでした。

というのも、 以下理由からでした。

  • gem 'recaptcha' が v3 非対応。
  • gem 'new_google_recaptcha' は v3 対応してますが、スコアが返ってこないのでテストし辛い。

その他に既にあるのかもわかりませんが、記事執筆時には探し出すことはできませんでした。

まず reCAPTCHA v3 発行

以下 reCAPTCHA コンソールにアクセスし発行してください。

https://g.co/recaptcha/v3

v3 を選択し、今回導入するドメインを登録します。*2

f:id:kenzo0107:20190216214351p:plain

発行されたサイトキー・シークレットキーを保存しておきます。

  • サイトキー

    • ユーザがサイトにアクセスした際にトークンを取得する際に必要なキーです。こちらはユーザ公開して問題ありません。
  • シークレットキー

    • トークンを元に Google に問い合わせする際に必要なキーです。こちらは秘密情報として扱います。

f:id:kenzo0107:20190216214558p:plain

Rails 側実装

Rails >= 5.2 を想定しています。

config/credentials.yml.enc

recaptcha:
  secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

シークレットを秘密情報に保存します。

app/controllers/application_controller.rb

require 'net/http'
require 'uri'

class ApplicationController < ActionController::Base
...
  RECAPTCHA_MINIMUM_SCORE = 0.5
  RECAPTCHA_ACTION = 'homepage'
...
  def verify_recaptcha?(token)
    secret_key = Rails.application.credentials.recaptcha[:secret_key]
    uri = URI.parse("https://www.google.com/recaptcha/api/siteverify?secret=#{secret_key}&response=#{token}")
    r = Net::HTTP.get_response(uri)
    j = JSON.parse(r.body)
    j['success'] && j['score'] > RECAPTCHA_MINIMUM_SCORE && j['action'] == RECAPTCHA_ACTION
  end
end

共通メソッドとして、recaptcha の認証メソッド verify_recaptcha? を設定しています。

ここで、bot となるスコアを 0.5 以下としています。

通常通り操作していれば、十分超える数値です。

config/locales/en.yml

en:
  recaptcha:
    errors:
      verification_failed: 'reCAPTCHA Authorization Failed. Please try again later.'

local en 設定です。

config/locales/ja.yml

ja:
  recaptcha:
    errors:
      verification_failed: 'reCAPTCHA 認証失敗しました。しばらくしてからもう一度お試しください。'

local ja 設定です。

app/controllers/hoges_controller.rb

class HogesController < ApplicationController
  def new; end

  def create
    unless verify_recaptcha?(params[:recaptcha_token])
      flash.now[:recaptcha_error] = I18n.t('recaptcha.errors.verification_failed')
      return render action: :new
    end

    # something to do

    redirect_to hoge_finish_path
  end

  def finish; end
end

new から create に post して reCAPTCHA で bot 判定して

  • OK → finish へ進む
  • NG → new に戻る

という設計です。

app/views/hoges/new.html.erb

<% if flash[:recaptcha_error] %>
<div class="text">
  <p><spacn class="error"><%= flash[:recaptcha_error] %></span></p>
</div>
<% end %>

<%= form_tag({action: :create}, {method: :post}) do %>
...
  <input id="recaptcha_token" name="recaptcha_token" type="hidden"/>
  <%= submit_tag "送信する", :class => "submit-recaptcha btn", :disabled => true %>
<% end %>

<script src="https://www.google.com/recaptcha/api.js?render=<%= Settings.recaptcha.site_key %>&ver=3.0"></script>
<script>
grecaptcha.ready(function() {
  grecaptcha.execute('<%= Settings.recaptcha.site_key %>', {action: 'homepage'}).then(function(token) {
    $('#recaptcha_token').val(token);
    $('.submit-recaptcha').prop('disabled', false);
  });
});
</script>
エラーメッセージ表示
<% if flash[:recaptcha_error] %>
<div class="text">
  <p><spacn class="error"><%= flash[:recaptcha_error] %></span></p>
</div>
<% end %>
<form> ~ </form> 内に以下 name=recaptcha_token input タグを追加します。
<input id="recaptcha_token" name="recaptcha_token" type="hidden"/>
ページアクセス時に reCAPTCHA の token を取得すべく、スクリプトを仕込みます。
<script src="https://www.google.com/recaptcha/api.js?render=<%= Settings.recaptcha.site_key %>&ver=3.0"></script>
<script>
grecaptcha.ready(function() {
  grecaptcha.execute('<%= Settings.recaptcha.site_key %>', {action: 'homepage'}).then(function(token) {
    $('#recaptcha_token').val(token);
    $('.submit-recaptcha').prop('disabled', false);
  });
});
</script>

reCAPTCHA トークン取得が成功した場合に以下実行します。

  • id="recaptcha_token" input タグの valueトークンを設定
  • submit ボタンの有効化

<%= Settings.recaptcha.site_key %> について
gem 'settingslogic' をインストールしている前提で設定しています。

導入していない場合は、簡易的に処理を試す程度であれば、 <%= Settings.recaptcha.site_key %> を取得したサイトキーに置き換えて下さい。*3

以上で設定は完了です。

ページにアクセスしてみる

ページ右下に reCAPTCHA マークが常に表示される様になります。

f:id:kenzo0107:20190216234742p:plain

集計情報を見る

reCAPTCHA コンソールを見ると、以下の様な表示が出ていてすぐには集計情報が反映されていないと思います。

f:id:kenzo0107:20190216235044p:plain

しばらく経つと以下の様なグラフが表示される様になります。

f:id:kenzo0107:20190216235204p:plain

注意

例えば、社内 IP 等固定された IP からテストで頻繁にアクセスすると、 bot 扱いされます。

reCAPTCHA 側で IP のホワイトリストはないので、その場合、 Rails 側で許可 IP リストを作る必要があります。

以上
参考になれば幸いです。

*1:2019年2月現在最新バージョン

*2:ドメインは複数登録可能です。ドメイン毎に集計や、 bot 対策の傾向を変えたい場合は、個々に発行します。 また、 RAILS_ENV = production とそれ以外で発行する方が本番への影響がないので推奨されます。

*3:前にもお伝えしましたが、サイトキーの管理は直指定でなく、何かしら管理が推奨です。

AWS ECS トラブルシューティング

f:id:kenzo0107:20190208225212j:plain

ECS を利用していて幾つかはまったポイントがあったのでまとめました。

started 1 task が複数回実行されるが、コンテナが起動しない

$ ecs-cli compose service up ...

level=info msg="(service hogehoge) has started 1 tasks ..."
level=info msg="(service hogehoge) has started 1 tasks ..."
level=info msg="(service hogehoge) has started 1 tasks ..."

ecs-cli compose service up でデプロイ時にタスク起動を実行するものの、起動が正しくできていない状態です。
こちらはコンテナ起動時の処理に問題がある場合があります。

  • コンテナログを確認して、コンテナ起動失敗時刻付近のログを確認してください。
  • 例えば、Nginx の設定ファイル, Rails のコードに typo, syntax error がある等です。

already using a port required by your task

service hogehoge was unable to place a task because no container instance met all of its requirements. 
The closest matching container-instance a1b2c3d4-e5f6-g7h8-j9k0-l1m2n3o4p5q6 is already using a port required by your task

port mapping を以下の様に設定していた。

"portMappings": [
   {
     "hostPort": 0,
     "protocol": "tcp",
     "containerPort": 80
   }
 ],

新しいタスクでも 0:80 のポートを利用しようとする為、エラーとなります。 以下の様に設定することで回避できました。

"portMappings": [
   {
     "containerPort": 80
   }
 ],

insufficient memory available

INFO[0031] (service hogehoge) was unable to place a task because no container instance met all of its requirements. The closest matching (container-instance a1b2c3d4-e5f6-g7h8-j9k0-l1m2n3o4p5q6) has insufficient memory available. For more information, see the Troubleshooting section of the Amazon ECS Developer Guide.  timestamp=2018-03-09 15:45:24 +0000 UTC

タスク更新(ecs-cli compose service up)実行時、
上記の様なメモリ不足が出る場合はインスタンスタイプを上げる、また、他タスクを削除する等、メモリーリソースを増やす対応が必要です。

no space on device

no space on device で イメージを pull できない。

f:id:kenzo0107:20191003173809p:plain

df -hT コマンドで 容量の使用状況確認

未使用のコンテナ・ボリュームを強制削除しお掃除

docker system prune -af --volumes

msg="Couldn't run containers" reason="RESOURCE:CPU"

msg="Couldn't run containers" reason="RESOURCE:CPU"

タスクで指定している cpu (vCPU) が不足しています。 インスタンスタイプを上げる、もしくは、他タスクを削除する等、 CPU リソースを増やす対応が必要です。

Fargate - Port Mapping Error

level=error msg="Create task definition failed" error="ClientException: When networkMode=awsvpc, the host ports and container ports in port mappings must match.\n\tstatus code: 400, request id: a1b2c3d4-e5f6-g7h8-j9k0-l1m2n3o4p5q6"

起動タイプ Fargate で以下の様な設定だと、NG

    ports:
      - "80"

こちらだと OK。

    ports:
      - "80:80"

ホストポートとコンテナポートのマッピングが必要です。

Fargate volume_from は利用できない

volume_from は Fargate では使用できません。

level=error msg="Create task definition failed" error="ClientException: host.sourcePath should not be set for volumes in Fargate.\n\tstatus code: 400, request id: a1b2c3d4-e5f6-g7h8-j9k0-l1m2n3o4p5q6"

指定された IAM Role が適切なパーミッションを与えられていない

IAM Role に権限を適宜付与します。

level=info msg="(service hogehoge) failed to launch a task with (error ECS was unable to assume the role 'arn:aws:iam::123456789012:role/ecsTask
ExecutionRole' that was provided for this task. Please verify that the role being passed has the proper trust relationship and permissions and that your IAM user has permissions to pass this role.)." timestamp=2018-06-21 08:15:43 +0000 UTC

イメージ pull できないというエラーも権限を付与していないことに起因することが主です。

CannotPullContainerError: API error (500): Get https://123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/v2/: net/http: request canceled while waiting for connection"

現在稼働している ECS の IAM Role の権限を参考してください。変更される可能性があるのであくまで参考にし、適宜最新の情報を以ってご対応ください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "logs:PutLogEvents",
                "logs:CreateLogStream",
                "logs:CreateLogGroup",
                "elasticloadbalancing:RegisterTargets",
                "elasticloadbalancing:Describe*",
                "elasticloadbalancing:DeregisterTargets",
                "ecs:UpdateService",
                "ecs:Submit*",
                "ecs:StartTelemetrySession",
                "ecs:StartTask",
                "ecs:RunTask",
                "ecs:RegisterTaskDefinition",
                "ecs:RegisterContainerInstance",
                "ecs:Poll",
                "ecs:ListTasks",
                "ecs:DiscoverPollEndpoint",
                "ecs:DescribeTasks",
                "ecs:DescribeServices",
                "ecs:DescribeContainerInstances",
                "ecs:DeregisterContainerInstance",
                "ecs:CreateService",
                "ecr:UploadLayerPart",
                "ecr:PutImage",
                "ecr:InitiateLayerUpload",
                "ecr:GetDownloadUrlForLayer",
                "ecr:GetAuthorizationToken",
                "ecr:CompleteLayerUpload",
                "ecr:BatchGetImage",
                "ecr:BatchCheckLayerAvailability",
                "ec2:Describe*"
            ],
            "Resource": "*"
        }
    ]
}

以上です。

また何か発生したら追記していきたいと思います。

Reference