Roadworker / Piculet / Radiosondeを運用するにあたって実施したこと

これまでRoute53やセキュリティグループが手で管理されてたので、煩雑な運用を打開すべくwinebarrelさんのツールを導入して運用を始めた。

まだまだ改善する余地がありそうなんだけど、現状やってきたことをサクッとまとめてみます。

## 前置き：各ツールの紹介

http://codenize.tools/
早い話↑見てって感じなんですが、それぞれ

- Roadworker：Route53の管理
- Piculet：EC2 セキュリティグループの管理
- Radiosonde：CloudWatchのアラームの管理

をするツールです。
RubyのDSLによって設定を反映させることができるため、設定値バージョニングすることができます。

## ディレクトリ構成

1サービス1アカウントでAWSの運用している会社だとそこまでの量にはなりませんが、複数のサービスを1アカウントで運用していると、exportしたときのファイルが膨大になって悪夢になります。
（Piculetでセキュリティグループの現状値をexportしたとき、1ファイル5000行になりました。）

さすがにこの状態で運用はできないので、一定の粒度で分割しました。

### Roadworker

ホストゾーンごとにファイルを分割し、Routefile内で他のファイルをrequireする。
Routefile自体には何も記述しない。サブドメイン切った場合も同一ディレクトリに配置した。
（そのうち破綻しそう。ディレクトリ切るかも。）

```
$ tree .
.
├── Gemfile
├── Gemfile.lock
├── README.md
├── Routefile
├── Routefile.example.com
├── Routefile.hoge.example.com
├── Routefile.corporate.co.jp
└── circle.yml
```

```rb:Routefile
# -*- mode: ruby -*-
# vi: set ft=ruby :

require 'Routefile.example.com'
require 'Routefile.hoge.example.com'
require 'Routefile.corporate'
```

### Piculet

リージョンごとにディレクトリを分割し、所属するリージョンのサービスごとにファイルを分割。
Groupfileで他のファイルをrequireしつつ、特定のサービスに属さないセキュリティグループに関してはGroupfile内に記述した。

```
$ tree .
.
├── Gemfile
├── Gemfile.lock
├── README.md
├── circle.yml
├── oregon
│   ├── Groupfile
│   ├── Groupfile.us-news
│   └── Groupfile.us-app
└── tokyo
    ├── Groupfile
    ├── Groupfile.japan-news
    └── Groupfile.japan-app
```

サービス追加でファイル追加になるんですが、毎度require記述するのがめんどくさかったので、Dir::globで拾ってrequireしている。

```rb:tokyo/Groupfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

Dir::glob("./tokyo/Groupfile.*").each do |file|
  require File.basename(file)
end

ec2 "vpc-XXXXXX" do
  security-group "共用で使ってるRDSとかRedshiftはここ" do
    ...
  end
end
```

### Radiosonde

Riculetとほぼ同じ。

## CircleCIでdry-run & apply

真っ先に思いつくネタで目新しさはないですが。
Roadworkerに限った話をすると、導入当初は

- `--dry-run`だけを走らせてテスト通ったらローカルからapply
- プライベートホストゾーンは除外
    - chefレシピで自動的に登録/削除が実施されるので、インスタンス追加削除等でコロコロ変更されるため
- `--test`は使ってない。

という感じです。

最初のcircle.ymlファイルこんな感じ

```circle.yml
test:
  override:
    - bundle exec roadwork --dry-run --apply --exclude-zone \.company\.internal$
```

現在は`deployment`でmasterにpushが走ったタイミングで変更実施されます。

```
test:
  override:
    - bundle exec roadwork --dry-run --apply --exclude-zone \.company\.internal$

deployment:
  production:
    branch: master
    commands:
      - bundle exec roadwork --apply --exclude-zone \.company\.internal$
```



## CircleCIに渡すAWS_ACCESS_KEYのユーザのIAM

まぁまさかルートユーザのKEY/SECRET渡してる人はいないですよね^^

ってことで、それ専用ユーザを作ってポリシーで権限絞ってKEY/SECRETをCircleCIにぶっこんでます。
で、各ツールでどのAction使うかが実は明記されてない（見逃してたらすみません、コメントで指摘していただけると🙇）ので、いま動かしてるポリシーこんな感じで運用してます。Allowが漏れてる可能性はあります。

### Roadworker

Route53だけかとおもいきや、ELB:DescribeLoadBalancersも使ってるようなので、これだけallow。
とりあえず実行されるとめんどくさいactionだけdenyして、その他はallowの運用してます。


```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1",
            "Effect": "Allow",
            "Action": [
                "route53:*"
            ],
            "Resource": [
                "arn:aws:route53:::*/*"
            ]
        },
        {
            "Sid": "Stmt2",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:DescribeLoadBalancers"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "Stmt3",
            "Effect": "Deny",
            "Action": [
                "route53:DeleteHostedZone",
                "route53:DisassociateVPCFromHostedZone"
            ],
            "Resource": [
                "arn:aws:route53:::*/*"
            ]
        }
    ]
}
```

### Piculet

`ec2:*`だとガバガバなので、トライアンドエラーで使うもの絞りました。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt4",
            "Effect": "Allow",
            "Action": [
                "ec2:AuthorizeSecurityGroupEgress",
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:CreateSecurityGroup",
                "ec2:DeleteSecurityGroup",
                "ec2:DescribeSecurityGroups",
                "ec2:RevokeSecurityGroupEgress",
                "ec2:RevokeSecurityGroupIngress",
                "ec2:CreateTags",
                "ec2:DeleteTags",
                "ec2:DescribeTags"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
```

### Radiosonde

アラーム消されても即時対応必要ない（とは言うものの緊急性はある）ので、ゴソっとつけました

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt5",
            "Effect": "Allow",
            "Action": [
                "cloudwatch:*"
            ],
            "Resource": "*"
        }
    ]
}
```

## CircleCIでのdry-run/applyの結果をSlackに通知

わざわざCircleCI覗きに行くのも面倒だよねってことで、Slackに通知するようにしました。
イメージ的にはリダイレクトでファイルに吐いて、そのファイルの中身をSlack Webhookのpayloadに入れてPOSTする感じです。

プラス

- Slackで結果の出力を見たい
- かつCircleCIでも残したい

という要望のためリダイレクトではなく`tee`コマンドでパイプしています。
ただ、exit statusがteeコマンドの結果が採用されてbuild failしないので若干手を加えました。

あと、ハマった点としては

- 出力結果にダブルクォートがあるとInvalid JSONになるため、sedでシングルクォートに置換した
- Slackの文言が大変なことになるのでANSI colorを自力で無効化してたところ `--no-color` オプションがあることに気づく

というところでしょうか。

結局、Piculetのcircle.ymlはこんな感じになっています。
（超絶見難くてすみません...

```yaml:circle.yml
machine:
  environment:
    SLACK_CHANNEL: "#infrastructure"
    SLACK_ICON_EMOJI: ":shuzo:"

test:
  override:
    - bundle exec piculet --dry-run --apply -f oregon/Groupfile --region us-west-2 --no-color 2>&1 | tee -a output; sh -c "exit ${PIPESTATUS[0]}"
    - bundle exec piculet --dry-run --apply -f tokyo/Groupfile --region ap-northeast-1 --no-color 2>&1 | tee -a output; sh -c "exit ${PIPESTATUS[0]}"
    - curl -X POST --data-urlencode  "payload={\"channel\":\"${SLACK_CHANNEL}\",\"username\":\"piculet\",\"text\":\"$(cat output | sed -e "s/\"/\'/g")\",\"icon_emoji\":\"${SLACK_ICON_EMOJI}\"}" ${SLACK_WEBHOOK_URL}

deployment:
  production:
    branch: master
    commands:
      - echo "====== [ EXECUTE ] ======" > executed
      - bundle exec piculet --apply -f oregon/Groupfile --region us-west-2 --no-color 2>&1 | tee -a executed; sh -c "exit ${PIPESTATUS[0]}"
      - bundle exec piculet --apply -f tokyo/Groupfile --region ap-northeast-1 --no-color 2>&1 | tee -a executed; sh -c "exit ${PIPESTATUS[0]}"
      - echo "====== [ COMPLETE ] ======" > executed
      - curl -X POST --data-urlencode  "payload={\"channel\":\"${SLACK_CHANNEL}\",\"username\":\"piculet\",\"text\":\"$(cat executed | sed -e "s/\"/\'/g")\",\"icon_emoji\":\"${SLACK_ICON_EMOJI}\"}" ${SLACK_WEBHOOK_URL}
```

RoadworkerやRadiosondeのcircle.ymlも大体似たような感じです。

## 実運用に当たって

インフラエンジニアが少ないこともあり、「可能ならpull req送ってきてください」という風な運用をしています。
CircleCIで変更適用できるようになったタイミングで、「手動書き換え禁止でー」という通達を出してきました。


### Roadworker

Route53の場合は、`dns_name`使うかCNAMEにするかグローバルIPのAレコードにするかで悩ましい問題だと思いますが、これまでELB・S3をdns_nameで運用してきたため、「ここコピペして編集してください、ELBのアドレスはここに書いてます」と伝えると、大体問題なくpull reqを送ってきて頂いてます。
HealthCheck入れてDNS Failoverさせるような場合はインフラ側で設定しています。

Roadworkerは、オンプレデータセンターの会社でも使われているとおり、あらゆるサービスで恩恵を受ける便利なツールだと思います。

### Piculet

Piculetの最大の恩恵は、コメントを入れられるようになったことだと感じています。
これまで時間をかけて調べていたグローバルIPに対して、IP記述行の真後ろにコメントを付与することができるようになり、このIPが何かであるかが一発で見分けられるようになりました。
（まぁ、それに至るまでお刺身たんぽぽ作業が必要でしたが・・・。）

ただ、Roadworkerと比べて若干敷居が高く感じられました。
軽微な修正であればpull req送って来てもらえてますが、セキュリティグループ名の変更になるとセキュリティグループ削除→作成になり、アタッチされてるインスタンスがあるとエラー吐くから注意してね、というところまでは理解してもらうのが難しい感じがしました。

グローバルIPの記載が多かったので、かなり助かりました。

### Radiosonde

サービスごとに分けておくことで、テンプレートファイルとして扱うことができ、新規サービス導入時にファイルコピペ→必要なところの編集だけで済むようになりました。
そもそも非インフラにはあまり触られないのですが、かなりの恩恵を受けています。

## 最後に

まだまだ改善の余地はありそうなので、これからも手探りで修正していこうと思っています。
また、これほどまでに便利なツールを提供してくださっている winebarrel さんに心から御礼申し上げます。

## 免責事項

本内容に書かれている設定やスクリプト等を使用して発生した問題につきまして、当方は一切責任を負いません。
恐れ入りますが、ご利用される場合は自己責任でお願い致します。
また、こちらは個人の意見で、所属する企業や団体は関係ありません。