Amazon ECSを用いたDocker本番運用の実現

はじめに

現在お手伝いしているアカウンティング・サース・ジャパンにて、ECSを使ったDockerの本番運用を始めたので、その一連の流れについてまとめました。

税理士向け会計システムを扱うアカウンティング・サース・ジャパンでは最近Scalaでの新規プロジェクトが立ち上がってきており、既存のプロジェクトはJavaであったり、Erlangであったりと様々な言語が用いられていますが、インフラ人員が少ないということもあり、なるべくシンプルなインフラ構成を実現する必要がありました。

そういった中、各アプリケーションをDocker化することでインフラとしては共通基盤としてのDockerクラスタのみの管理になり、運用コストが下がるのではないかという仮説からDocker化を進めることになりました。クラスタを実現するに辺りKubenatesなどの選択肢もありましたが、今回はECSを選択し、下記のようにAWSのマネージドサービスを最大限に活用しています。

  • オーケストレーションツール: Amazon EC2 Container Service (ECS)
  • サービスディスカバリ: Application Load Balancer (ALB)
  • Dockerレジストリ: Amazon ECR
  • ログ、メトリクス収集: CloudWatch, CloudWatch Logs
  • 監視: CloudWatch Alarms
  • Infrastructure as Code: CloudFormation
  • CIツール: Jenkins

各技術の選定理由

今回Docker化を行うに辺り、下記を優先的に技術選定を行いました。

  • 運用が楽であること
  • 構成がシンプルで、技術の学習コストが低いこと

まずは、オーケストレーションツールの選定です。候補に上がったのは、Docker Swarm、Kubernetes、ECSです。

DockerのSwarm modeは本番での運用例が技術選定時点であまり見当たらなかったので候補から落としました。次にKubernetesとECSですが、海外の事例などではどちらも多く使われているようです。

今回は多機能さよりも運用に手間がかからない方が良いと考え、マネージドサービスであるECSが第一候補にあがりました。ここは詳細に調査したというよりも、ある種勢いで決めています。その上でやりたいことが実現できるかどうか一つ一つ技術検証を行った上で導入判断を行いました。

同じようにマネージドサービスを優先的に使ったほうが良いという考えで、ログなどでもCloudWatchを使っています。

AWSインフラをコードで記述するものとしてはTerraformが良く取り上げられている気がしますが、個人的にはいくつかの理由でCloudFormationを推しているのでこちらを使っています。

CIツールですが、社内の標準であるJenkinsをそのまま使うことにしました。

全体構成

下記のような構成になっています。

スクリーンショット 2017-05-21 12.46.39.png

ざっくりと説明すると、developmentブランチにプッシュするとGithub HookでJenkinsがDockerイメージをビルドして、ECRにPushします。ユーザはJenkinsでDeployジョブを実行(あるいはBuildの後続ジョブとして自動実行)し、CloudFormationにyamlファイルを適用することでTask, Service, ALB, Route53設定, CloudWatch設定を一通り実行します。またECSのClusterはあらかじめCloudFormationテンプレートを作成して作っておきます。

Task/Serviceの更新についてはCloudFormationを経由しない方がシンプルかとは思いまいしたが、Service毎に管理するRoute53やCloudWatchと合わせて一つのテンプレートにしてしまうのが良いと判断しました。

ここまでやるなら専用のデプロイ管理ツールを作った方がとも思ったのですが、業務委託という立場で自分しかメンテができないものを残すものは躊躇されたため、あくまでAWSとJenkinsの標準的な機能を組み合わせて実現しています。

CloudFormationテンプレートの解説

上記の流れが全てなので理解は難しくないと思いますが、一連の処理で重要なポイントとなるのはCloudFormationテンプレートなのでこれについてだけ触れておきます。長いテンプレートなのでざっくりとだけ雰囲気を掴んでもらえればと思います。

ECSクラスタのテンプレート

cluster作成用のCloudFormationテンプレートは下記のようになっています。

gist:cluster.yaml

一見複雑に見えますが、Amazon EC2 Container Service テンプレートスニペットを参考に作ると簡単に作成できると思います。

(あまりそのまま書くと会社に怒られそうなため)省略していますが、実際にはここにECSクラスタの監視を行うCloudWatch Alarmなどを設定することで、監視設定までこのテンプレートだけで完了します。

ECSクラスタはインフラチーム側であらかじめ用意しておき、リソースが足りなくなったときなどには適宜インスタンス数を変更したりクラスタ自体を別途作ったりしていきます。オートスケーリングを導入すればそれすら必要なくなります(今回はDocker運用が初めてだったので知見がたまるまで手動での対応にしています)。

インフラ側としての責務はここまでで、下記のテンプレートで定義される個別のサービスについてはアプリ開発者側の責務として明確に責任境界を分けました。(もちろん実際にはサポートはかなりの部分でしています。)

これにより全員が今までよりインフラに近い領域まで意識するように個人の意識が変わっていくことを期待しています。

個別サービス用テンプレート

開発環境、ステージング環境、プロダクション環境などそれぞれで同一のテンプレートを使うようにし、パラメータを使用します。そのパラメータをJenkinsのジョブ内で注入することで実現します。VPCなどの環境で決まる値はJenkinsジョブで実行するスクリプト内で定義し、アプリケーションごとの値は environment.yaml というファイルを用意してスクリプトから読み込みます。

environment.yamlは例えば下記のようになっています。アプリケーション開発者は、特殊なことをしない限りは service.yaml をインフラチームが用意したservice.yamlをコピーして、environment.yamlだけ編集すれば良い形になっています。DSLですら無いのでアプリ側のメンバーも心理的な抵抗が少ないようで良かったです。

environment.yaml
images:
- xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hoge-image
parameters:
  default:
    TaskMemory: 512
    TaskMaxMemory: 990
    ImageRepositoryUrl: xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hoge-image
    ServiceDesiredCount: 1
  dev:
    ClusterName: dev-default
    JavaOpts: "-Xmx256MB"
  stg:
    ClusterName: stg-default
    JavaOpts: "-Xmx256MB"
  prod:
    ClusterName: default
    JavaOpts: "-Xmx1500MB -Xms1500MB"
    TaskMemory: 1990
    TaskMaxMemory: 1990
    ServiceDesiredCount: 2

そして service.yaml は下記のようなファイルです。

gist:service.yaml

これもAmazon EC2 Container Service テンプレートスニペットから作ればすぐにできるのではないかと思います。(もちろん全てのパラメータは一つ一つ値を検討します。)

こちらもCloudWatch周りや重要でないところは削除しています。色々と手で削ってるのでコピペだと動かない可能性大ですが雰囲気だけ掴んで貰えればと思います。

このファイルは全アプリケーションで同一ファイルを使うのではなく、アプリケーションごとにコピー/編集して利用します。全体の変更を行うときには全プロジェクトのファイルを更新しなければいけませんが、共通基盤がアプリケーション側を制約しないように、プロジェクト毎のyamlファイル管理としています。ファイルの配置場所は各Gitリポジトリに配置するのが理想ですが、現状ではDocker運用になれてくるまで全てのyamlファイルを管理するリポジトリを作成してインフラチーム側が主に編集する形を取っています。

デプロイ

あとは、このservice.yamlとenvironment.yamlを組み合わせてデプロイするRubyスクリプトでもJenkinsのPipelineのコードでも適当に書いてJenkinsのJobを登録すれば完了です。(environment.yamlファイルを読み込んで aws cloudformation create-stack でservice.yamlと共にパラメータとして渡すだけなので簡単です!)

新規アプリ開発時も社内標準のservice.yamlとenvironment.yamlをファイルを持ってきて、environment.yamlを修正した上で、Jenkinsにジョブを登録すればすぐにDockerクラスタへのデプロイ準備が整います。しかも、上記のテンプレート例では割愛していますが、テンプレートには監視項目/通知設定まで書かれているので、インフラ側で設定を行う必要もなく監視が開始されます。CloudFormation最高ですね。

おわりに

実際の運用ではミッションクリティカルなアプリケーションならではの品質管理のために、JenkinsのPipeline機能を利用して開発→検証→リリースまでのデプロイメントパイプラインを実現しています。

アプリケーションのSECRETなどコミットしない情報をどう管理するかも検討する必要がありますが、これは管理の仕方はチームによって異なると思ったため割愛しています。

また、ログ解析としてはS3に出されたALBのログをRedash+Amazon Athenaでエラー率やアクセス数を分析できるようにし、CPU使用率やメモリ使用率などのパフォーマンス状況をCloudWatchの内容をGrafanaで可視化しています。これによりログ収集の基盤などを作らずに必要な可視化を実現することができました。ベンチャーでは分析基盤の運用も大きなコストになってしまうため、こういった工夫も必要です。(もちろん重要なKPIについては別途分析する仕組みが整っています。)

今回の構成が最高とは思いませんが、ある程度満足行くところまではできたかなと思います。もっとよくできるよ!とか一緒にやりたいな!とかもっと詳細聞きたいな!いう方はぜひ @miyasakura_ までご一報ください。

続きを読む

EMRの基本的な利用手順

Hadoopに関する基本的な内容をまとめてみたものです。Hadoopに関する、Web上にすでにある解説コンテンツをまとめたサイトの抜粋です。
各種BIツールが使えるAmzon EMR

Amazon EMRとは

Amazon EMRとは、AWS上でオープンソース型フレームワーク Hadoopが動作出来る環境を提供するものです。

一般的にはHadoopを使用するためには複数のサーバーを用意する必要がありますが、Amazon EMRを使えば新たにサーバーを購入したりシステムの構成を変更する必要はありません。
Amazon EMRがHadoopを常時使用可能な環境を構築しますので、容易にデプロイする事が可能となります。

Amazon EMRの特徴

Amazon EMRには、以下のような特徴があります。

・Amazon EMR でHadoopクラスターを実行した時の仮想サーバー数の増減が簡単に出来ます
・ハードウェア等のメンテナンス料金を支払う必要はなく、Amazon EMRでクラスターを使用した分だけの料金を払うだけです
・Hive、Pig、HBase などのHadoop アプリケーションの使用が可能
・Amazon EC2、Amazon S3、DynamoDB、Amazon RDSなどのAWSとAmazon EMRを統合する事が出来ます
・Microsoft Excel、MicroStrategy、QlikView、TableauなどのBIツールを使って、データの分析を行う事が出来ます

EMRの基本的な利用手順

EMRの基本的な利用手順は、次のようなものです。
EMRの入門者でも、比較的容易に操作する事が出来ます。

入出力データ・データ格納領域をS3等に用意
Amazon S3 を使って、Amazon EMRに入出力データやログファイルなどを格納出来ます。
S3コンソールを開きパケット名とデータのあるパス等を指定する事で、S3パケットを作成します。

クラスターの起動
Amazon EMRコンソールを開き、ソフトウェア・ファイルシステム・ハードウェア等の設定を行ってから、クラスターを作成します。

Hiveスクリプトを実行する
Amazon EMRコンソールを使って、Hiveスクリプトを実行します。
Hiveスクリプトをステップとして送信する事で、出力を確認する事が出来ます。
S3コンソールを開いて、出力したパケットのフォルダ内の出力ファイルで確認します。

Hue を使ってクエリを送信する
Hadoop用オープンソースウェブユーザーインターフェイスであるHueにログインして、クエリを送信します。
Hueを使う事で簡単にクエリを送信したり、スクリプトが作成出来るようになります。

EMRを使用した後の処理

EMRを使用した後は追加料金が発生しないように、不要なリソースは削除しなくてはなりません。

Amazon S3のバケットの削除
Amazon S3 コンソールを使えば、選択したオブジェクトを削除出来ます。

Amazon EMRクラスターの終了
Amazon EMRクラスターを終了するには、Amazon EMRコンソールを開いてCluster Listページで終了したいクラスターのチェックボックスをオンにして、Terminateを選択します。

続きを読む

EC2 instance起動時にtagをつけるTagSpecifications

AWSCLIでEC2 instance起動時に同時にタグをつける方法としては、instance起動してinstance-idを取得しておいて、パイプでつないでtagをつけたり、スクリプトの中で後でタグ付けする方法があったと思います。
http://kurochan-note.hatenablog.jp/entry/2017/01/08/220155

AWSCLI EC2 Run-Instanceのなかに–tag-specificationsというoptionが入って、run-instancesの中でタグが作成できるようになりました。地味なアップデートかもしれませんが、結構うれしいです。

instanceの詳細はjsonに記述して、下記のように指定して実行します。

aws ec2 run-instances --cli-input-json file://instance.json

EC2は山ほど設定項目があるので、generate-cli-skeltonでフォーマットを出力して、必要な項目だけ入力して、不必要なものは消すとinstanceの詳細を記述したjsonの完成です。Gitにでも入れておきましょう。
http://docs.aws.amazon.com/cli/latest/userguide/generate-cli-skeleton.html

aws ec2 run-instances --generate-cli-skeleton

Instanceの設定詳細を記述したjsonサンプル

instance.json
{
    "ImageId": "<image-id>",
    "KeyName": "<my-key>",
    "SecurityGroupIds": [
        "<my-sgid>"
    ],
    "InstanceType": "<instance-type>",
    "BlockDeviceMappings": [
        {
            "VirtualName": "Root",
            "DeviceName": "/dev/sda1",
            "Ebs": {
                "VolumeSize": 100,
                "DeleteOnTermination": true,
                "VolumeType": "gp2"
            }
        }
    ],
    "Monitoring": {
        "Enabled": false
    },
    "SubnetId": "<subnet-id>",
    "DisableApiTermination": false,
    "IamInstanceProfile": {
        "Name": "<instance-iam-role>"
    },
    "TagSpecifications":[
        {
            "ResourceType": "instance",
            "Tags": [
              {
                "Key": "Name",
                "Value": "<server-name>"
              },
              {
                "Key": "ClusterName",
                "Value": "<cluster-name>"
              },
              {
                "Key": "Application",
                "Value": "<myapp>"
              },
              {
                "Key": "CostCenter",
                "Value": "<my-cost-center>"
              },
              {
                "Key": "Environment",
                "Value": "Test"
              },
              {
                "Key": "User",
                "Value": "<user-name>"
              }
            ]
        },
        {
          "ResourceType": "volume",
          "Tags": [
            {
              "Key": "Device",
              "Value": "<device-name>"
            },
{
              "Key": "CostCenter",
              "Value": "<my-cost-center>"
            },
            {
              "Key": "backup_key",
              "Value": "true"
            }
          ]
        }
    ]
}

続きを読む

【MySQL】AWS Aurora上でもgh-ostオンラインマイグレーション

【MySQL】gh-ostでオンラインマイグレーションの応用編です。

gh-ostはMySQLのバイナリログを使ってテーブルを同期します。
一方で、AWSのAmazon Auroraは、バイナリログを使わない方法でリードレプリカを作成します。

※Auroraのリードレプリカが一体どういう仕組みなのかここでは省きますが、過去のAWS SummitのDeep DiveとかでAmazonの超技術を垣間見ることができます。
[レポート] Amazon Aurora deep dive ~性能向上の仕組みと最新アップデート~ #AWSSummit | Developers.IO

さてバイナリログを使っていないとなると、gh-ostの利用はどうなるのでしょうか。
gh-ostにもドキュメントがありますが

https://github.com/github/gh-ost/blob/master/doc/rds.md

gh-ost has been updated to work with Amazon RDS however due to GitHub not relying using AWS for databases, this documentation is community driven so if you find a bug please open an issue!

(gh-ostはAmazon RDSでも動くようになったが、我々GitHubはDBにAWSは使ってないので、このドキュメントはコミュニティに懸かっている。だからバグを見つけたら是非issueを上げて欲しい)

とあり、gh-ostの特徴のひとつである「GitHub社での実績」が欠けます。
実際に使ってみました。

準備

下記条件でAWS RDS Auroraインスタンスを作ります。
t2.smallインスタンスが解禁されたので、検証用途でもAuroraを作りやすくなりました。

  • Aurora 5.6.10a
  • db.t2.small
  • writer×1、reader×1

また、接続元のIPアドレスを調べてセキュリティグループでポートを開けておきます。

gh-ostを使うにはバイナリログが必要です。
先述のとおりAuroraのリードレプリカ作成にはバイナリログは使われませんが、Auroraと他のMySQL等とのレプリケーションのためにバイナリログの出力機能は存在しています。

Aurora と MySQL との間、または Aurora と別の Aurora DB クラスターとの間のレプリケーション – Amazon Relational Database Service

gh-ostを利用するには、DB Cluster Parameter Groupにて「binlog_format=ROW」に設定しておく必要があります。

クラスターエンドポイントに接続し、show binary logsでバイナリログファイルが確認できればOKです。

mysql> SHOW VARIABLES LIKE "binlog_format";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
1 row in set (0.01 sec)

mysql> show binary logs;
+----------------------------+-----------+
| Log_name                   | File_size |
+----------------------------+-----------+
| mysql-bin-changelog.000001 |       120 |
| mysql-bin-changelog.000002 |     16062 |
+----------------------------+-----------+
2 rows in set (0.01 sec)

今回の検証には下記のデータベース/テーブルを使います。

USE sushiya;

CREATE TABLE sushi(
  id   INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(20),
  INDEX(id)
);

gh-ostの実行

まず入手します。

$ wget https://github.com/github/gh-ost/releases/download/v1.0.36/gh-ost-binary-linux-20170403125842.tar.gz
$ tar xzvf ./gh-ost-binary-linux-20170403125842.tar.gz

gh-ostには3つのモードがありますが、Aurora上ではb. Connect to masterにて実行します。

$ ./gh-ost 
  --user="(ユーザー)" 
  --password="(パスワード)" 
  --host="(クラスターエンドポイント)" 
  --port=3306 
  --database="sushiya" 
  --table="sushi" 
  --alter="ADD COLUMN price INT DEFAULT 100, ADD COLUMN created_at DATETIME" 
  --allow-on-master 
  --execute

確認します。

mysql> show create table sushiG
*************************** 1. row ***************************
       Table: sushi
Create Table: CREATE TABLE `sushi` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `price` int(11) DEFAULT '100',
  `created_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.01 sec)

カラムが増えていることを確認できました。

gh-ostの特徴としてa. Connect to replica, migrate on masterモードやc. Migrate/test on replicaモードがありますが、これらを使うためにリードレプリカに接続して実行しようとしても「バイナリログが無効」というエラーで失敗してしまいます。

$ ./gh-ost 
>   --user="(ユーザー)" 
>   --password="(パスワード)" 
>   --host="(リーダーエンドポイント)" 
>   --port=3306 
>   --database="sushiya" 
>   --table="sushi" 
>   --alter="ADD COLUMN price INT DEFAULT 100, ADD COLUMN created_at DATETIME" 
>   --test-on-replica
2017-05-02 01:11:40 FATAL (リーダーエンドポイント):3306 must have binary logs enabled

もしAuroraからバイナリログを受け取って同期しているSlaveデータベースがあれば、c. Migrate/test on replicaモードでの実行も可能なのではないかと思います。

Interactive commandsの使用

通常のgh-ost利用時と同様に、UNIXドメインソケットを使った制御もAurora経由で可能です。

gh-ost/interactive-commands.md at master · github/gh-ost

テーブル切り替えを保留にするフラグファイルを作成します。

$ touch /tmp/ghost.postpone.flag

実行します。

$ ./gh-ost 
  --user="(ユーザー)" 
  --password="(パスワード)" 
  --host="(クラスターエンドポイント)" 
  --port=3306 
  --database="sushiya" 
  --table="sushi" 
  --alter="ADD COLUMN price INT DEFAULT 100, ADD COLUMN created_at DATETIME" 
  --allow-on-master 
  --postpone-cut-over-flag-file=/tmp/ghost.postpone.flag 
  --execute

--postpone-cut-over-flag-fileオプションで、先程作成したフラグファイルを指定するとテーブルの同期だけが進み、切り替えが保留状態になります。

ここからステータスの確認など、gh-ost実行途中での操作ができます。

$ echo status | nc -U /tmp/gh-ost.sushiya.sushi.sock

unposeponeでテーブルの切り替えが実行されます。

$ echo unpostpone | nc -U /tmp/gh-ost.sushiya.sushi.sock

確認します。

mysql> SHOW CREATE TABLE sushiG
*************************** 1. row ***************************
       Table: sushi
Create Table: CREATE TABLE `sushi` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `price` int(11) DEFAULT '100',
  `created_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.01 sec)

カラムが追加されました。

Auroraでgh-ostの必要性がどれくらいあるかはさておき、Auroraでも一部条件下にてgh-ostを利用できることがわかりました。

参考文献

続きを読む

IAM Database AuthenticationをPythonから試す

背景

RDS for MySQLとAuroraのDB接続にIAMが使えるようになったそうです。
Manage access to your RDS for MySQL and Amazon Aurora databases using AWS IAM
IAM Database Authentication for MySQL and Amazon Aurora
サンプルがJavaだったのでPythonから試してみました。

環境

  • Amazon Linux 2017.03
  • Aurora 1.12
  • Python 2.7.12
  • boto3-1.4.4

準備

Aurora

RDSのクラスターから「クラスターの変更」を開き「IAMのDB認証を有効にする」を「はい」に設定します。Auroraの場合db.t2.smallはIAMデータベース認証をサポートしていないためdb.t2.medium以上で試してください。
RDS_·_AWS_Console.png

DBユーザーの作成

IAMアクセス用のDBユーザーを作成し、必要な権限を付与します。

mysql> CREATE USER iam_auth_user@'testdb-cluster.cluster-abcdefghijkl.ap-northeast-1.rds.amazonaws.com' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS';
mysql> GRANT SELECT ON `testdb`.* TO iam_auth_user@'%';

公開鍵のダウンロード

IAMデータベース認証はSSL接続が必須という事なので公開鍵をダウンロードしてec2上の適当なパスに配置しておきます
http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Aurora.Overview.html#Aurora.Overview.Security.SSL

IAM

ドキュメントを参考にIAM Roleに権限を付与します。リソースIDはクラスターのものを指定しています。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "rds-db:connect"
      ],
      "Resource": [
        "arn:aws:rds-db:ap-northeast-1:12345678:dbuser:cluster-12ABC34DEFG5HIJ6KLMNOP78QR/iam_auth_user"
      ]
    }
  ]
}

接続

iam_db_auth.py
#  -*- coding: utf-8 -*-
from __future__ import print_function
import boto3
import mysql.connector
from mysql.connector.constants import ClientFlag

rds = boto3.client('rds', region_name='ap-northeast-1')

user = 'iam_auth_user'
host = 'testdb-cluster.cluster-abcdefghijkl.ap-northeast-1.rds.amazonaws.com'
db_auth_token = rds.generate_db_auth_token(host, 3306, user, 'ap-northeast-1')

config = {
    'user': user,
    'password': db_auth_token,
    'host': host,
    'db': 'testdb',
    'client_flags': [ClientFlag.SSL],
    'ssl_ca': 'rds-combined-ca-bundle.pem'
}

cnx = mysql.connector.connect(**config)
cur = cnx.cursor(buffered=True)

cur.execute('SELECT AURORA_VERSION();')
print(cur.fetchone())

cur.close()
cnx.close()
$ python iam_db_auth.py 
[(u'1.12',)]

以上です。

参考

続きを読む

GolangのWebアプリケーションをECSにデプロイするCI環境をCodeCommit、CodeBuild、CodePipelineでつくる【cloudpack大阪ブログ】

cloudpack大阪の佐々木です。
内部で開発しているGolangのWebアプリをECSで稼働させるように、CI環境をつくってみました。
パブリックのgithubとかであれば、CircleCIとかでもう少しいい感じにできるとおもうのですが、ローカルのGitリポジトリで開発しているため、パブリックアクセスできないCodeCommitを使う形にしました。

概要

動作イメージは下記のような感じです。

Kobito.HvgCcI.png

  1. CodeCommitにプッシュ
  2. CodePipelineでコミットを検知し、CodeBuildを起動
  3. CodeBuildでコンパイルし、実行ファイルをビルド、S3に保存
  4. S3に保存後、ECSのサービスの既存タスクを停止
  5. サービスが新しいタスクを起動
  6. 新しいコンテナがS3から実行ファイルをダウンロードし、実行

環境

アプリケーション

もとのアプリケーションのファイルは下記のような感じです。

.
├── app
│   ├── model
│   ├── util
│   └── view
├── bindata.go
├── glide.yaml
├── server.go
└── static
    └── js

リージョン

CodeCommitが東京リージョン未対応のため、すべてus-east-1で作成します。

設定

CodeCommitリポジトリの作成

リポジトリの作成

リポジトリ名をgotestにします。
Kobito.WZjQG3.png

CodeCommitリポジトリにアクセスするユーザを作成

IAMでユーザを作成します。
AccessKeyを使ったHTTPSでのアクセスも可能ですが、SSH接続でやってみます。
ユーザを作成したら、認証情報のタブを開いて、AWS CodeCommitのSSHキーSSH 公開キーのアップロード をクリックし、SSH公開鍵を貼り付けます。
Kobito.zYMfwr.png

アップロードすると、SSHキーIDが発行されます。
Kobito.7LDjWt.png

SSHのconfigに情報を追加します。

~/.ssh/config
Host git-codecommit.*.amazonaws.com
    User `SSH キー ID`
    IdentityFile ~/.ssh/秘密鍵

コードをpushしておきます。

$ git push origin master                                                          
Counting objects: 41, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (34/34), done.
Writing objects: 100% (41/41), 73.27 KiB | 0 bytes/s, done.
Total 41 (delta 3), reused 0 (delta 0)
To ssh://git-codecommit.us-east-1.amazonaws.com/v1/repos/gotest
 * [new branch]      master -> master

Buildしたファイル保存用のS3バケットを作成

保存用S3バケットを作成します。

Kobito.hjTh2t.png

CodeBuildプロジェクトを作成

下記のようなプロジェクトを作成します。

Kobito.KHdvNj.png

環境イメージはあらかじめ用意されているGolangのものを使用しました。
当初、自分でコンテナを作成し、ビルド+Dockerコンテナ作成+ECRにプッシュという感じでやりたかったのですが、独自コンテナでDocker on Docker はできない(下記参照)、DockerイメージにGolangをインストールするとビルドのたびに時間がかかりすぎるということで、独自コンテナはあきらめました。
https://forums.aws.amazon.com/thread.jspa?messageID=761184

ロールには自動で生成されるPolicyに、ECSのタスクをコントロールするためにAmazonEC2ContainerServiceFullAccessを追加しています。

CodeBuild用のファイルを作成

下記の2つのファイルを作成します。

buildspec.yml
version: 0.1

phases:
  install:
    commands:
      - go get github.com/Masterminds/glide
      - go get -u github.com/jteeuwen/go-bindata/...    
  build:
    commands:
      - ./build.sh
  post_build:
    commands:
      - aws s3 cp /go/src/git.local/sasaki/gotest/server s3://gotest-pkg/server
      - aws ecs stop-task --task `aws ecs list-tasks --cluster $ECS_CLUSTER_NAME --service-name $ECS_SERVICE_NAME --region $AWS_DEFAULT_REGION --query taskArns --output text` --cluster $ECS_CLUSTER_NAME --region $AWS_DEFAULT_REGION
build.sh
mkdir -p $GOPATH/src/git.local/sasaki/gotest
cp -r app $GOPATH/src/git.local/sasaki/gotest
cp -r static $GOPATH/src/git.local/sasaki/gotest
cp server.go $GOPATH/src/git.local/sasaki/gotest
cp bindata.go $GOPATH/src/git.local/sasaki/gotest
cp glide.yaml $GOPATH/src/git.local/sasaki/gotest
cd $GOPATH/src/git.local/sasaki/gotest
glide up
go-bindata app/view
GOOS=linux GOARCH=amd64 go build server.go bindata.go

buildspec.yml がCodeBuildの設定ファイルになります。
CodeBuildeでGolang用のコンテナを起動しますので、goコマンドは使える状態になっています。
まずinstallフェーズでgolangのglidego-bindataをインストールします。

次にbuildフェーズでbuild.shを実行します。
開発時はローカルにあるGitリポジトリを使っているので、glide up でエラーになることを回避するために、ローカル環境と同じパスにソースをコピーしています。
そのあと、go build でコンパイルしています。

post_buildフェーズでは、S3に実行ファイルをアップロードし、ECSの現在のタスクを停止しています。
artifactでもアップロードはできるのですが、post_buildのECSタスク停止後にアップロードされ、タスクに新しいファイルが反映されないため、このようにしています。

ECRにDockerイメージを作成

下記のDockerfileでDockerイメージを作成し、ECRにプッシュしておきます。

Dockerfile
FROM ubuntu:16.04
RUN apt-get -y update
RUN apt-get install -y python-pip
RUN yes | pip install --upgrade awscli
RUN apt-get remove -y python-pip
ADD start_gotest.sh /
ENTRYPOINT ./start_gotest.sh
start_gotest.sh
aws s3 cp s3://gotest-pkg/server ./
chmod +x server
./server

UbuntuにS3でビルドしたファイルをダウンロードし、実行するだけです。
Alpineでやると、 ./server がどういうわけか、not foundになるので、断念・・・

ECS設定

ECRに保存したイメージを常時稼働するように、タスク定義、サービスの設定します。

CodePipeline

CodePipelineを作成します。

ソースの場所 は作成したCodeCommitリポジトリを指定します。
Kobito.H6ZYi8.png

ビルド は作成したCodeBuildを指定します。
Kobito.xAGCCv.png

デプロイはCodeDeploy等は使用しないので、デプロイなしを選択します。
Kobito.ppkLhz.png

実行

作成したリポジトリに、buildspec.yml と、build.shを追加し、プッシュすると、CodePipelineが反応し、ECSのタスクが再起動されるまで自動実行されます。

Kobito.NZkYFB.png

イケてないところ

  • CodeBuildで使うコンテナが融通がきかない

    • 独自コンテナではDocker on Dockerができないとか
    • Docker + Go等 みたいなコンテナがないとか
    • 本来はビルドプロセスでコンテナイメージつくってECRにプッシュするまでやりたい
  • Artifactが使えてない
    • そもそもあんまりよくわかってない・・・
  • ECRに保存しているアプリケーションを動かすだけのコンテナのサイズがデカすぎ
    • S3からダウンロードするためにubuntuにpipインストールして、aws-cliをpipインストールしただけで、こんなに・・・
ubuntu              16.04               117 MB
gotest-image        latest              462 MB

続きを読む

Mastodonのrake mastodon:dailyをLambdaで定期実行する

ドキュメント
https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md
これやらないとこういうことが起きるので大事。
http://cryks.hateblo.jp/entry/2017/04/18/125351
まさに自分のサーバーで起きて大変だった…。

環境

前回のAWS EC2 Container Service
http://qiita.com/kawax/items/5307b58e549dd9cc3928

これでcronはどうやるんだろうと調べたけど結構簡単だった。

ECSタスク

aws-daily.yml

version: '2'
services:
  web:
    image: {image}
    env_file: .env.production
    command: bundle exec rake mastodon:daily
    mem_limit: 536870912
    ports:
      - "3000"
ecs-cli compose -f aws-daily.yml --project-name mastodon-daily create

Lambda

ランタイム:Node.js 6.10
ブランク関数で新規に作っていく。

トリガーで CloudWatch イベント - スケジュール を選択。
ルール名やルールの説明は適当なものを。
スケジュール式は rate(1 day)

ロールはカスタムロールの作成から新規ロールを作った後で
管理ポリシーAmazonEC2ContainerServiceFullAccessを付ける。
インラインポリシーoneClick_lambda_basic_executionも付いてるはず。
この辺は後でLambda 関数が実行できるポリシーを付ける。

コード

clusterとtaskDefinitionを自分のものに書き換える。

var AWS = require('aws-sdk');
var ecs = new AWS.ECS();

exports.handler = (event, context, callback) => {
    var params = {
        cluster: "mastodon", 
        taskDefinition: "ecscompose-mastodon-daily"
    };
    ecs.runTask(params, function(err, data) {
        if (err) console.log(err, err.stack); // an error occurred
        else     console.log(data);           // successful response
    });
};

テストしてみてエラーが出てなければトリガーを有効化して終わり。
エラーが出てる場合はロールを確認する。

続きを読む

全力AWSでMastodonサーバー立てました chitose.moe

https://chitose.moe/

スポンサーが1社付いてるのですぐに消えたりはしないはず。
メール送信数制限の都合で1日のユーザー登録数には上限がある。
メールが届かない時は暫く待つか24時間後に再送信。
とはいえ5万なので上限になることはないか。

環境

  • AWS
  • EC2 Container Service
  • ecs-cli
  • RDS(PostgreSQL)
  • ElastiCache(Redis)
  • S3
  • CloudFront
  • ALB
  • SES

やれるだけやって分散した。
いくらでもサーバー増やせるけどたぶんそこまで必要にはならない。

最後SESのsandbox解除待ちで時間かかった…。

途中段階の役に立たないメモ

ローカルで動かすだけならVagrantのほうが早い。
最初から管理者アカウントも作られる。

docker-composeはproduction用。

git clone https://github.com/tootsuite/mastodon
cd mastodon
sudo curl -o /usr/local/bin/ecs-cli https://s3.amazonaws.com/amazon-ecs-cli/ecs-cli-darwin-amd64-latest
sudo chmod +x /usr/local/bin/ecs-cli
ecs-cli help
ecs-cli configure -p default --cluster mastodon
ecs-cli up ...
aws ecr get-login
docker login ...
aws ecr create-repository --repository-name mastodon
docker build -t mastodon .
docker tag mastodon:latest ...
docker push ...

docker-compose.ymlを編集
– buildの代わりにimage: mastodon
– DB永続化するためコメントを消す

ecs-cli compose service up

ここで上手く動かなくなった。
Elastic Beanstalkでやろうとしたけどこっちもだめ。
いろいろやってるうちにオリジナルのdocker-compose.ymlが事前に用意したDocker image使うようになってた。
ECS使う方法に戻る…。

assetsを削除して再生成してS3にアップ。
precompileを繰り返すとファイルが増える…?

rm -rf ./public/assets
docker-compose run --rm web rails assets:precompile

aws s3 cp ./public/assets s3://{S3バケット}/assets --recursive --acl public-read --cache-control "max-age=604800"

S3に置くと/public/assets以下がないのでprecompile済みファイルが使われない。
sprockets-manifestが必要。
S3に置くのはやめた。
image: gargron/mastodonは使わず自分でビルドする方法に戻る…。

.dockerignoreを変更してimageにassetsも含まれるように。

#public/assets

docker-compose.ymlをコピーしてaws.ymlを作りこっちを書き換えて行く。
docker-compose.ymlはローカル用に元のまま。
あ、当然gitのブランチは分けてる。

AWSのECSでは

  • buildが使えない
  • volumesで相対パスが使えない

という仕様なのでそれに合わせる。
volumesはよくわからないのでほぼ使わないように…。

ここからも色々苦労したけど細かすぎてもう忘れたのでメモ程度。
AWSのサービスで使えるものは使う。
db,redisはもちろん分離。
S3も当たり前に使う。
nginxは不要だった。nginx使おうとして無駄に混乱…。
ポートマッピングはELB。
httpsへのリダイレクトはCF。

LOCAL_HTTPS=falseでも問題なくhttpsで動くけどメール内のURLだけhttpになる。
リダイレクトされるので妥協。

ecs-cli compose serviceで--load-balancer-name付きで起動だけどうしてもできなかったので諦めた。

/api/v1/streamingはサブドメインにした。
STREAMING_API_BASE_URLで指定すればそれが使われる。
LOCAL_HTTPS=falseだとwebsocketでエラー出てたので無理矢理な対応。

S3_BUCKETS3_HOSTNAMEはS3のドメインそのまま使う用で
自分のサブドメイン使う場合はS3_CLOUDFRONT_HOSTも設定する。
CF使ってるかに関係なく静的ホスティングなら。

nginx使わなくても動いてるけど何か問題あれば後で修正していく。

docker-compose.ymlあるから簡単に動かせるかと思ったけどそんなことはなかった。
EC2上でdocker-compose upすれば簡単だけどそれじゃAWSの意味がない。

ALBの使用

その後調べて分かったので追記。
--load-balancer-nameはClassic Load Balancer用なので違う。
ALBは--target-group-arnを使う。
ただしサービスごとに一つしか設定できないのでサービスを複数作るしかない?

ecs-cli compose -f aws.yml --project-name mastodon-web service create --target-group-arn {web用のターゲットグループ} --container-name web --container-port 3000 --role ecsServiceRole

ecs-cli compose -f aws.yml --project-name mastodon-api service create --target-group-arn {streaming用のターゲットグループ} --container-name streaming --container-port 4000 --role ecsServiceRole

--project-nameの指定も必要になった。

ecs-cli compose -f aws.yml --project-name mastodon-web service up

AutoScalingでEC2インスタンス数を増減。
一つのEC2内で複数のタスクが動くし、タスクもAutoScalingできるようになった。

サービスを分けるならaws.ymlも分割したほうがメモリの無駄もない。

最終版

ごちゃごちゃしたのでまとめ。

aws-web.yml

mem_limitは分からないので仮。
portsの0が動的ポートのために必要。
もしくは0:部分なしでいいかも。こっちは未検証。

version: '2'
services:

  web:
    restart: always
    image: {自分のimage}
    env_file: .env.production
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    mem_limit: 536870912
    ports:
      - "0:3000"

aws-api.yml

version: '2'
services:
  streaming:
    restart: always
    image: {自分のimage}
    env_file: .env.production
    command: npm run start
    mem_limit: 268435456
    ports:
      - "0:4000"

  sidekiq:
    restart: always
    image: {自分のimage}
    env_file: .env.production
    command: bundle exec sidekiq -q default -q mailers -q pull -q push
    mem_limit: 268435456

ALB

ターゲットグループ

  • port3000のweb用
  • port4000のstreaming用

を作る。

リスナーHTTPS 443
ヘルスチェックのポートをトラフィックポートにする。

  • HostがAPI用のサブドメインならstreaming
  • Pathが/ならweb

というルールを設定する。

CloudFrontはこのALBをOriginにする。
/assets以下はキャッシュ強く。他は短め。

Route53はCloudFrontを指定。

今後のためのアップデート手順

masterブランチを最新にしてからマージ。

DB_HOSTはRDSを指定してるのでdb:migrateはローカルから直接行う。
これはどうなんだろうとは思うけど他の方法が分からなかった。

docker-compose build
docker-compose run --rm web rails db:migrate
docker-compose run --rm web rails assets:precompile

docker imageの更新はECRの手順通りに。

aws ecr get-login --region ap-northeast-1 | bash

docker build ...
docker tag ...
docker push ...

ecs-cli使ったほうが少し短いかも。

docker build ...
docker tag ...

ecs-cli push ...

後はup。upはymlが変更されてないと以前のタスクのままなのでimageだけ更新した場合は更新されない。
CIで動かしてdocker tagを設定してymlの書き換えまで自動化かな。

ecs-cli compose -f aws-web.yml --project-name mastodon-web service up
ecs-cli compose -f aws-api.yml --project-name mastodon-api service up

動きさえすれば運用段階では楽になる。

続きを読む

Aurora Note

Intra region replication individual cluster

Snapshot from main cluster
Create slave cluster from 1. snapshot
Get log name/position from slave cluster’s Log
set remote master @ slave cluster
start slave

Replication for mysql-DB? (user or schema….. 続きを読む