Quantcast
Channel: chrojuの記事 - Qiita

Terraform を自動実行したいなら Atlantis

$
0
0
この記事は Terraform Advent Calendar 2021 の5日目です。 Atlantis の話が書きたいけど書く場所がなくて遅ればせながら枠を探したところ、5日目の枠が空いていることに6日朝に気付いて押さえたため、投稿は遅くなっております。 ということでこのエントリーでは全力で Atlantis を推します。 Atlantis とは Terraform の自動実行にはみなさん何を用いているでしょうか。2021年現在だと HashiCorp 提供の Terraform Cloud でマネージドなパイプラインが簡単に組めますし、同じく HashiCorp が GitHub Actions を使ったワークフローを Automate Terraform with GitHub Actions | Terraform - HashiCorp Learn で公開していたりと、自動実行環境はかなり組みやすくなっています。その他様々な CI ツールなどを用いている場合もあるかもしれません。 Atlantis は Terraform の自動実行環境を提供する OSS です。クラウドサービスではなく OSS ですので、自前で Atlantis サーバーをホストしなくてはならないという手間はあるのですが、一度建ててしまえば非常に簡単にワークフローが整備できます。 Qiita で terraform atlantis と検索しても15件しか現時点でヒットしないなど、残念ながら国内だとそこまでメジャーではないのかなという印象です。が、個人的には Terraform を自動実行するならまずこれを push したいぐらいにはオススメのツールです。 Atlantis の基本動作 Atlantis は VCS と連携させることで動作します。 Github, GitLab, Bitbucket, Azure Devops などに対応しているようですが、筆者は GitHub 連携でしか使ったことがないので、以下、 GitHub 連携を前提として書きます。 GitHub と Atlantis を繋ぎ、自動実行のワークフローを作ると、 Atlantis は以下のことをしてくれます。 Pull Request 作成時に terraform plan を自動実行、結果をコメントに貼付 Pull Request 内で atlantis apply とコメントすると terraform apply を実行、結果をコメントに貼付 基本的なワークフローはザックリ言えばこの2つだけですが、これで必要十分だとも言えます。自前でワークフローを書いたりせずとも、とりあえずこの2つはやってくれるというだけで「なかなかいいんじゃない?」と言えるようにも思いますが、もちろん機能はこれだけではありません。次から、 Atlantis の好きなところを紹介していきます。 Atlantis のここが好き! merge 前に apply ができる 例えば Terraform Cloud では、 merge をトリガーとして terraform apply が始まりますが、 Atlantis は merge 前に apply するスタイルが基本です。好みが分かれるかもしれませんが、僕はこれが好きなポイントの1つです。 というのも、 apply は Fail する可能性が常にあります。従って merge 後 apply のワークフローでは、 GitHub Flow が言うところの「デフォルトブランチが常に deploy 可能」な状態を保てないおそれがあります。 merge 後に apply が失敗し、デフォルトブランチが「壊れた」状態になれば、下手をすれば、そこで他メンバーの開発にストップをかけなくてはならなくなります。 merge 前 apply ですと、デフォルトブランチは常に deploy されたものと同期が取れた状態を保てます。延々と Fail して PR がなかなか merge できない、ということにも成り得ますが、例えばアプリケーション開発において、テストの通っていないブランチを merge するか?と考えれば、それは致し方ないことと言えます。僕としてはこちらの方が安心感があって好きです。 lock 機能も持っている Terraform の運用では、複数人が同時に plan や apply を行うと結果に不整合が出るため、 state lock を行うことが推奨されています。 Atlantis では state lock は行いませんが、 PR が作成されて plan が開始された時点で、その対象 workspace を Atlantis 上で lock し、実質的に state lock と同様の効果が臨めます。 lock されている workspace に対し、別の変更をかけた PR を並行で作ると、どの PR が lock を取っているかをコメントで教えてくれます。 なお、必要であれば atlantis unlock コマンドや、 GUI を用いることで lock の解除が可能です。 構築と設定が(個人的には)簡単 Atlantis サーバーは自前運用が必要です。イマドキ operation 用のサーバー構築・運用なんてなるべくやりたくないところですが、 Atlantis は構築が簡単になるよう Helm Chart や Terraform module などの手段が用意されており、ハードルはある程度下げられています。僕が実際に使ったことがあるのは Terraform module だけですが、1時間もかからずに構築を終えられました。構成としては ECS Fargate 上に展開し、 ALB 経由でアクセスする形になります。 また設定もわりと簡単な部類だと思います。構築後、 GitHub との連携は GitHub App を設けることと、 PR の連携のために GitHub webhook を Atlantis の /events に送ってあげるという、主に2点で設定します。 Terraform を管理しているレポジトリを Atlantis 管理下に入れるには、レポジトリ内に atlantis.yaml を置くだけです。 version: 3 projects: - dir: project1 autoplan: when_modified: ["*.tf*"] この設定で、 ./project1 配下の .tf ファイルが更新されたら自動で plan が走るようになります。 一度建ててさえしまえば、あとは YAML に数行追加していくだけでワークフローが自動化できるので、運用としては非常に楽だと感じています。 ワークフローをカスタマイズできる ワークフローにはカスタマイズの余地があります。 例えば atlantis apply を実行可能な条件として approved と mergeable を指定できます。前者は PR が approve されていることが条件となり、後者は GitHub の status check が通って mergeable になっていることが条件になります。無秩序な apply はこれで防ぐことができます。 他にも、 terraform plan/apply 以外の任意のコマンドをワークフローに追加できたり、 Conftest による Policy Check を盛り込めたりなど、カスタマイズの対応は多岐に渡ります。これらの設定も基本的には atlantis.yaml の編集で行っていきます。 基本的なワークフローはデフォルトで設定されるので、ただ自動化だけが目的ならシンプルに扱えますし、その上で物足りない部分があれば自由にカスタマイズすることもできるという振れ幅は大きな魅力です。 Atlantis が required_version を読んでくれる Terraform の required_version を upgrade したのに、自動化ワークフロー側で使う Terraform Version の更新を忘れて plan がコケる、というのは Terraform 自動化あるあるだと思っています。 Atlantis の場合、 required_version で = により固定的にバージョン指定していれば、そのバージョンを読んで自動的に使ってくれるので、設定の手間がありません。個人的には = 指定で特に差し支えがないと思っています。バージョンアップも renovate などを使えば自動化可能ですし、最近は Atlantis にあわせて = 指定を取ることが多くなりました。 terraform plan 結果ハイライトに diff を使っている 地味ですが、最初見たときは天才の発想だと思いました。要はこうなります。 Atlantis のここがつらい つらみポイントも書いておきます。 merge 前に apply を忘れる merge 前に apply を行うのは手動ですので、つい忘れることがあります。 Atlantis は PR 抜きにして任意のタイミングでの手動実行には対応していないので、こうなるとちょっと面倒です。 GitHub の status check で apply の成功を含めることもできます。が、その場合先述の mergeable を apply 条件として設定すると、 apply 前は mergeable ではないので apply ができないという鶏卵状態になるという困った問題がありました。 この点、最近改善の動きはあり、 feat: filter out atlantis/apply from mergeability clause by nishkrishnan · Pull Request #1856 · runatlantis/atlantis で apply 前の mergeability check から apply 自身を外すようになったっぽいです。まだ試せてないので、今後試してみたいです。 GUI に認証の仕組みがない Atlantis には GUI も存在しており、現在 lock されている workspace 一覧の表示と、その中から任意の workspace を unlock する操作ができます。 unlock 操作は PR 上にコメントを書くことでも可能なので、実運用の中で GUI を利用する機会はほとんどありませんし、クリティカルな情報が表示されるものでもないのですが、筒抜けの状態で運用するのもあまり良い気分ではありません。しかしながら Atlantis には GUI 認証の仕組みが長らくありませんでした。 回避手段は存在しており、先述の Terraform module では Amazon Cognito を使った認証の仕組みを付与できるようになっていますし、自前で IP 制限したり、 ALB や CloudFront で BASIC 認証を設けることも可能でしょう。また、こちらも最近改善されているところで、 BASIC 認証のサポートが feat: add BasicAuth Support to Atlantis ServeHTTP by fblgit · Pull Request #1777 · runatlantis/atlantis で Atlantis 本体に組み込まれました。 Progress が見えない plan や apply の Progress を見る仕組みがありません。ですので、長くかかる plan や apply を仕掛けると、しばらく Atlantis が何の応答もしなくなるので不安になります。本気で不安になったときは Atlantis サーバーのメトリクスを見に行って、 CPU がゴリゴリ使われているのを見て安堵するときもあります。 実行が遅いと感じるならサーバースペックを積めばある程度解決する話でもあるので、その点はセルフホストする故のメリットとも言えるかもしれません。 Docs に書いていない仕様もある これは OSS ではよくあることですし、人によっては特につらみでもないと思います。たまにですが、 Docs に記載がなくて、 Issues を探ったりコードを読んだりして初めて気付く仕様はあったりします。例えば先述した BASIC 認証が実装された際、 LB の Health Check はどうすればいいのだろうと調べているうちに /healthz を認証対象から除外した という PR に辿り着いたのですが、Health Check 用の endpoint が切られていること自体、このとき初めて知りました(もし Docs 内など見逃していたらすみません)。 まぁ Docs が本当に足りないと思ったら PR すればよい話ですし、 OSS に慣れていればそれほど気にする話ではないとは思います。 まとめ 実は、この記事を書いている最中に tfmigrate + Atlantis でTerraformリファクタリング機能をCI/CDに組み込む - Qiita という記事が上がって拝見し、「あ、一部被った内容の記事を上げてしまうかもしれない……申し訳ない……」と気付いたのですが、そのまま上げさせてもらいました。 Atlantis 推しの記事はそれほど多くないので、増えれば増えるほど嬉しいと言うのが個人的な思いです。僕の記事は基本的な内容しか書けていませんので、より Atlantis の拡張性などを実感したい場合には是非こちらの記事を読んでいただければと、勝手に勧めさせてもらいます。 Atlantis は「大事なことに注力する」ために使える OSS だな、というのが僕の印象です。ワークフローを一から考えてメンテするのはあまりやりたくなくて、 Terraform Cloud と同様、ツール側でうまいことやってくれるほうが好みです。その上で、自分たちに必要なワークフローを上に積み重ねながら使っていける、必要ならば Issues を開いたり PR を投げながら使っていけるというのはとてもワクワクするところでもあります。 Terraform 自動実行環境の整備に Toil 感を覚えていたりしましたら、是非一度検討してみてはいかがでしょうか。

Slack + AWS WAF でサービスをメンテナンス画面 (Sorry Page) に切り替える

$
0
0
この記事は GLOBIS Advent Calendar 2021 の8日目です。 ウェブサービスを更新する際、更新内容の特性上、サービスを一時的に「メンテナンス画面」に切り替えて閉塞することがあります。弊社ではこの状態を主に「メンテナンスモード」と呼んでおり、この記事でもそう呼ぶこととします(他では「Sorry Page」と呼ぶ場合も多いと思います)。 このメンテナンスモード、弊社においてはサービスごとに実装がまちまちの状況でした。そして GLOBIS SRE チームは、サービス横断のインフラ兼 ops チームから変化したチームだという経緯もあり、メンテナンスモードの切り替え作業は長らく SRE の担当作業になっていました。しかしサービスが徐々に増える中、アプリケーションごとに個別実装された運用を、 SRE が一手に引き受けるのは徐々に難しくなってきていましたし、リリースの際、わざわざメンテナンスモードのためだけに SRE を招聘してもらうのも不健全だなという気持ちが強くなってきていました。 そこで認知負荷や運用負荷を下げつつ、統一的な形でメンテナンスモードを実現できるよう、今年ブラッシュアップを図ったので、そのあたりの話を書きます。 最初に話の前提として、構成情報をいくつか書いておきます。実際はサービスごとに細かな差分はありますが、話を簡単にするために、統一した前提を設けることにします。 弊社のサービスは基本的に AWS で構築しています。 メンテナンスモードの対象となるのは Rails サーバです。 Rails が直接エンドユーザーへ HTML を返す場合もあれば、 SPA から呼ばれる API サーバとして動作している場合もあります。 Rails サーバの構成はエンドユーザーに近い側から CloudFront -> ALB (+ WAF) -> Kubernetes Service (ClusterIP) -> Kubernetes Pod (Rails) という形とします。 要件 ブラッシュアップするにあたり、現状の何が問題なのか、理想的なメンテナンスモードとはどういう状態なのか、要件を洗い出してみます。 切り替え手順は簡易でありながら、権限は限定する 先述したような、サービスごとに切り替え手順がまちまちという状態を避け、統一的かつよりシンプルな方法で切り替えをしたい、というのがこの話の発端です。 ただ、「シンプルで簡単で 誰でも 使える」というところまで行ってしまうと行き過ぎです。メンテナンスモードへの切り替えは、サービスの死活を左右する極めて重要な機能ですので、シンプルでありながら、実行可能なメンバーは絞れる仕組みになっている必要があります。 SRE を介さず完結する 1つ前の項目の、手順をシンプルにする、という点と若干重複しますが、メンテナンス作業は基本的に開発ドリブンで発生するものですので、本来 SRE が介在する必要はないですし、しない形が望ましいと思っています。従来は SRE が踏み台サーバに入ってコマンドを打つパターンが多かったのですが、これをやめて簡単に権限委譲できるようにします。 また、単に「切り替えを行う」という点だけではなく、メンテナンス画面にメンテナンス予定日時を書き入れるなどの作業も含め、 SRE が介さず完結する形とします。 よりクライアントに近いところで切り替える 従来のメンテナンスモードは、 Rails や nginx で処理している場合がありましたが、バックエンドに処理を持たせると、そのバックエンド自体が動かない場合にメンテナンスモードが稼働しないという問題があります。 Kubernetes 上でアプリケーションサーバーを動かしているので、 k8s cluster の障害も想定したいところです。従ってメンテナンスモードの処理は CDN や Load balancer など、よりクライアントに近いところで実装するのがベターと考えました。 SEO やユーザビリティを考慮する 具体的には、メンテナンス中は 503 を返し、一時的な unavailable であると明示することがひとつ。もうひとつは 302 などでメンテナンスページへ redirect するのではなく、 URL のパスはそのままに 503 とすることで、メンテナンスを終えた際にユーザーがリロードすれば済むようにすることです。 予定メンテナンスだけではなく障害時にも応用可能とする メンテナンスモードは予定メンテナンスで使うものですが、 Rails が何かしらの不具合で応答できなくなった場合にも、仕組みを応用してメンテナンス画面を返せるような構成とします。 構成 結論としては以下のような構成となりました。 作業者は Slack から ChatOps により、対象 WAF を選択してメンテナンスモード切り替えを実行します。 Slack App が WAF Web ACL の default rule を allow から block に切り替え、さらにカスタムレスポンスで 503 を返すよう設定します。 Rails がエンドユーザーへ直接 HTML を返すサービスの場合、これを CloudFront が受け、 503 に対するカスタムエラーレスポンスで、 S3 に用意したメンテナンス画面用 HTML を返します。 SPA が Rails の API を call している構成の場合、 SPA が 503 を受けたときにメンテナンス画面を表示するようあらかじめ設定しておきます。 以下、それぞれ詳しく見て行きます。 AWS WAF で柔軟にメンテナンスモードを制御 制御の要は AWS WAF です。Kubernetes よりも外、 AWS managed なところで制御を行っているので、バックエンドの障害にメンテナンスモードも引きずられてしまうということがなくなりました。 WAF や ALB 自体の障害などがない限りはメンテナンスモードを活用できます。 AWS WAF は、ソース IP や Request header などを条件とした柔軟なアクセス制御を行いつつ、マネージドサービスなので統一的な API で扱えるのが魅力です。例えばメンテナンスモードへの切り替えを行いつつ、社内からのアクセスだけは作業の確認用に許可しておく、といったことも WAF rules を工夫することで実現できます。特定のパスだけをメンテナンスモードにする、といったことも技術的には可能です。 これによりメンテナンスモードの切り替えが弊社アプリケーションの実装に依存せず、 AWS API だけで制御できることになりました。これは手順を整備したり、切り替えツールを実装する上では大きなメリットで、後述する Slack App の実現にも繋がりました。 ちなみに ALB 単体でも似たことは実現できますが、 WAF であれば「default rule の allow/block 切り替え」というシンプルな運用だけを考えれば良く、採用の決め手になりました。 画面表示の責務はクライアント側に寄せる AWS WAF でメンテナンスモードを制御することにより、メンテナンスモードとは「バックエンドが 503 を返している状態」と定義できるようになりました。メンテナンス画面を表示するのは、バックエンドの 503 を受け取る SPA や CloudFront の責務となり、役割が明確に整理できました。 メンテナンスの日時を書き入れるなど、メンテナンス画面の表示内容を書き換えたい場合は、 SPA もしくは S3 上の静的ページを書き換えれば OK です。これらは開発チームのレポジトリで管理、デプロイされるので、 SRE が関与する必要がありません。 切り替え実行は slack 切り替えには Slack App を用意し、スラッシュコマンドで直感的に扱えるようにしました。これは僕が作ったのですが、メンバーからは「『!!』とか、やたらテンションたけーな」と言われました。仕様です。 実行した際は、誰がどの WAF について操作したかを Slack 上に残すようになっているので、 message を削除しない限りは実行の記録も残せます。 権限制御のため、 Slack App 側で実行元の channel ID をチェックし、許可された channel からの実行のみを受け付けるようにしています。許可 channel を特定の private channel だけにすることにより、権限の限定を実現しています。 なお、現状はまだ SRE だけしか操作できない形に限定しています。各サービス開発サイドへの権限委譲は今後進めていく予定です。 没構成 上記構成に至るまでにいくつか没になった構成がありました。 CloudFront + Lambda@Edge 先述の通り、 WAF が block する際にはステータスコードを 503 に変更していますが、これが出来るようになったのは2021年3月とかなり最近のことです。メンテナンスモードの構想を始めたのは2021年1月頃で、その頃、 WAF が block した際のステータスコードは 403 で固定されていました。これが困るのは、 CloudFront のカスタムエラーレスポンスはステータスコードしか条件としていないので、 WAF による 403 と、 API の純粋な Forbidden による 403 とを区別できないということです。もしもこの当時、 WAF でメンテナンスモードを実装した場合、 CloudFront は 403 を受けたらメンテナンス画面を表示する形になりますが、これでは単なる未ログインユーザーなどにもメンテナンス画面が表示されてしまいます。 従って構想当初は WAF による実装を断念しました。その代わりとしていたのが CloudFront のオリジンレスポンスに Lambda@Edge を設定し、その中でメンテナンスモードの処理を行う実装です。このときは slack コマンドでメンテナンスモードを有効化すると、 SSM Parameter Store に対象 CloudFront の ID をセットし、 Lambda@Edge はそれをチェックして、メンテナンスモード有効の場合には 503 を返す、ということを行っていました。 'use strict'; const AWS = require('aws-sdk'); const ssm = new AWS.SSM({ region: 'ap-northeast-1' }); const timeoutSec = 300; const cache = { in_maintenance_cfs: null, timeout_at: null, }; exports.handler = async event => { const request = event.Records[0].cf.request; const distributionId = event.Records[0].cf.config.distributionId; const now = new Date().getTime(); // cache の存在判定 if (cache['in_maintenance_cfs'] === null || now > cache['timeout_at']) { console.log('cache does not exist.'); const ssmRequest = { Name: '/hoge/fuga/maintenance', WithDecryption: false }; const ssmResponse = await ssm.getParameter(ssmRequest).promise(); cache['in_maintenance_cfs'] = ssmResponse.Parameter.Value; cache['timeout_at'] = now + timeoutSec * 1000; } const inMaintenanceCfs = cache['in_maintenance_cfs']; if inMaintenanceCfs.indexOf(distributionId) != -1) { console.log(distributionId + ' in maintenace.'); return { status: '503', statusDescription: 'Service Temporarily Unavailable', headers: { "content-type": [{ key: "Content-Type", value: "text/plain; charset=utf-8" }] }, body: "503 Service Temporarily Unavailable" }; } return request; }; 一度取得した SSM Parameter をキャッシュする仕組みは入れてあるとはいえ、 Lambda@Edge で AWS API を叩くというのはレスポンスへの影響を考えるとなかなかにアグレッシブな構成で、本音を言えばやりたくありませんでした。 AWS WAF のカスタムレスポンス対応が、弊社からすると本当に神がかったタイミングで来てくれました。 CloudFront + WAF もう一つ検討に挙がっていたのが、 ALB ではなく CloudFront に連携させた WAF を用いる構成です。ALB + WAF の場合とそれほど変わりがないのではと思われるかもしれませんが、例えば同じ ALB が提供する API に対して、複数の CloudFront を連携させ、それぞれ利用者用と管理者用といった形で別のサブドメインを振ることがあります。このとき、 CloudFront 連携の WAF で制御を行えば、利用者用サブドメインだけを閉塞する、といったケースに対応できます。 しかし、この構成については技術的に不可能であることがわかりました。というのも、 CloudFront に連携させた WAF によるカスタムレスポンスを、 CloudFront のカスタムエラーレスポンスで上書きすることができないためです(ちなみに WAF のデフォルト 403 のレスポンスは、カスタムエラーレスポンスで上書きできます)。したがって WAF からのレスポンスが直接利用者側に見えてしまうことになります。 WAF のカスタムレスポンスで body を書き換えることもできるので、その中でメンテナンス画面の HTML を描画することもできなくはありませんが、ちょっと実装が複雑になりそうなのでやめました。サブドメイン別でのアクセス制御を行いたければ、 WAF の rule を用いることとしています。 Slack App の実装 Slack App のソースコード全文はここには貼れない量なので割愛します。インフラとしては AWS Lambda + API Gateway を活用しており、 Serverless Framework でデプロイしています。 実装はなるべく Slack の現時点での Best Practice に則るようにしました。気をつけたのは以下のような点です。 馴染み深い Secondary message attachment による装飾は Legacy とされているので、推奨に従い Layout Blocks を使う。 Verifying requests from Slack に則り、アプリケーション側で Slack App からのリクエストを verifying する。 Slack App からのリクエストをハンドリングする上では Handling user interaction in your Slack apps | Slack に従う。 例えばリクエストを受けたらまず 200 を返し、メッセージの表示などは別途非同期で行う。 まとめ 実際のところ、1つのサービスにつき、メンテナンスモードを使う機会は年に数回程度です。しかし、弊社では年々サービス数が増えており、そのたびにオリジナルのメンテナンスモードを作っていては、徐々に侮れないトイル、負債になっていくおそれがあります。こういったところを早めに見直し、なるべく簡潔かつ権限委譲が可能な形へ組み替えることで開発組織のスケールを支えるのは、 SRE として重要な活動だと捉えています。

Docker build を GitHub Actions に最適化する

$
0
0
この記事は GLOBIS Advent Calendar 2022 - Qiita の 15 日目です。 Docker build を継続的に行う環境として、2022 年末の現時点では GitHub…

数字で振り返るGLOBIS SRE 2023 - 手動作業の依頼は年間で185件でした

$
0
0
この記事は GLOBIS Advent Calendar 2023 の21日目の記事です。 計測はとっても大事ですね。Rob Pike先生もそう言っています。ということで、この記事ではGLOBIS …