インフラチームによくある極小サーバーやバッチジョブをECSで集約したい

すごい。この記事には文字しかない。

話の発端

ほとんどアクセスが無く、あっても昼間しかなく、しかし消すわけにはいかない社内サービスのようなデモサービスのようなサーバーが豪勢にもEC2単独インスタンスを持っていると、CPUを使わなすぎてリソースがもったいなかったり、たまったCPUクレジットを捨ててしまったりしてもったいない。日時バッチサーバーみたいなものも似たような状況で、バッチ稼働時以外は上と同じ無駄を今も発生させている。

では、全部Elastic Container Serviceでコンテナにしてインスタンスを集約して、昼間ためたクレジットを夜中の日時バッチで使うのはどうか、という手を考えた。外国リージョンではAWS Batchってやつが使えるようになっているので、そいつが東京リージョンに来たときにうまいことECSから移行できると最高に素敵だ。

各ミニサービスのECSへの集約

上述ミニサーバー群はまがいなりにもサーバーで、しかもSSLなので、IPを固定したい。

が、コンテナにEIPを付けることはできず、Global IPを持たせたい場合は、かわりにELBを使うのが定石1

なのだが、さして重要でもないのにELBを使うのはオーバースペック、というかお金の無駄なので、Unmanagedなコンテナインスタンスを作成し、その中でnginxを動かして、コンテナにリバースプロキシすればーーー、ということを考えていた。コンテナインスタンス障害時にはまるごと作り直せばいいようなサービスレベルだし、それが簡単にできちゃうのがコンテナの長所だ。

が、じゃあそもそもnginxもコンテナでよくね、っていうことに気付いた。コンテナ間でリバースプロキシしちゃえばいいじゃん。

ELB無しでSSLできるコンテナインスタンスの作成

つまりコンテナインスタンスにEIPをつけたい、ということ。ECSのウィザードからクラスタを作成する場合、コンテナインスタンスのカスタマイズ可能項目は少ない。カスタム可能 (つまりコンテナインスタンス作成時に指定可能) なものは

  • インスタンスタイプ
  • ディスク容量
  • VPC
  • Security Group

だけで、Public IPとPrivate IPは自動的に割り当てられるため、コンテナインスタンス作成時にIPやEIPの指定はできない。なので、このあとに、作成されたEC2インスタンスに対して

  1. EIPを作成
  2. EIPをコンテナインスタンスのインターフェースに割り当て

ってやるとコンテナインスタンスのIPを安全に固定できる。

実際にEIPを付けた直後からコンテナに疎通可能になっていて、しかもECSから見えるコンテナインスタンス情報も自動で更新され、Global IPは新しいEIPになっていることが確認できる。便利な世の中になったもんだ。

コンテナ間リンク

nginxもコンテナなので、リバースプロキシ先はコンテナのリンクでOK、と思っていたのだが、なんとコンテナのリンクは同じタスク定義に属したコンテナ間のみで可能とのこと2。つまりexternal_linkは書けない。でも今回のユースケースだと、コンテナごとにタスク定義を変えるタイミング (Docker imageの更新とか) は異なるので、コンテナごとにタスク定義を作ることになる。

ってことは、コンテナごとに80:10080443:10443みたいなPort Mappingを書き、nginxはこのポートとコンテナインスタンスのローカルアドレスで、各コンテナへリバースプロキシする。まぁPort Mappingは複数コンテナインスタンスにまたがってコンテナを配置をしたい場合とかに結局必要だよね、たぶん。

Unmanaged Container Instanceを作りたい場合

ちなみに、どうしてもオレオレカスタムなUnmanagedコンテナインスタンスを作りたいときは

  1. 自力でEC2インスタンスを作成する

    • AWSが用意しているECS用のAMIを使う
    • か、それ以外を使う場合は自分でAgentのインストールなどが必要
    • めんどくさいけどEC2でインスタンスをいろいろいじれる
  2. ECSクラスタ作成時に空のクラスタを作る
  3. さっきのインスタンスをクラスタに追加する

という手順で可能らしい3

でもnginxもコンテナにしちゃうなら、ECSウィザード経由でデフォルトのAMI使う方が再作成と運用が楽な気がする。できるだけ外の世界へ出て行こうとしないのがパブリッククラウドの鉄則。

AWS Batch

ドキュメントを†熟読† (3分) した感じ、ECSにジョブキューを付けて、順番やリトライ、キューごとの計算環境の割り当て、実行時間やリソースの制限、優先度とかを定義できるようにしたものっぽい。†熟読†して思ったことは、

  • 要はPBS4サービスみたいなもんじゃね? PBSのインストールと運用は意外にめんどくさいらしい。というか需要がニッチすぎて活発には開発されてないらしい。通常SQSとかAMQPが実装された何かを使うんじゃないかしら。
  • GPUインスタンスが使えたりとか、AWSをスパコン的に使いたい人にはグッとくるのかもしれない5が、いるのかそんな人?並列性能上げると青天井でお金かかるし、1週間かかる機械学習だとやっぱり青天井でお金かかるし、そういう人は京とかどっかのスパコン借りた方が安いんじゃ……ぶつぶつ……

というわけで、AWS Lambdaで時間足りない人向け、という位置づけが現実的。

ECSからAWS Batchへの移行を考える

東京リージョンで今でも使えるECSから、今後東京リージョンにも展開されるであろうAWS Batchへの移行を考えると、既存のECSクラスタをAWS BatchのCompute Environment (CE) に変換できるとうれしいぞ。ドキュメントを読んでみると

By default, AWS Batch managed compute environments use the latest Amazon ECS-optimized AMI for compute resources.6

どうやらCEはECSのAMIをそのまま使うだけっぽいので、ECSクラスタを作ったあとにCEに追加できるのでは?と思ったが、 (画面上からは) ECSクラスタをCEに変換することはできなさそう。逆はできる、つまりCEをECSクラスタとして扱うことはできる。というか、CEを作った時点で空のECSクラスタが作成されている。末尾にUUIDとかついた、とてもゴチャったECSクラスタが作成される。

AWSというサービスの哲学として、Manageされるもの (e.g. ECSクラスタ) はManageするもの (e.g. CE) にはできない。これは言われれば分かるけど、気付かないと「不便だ」と不要な精神の不衛生を招く。

というわけで、ECSからそのままBatchに移行はできなさそうなので、今回のように特定のEIPを使い続けたいような場合、ECSコンテナインスタンスのEIPを、CEから生まれたECSコンテナインスタンスに移行時に付け替えることになる。

API使っても瞬断が出るが、まぁそもそもそこまでサービスレベルは高くない。はずだ。

でもこれって、ECSクラスタ間でコンテナインスタンスの移動ができれば解決するんじゃ?

と思って調べてみると

各コンテナインスタンスには、それぞれに固有の状態情報がコンテナインスタンスにローカルで保存され、Amazon ECS にも保存されているため、コンテナインスタンスを 1 つのクラスターから登録解除して別のクラスターに再登録しないでください。コンテナインスタンスリソースを再配置するには、1 つのクラスターからコンテナインスタンスを終了し、新しいクラスター内の Amazon ECS に最適化された最新の AMI で、新しいコンテナインスタンスを起動することをお勧めします。7

ぐはっ……。

サーバー系はECSに残し、バッチ系だけAWS Batchに、という住み分けが清潔なのかもしれないけど、それじゃ集約にならないなぁ。

Refs.

続きを読む

[AWS] Terraform で EFS 作って、EC2 起動時にマウントさせておく

Terraform を使うと、EFS を作成して EC2 にマウントさせておくなんてことが簡単にできます。
Autoscaling 環境で Web ドキュメントルートを共有したい時とかに便利なんで、みんな使えばいいと思うよ。
なお、この記事の想定読者は AWS はダッシュボードからポチポチしてインスタンス立てたりしてるけど、そろそろインフラをコードで管理したいな。Terraform とか便利そうだねー使ってみたいねーって人です。
てわけで、単純に EC2 立ち上げても面白くないので EFS をマウントさせてみました。

そもそも、Terraform ってなんだ?って人は、以下のページとか参考になると思います。
Terraform簡易チュートリアル on AWS

実際の設定

Terraform は、特定のディレクトリ下にある拡張子が .tf なファイルを全部読み込んでいい感じにリソースを起動してくれます。なので、機能別に .tf 作成していってみましょう。

メイン設定

まず、メインの設定を作成。
プロバイダーとか、設定ファイル内で使用する変数とか設定していってみましょうか。

main.tf
# 今回のプロジェクト名
variable "project" {}
variable "domain" {}

# AWS リソースを起動するリージョンとかの情報
variable "region" { default = "us-west-2" }
variable "azs" {
    default {
        "a" = "us-west-2a"
        "b" = "us-west-2b"
        "c" = "us-west-2c"
    }
}

# AMI ID (Amazon Linux)
variable "ami" { 
    default {
        "us-west-2" = "ami-8ca83fec"
    }
}

# EC2 接続用の SSH 鍵の公開鍵
variable "ssh_public_key" {}

provider "aws" {
    region = "${var.region}"
}

variable で設定した値は tf ファイル内で ${var.region} のようにして参照可能です。
また、terraform の各種コマンドを実行する際に以下のようにパラメータとして変数を渡して上書きすることもできます。

$ terraform plan \
  -var 'project=example' \
  -var 'domain=example.com'

同じディレクトリ内に terraform.tfvars というファイルがあれば、それを読み込んで値が上書きされたりします。この辺の詳細は以下を参照してください。
Input Variables – Terraform by HashiCorp

provider "aws" は、aws を使いますよって宣言です。
以下のように アクセスキーを書いておくこともできますが、それやるとうっかり github とかに公開した時とかに切ない目にあうのでやめたほうが吉でしょう。

provider "aws" {
    access_key = "__ACCESS_KEY__"
    secret_key = "__SECRET_KEY__"
    region = "us-west-2"
}

環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY を読み込んでいい感じでやってくれるので、僕は direnv 使って作業ディレクトリ内で環境変数を変更することで対応してます。
(もちろん、この場合でも .gitignore.envrc を含めておいて間違って公開しないようにしないと切ない目にあうので注意)

VPC の作成

こんな感じの .tf ファイルで VPC と subnet が作成できます。

vpc.tf
## VPC
resource "aws_vpc" "app" {
    cidr_block           = "172.31.0.0/16"
    enable_dns_hostnames = true
    enable_dns_support   = true
    instance_tenancy     = "default"

    tags {
        "Name" = "${var.project}"
    }
}

## Subnet
resource "aws_subnet" "a" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.0.0/20"
    availability_zone       = "${lookup(var.azs,"a")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-a"
    }
}

resource "aws_subnet" "b" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.16.0/20"
    availability_zone       = "${lookup(var.azs,"b")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-b"
    }
}

resource "aws_subnet" "c" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.32.0/20"
    availability_zone       = "${lookup(var.azs,"c")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-c"
    }
}

resource "aws_subnet" の中に ${aws_vpc.app.id} ってのが出てきましたね。
Terraform の中では、管理下にあるリソースの情報を他のリソースの設定でも参照することが可能です。
各リソースで使用できる値が異なってくるので、その辺は公式ドキュメント読みましょう。
例えば aws_vpc で使用できる値は aws_vpc を参照すればわかります。

また、${lookup(var.azs,"a")} ってのも出てきましたね。
これは Terraform の組み込み関数です、lookup は配列の中からキーをもとに値を探す関数です。
詳しくは Built-in Functions を読んでください。

ついでに Internet Gateway と Route Table も設定しておきましょう。

route-table.tf
## Internet Gateway
resource "aws_internet_gateway" "igw" {
    vpc_id = "${aws_vpc.app.id}"
}

## Route Table
resource "aws_route_table" "rtb" {
    vpc_id     = "${aws_vpc.app.id}"
    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.igw.id}"
    }
}

resource "aws_route_table_association" "route_a" {
    subnet_id = "${aws_subnet.a.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

resource "aws_route_table_association" "route_b" {
    subnet_id = "${aws_subnet.b.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

resource "aws_route_table_association" "route_c" {
    subnet_id = "${aws_subnet.c.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

IAM ロールの作成

次に EC2 に割り当てるための IAM ロールを作ってみましょう。
ポリシーは、AWS が用意している AmazonEC2RoleforDataPipelineRole と、EC2 から CloudwatchLogs にログを送信するためのカスタムポリシーを作ってアタッチしてみます。

iam-role.tf
## For EC2 instance Role
resource "aws_iam_role" "instance_role" {
    name               = "instance_role"
    path               = "/"
    assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

## AmazonEC2RoleforDataPipelineRole
resource "aws_iam_role_policy_attachment" "data-pipeline" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforDataPipelineRole"
}

## PutCloudwatchLogs
resource "aws_iam_policy" "put-cloudwatch-logs" {
    name        = "AmazonEC2PutCloudwatchLogs"
    description = ""
    policy      = <<POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "put-cloudwatch-logs" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "${aws_iam_policy.put-cloudwatch-logs.arn}"
}

aws_iam_roleassume_role_policy のところと、aws_iam_policypolicy のところでヒアドキュメントが出てきましたね。
こんな風に複数行にわたるインラインポリシーはヒアドキュメントで記述することが可能です。
また、以下のように別ファイルにしておいて読み込ませることも可能です。
管理しやすい方でやってください。

iam-role.tf
resource "aws_iam_role" "instance_role" {
    name               = "instance_role"
    path               = "/"
    assume_role_policy = "${file("data/instance_role_assume_policy.json")}"
}
data/instance_role_assume_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

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

EC2 から EFS へのアクセスは 2049 番ポートを介して行われるので、EFS が所属するセキュリティグループに穴を開けないといけません。
EC2 は 80, 443, 22 を解放してみます。

security-group.tf
## For EC2
resource "aws_security_group" "ec2" {
    name        = "${var.project}-EC2"
    description = "for ${var.project} EC2"
    vpc_id      = "${aws_vpc.app.id}"

    ingress = [
        {
            from_port       = 80
            to_port         = 80
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        },
        {
            from_port       = 443
            to_port         = 443
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        },
        {
            from_port       = 22
            to_port         = 22
            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"]
    }
}

## For EFS
resource "aws_security_group" "efs" {
    name        = "${var.project}-EFS"
    description = "for ${var.project} EFS"
    vpc_id      = "${aws_vpc.app.id}"

    ingress {
        from_port       = 2049
        to_port         = 2049
        protocol        = "tcp"
        security_groups = ["${aws_security_group.ec2.id}"]
    }

    egress {
        from_port       = 0
        to_port         = 0
        protocol        = "-1"
        cidr_blocks     = ["0.0.0.0/0"]
    }
}

EFS の作成

こんな感じで EFS が作成できます。
各サブネットごとにマウントターゲットを作成して、そいつをセキュリティグループに所属させる形ですね。

efs.tf
resource "aws_efs_file_system" "app" {
  tags {
        "Name" = "${var.domain}"
  }
}

resource "aws_efs_mount_target" "app-a" {
  file_system_id  = "${aws_efs_file_system.app.id}"
  subnet_id       = "${aws_subnet.a.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

resource "aws_efs_mount_target" "app-b" {
  file_system_id = "${aws_efs_file_system.app.id}"
  subnet_id      = "${aws_subnet.b.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

resource "aws_efs_mount_target" "app-c" {
  file_system_id = "${aws_efs_file_system.app.id}"
  subnet_id      = "${aws_subnet.c.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

EC2 の作成

さて、いよいよ EC2 です。
ここでは、user-data を使って、初回ローンチ時に EFS をマウントさせてしまいます。
さらにマウントした EFS 内に html/ ってディレクトリを作成して、そいつを /var/www/html にシンボリックリンクしてみましょうか。
と言っても、こんな感じで大丈夫です。

ec2.tf
## IAM Instance Profile
resource "aws_iam_instance_profile" "instance_role" {
    name = "instance_role"
    role = "${aws_iam_role.instance_role.name}"
}

## SSH Key
resource "aws_key_pair" "deployer" {
  key_name   = "${var.project}"
  public_key = "${var.ssh_public_key}"
}

## EC2
resource "aws_instance" "app" {
    ami                         = "${lookup(var.ami,var.region)}"
    availability_zone           = "${aws_subnet.a.availability_zone}"
    ebs_optimized               = false
    instance_type               = "t2.micro"
    monitoring                  = true
    key_name                    = "${aws_key_pair.deployer.key_name}"
    subnet_id                   = "${aws_subnet.a.id}"
    vpc_security_group_ids      = ["${aws_security_group.ec2.id}"]
    associate_public_ip_address = true
    source_dest_check           = true
    iam_instance_profile        = "${aws_iam_instance_profile.instance_role.id}"
    disable_api_termination     = false

    user_data                   = <<USERDATA
#!/bin/bash
az="${aws_subnet.a.availability_zone}"
efs_region="${var.region}"
efs_id="${aws_efs_file_system.app.id}"
efs_mount_target="${aws_efs_mount_target.app-a.dns_name}:/"
efs_mount_point="/mnt/efs/$${efs_id}/$${az}"
web_doc_root="/var/www/html"

# EFS Mount
/usr/bin/yum -y install nfs-utils || /usr/bin/yum -y update nfs-utils
if [ ! -d $${efs_mount_point} ]; then
  mkdir -p $${efs_mount_point}
fi
cp -pi /etc/fstab /etc/fstab.$(date "+%Y%m%d")
echo "$${efs_mount_target}    $${efs_mount_point}   nfs4    defaults" | tee -a /etc/fstab
mount $${efs_mount_point}

# create Web document root
if [ -d $${web_doc_root} ]; then
  rm -rf $${web_doc_root}
fi
if [ ! -d $${efs_mount_point}/html ]; then
  mkdir $${efs_mount_point}/html
  chown ec2-user:ec2-user $${efs_mount_point}/html
fi
ln -s $${efs_mount_point}/html $${web_doc_root}
chown -h ec2-user:ec2-user $${web_doc_root}
USERDATA

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 10
        delete_on_termination = true
    }

    tags {
        "Name"          = "${var.domain}"
    }
}

user_data は長めのシェルスクリプトなので、可読性が悪いから ${file("data/user_data.sh")} とかってやって別ファイルで管理したいですよね。
でも待ってください、ヒアドキュメントでやってるのは理由があるのです。

ヒアドキュメントで書くと、user_data 用のシェルスクリプトの中で Terraform の変数が使えます。
マウントするには EFS の ID とか、マウントターゲットの dns_name とか必要になってきますが、それを作成前に知らなくてもこのように書いておけるのです。便利ですね。
その代わり、user_data 用のシェルスクリプト内でローカルな環境変数を使いたい場合は $${efs_mount_point} のように書いてあげてくださいね。

ざっと、こんな感じです。
慣れちゃえば、tf ファイルを使い回しできるので便利ですよ。
また、すでに作成済みの AWS リソースを Terraform 管理下に置きたい場合は

$ terraform import aws_instance.app ${instance_id}

のようにして管理下に置くことができます。
管理されているリソースは terraform.tfstate というファイルに書き込まれます。
さらに別プロダクトになるのですが Terraforming と言うツールを使用すると、既存の AWS リソースから Terraform 用の tf ファイルを作成したり、terraform.tfstate を作成したりもできるので便利です。
Terraforming については、Terraforming で既存のインフラを Terraform 管理下におく を参考にしてください。

実際にリソース作成してみる

tf ファイル書いたら

$ terraform plan

で、設定ファイルに誤りがないか?既存のリソースへの影響はどの程度あるのかが確認できます。
実際に反映させたい時は

$ terraform apply

で、おっけ。

では、良い Terraform を!

続きを読む

AWSの無料SSLを使ってmastodonインスタンスを立てる手順

サーバー周りをAWSで固めてmastodonインスタンス( https://mastodon.kumano-ryo.tech )を立てたので、その手順と資料をまとめました。

SSLは無料で使えますが、サーバーの使用量とかは普通にかかります。とはいえ、お1人〜10人鯖ぐらいならEC2のmicroでも動かせるので、そんなにお金はかからなそう。
立て終わった後に書いたので、記憶違いなどあるかもしれません。コメント・編集リクエストしてください :pray:

なぜAWSなのか

人数が少ないときは安く済ませられるし、リソースが足りなくなってもお金を突っ込めばスケールできるから(要出典)。

必要なもの

  • ドメイン
  • メールを配信する手段
  • AWSのアカウント
  • docker・Web・Linux・AWSなどについての基本的な知識

構成

  • mastodonの公式レポジトリに出てくるdocker-compose.ymlを使う
  • 無料SSLを使うためにRoute53とEastic Load Balancerを使う
  • Redisを外に出すためにElastic Cacheを使う(Optional)
  • Postgresを外に出すためにRDSを使う(Optional)
  • メール送信サービスとしてはSendGridを使った
    • 本筋とは関係ないので今回は省略
    • SMTPが使えればなんでもいい

手順

  1. 設定ファイルを埋める
  2. SSL接続を可能にする
  3. HTTPでのアクセスを禁止する
  4. 画像や動画などをS3に保存するようにする
  5. RedisとPostgresを外に出す(Optional)

設定ファイルを埋める

まずはEC2のインスタンスを立てましょう。OSはubuntu、インスタンスタイプはmediumでいいと思います。microにすると、CPUだかメモリが足りなくてAssetsのコンパイルでコケます。
しかし、サーバー自体はmicroでもそこそこ快適に動くので、デプロイが終わったらmicroにしましょう。
Security Groupの設定については、とりあえず80番と22番が任意の場所から接続できるようになってれば良いです。

そしてEC2上で、mastodon本体をcloneしてきます:

$ git clone https://github.com/tootsuite/mastodon.git

公式のREADMEを参考に、mastodonの設定をします。この時、LOCAL_HTTPSについては、falseにしてください

sudo docker-compose up まで行けたら、EC2インスタンスの80番ポートを3000番ポートにリダイレクトします:

$ sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000

これにて http://yourdomain.com/から、mastodonインスタンスにアクセスできるようになりました!!!!

SSL接続を可能にする

とはいえhttpでログインするのは怖いので、SSLの設定をしましょう。
Elastic Load Balancerの無料SSLを使います。

ELBの作成

Elastic Load Balancerを作ります。
EC2のダッシュボード > ロードバランサー > ロードバランサーの作成から、ロードバランサーを作成します:

  • リスナー

    • HTTPSとHTTPの両方を選んでください
  • 証明書の選択

  • セキュリティグループの設定

    • 任意のIPアドレスからHTTPSが受けられるようなセキュリティグループを作成して設定する
  • ターゲットグループ

    • HTTPだけを追加する
  • ヘルスチェック

    • HTTPで/aboutにする
  • ターゲットの登録

    • 先ほど作成したEC2のインスタンスを選択する

      • ポートは80番を選択する

※作成されたELBのarnはメモっておきましょう。

ELBとインスタンスの接続

EC2ダッシュボード > ターゲットグループから、先ほど作成したターゲットグループを選び、「ターゲット」に登録してあるEC2インスタンスの状態がhealthyであることを確認する。

もしunhealthyであれば、ドキュメント等を参考にして解決しましょう。

ドメインとRoute 53を連携する

ドメインをELBで使えるようにします。
Route 53 > HostedZones > Create Hosted ZonesからHostedZonesを作成します。
HostedZoneを作成したら、ドメインのNSをHostedZonesのNSの値で置き換えます(参考: お名前.comのドメインをAWSで使用する4つの方法
)。
そして、Create Record Setから、Type ARecord Setを作成します。この時、ALIASをYesにして、Alias Targetを先ほどのELBのarnに設定します。

Congrats!

ここまでの手順で、https://yourdomain.comから自分のmastodonインスタンスにアクセスできるようになったはずです!やったね!

もし繋がらなかった場合は、ELBのセキュリティグループやターゲットグループの設定を見直してみてください。

最終的な状態では、

  • ELBにはHTTPSかHTTPでアクセスできる

    • ELBのリスナーに80番ポートと443番ポートが設定されている
  • ELBに入ってきたHTTPSまたはHTTPのアクセスは、EC2の80番ポートに転送される
    • ターゲットグループの「ターゲット」のEC2インスタンスの「ポート」の欄が80である
  • EC2のインスタンスは80番ポートでアクセスを受ける
  • 以上を妨げないようなセキュリティグループの設定である
    • ELBとEC2やその他のAWSのサービスが、すべて別のセキュリティグループを持つ

が満たされているはずです。

HTTPでのアクセスを禁止する

前節まででSSLでのアクセスが実現されましたが、依然HTTPでのアクセスも可能です。
EC2インスタンスへのアクセスがELB経由で行われるようにし、また、HTTPでアクセスされたときはHTTPSにリダイレクトするようにしましょう。

まず、EC2インスタンスのセキュリティグループのインバウンドルールについて、HTTPの行の「送信元」の欄に、ELBのセキュリティグループのID(sg-********)を入力して保存します。
これによって、ELBからのみEC2インスタンスにHTTPSでアクセスできるようになりました。

次に、EC2インスタンスの中で、以下のような設定ファイルでnginxを起動します:

server {
  listen 80;

  location / {
    if ($http_x_forwarded_proto != 'https') {
      rewrite ^ https://$host$request_uri? permanent;
    }
  }
}

これにより、HTTPアクセスをHTTPS付きのURLにリダイレクトすることができます。

画像や動画などをS3に保存するようにする

この状態だと、アップロードされた画像・動画やユーザーのアイコン画像は、すべてdockerコンテナの中に保存されます。
そのため、sudo docker-compose downするたびに、画像と動画が消えてしまうし、アイコン画像もなくなってしまいます。
これではうまくないので、メディアファイルのストレージをS3に設定しましょう。

まず、S3のバケットを作成します。バケットの名前とリージョンは覚えておきましょう。
次に、AWSのIAMコンソールからユーザーを作成して、アクセスキーを取得します。このとき、既存のポリシーを直接アタッチから、** AmazonS3FullAccess**を追加しておきましょう。

そして、.env.production.sampleのコメントアウトしてある設定を利用して、以下のように設定を書き加えます:

S3_ENABLED=true
S3_BUCKET=${your-bucket-name}
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
S3_REGION=${your-bucket-region}
S3_PROTOCOL=https
S3_ENDPOINT=https://s3-${your-bucket-region}.amazonaws.com

これで、画像・動画がS3にアップロードされるようになりました。

RedisとPostgresを外に出す(Optional)

前節までで、所望のmastodonインスタンスが手に入ったと思います。
この構成では、中央のEC2インスタンスの中で、Rails・Redis・Postgres・Sidekiq・nodejsの5つのDockerコンテナが動いていることになり、負荷が集中しています。

ところで、AWSはRedis専用のインスンタンスを提供していますし(ElasticCache)、postgres専用のインスタンス(RDS)も提供しています。
実際どのぐらい効果があるのかわかりませんが、これらのコンテナを中央のEC2から切り離してみましょう。

この辺からは適当です。

インスタンスを立てる

PostgresとRedisのインスタンスを立てます。

このとき、Postgresの設定は、mastodonのレポジトリ内の設定ファイル(.env.production)のDB_*変数に合わせて適当に変えましょう。
DB_HOSTの値は後で取得します。Redisも同様です。

次に、PostgresとRedisのセキュリティグループを編集して、EC2のインスタンスからアクセスできるように設定します。
「HTTPでのアクセスを禁止する」の節と同じ要領でできます。

Postgresのインスタンスタイプについてですが、無料枠に収まるようにmicroにしましたが、ちゃんと動いてるようです。Redisのインスタンスは、無料枠が無いようなので、適当にmicroにしました。今のところ大丈夫そうです。

PostgresとRedisのEndpointをメモっておきます

.env.productionを編集

.env.productionREDIS_HOSTDB_HOSTを、先程メモったEndpointに書き換えます。

docker-compose.ymlを編集

mastodonのdocker-compose.ymlを修正して、dbコンテナとredisコンテナの部分をコメントアウトしましょう。また、他のコンテナのdepend_onの部分もそれに合わせてコメントアウトします。

再起動

$ sudo docker-compose build
$ sudo docker-compose up -d

にてmastodonを再起動します。
これによって、RedisとPostgresがAWSのサービスを利用する形で切り出されたことになります。

その他

続きを読む

SSMを用いたCloudWatchLogsへのイベントログ出力

WindowsServer2012のEC2Config Ver4.x以降とWindowsServer2016からはCloudWatchLogsへのログ出力設定方法が変わりました。

今回は東京リージョンにイベントログを出力する手順に絞って記載します。
※いままでEC2Config経由でログ出力設定をしていた方であれば、任意ログの設定方法もわかると思います。

EC2でWindowsインスタンス作成

  • 割愛

EC2インスタンスにアタッチしたIAMRoleの設定

  • AmazonEC2RoleforSSM を対象インスタンスのIAMRoleにアタッチする

ドキュメント作成

  1. 以下の画面で[Create Document]をクリック
    https://ap-northeast-1.console.aws.amazon.com/ec2/v2/home#Documents:Owner=MeOrAmazon;sort=Name
  2. 以下を入力
    Name: [ドキュメント名]
    Content: 以下のjsonファイルの内容をコピペ
cloudwatchlogs_conf.json
{
    "schemaVersion": "1.2",
    "description": "Example CloudWatch Logs tasks",
    "runtimeConfig": {
        "aws:cloudWatch": {
            "settings": {
                "startType": "Enabled"
            },
            "properties": {
                "EngineConfiguration": {
                    "PollInterval": "00:00:15",
                    "Components": [
                        {
                            "Id": "ApplicationEventLog",
                            "FullName": "AWS.EC2.Windows.CloudWatch.EventLog.EventLogInputComponent,AWS.EC2.Windows.CloudWatch",
                            "Parameters": {
                                "LogName": "Application",
                                "Levels": "1"
                            }
                        },
                        {
                            "Id": "SystemEventLog",
                            "FullName": "AWS.EC2.Windows.CloudWatch.EventLog.EventLogInputComponent,AWS.EC2.Windows.CloudWatch",
                            "Parameters": {
                                "LogName": "System",
                                "Levels": "7"
                            }
                        },
                        {
                            "Id": "SecurityEventLog",
                            "FullName": "AWS.EC2.Windows.CloudWatch.EventLog.EventLogInputComponent,AWS.EC2.Windows.CloudWatch",
                            "Parameters": {
                            "LogName": "Security",
                            "Levels": "7"
                            }
                        },
                        {
                            "Id": "ETW",
                            "FullName": "AWS.EC2.Windows.CloudWatch.EventLog.EventLogInputComponent,AWS.EC2.Windows.CloudWatch",
                            "Parameters": {
                                "LogName": "Microsoft-Windows-WinINet/Analytic",
                                "Levels": "7"
                            }
                        },
                        {
                            "Id": "IISLog",
                            "FullName": "AWS.EC2.Windows.CloudWatch.IisLog.IisLogInputComponent,AWS.EC2.Windows.CloudWatch",
                            "Parameters": {
                                "LogDirectoryPath": "C:\inetpub\logs\LogFiles\W3SVC1"
                            }
                        },
                        {
                            "Id": "CustomLogs",
                            "FullName": "AWS.EC2.Windows.CloudWatch.CustomLog.CustomLogInputComponent,AWS.EC2.Windows.CloudWatch",
                            "Parameters": {
                                "LogDirectoryPath": "C:\CustomLogs\",
                                "TimestampFormat": "MM/dd/yyyy HH:mm:ss",
                                "Encoding": "UTF-8",
                                "Filter": "",
                                "CultureName": "en-US",
                                "TimeZoneKind": "Local"
                            }
                        },
                        {
                            "Id": "PerformanceCounter",
                            "FullName": "AWS.EC2.Windows.CloudWatch.PerformanceCounterComponent.PerformanceCounterInputComponent,AWS.EC2.Windows.CloudWatch",
                            "Parameters": {
                                "CategoryName": "Memory",
                                "CounterName": "Available MBytes",
                                "InstanceName": "",
                                "MetricName": "Memory",
                                "Unit": "Megabytes",
                                "DimensionName": "",
                                "DimensionValue": ""
                            }
                        },
                        {
                            "Id": "CloudWatchLogs",
                            "FullName": "AWS.EC2.Windows.CloudWatch.CloudWatchLogsOutput,AWS.EC2.Windows.CloudWatch",
                            "Parameters": {
                                "AccessKey": "",
                                "SecretKey": "",
                                "Region": "ap-northeast-1",
                                "LogGroup": "Default-Log-Group",
                                "LogStream": "{instance_id}"
                            }
                        },
                        {
                            "Id": "CloudWatch",
                            "FullName": "AWS.EC2.Windows.CloudWatch.CloudWatch.CloudWatchOutputComponent,AWS.EC2.Windows.CloudWatch",
                            "Parameters": 
                            {
                                "AccessKey": "",
                                "SecretKey": "",
                                "Region": "ap-northeast-1",
                                "NameSpace": "Windows/Default"
                            }
                        }
                    ],
                    "Flows": {
                        "Flows": 
                        [
                            "(ApplicationEventLog,SystemEventLog,SecurityEventLog),CloudWatchLogs"
                        ]
                    }
                } 
            }
        }
    }
}

3.[Create Document]をクリック

ドキュメント紐付け

  1. 以下の画面で[Create Association]をクリック
    https://ap-northeast-1.console.aws.amazon.com/ec2/v2/home#ManagedInstances:sort=InstanceId
  2. Command documentで作成したドキュメントを選択
  3. Select Targets byでManually Selecting Instancesを選択後、対象インスタンスを選択
    ※インスタンス作成直後だと、表示されないことがあります。待ちましょう。
  4. [Create Association]をクリック

まとめ

ここまでの手順からわかるとおり、ログ出力設定はドキュメントとして外出しする形になりました。
インスタンス構築の度に同じログ出力設定を書く手間がなくなり、かつログインせずにログ出力設定ができるようになりました。
複数台に同じ設定を適用する場面ではうれしい機能ですね。

続きを読む

AWS Transit VPCソリューションを使って 手軽に マルチリージョン間 VPN環境を構築してみる

Transit VPC とは

AWS の VPC 間 VPN 接続を自動化するソリューションです。
高度なネットワークの知識がなくても、超カンタンに、世界中の リージョンにまたがる VPC を接続するネットワークを構築することができます。

概要


ある VPC を転送専用の VPN HUB(Transit VPC)として作成することで、他のVPCからのVPN接続を終端し、スター型のトポロジを自動作成します。
利用している AWSのアカウントに紐づいた VPC で VGW を作成すれば、HUBルーターに自動的に設定が投入され、VPNの接続が完了します。

予めテンプレートが用意されているので、ルーターの細かい設定を行う必要がありません。

また、高度な設定が必要な場合も、Cisco CSR1000V を利用しているので、Cisco IOS の機能は自由に利用することも可能です。

Adobe社では、大規模なネットワークを、AWS上の CSR1000V で構築しています。
https://blogs.cisco.com/enterprise/adobe-uses-cisco-and-aws-to-help-deliver-rich-digital-experiences

仕組み

  • AWS CloudFormation を使って、Transit VPC の CSR1000v のプロビジョニングが非常に簡単に行えるようになっています。

  1. VGW Poller という Lambda ファンクションが用意されており、Amazon CloudWatch を使って、Transit VPC に接続すべき VGW が存在するかを常に確認しています。
  2. VGWが存在する場合、VGW Poller が必要な設定を行い、設定情報を Amazon S3 bucket に格納します
  3. Cisco Configurator という名前の Lambda ファンクションが用意されており、格納した設定情報から、CSR1000v 用の設定を作成します
  4. Cisco Configurator が Transit VPC の CSR1000v へ SSH でログインし設定を流し込みます。
  5. VPN接続が完了します。

詳細については、下記、ご参照ください。
– AWS ソリューション – Transit VPC
https://aws.amazon.com/jp/blogs/news/aws-solution-transit-vpc/
– Transit Network VPC (Cisco CSR)
https://docs.aws.amazon.com/solutions/latest/cisco-based-transit-vpc/welcome.html

Transit VPC 構築方法

基本は、Wizard に従い、3(+1)ステップで設定が可能です。

事前準備 – CSR1000V用 Key Pair の作成

CSR1000V へログインする際に使う、Key Pair を EC2 で一つ作っておきましょう。
EC2 > Network & Security > Key Pairs から作成できます。名前はなんでもいいです。

.pem ファイルが手に入るので、後ほど、CSR1000V へログインする際にはこれを使います。

Step 1. Cisco CSR1000v ソフトウェア利用条件に同意

AWS マーケットプレイスの下記のページから、リージョンごとの値段が確認できます。

https://aws.amazon.com/marketplace/pp/B01IAFXXVO

  • For Region で 好きなリージョンを選択
  • Delivery Method で Transit Network VPC with the CSR 1000v を選択

その後 Continue

  • Manual Launch タブが選択されていること
  • Region / Deployment Options が先に設定したものとなっていること

上記を確認し、Accept Software Terms をクリックします。

Step 2. Transit VPC スタックの起動

下記のページより、Transit VPC 用の CloudFormation を起動することで、Transit VPC に必要な CSR1000v をセットアップすることができます。

http://docs.aws.amazon.com/ja_jp/solutions/latest/cisco-based-transit-vpc/step2.html

上記ページから Launch Solution をクリックします。
CloudFormation のページが立ち上がるので、まず、VPN HUB を 設置するリージョンになっているか確認します。
このソリューションは、Lambda を使うので、Transit VPC を設置できるのは、Lambda が使えるリージョンに限ります。東京は対応してます。
Lambda 対応リージョンは、こちらで確認できます。
https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/

正しいリージョンになっていたら、設定を進めます。

2-1. Select Template

テンプレートがすでに選択された状態になっていると思うので、特に設定の必要なし

Next をクリック

2-2. Specify Details

幾つかのパラメーターがありますが、最低限下記を変更すれば、動作します。

  • Specify Details

    • Stack name を設定します。
  • Parameters
    - Cisco CSR Configuration

    • SSH Key to access CSR で、事前準備で作成した key を選択します。
    • License Modelで、BYOL (Bring Your Own License) もしくは License Included を選択します。

CSR1000v のライセンスについて、Cisco から購入したものを使う場合は、BYOL を選択します。
ライセンス費用も利用料に含まれた利用形式が良ければ、License included version を選択します。
検証ライセンスがあるので、テストの場合は、BYOL で良いかと思います

入力が完了したら、 Next

2-3. Options

特に設定の必要なし Next をクリック

2-4. Review

  • 設定を確認して、一番下の acknowledge のチェックボックスをクリック

その後、Create で CSR1000V のインスタンス作成・セットアップが始まります

5分ほどすると、CSR1000V が 2インスタンス 立ち上がります。

事後設定(オプション)

起動した CSR1000V は デフォルトで、Lambda ファンクションからのログインのみを許可する設定になっているので、ssh で CLI にアクセスしたい場合は、Security Group の設定を変更します。

EC2 のダッシュボードから CSR1000V を選択し、Security Group の設定で SSH を許可します。
すべてのPCからSSHを受ける場合は、ソースアドレスとして 0.0.0.0/0 を指定します。

設定後、ssh でログインするには、事前準備で作成した キーペアで取得した .pem ファイルのパーミッションを変更します。

chmod 400 <キーペア名>.pem

これを使って、下記のコマンドで CSR1000V にログインできます

ssh -i <キーペア名>.pem <CSR1000V のパブリックIP>

ログインすれば、CSR1000v であることがわかると思います。

ip-XXX-XXX-XXX-XXX #sh ver
Cisco IOS XE Software, Version 16.03.01a
Cisco IOS Software [Denali], CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.3.1a, RELEASE SOFTWARE (fc4)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2016 by Cisco Systems, Inc.
Compiled Fri 30-Sep-16 02:53 by mcpre

<省略>

Step 3. スポーク VPC のタグ付け

上記、CSR1000V が立ち上がれば、あとは、Spoke VPC の VGW でタグを付けるだけで、CSR1000V への VPN 接続が完了します。

3-1. Spoke VPC の作成

新たに Spoke VPC を作成する場合は、下記の手順で作ります。

  • 好きなリージョンに、VPC を作成します。

    • VPC Dashboard > Virtual Private Cloud > Your VPC > Create VPC で作成します。
  • Virtual Private Gateway(VPG) を作成します。
    • VPC Dashboard > VPN Connection > Virtual Private Gateways > Create Virtual Private Gateway で作成します。
  • 作成した VPG を、先程作成した VPCにアタッチします。
    • VPC Dashboard > VPN Connection > Virtual Private Gateways > Attach to VPC  から、VPCへVGWをアタッチします。

3-2. VGW のタグ付け

  • 先程作成した VGW を選択し、下部の Tags タブを選択します。

    • Edit をクリックし Tag を追加します。

デフォルトの設定では、下記のタグを利用する設定になっていますので、下記タグを追加します
Key = transitvpc:spoke
Value = true

上記の設定が完了すると、VGWと Step2. で作成した、2台の CSR1000Vの間で自動的に VPN 接続が確立します。

http://docs.aws.amazon.com/ja_jp/solutions/latest/cisco-based-transit-vpc/step3.html

Step 4. 他の AWS アカウントからの接続設定 (オプション)

http://docs.aws.amazon.com/ja_jp/solutions/latest/cisco-based-transit-vpc/step4.html

まとめ

AWS の Transit VPC を使えば、世界中 にまたがる AWS の 各リージョンのVPCを簡単に VPN 接続できます。

また、VPN HUB ルーターは Cisco CSR1000v なので、企業内のシスコルーターと VPN 接続すれば、DC を AWS にして、世界中にまたがる VPN ネットワークを構築することができます。
ダイレクトコネクトとも連携可能です。

参考

On board AWS TransitVPC with Cisco CSR1000v (英語ですが、手順がわかりやすいです)

Cisco CSR 1000v: Securely Extend your Apps to the Cloud
https://www.slideshare.net/AmazonWebServices/cisco-csr-1000v-securely-extend-your-apps-to-the-cloud

続きを読む

Android端末で、FCM経由でAWSSNSを受け取るまで

はじめに

GoogleがFirebaseを吸収した結果、GCM(Google Cloud Messaging)がFCM(Firebase Cloud Massaging)に切り替わった。
そのため、諸々の設定の方法がGCM時代の手順から変わっている。
ここでは、ゼロからFCMの環境とAWS、FCM、Androidの設定や実装を書いていく。
なお、Server側の実装に関しては記載してない

前提条件

任意のAndroidプロジェクトが作られている
AWSのAccess KeyとSecret Keyが生成されていること

Firebaseの設定

プロジェクトを新規追加し、Androidアプリとの紐づけを行う
1. Firebase ConsoleにGoogleアカウントでログインする。ログインするアカウントにサービスが紐づけられるので、注意すること
2. 「新規プロジェクトを作成」ボタンを押す
3. プロジェクト名を任意で入力。国/地域を「日本」に。
4. プロジェクト作成ボタンを押す。しばらくすると画面が切り替わる
5. 「AndroidアプリにFireabaseを追加」を選択すると、ダイアログが表示されるので情報を入力していく
6. Android package nameにSNSをアプリのパッケージ名を入力する
7. アプリのニックネームはコンソールの表示用なので任意。デバッグ署名は空欄でOK
8. REGISTER APPをクリック
9. Download google-services.jsonをダウンロードして、ここに配置してね、と言われるので言われた通りに配置する
10. 終わったら続行ボタンを押す
11. Gradleの設定してね、と言われるので、言われた通りに設定する。その後、終了をクリック
12. 登録されたアプリの設定を開く(右上の赤丸のやつをクリックするとメニューが出てくる)
スクリーンショット 2017-04-10 17.13.47.png
13. クラウドメッセージングタブを選択する
14. Legacy server key をコピーする。AWS側の設定で使う。

AWS側の設定

AWSSNSサービスとFCMを紐づける
1. AWSコンソールにサインインする
2. 検索窓に「SNS」と入力し、「Simple Notification Service」を選択する
スクリーンショット 2017-04-10 16.52.20.png
3. メニューから「Application」を選択
4. 画面が切り替わるので、「Create platform application」ボタンをクリック
5. Application Nameを入力する。任意
6. push Platform NotificationでGoogle Cloud Messaging(GCM)を選択する
7. API Keyにコピーした「Legacy server key」を貼り付け、Create platform applicationをクリックする
8. 登録されると一覧に情報が追加されるので「ARN」をクリックし、詳細を開く
9. 画面上部にある、「Application ARN」をコピーする

Android Studio側の実装

Gradleの設定

公式の説明が結構わかりやすいのでそちらを見てもOK。特にバージョン情報は最新のものを参照したほうが良い

設定はGradleに以下を追加(Fireabase設定手順でclasspath等の設定は終わっている前提)

build.gradle
dependencies {
  // ...
  compile 'com.google.firebase:firebase-core:10.0.1'
  compile 'com.google.firebase:firebase-messaging:10.0.1'
  // Getting a "Could not find" error? Make sure you have
  // the latest Google Repository in the Android SDK manager
}

メッセージを受け取る用のサービスを追加

先のGradleの設定の公式説明の続きに書いてあるのでそちらを参考したほうがわかりやすいかも。また、メッセージの細かな仕様も同一ページに書いてあるので一読推奨。

やることはAndrdoidManifestの追加とServiceの実装を追加

AndroidManifest.xml
<service android:name=".MyFirebaseMessagingService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
</service>
MyFirebaseMessagingService.java
public class MyFirebaseMessagingService extends FirebaseMessagingService {

    private static final String TAG = "MyFirebaseMsgService";

    /**
     * Called when message is received.
     *
     * @param remoteMessage Object representing the message received from Firebase Cloud Messaging.
     */
    // [START receive_message]
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // TODO(developer): Handle FCM messages here.
        // Not getting messages here? See why this may be: https://goo.gl/39bRNJ
        Log.d(TAG, "From: " + remoteMessage.getFrom());

        // Check if message contains a data payload.
        if (remoteMessage.getData().size() > 0) {
            Log.d(TAG, "Message data payload: " + remoteMessage.getData());
        }
    }
}

AWSにEndpointを登録するための実装を追加

これだけがAWSとFCMの合わせ技になる。
AWS側の公式の説明FCM側の公式の説明を参照。

AWS SDKを追加する

build.gradleに以下を追加

build.gradle
dependencies {
  // ...
    compile 'com.amazonaws:aws-android-sdk-core:2.4.+'
    compile 'com.amazonaws:aws-android-sdk-sns:2.4.+'
}

AndroidManifestにサービスを追加する

AndroidManifest.xmlに以下を追加

AndroidManifest.xml
<service android:name=".MyFirebaseInstanceIDService">
    <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
    </intent-filter>
</service>

MyFirebaseInstanceIDServiceクラスを実装する

MyFirebaseInstanceIDService.java
public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {
    /**
     * Called if InstanceID token is updated. This may occur if the security of
     * the previous token had been compromised. Note that this is called when the InstanceID token
     * is initially generated so this is where you would retrieve the token.
     */
    // [START refresh_token]
    @Override
    public void onTokenRefresh() {
        // Get updated InstanceID token.
        String refreshedToken = FirebaseInstanceId.getInstance().getToken();
        Log.d(TAG, "Refreshed token: " + refreshedToken);

        // If you want to send messages to this application instance or
        // manage this apps subscriptions on the server side, send the
        // Instance ID token to your app server.
        sendRegistrationToServer(refreshedToken);
    }
    // [END refresh_token]

    /**
     * Persist token to third-party servers.
     *
     * Modify this method to associate the user's FCM InstanceID token with any server-side account
     * maintained by your application.
     *
     * @param token The new token.
     */
    private void sendRegistrationToServer(String token) {
        // TODO: Implement this method to send token to your app server.
    }
}

上記のsendRegistrationToServer内で、AWS側にtoken情報を送る。ここはAWS側の実装を参考にすればOK。最終的には以下のようになる

MyFirebaseInstanceIDService.java
public class NotificationInstanceIdServiceTemp extends FirebaseInstanceIdService {

    private static final String APPLICATION_ARN = "AWSSNSでコピーしたApplication ARN";
    private static final String ENDPOINT = "https://sns.ap-northeast-1.amazonaws.com";
    private static final String ACCESS_KEY = "AWSのアクセスキー";
    private static final String SECRET_KEY = "AWSのSecretKey";

    @Override
    public void onTokenRefresh() {
        super.onTokenRefresh();
        String token = FirebaseInstanceId.getInstance().getToken();
        sendRegistrationToServer(token);
    }

    private void sendRegistrationToServer(String token) {
        AmazonSNSClient client = new AmazonSNSClient(generateAWSCredentials());
        client.setEndpoint(ENDPOINT);
        //SharedPreferenceに保存したendpointArnが存在したらそちらから取得するようにしてもOK
        String endpointArn = createEndpointArn(token, client);
        HashMap<String, String> attr = new HashMap<>();
        attr.put("Token", token);
        attr.put("Enabled", "true");
        SetEndpointAttributesRequest req = new SetEndpointAttributesRequest().withEndpointArn(endpointArn).withAttributes(attr);
        client.setEndpointAttributes(req);
    }

    private String createEndpointArn(String token, AmazonSNSClient client) {
        String endpointArn;
        try {
            System.out.println("Creating platform endpoint with token " + token);
            CreatePlatformEndpointRequest cpeReq =
                    new CreatePlatformEndpointRequest()
                            .withPlatformApplicationArn(APPLICATION_ARN)
                            .withToken(token);
            CreatePlatformEndpointResult cpeRes = client
                    .createPlatformEndpoint(cpeReq);
            endpointArn = cpeRes.getEndpointArn();
        } catch (InvalidParameterException ipe) {
            String message = ipe.getErrorMessage();
            System.out.println("Exception message: " + message);
            Pattern p = Pattern
                    .compile(".*Endpoint (arn:aws:sns[^ ]+) already exists " +
                            "with the same token.*");
            Matcher m = p.matcher(message);
            if (m.matches()) {
                // The platform endpoint already exists for this token, but with
                // additional custom data that
                // createEndpoint doesn't want to overwrite. Just use the
                // existing platform endpoint.
                endpointArn = m.group(1);
            } else {
                // Rethrow the exception, the input is actually bad.
                throw ipe;
            }
        }
        storeEndpointArn(endpointArn);
        return endpointArn;
    }

    private AWSCredentials generateAWSCredentials() {
        return new AWSCredentials() {
            @Override
            public String getAWSAccessKeyId() {
                return ACCESS_KEY;
            }

            @Override
            public String getAWSSecretKey() {
                return SECRET_KEY;
            }
        };
    }

    private void storeEndpointArn(String endpointArn) {
        //SharedPreferenceにでもendpointArnを保存して、次回以降はcreateEndpointArnの処理を省略しても良い(公式はその方式になってる)
    }

    private String getEndPointArn() {
        //SharedPreferenceからendpointArnを取得して、次回以降はcreateEndpointArnの処理を省略しても良い(公式はその方式になってる)
    }
}

APPLICATION_ARNはAWSSNSの設定の過程でコピーしたやつを貼り付ける
ACCESS_KEYSECRET_KEYはそれぞれAWSに登録されているものを設定
END_POINTはAPPLICATION_ARNのパラメータと合致するものを記載する。
application ARNのリージョンが「arn:aws:sns:ap-northeast-1〜」となっているならこのページを参考に、該当するURLを記載する。日本のAWSSNSならhttps://sns.ap-northeast-1.amazonaws.comになる。

メッセージを送信/受信する

アプリを起動すると最初にEndpointArnの登録処理が行われる。正常に行われれば、AWSSNS側の「End points」にトークン情報が追加される。
End pointsの画面に行き方は、AWSにログイン→SNSサービスを開く→Applicationsメニューを選ぶ→Arnのリンクをクリック で開ける

メッセージを送信する場合は、Tokenの横のチェックボックスにチェックを入れると「Publish to Endpoint」ボタンが有効になるのでそれをクリックする。
送信メッセージ作成用の画面に切り替わるので任意のメッセージを入力して「Publish message」を押せばOK。正常に実装できていればMyFirebaseMessagingService.javaにメッセージが飛んでくる

続きを読む

AWS CloudFormation(CFn) ことはじめ。(とりあえずシンプルなEC2インスタンスを立ててみる)

AWS CloudFormation(CFn) ことはじめ。(とりあえずシンプルなEC2インスタンスを立ててみる)

まえおき

  • 世のはやりは Infrastructure as Code なのです (博士)
  • AWS でもやって当然なのです (助手)

前提

目的

  • 趣味で AWS いじる範囲だと GUI で EC2 インスタンス建てるのに慣れてしまっていつしか EC2 インスタンスに必要な要素を忘れがちではないですか?

    • セキュリティグループ
    • デフォルトサブネット有無
    • パブリック自動IP付与
    • etc …
  • コード化の際にいろいろ気付きもあり、あとでコードを見返せば必要な要素を一覧することもできるという一挙両得
  • ひとり適当運用で セキュリティグループ や キーペア ぐっちゃぐちゃに増えていってませんか? (自分だけ?)
  • 運用をコード化しつつ見直して行きましょう

今回は CFn の練習がてら EC2 に以下の要素を盛り込んでいきます

  1. AMI

    • Amazon Linux
  2. タイプ
    • t2.micro
  3. 手元の作業PCのから SSH 接続出来るようにする
    • サブネット

      • パブリックIP自動付与
    • キーペア
  4. セキュリティグループ
    • 22 番ポートが開いている
  5. IAM プロファイル(適宜)

実行環境

  • AWS CLI を利用します
  • 適宜 pip 等でインストールして下さい

ref. https://github.com/aws/aws-cli

テンプレートを準備する

  • JSON, YAML などで記載します
  • 自分で使いやすい方を選びましょう
  • JSON は(一般に)コメントが利用できないため、コメントを書き込みたい場合は YAML を選択しましょう

ref. http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-whatis-concepts.html

1. CFn の構文とプロパティ(EC2インスタンス)

ref. http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-formats.html

  • 基本は以下の入れ子構造になります

  • Resources

    • {リソース名}

      • Type
      • Properties
        • {各プロパティ}
  • YAML の場合は以下 (注意: デフォルトサブネットを削除している場合などで以下のサンプルをそのまま使うと失敗します)

Resources:
        myec2instance:
                Type: "AWS::EC2::Instance"
                Properties:
                        ImageId: "ami-859bbfe2" #Amazon Linux AMI
                        InstanceType: "t2.micro"
  • JSON の場合は以下のようになります (注意: デフォルトサブネットを削除している場合などで以下のサンプルをそのまま使うと失敗します)
{
"Resources" : {
    "myec2instance" : {
        "Type" : "AWS::EC2::Instance",
        "Properties" : {
            "ImageId" : "ami-859bbfe2",
            "InstanceType" : "t2.micro"
            }
        }
    }
}

2. 必要なプロパティを収集しましょう

ref. http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance.html#aws-properties-ec2-instance-prop

必要な機能 プロパティ名 タイプ 種別
AMIイメージID ImageId String ami-859bbfe2 既定値
インスタンスタイプ InstanceType String t2.micro 既定値
サブネットID SubnetId String subnet-f7ea1081 ユーザ個別
キーペア KeyName String aws-tokyo-default001 ユーザ個別
セキュリティグループ SecurityGroupIds List sg-f9b58f9e ユーザ個別
IAMロール名 IamInstanceProfile String ec2-001 ユーザ個別

2-1. AWS CLI (と jq コマンド)を利用すると以下のように AMIイメージID を検索できます

ref. http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-images.html
ref. http://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/finding-an-ami.html#finding-an-ami-aws-cli

% aws ec2 describe-images --owners amazon \
--filters "Name=name,Values=amzn-ami-hvm-2017*x86_64-gp2" \
| jq '.Images[] | { Name: .Name, ImageId: .ImageId }'
{
  "Name": "amzn-ami-hvm-2017.03.rc-1.20170327-x86_64-gp2",
  "ImageId": "ami-10207a77"
}
{
  "Name": "amzn-ami-hvm-2017.03.0.20170401-x86_64-gp2",
  "ImageId": "ami-859bbfe2"
}
{
  "Name": "amzn-ami-hvm-2017.03.rc-0.20170320-x86_64-gp2",
  "ImageId": "ami-be154bd9"
}

2-2. サブネットも同様に

ref. http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-subnets.html

% aws ec2 describe-subnets \
| jq '.Subnets[] | { CIDR: .CidrBlock, PublicIPMapping: .MapPublicIpOnLaunch, SubnetId: .SubnetId }'
{
  "CIDR": "172.31.0.192/26",
  "PublicIPMapping": true,
  "SubnetId": "subnet-7bf8a60d"
}
{
  "CIDR": "172.31.0.128/26",
  "PublicIPMapping": true,
  "SubnetId": "subnet-f7ea1081"
}

2-3. キーペア名

ref. http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-key-pairs.html

% aws ec2 describe-key-pairs \
> | jq '.KeyPairs[].KeyName'
"aws-tokyo-default001"

2-4. IAM (以下手抜き)

ref. http://docs.aws.amazon.com/cli/latest/reference/iam/list-roles.html

% aws iam list-roles | jq '.Roles[]'

2-5. セキュリティグループ

ref. http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-security-groups.html

% aws ec2 describe-security-groups | jq '.SecurityGroups[]'

3. テンプレートを書きます

JSONの場合
% cat cloudformation-study.json
{
    "Description" : "test template",
    "Resources" : {
        "myec2instance" : {
            "Type" : "AWS::EC2::Instance",
            "Properties" : {
                "ImageId" : "ami-859bbfe2",
                "InstanceType" : "t2.micro",
                "SubnetId" : "subnet-f7ea1081",
                "KeyName" : "aws-tokyo-default001",
                "SecurityGroupIds" : [ "sg-f9b58f9e" ],
                "IamInstanceProfile" : "ec2-001"
            }
        }
    }
}
YAMLの場合
% cat cloudformation-study.yaml
Resources:
        myec2instance:
                Type: "AWS::EC2::Instance"
                Properties:
                        ImageId: "ami-859bbfe2" #Amazon Linux AMI
                        InstanceType: "t2.micro"
                        SubnetId: "subnet-f7ea1081"
                        KeyName: "aws-tokyo-default001"
                        SecurityGroupIds: [ "sg-f9b58f9e" ]
                        IamInstanceProfile: "ec2-001"

4. では実行します

4-1. ユニークなスタック名を確認します

スタックが1個もない場合の出力例
% aws cloudformation describe-stacks
{
    "Stacks": []
}

4-2. 実行

コマンド
% aws cloudformation create-stack --stack-name create-ec2-001 --template-body file://cloudformation-study.json
実行結果
{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:942162428772:stack/create-ec2-001/1b059fa0-1dbc-11e7-9600-50a68a175ad2"
}

4-3. 実行ステータスはマネコン(WEB GUI)か、さきほど実行したコマンドで確認できます

  • “StackStatus”: “CREATE_COMPLETE” を確認
% aws cloudformation describe-stacks
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:942162428772:stack/create-ec2-001/1b059fa0-1dbc-11e7-9600-50a68a175ad2",
            "Description": "test template",
            "Tags": [],
            "CreationTime": "2017-04-10T07:05:40.261Z",
            "StackName": "create-ec2-001",
            "NotificationARNs": [],
            "StackStatus": "CREATE_COMPLETE",
            "DisableRollback": false
        }
    ]
}

4-4. EC2 インスタンスが作成されていて、SSH で接続可能なことを確認します

% aws ec2 describe-instances --filter "Name=instance-state-name,Values=running"
以下の例はキーファイルがあるディレクトリで実行したもの
% ssh -i "aws-tokyo-default001.pem" ec2-user@ec2-hogehoge.ap-northeast-1.compute.amazonaws.com

5. テンプレートの更新

5-1. AMI イメージID を ami-0099bd67 に変更してみましょう

% aws cloudformation update-stack --stack-name create-ec2-001 --template-body file://cloudformation-study.json
{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:942162428772:stack/create-ec2-001/1b059fa0-1dbc-11e7-9600-50a68a175ad2"
}
% aws cloudformation describe-stacks
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:942162428772:stack/create-ec2-001/1b059fa0-1dbc-11e7-9600-50a68a175ad2",
            "Description": "test template",
            "Tags": [],
            "CreationTime": "2017-04-10T07:05:40.261Z",
            "StackName": "create-ec2-001",
            "NotificationARNs": [],
            "StackStatus": "UPDATE_IN_PROGRESS",
            "DisableRollback": false,
            "LastUpdatedTime": "2017-04-10T07:20:37.436Z"
        }
    ]
}

5-2. 何が起こったか確認できましたか?

Screenshot 2017-04-10 16.22.28.png

からの

Screenshot 2017-04-10 16.22.56.png

最初に作成した EC2インスタンス は自動シャットダウンからターミネートされ、あたらしい EC2インスタンス が起動しました

5-3. はい、そういうことですね

% aws cloudformation describe-stack-events --stack-name create-ec2-001 \
| jq '.StackEvents[] | { ResourceStatus: .ResourceStatus,Timestamp: .Timestamp }'

(snip)

{
  "ResourceStatus": "DELETE_COMPLETE",
  "Timestamp": "2017-04-10T07:22:47.470Z"
}
{
  "ResourceStatus": "DELETE_IN_PROGRESS",
  "Timestamp": "2017-04-10T07:21:40.963Z"
}

(snip)

6. おわり

7. おまけ

  • コードですので github などに置いて差分管理するとなお良いかと
  • 認証情報などをコード内に書かないように注意

https://github.com/hirofumihida/my-cfn

続きを読む