RundeckをECS上に構築してGitHubログインできるようにする

この記事はscouty Advent Calendar 2018 の3日目です.

ECSで動かしているサービスのスケジュールジョブが多くなってきた. もともとECS Scheduled Taskを使っていたんだけど,数十個レベルになってくると,これで管理するのはだいぶつらい.

というわけでRundeckを構築したメモ.

そもそもECS Scheduled Taskやる気あんの?

ECS Scheduled Taskの辛いところ.

  • Scheduled Taskは既存のTaskDefinitionを上書きしてタスクを実行するため,Override用にjsonでTaskDefinitionの一部を与える必要がある
  • Scheduled Taskの一覧画面はあるのだが,そこから「何時に,どのタスクIDで実行されたか」を知る術がない
  • そんなだから,タスクが正常に終了したのか,何かのエラーで途中終了したのかもわからない
  • そして,Scheduled Taskの画面上からは実行ログを辿ることもできないので,もはや何もわからない

という事情があるため,真面目にログをトレースしようと思うと,TaskDefinitionをOverrideするときに,logDriverの設定を上書きして,タスク固有の識別子を埋めておくしかない.

この状態で数十個単位のジョブを管理するなんて不可能じゃないか? AWSは本当にこんなものでスケジュールジョブを管理できると思ってるんだろうか?

単にKubernetesのcronに対抗したくて既存の仕組みの上で作っただけなのでは?という気がしないでもない. 実際CloudWatchEventsのtargetにrun taskが指定できるようになっただけだし.

というわけでもう諦めてRundeckを立てることにしたのであった.

Rundeck目指す構成

Rundeck自体もECSに乗せてやりたい.そして,ジョブの実行はECS run-task API経由で行うものとしたい. そうしておけば,ssh等の設定は一切必要なく,単にRundeckを動かしているインスタンス上のローカルコマンドでecs run-taskができれば良い.そうしておけばssh周りの設定,VPCや,SecurityGroupや,ssh鍵の置き場所などを悩む必要がない.

ただし,run-task APIを,同期的な実行として,ログ出力を標準出力に行う必要がある.ログはRundeck上で見られるようにしたいし,コマンドの実行結果もSlack通知とかしたいじゃん? これに関しては先日書いた,

h3poteto.hatenablog.com

ecs-taskをそのまま使えば実現できるので,今回はあまり触れない.

というわけで大事なこと.

  • RundeckをECS上に構築するのでクラスタ構成となる
  • DBはRDSを使う
  • GitHubログインをしたい
  • ジョブの実行結果をSlack通知したい

という要求を満たすRundeckを作ることにする.

RundeckをDockerで立てる

公式がRundeckのDockerを用意してくれているので,これを使えばよい.ただし,環境変数でかなりのことをカスタマイズできるようになっているので,今回はそこの説明をする.

https://hub.docker.com/r/rundeck/rundeck/

前提

公式のRundeck Docker imageは,設定の多くを環境変数で変更することができる. ただし,Rundeck自身は,環境変数を直接読むのではなく設定ファイルを読み込んでいる.

そのため,remcoというツールを使って,entrypoint内で,環境変数を元に設定ファイルを生成している.

https://github.com/rundeck/rundeck/blob/v3.0.8/docker/official/lib/entry.sh#L17

remcoというのはこれ.

github.com

というわけで,今から説明する設定は,だいたいremcoに食わせる環境変数を設定するのが主となる.

SSL

今回はAWS ECS上に構築することもあって,SSL証明書の管理はACMで行い,SSLの終端はALBとする.

ただし,SSLの終端がALBであっても,多少の設定が必要になる.

それがこれ.

https://rundeck.org/docs/administration/security/configuring-ssl.html#using-an-ssl-terminated-proxy

で,この設定は環境変数で切り替えられる. こいつはremcoではなく,実はentrypoint内で環境変数から読み込みを行っている.

https://github.com/rundeck/rundeck/blob/v3.0.8/docker/official/lib/entry.sh#L34

というわけなので,

export RUNDECK_SERVER_FORWARDED=true

環境変数を与えてやれば良い.

あと,Rundeck内で利用するURLも与えてやる必要があるのだが,これもSSL化しておく.

export RUNDECK_GRAILS_UR=https://rundeck.example.com

ついでに以下の環境変数も設定しておくと,外部から接続できるようになる.

export RUNDECK_SERVER_ADDRESS=0.0.0.0

これらは,

https://github.com/rundeck/rundeck/blob/v3.0.8/docker/official/remco/templates/rundeck-config.properties#L7-L8

この辺で読み込まれ,remcoによって設定ファイルに書き出される.

Storage

次にStorageの設定だ. RundeckのDBはデフォルトではh2を使う設定になっており,これはローカルにファイルとして書き出される. そのため,Rundeck上で作成するプロジェクトの設定等もすべてローカルに保存される. クラスタ構成にする上でこれをRDS等のリモートのRDBに変更してやる必要がある.

とはいえ,普通にDB設定の環境変数を設定すれば良い.

https://hub.docker.com/r/rundeck/rundeck/

export RUNDECK_DATABASE_URL=postgres_url
export RUNDECK_DATABASE_DRIVER=org.postgresql.Driver
export RUNDECK_DATABASE_USERNAME=rundeck
export RUNDECK_DATABASE_PASSWORD=hogehoge

これはpostgresの設定だが,mysqlでも同じような設定だけで行ける.

ExecutionFileStorage

Rundeckはcronなので何かしらのコマンドを実行する.そしてその実行ログ(ExecutionLogFile)をRundeck上から確認できるのもメリットだ.

デフォルトの場合,その実行ログもローカルファイルに書き出されてしまう. これもクラスタ構成にするために,ディスクではなくS3あたりに書き出しておく必要がある.

これに関してはこの辺が参考になる.

github.com

まず,rundeck-s3-log-pluginを入れる必要がある.

github.com

これは,rundeckがインストールされたディレクトリのlibext配下にプラグインを置くことで自動的に読み込まれる. つまり,こんな感じのDockerfileを書く必要がある.

FROM rundeck/rundeck:3.0.8

USER root

RUN set -ex && \
    mkdir libext && cd libext && \
    wget https://github.com/rundeck-plugins/rundeck-s3-log-plugin/releases/download/v1.0.5/rundeck-s3-log-plugin-1.0.5.jar && \
    chown -R rundeck:rundeck /home/rundeck/libext && \

USER rundeck

次にこのプラグインを使うように本体側に設定をしてやる必要がある.

この設定も環境変数のみでコントロールできる.読み込んでいるのはここ.

https://github.com/rundeck/rundeck/blob/v3.0.8/docker/official/remco/templates/plugin-s3-logstore.properties#L1-L4

# execution logをS3に保存するための設定
export RUNDECK_PLUGIN_EXECUTIONFILESTORAGE_NAME=org.rundeck.amazon-s3
export RUNDECK_PLUGIN_EXECUTIONFILESTORAGE_S3_BUCKET=my_s3_bucket
export RUNDECK_PLUGIN_EXECUTIONFILESTORAGE_S3_REGION=ap-northeast-1

これで,ExecutionLogFileがS3に吐き出されるようになる. 適当なジョブを実行してみると,ちゃんとS3にログが出てくる.

GitHub認証

Rundeckは,ユーザ認証に,ユーザ名+パスワードを要求してくる.しかし,この設定は実はファイルに書かれて管理されているだけだ.こうなると,新しいメンバーが入ってきた時や,メンバーが抜けたときにいちいちユーザ管理ファイルを書き換える必要がある. また,パスワード漏洩のリスクも増える.

そいういうのを考慮して,できればGitHubGoogle認証に変えたい.

というわけで

qiita.com

この辺を参考にGitHub認証を入れてみた.

この情報は少し古くなっており,Rundeck 2.x系であれば問題ないのだが,3.x系だと少し設定が変わっている.

記事の通りRundeck自身はGitHub認証の機構を持たない.そのために,前段にoauth2_proxyを入れて,oauth2_proxyで認証したものをそのままRundeck側に渡す構成を取る.

Preauthenticated Mode

まず,元来Rundeck側で行っている認証をoauth2_proxyに任せるため,Rundeck側の認証を無効化しつつ,ユーザ情報をヘッダーから取れるようにする必要がある. それがこのPreauthenticateModeなのだが,もちろんこの設定も環境変数から行うことができる.

設定したい項目はこれ.

https://rundeck.org/docs/administration/security/authenticating-users.html#preauthenticated-mode

で,この設定ファイルを生成しているのがここ.

https://github.com/rundeck/rundeck/blob/v3.0.8/docker/official/remco/templates/rundeck-config.properties#L22-L31

というわけで該当の環境変数を出力する.

export RUNDECK_PREAUTH_ENABLED=true
export RUNDECK_PREAUTH_USERNAME_HEADER=X-Forwarded-User
export RUNDECK_PREAUTH_REDIRECT_LOGOUT=true

で,参考記事の場合,このあとweb.xml をいじって既存認証を無効化しているのだが,3.x系では web.xml は存在しない.

github.com

Preauthenticated Modeの設定させしてあれば,web.xml の変更がなくても動くという情報があり,このままでもうまくいったので,ここをスルーする.

ただし,ここで設定している X-Forwarded-Roles だけはoauth2_proxyが付与してくれないヘッダーなので,自分でnginx等を前段に置いてヘッダーを付与する必要がある.

https://github.com/rundeck/rundeck/blob/v3.0.8/docker/official/remco/templates/rundeck-config.properties#L28

nginxを設定する

証明書はACMで管理しているし,本当は必要ないはずだったんだけど,ALBだけで新しいヘッダーを付与するのがどうにも不可能っぽかったので(もしできるなら誰か教えてほしい),仕方なくnginxを建てた.

server {
    listen 80 default_server;
    server_name localhost;

    set_real_ip_from   172.0.0.0/8;
    real_ip_header     X-Forwarded-For;

    proxy_set_header   X-Real-IP $remote_addr;
    proxy_set_header   Host $http_host;
    proxy_set_header   X-Forwarded-Proto $http_x_forwarded_proto;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;

    # Set rundeck role
    proxy_set_header   X-Forwarded-Roles admin;

    location / {
        proxy_pass http://oauth2_proxy:4180;
    }
}

oauth2_proxyの設定

github.com

これを動かすだけのDocker imageを作った.

FROM alpine:3.8

RUN apk --no-cache add curl

RUN curl -sL -o oauth2_proxy.tar.gz \
    "https://github.com/bitly/oauth2_proxy/releases/download/v2.2/oauth2_proxy-2.2.0.linux-amd64.go1.8.1.tar.gz" \
    && tar xzvf oauth2_proxy.tar.gz \
    && mv oauth2_proxy-2.2.0.linux-amd64.go1.8.1/oauth2_proxy /bin/ \
    && chmod +x /bin/oauth2_proxy \
    && rm -r oauth2_proxy*

CMD /bin/oauth2_proxy \
    -provider="github" \
    -github-org="${OAUTH2_PROXY_GITHUB_ORG}" \
    -github-team="${OAUTH2_PROXY_GITHUB_TEAM}" \
    -http-address="0.0.0.0:4180" \
    -redirect-url="${OAUTH2_PROXY_REDIREC_URL}" \
    -upstream="http://rundeck:4440/" \
    -email-domain="*" \
    -cookie-domain="${OAUTH2_PROXY_COOKIE_DOMAIN}" \
    -cookie-refresh="${OAUTH2_PROXY_COOKIE_REFRESH}"

GitHub関連の情報は起動時に環境変数で差し込めるようにしてある.

これで,ALB -> nginx -> oauth2_proxy -> Rundeck という流れができて,無事GitHub認証できるようになった.

Slack通知

最後に,ジョブの実行終了のタイミングでSlack通知したいと思う. これはRundeck公式では用意してくれていないのだが,

github.com

こいつを使う.

これも追加プラグインなので,Dockerfileでプラグインを追加する.

FROM rundeck/rundeck:3.0.8

USER root

RUN set -ex && \
    mkdir libext && cd libext && \
    # S3 log pluginを用意
    wget https://github.com/rundeck-plugins/rundeck-s3-log-plugin/releases/download/v1.0.5/rundeck-s3-log-plugin-1.0.5.jar && \
    # Slack pluginを用意
    wget https://github.com/higanworks/rundeck-slack-incoming-webhook-plugin/releases/download/v0.9.dev/rundeck-slack-incoming-webhook-plugin-0.9.jar && \
    chown -R rundeck:rundeck /home/rundeck/libext && \
    # 以下のディレクトリはSlack pluginがテンプレートを吐き出すために必要
    mkdir /etc/rundeck && \
    chown -R rundeck:rundeck /etc/rundeck

USER rundeck

先にSlack側でIncoming Webhooks用のURLを出しておく.

そして,Slack通知を利用するプロジェクト側でも設定が必要になる.

https://github.com/higanworks/rundeck-slack-incoming-webhook-plugin#configuration

YOUR PROJECT > Project Settings > Edit Configuration > Edit Configuration File に,以下のようにしてSlack側で作成したIncoming WebhooksのURLを設定する.

project.plugin.Notification.SlackNotification.webhook_url=https\://hooks.slack.com/services/hogehoge/fugafuga

ここまでやっておくと,プロジェクトのジョブ設定から,通知でSlack通知の設定が可能となる.

Notification to Slack

その他注意事項

Cluster Mode

RundeckにはCluster Modeというのが用意されている. が,Docker版であればデフォルトのままでCluster Modeとなっているので,特に気にすることはない.

https://github.com/rundeck/rundeck/blob/v3.0.8/docker/official/remco/templates/rundeck-config.properties#L37

ssh

今回,コマンドを実行するにあたってssh鍵は用意していないので触れていないが,これもちゃんとDBに保存することができる. Rundeckではssh鍵の保存場所をKey Storageと呼んでいる.そしてこれには,filedb を選択することができる.

https://rundeck.org/docs/administration/security/key-storage.html

で,そのままセットアップすれば現状だとdb になるように設定されている.

https://github.com/rundeck/rundeck/blob/v3.0.8/docker/official/remco/templates/rundeck-config-storage.properties#L36-L37

ので特に気にする必要はない.

ただし,DBに保存する際には,BLOBのテキストとして保存されるため,このままだと生データのssh鍵を保存することになる. これが気になる場合は,Storage Converterを使って暗号化することをおすすめする.

完成形

最終的な構成はこんな感じ.

Rundeck on ECS

sshするのであれば同じVPCに配置してある方が楽なのだが,もうこの形式になってしまえばVPCは関係なくて,基本的にはすべてIAM Roleだけで権限管理が完結する.

そして用意したDockerfile

FROM rundeck/rundeck:3.0.8

USER root

RUN set -ex && \
    apt-get update && \
    apt-get install -y unzip && \
    rm -rf /var/lib/apt/lists/* && \
    wget https://github.com/h3poteto/ecs-task/releases/download/v0.1.2/ecs-task_v0.1.2_linux_amd64.zip && \
    unzip -d /usr/local/bin ecs-task_v0.1.2_linux_amd64.zip && \
    # https://github.com/rundeck-plugins/rundeck-s3-log-plugin
    # S3 pluginを用意
    mkdir libext && cd libext && \
    wget https://github.com/rundeck-plugins/rundeck-s3-log-plugin/releases/download/v1.0.5/rundeck-s3-log-plugin-1.0.5.jar && \
    # Slack pluginを用意
    wget https://github.com/higanworks/rundeck-slack-incoming-webhook-plugin/releases/download/v0.9.dev/rundeck-slack-incoming-webhook-plugin-0.9.jar && \
    chown -R rundeck:rundeck /home/rundeck/libext && \
    mkdir /etc/rundeck && \
    chown -R rundeck:rundeck /etc/rundeck

ADD entrypoint.sh /home/rundeck/docker-lib/entrypoint.sh

RUN chown rundeck:rundeck /home/rundeck/docker-lib/entrypoint.sh

USER rundeck

ENTRYPOINT [ "docker-lib/entrypoint.sh" ]

CMD [ "docker-lib/entry.sh" ]

と,entrypoint.shによる環境変数設定.

#!/bin/bash

export AWS_DEFAULT_REGION=ap-northeast-1

# DBの環境変数を埋めるだけでstorage.providerもDBにしてくれる
export RUNDECK_DATABASE_URL=postgresql_url
export RUNDECK_DATABASE_USERNAME=rundeck
export RUNDECK_DATABASE_PASSWORD=hogehoge
export RUNDECK_DATABASE_DRIVER=org.postgresql.Driver

# https://なアドレスを指定すればSSL対応される
export RUNDECK_GRAILS_URL=https://rundeck.example.com
export RUNDECK_SERVER_ADDRESS=0.0.0.0

# https://rundeck.org/docs/administration/security/configuring-ssl.html#using-an-ssl-terminated-proxy
# に従って
# https://github.com/rundeck/rundeck/blob/v3.0.8/docker/official/lib/entry.sh#L34
# この設定を上書きしてELB側でSSLさせる
export RUNDECK_SERVER_FORWARDED=true

# execution logをS3に保存するための設定
export RUNDECK_PLUGIN_EXECUTIONFILESTORAGE_NAME=org.rundeck.amazon-s3
export RUNDECK_PLUGIN_EXECUTIONFILESTORAGE_S3_BUCKET=rundeck-executionlog
export RUNDECK_PLUGIN_EXECUTIONFILESTORAGE_S3_REGION=ap-northeast-1

# githbuログインのためPreauthenticated Modeを有効化する
# https://qiita.com/minamijoyo/items/52041ff8628263355810#preauthenticated-mode%E3%81%AE%E6%9C%89%E5%8A%B9%E5%8C%96
# https://github.com/rundeck/rundeck/blob/v3.0.8/docker/official/remco/templates/rundeck-config.properties#L22-L31
export RUNDECK_PREAUTH_ENABLED=true
export RUNDECK_PREAUTH_USERNAME_HEADER=X-Forwarded-User
export RUNDECK_PREAUTH_REDIRECT_LOGOUT=true

exec "$@"

できたできた.