TECH PLAY

Ansible

イベント

該当するコンテンツが見つかりませんでした

マガジン

該当するコンテンツが見つかりませんでした

技術ブログ

はじめに こんにちは。バンダイナムネクサス データ戦略部の山野です。 今回は、Google Cloudのサービスを活用してエンジニア向けの開発環境を刷新した事例をご紹介します。私たちの課題と、それをどう解決したかについて、具体的なポイントを深掘りしていきます。 経緯と背景 エンジニア向けの開発環境を、ユーザーと管理者の両方にとってより使いやすく、効率的にしたいという要望がありました。そのため、以下の点に注目して改善を試みました。 マネージドサービスを活用した運用の効率化: 弊チームでは、これまでエンジニア向けの開発環境としてVM環境を提供してきました。しかし、運用コストの増
はじめに こんにちは。プラットフォームエンジニアリングチームに所属している徳富( @yannKazu1 )です。 新規プロダクトを立ち上げるとき、インフラ構築って意外とやることが多いですよね。その中でも地味にめんどくさいのが DBユーザーの作成と権限付与 。手動でやると「あ、権限つけ忘れた」「このユーザー名スペルミスってない?」みたいなヒヤリハットが発生しがちです。 今回は、この作業をTerraformでIaC化した話を書いていきます。 背景:ボイラープレートでインフラ構築を爆速にしている 弊社では Terraformのボイラープレート と、それをもとにインフラを構築するための Devinへの指示プロンプト をセットで管理しているリポジトリがあります。 新規プロダクトのインフラが必要になったら、このリポを使ってDevinにお願いするだけ。数時間もあれば、AWSアカウントの作成、VPC・ECS・Auroraなどの基盤構築、Terraform実行に必要なIAMロール・バックエンド・CI/CDワークフローの設定に加え、Datadog設定、監査設定、ログ基盤の作成まで、必要なインフラがひととおり立ち上がります。 このボイラープレートを整備するにあたって目指したのは、「Devinにお願いするだけで、新規プロダクトのインフラを簡単に作れる状態」でした。ところが、 DBユーザーの作成だけはどうしても手動作業が残ってしまっていました 。 せっかくDevinに投げれば数時間でインフラができるのに、DBユーザーだけは人間が踏み台経由でDBに繋いで CREATE USER して GRANT して……とやらないといけない。これだとボイラープレートの意味が薄れてしまいますし、手動オペレーションにはミスのリスクもあります。 ここをTerraformでIaC化できれば、ボイラープレートにサクッと組み込めて、Devinに任せるだけでインフラ構築が完結するようになります。 なぜTerraform? DBユーザーの管理といえばAnsibleを使うパターンも考えました。ただ、弊社のインフラは基本的にTerraformで一元管理しているので、できればTerraformの中で完結させたい。ツールが増えると学習コストも運用コストも増えますし、ボイラープレートにAnsibleのステップを追加するよりも、Terraformモジュールとして組み込む方がシンプルです。 というわけで、Terraformでなんとかする方向で調査を進めました。 Aurora Data APIという選択肢 弊社ではAurora MySQLを使用しており、バージョン3.07以降でRDS Data APIに対応しています。 Data APIは、HTTPSエンドポイント経由でSQLを実行できるAPIです。従来のようにVPC内から直接DBに接続する必要がなく、AWS CLIやSDKからサクッとSQLを叩けます。 これを terraform_data リソースの local-exec プロビジョナーと組み合わせれば、Terraformの中からDBユーザーを作成できるというわけです。 モジュールの実装 実際に作ったTerraformモジュールを紹介します。 DBユーザーの作成・削除 resource "terraform_data" "db_user" { input = { rds_cluster_arn = var.rds_cluster_arn rds_secret_arn = var.rds_secret_arn database_name = var.database_name username = var.username ssm_parameter_name = var.ssm_parameter_name } provisioner "local-exec" { command = <<-EOT PASSWORD=$(aws ssm get-parameter --name "$ { self.input.ssm_parameter_name } " --with-decryption --query 'Parameter.Value' --output text) aws rds-data execute-statement \ --resource-arn "$ { self.input.rds_cluster_arn } " \ --secret-arn "$ { self.input.rds_secret_arn } " \ --database "$ { self.input.database_name } " \ --sql "CREATE USER IF NOT EXISTS '$ { self.input.username } '@'%' IDENTIFIED BY '$PASSWORD'" EOT } provisioner "local-exec" { when = destroy command = <<-EOT aws rds-data execute-statement \ --resource-arn "$ { self.input.rds_cluster_arn } " \ --secret-arn "$ { self.input.rds_secret_arn } " \ --database "$ { self.input.database_name } " \ --sql "DROP USER IF EXISTS '$ { self.input.username } '@'%'" EOT } } ポイントは以下のとおりです。 terraform_data リソースを使って、 local-exec プロビジョナーでData API経由のSQLを実行 CREATE USER IF NOT EXISTS で冪等性を担保 when = destroy のプロビジョナーで、 terraform destroy 時にユーザーを自動削除 パスワードはSSM Parameter Storeから取得(後述の工夫で安全に管理) 権限の付与・取り消し resource "terraform_data" "db_grant" { for_each = { for idx, grant in var.grants : idx => grant } depends_on = [ terraform_data.db_user ] input = { rds_cluster_arn = var.rds_cluster_arn rds_secret_arn = var.rds_secret_arn database_name = var.database_name username = var.username privileges = each.value.privileges grant_database = coalesce (each.value.database, var.database_name) grant_table = coalesce (each.value.table, "*" ) } provisioner "local-exec" { command = <<-EOT aws rds-data execute-statement \ --resource-arn "$ { self.input.rds_cluster_arn } " \ --secret-arn "$ { self.input.rds_secret_arn } " \ --database "$ { self.input.database_name } " \ --sql "GRANT $ { self.input.privileges } ON $ { self.input.grant_database } .$ { self.input.grant_table } TO '$ { self.input.username } '@'%'" EOT } provisioner "local-exec" { when = destroy command = <<-EOT aws rds-data execute-statement \ --resource-arn "$ { self.input.rds_cluster_arn } " \ --secret-arn "$ { self.input.rds_secret_arn } " \ --database "$ { self.input.database_name } " \ --sql "REVOKE $ { self.input.privileges } ON $ { self.input.grant_database } .$ { self.input.grant_table } FROM '$ { self.input.username } '@'%'" || true EOT } } for_each で権限セットを柔軟に定義できるようにしています。DMLとDDLを分けて付与したい、みたいなケースにも対応可能です。 使い方 モジュールの呼び出しはこんな感じです。 module "db_user_app" { source = "../../modules/db_user" rds_cluster_arn = module.aurora_main.cluster_arn rds_secret_arn = module.aurora_main.cluster_master_user_secret [ 0 ] .secret_arn database_name = "hoge_db" username = "hoge_app_user" ssm_parameter_name = aws_ssm_parameter.app_db_password.name depends_on = [ aws_ssm_parameter.app_db_password, module.aurora_main, ] grants = [ { # DML権限: データの読み書き privileges = "SELECT, INSERT, UPDATE, DELETE" } , { # DDL権限: マイグレーション実行用(テーブル作成・変更・削除、インデックス操作) privileges = "CREATE, ALTER, DROP, INDEX, REFERENCES" } ] } DMLとDDLの権限を分けて書けるのが個人的に気に入っています。どの権限を付与しているかが一目でわかりますし、将来的に読み取り専用ユーザーを追加したいときもモジュールを再利用するだけでOKです。 パスワードをstateに残さない工夫 ここが今回一番こだわったポイントです。 Terraformでパスワードを扱うとき、普通にやるとstateファイルにパスワードが平文で残ってしまいます。 sensitive = true をつけても、plan出力でマスクされるだけでstateには書き込まれてしまう。これはセキュリティ的によろしくないですよね。 そこで活用したのが、 Terraform 1.10で導入された ephemeral リソース と、 Terraform 1.11で導入された value_wo (write-only引数) です。 ephemeral "random_password" "app_db" { length = 32 special = false } resource "aws_ssm_parameter" "app_db_password" { name = "/$ { var.app_name } /$ { var.env } /db/app_user_password" type = "SecureString" value_wo = ephemeral.random_password.app_db.result value_wo_version = 1 } ephemeral リソースとは Terraform 1.10で登場した新しいリソースタイプです。通常の resource や data と違い、planやstateに一切値が保存されません。毎回のplan/apply時に一時的に生成され、使い終わったら破棄されます。 ephemeral "random_password" を使うことで、パスワードの生成結果がstateに残ることを防げます。従来の resource "random_password" だと、生成したパスワードがstateファイルにそのまま記録されてしまっていたので、これは大きな改善です。 value_wo (write-only引数)とは Terraform 1.11で導入されたwrite-only引数の仕組みです。 aws_ssm_parameter リソースの value_wo を使うと、SSM Parameter Storeへの書き込みは行われますが、その値がTerraformのstateやplanファイルに保存されることはありません。 value_wo_version は変更検知のためのバージョン番号です。パスワードをローテーションしたい場合はこの値をインクリメントすれば、次のapply時に新しいパスワードが生成・設定されます。逆にバージョンを変えなければ、 ephemeral が毎回新しいパスワードを生成しても、SSM Parameter Storeの値は更新されません。 この2つを組み合わせることで、パスワードの生成から保存まで、 一度もstateファイルにパスワードが記録されない フローが実現できました。 現時点での制約:パスワードの更新には対応していない ここまで読んで「パスワードのローテーションはどうするの?」と思った方もいるかもしれません。正直に言うと、 このモジュールはパスワードの更新( ALTER USER )には対応していません 。 理由はシンプルで、Terraformのプロビジョナーには when = create と when = destroy しかなく、 when = update が存在しない からです。つまり、リソースの入力値が変わったときに「更新用のSQLを実行する」ということができません。 terraform_data の input が変わると、Terraformは古いリソースをdestroyしてから新しいリソースをcreateする(replace)動きになります。DBユーザーの場合、これは DROP USER → CREATE USER という流れになるので、パスワード変更だけしたいケースには少々大げさです。 この when = update の追加については、HashiCorpのGitHubリポジトリにIssueが上がっています( hashicorp/terraform#35825 )。いつか実装されれば、パスワードローテーションもこのモジュールの中でスマートに対応できるようになるはずです。それまでは、パスワードの更新が必要になった場合は手動対応か、別の仕組みで対応する必要があります。 とはいえ、新規プロダクト立ち上げ時の初期ユーザー作成という用途においては、create/destroyだけで十分に機能しています。 まとめ やったことをまとめると以下のとおりです。 Aurora MySQLのData API(3.07以降で利用可能)を使って、Terraformの terraform_data + local-exec でDBユーザーの作成・権限付与をIaC化 Terraform 1.10の ephemeral リソースと1.11の value_wo を活用して、パスワードをstateに残さないセキュアな構成を実現 これらをモジュール化してボイラープレートに組み込み、新規プロダクトのインフラ構築をさらにスムーズに プロビジョナーに when = update がないため、パスワードの更新には現時点では未対応 Terraformの ephemeral や value_wo はまだ比較的新しい機能なので、知らない方も多いかもしれません。パスワード管理で困っている方はぜひ試してみてください。 参考リンク Amazon Aurora MySQL で RDS Data API のサポートを開始 RDS Data API でサポートされているリージョンと Aurora DB エンジン - Amazon Aurora Terraform 1.11 brings ephemeral values to managed resources with write-only arguments Ephemeral values in Terraform Ephemeral values in resources | Terraform | HashiCorp Developer Use temporary write-only arguments | Terraform | HashiCorp Developer I would like provisioner/s to support when=update - hashicorp/terraform#35825
1. はじめに 弊社では入社一年目のエンジニアは全三期のOJTを通して部署を渡り歩き、業務や会社について知見を深めていくという制度があります。 OJTについての詳細は、私の同期が入社一年目の経験を基に記事を書いていますので、是非こちらをご覧ください! ニフティでの新卒一年目について そのOJTの第三期で、新システムへの移行に伴い旧システムの運用が停止したため、対象システムが動作していた環境を廃棄するための作業を行いました。 この記事では、システムの廃止で苦労した点、意識した点、学びを共有したいと思います。 2. 意識していた部分 「システムを廃止する」と聞くと一見単純に見えますが、実際には関連システムとの依存関係を一つずつ切り離し、必要なデータを保全し、ユーザー影響を最小化しながら進める必要があるため、想像以上に手順が多く、判断ポイントも多い作業でした。 特に、今回触れた環境は構築年代が古く資料が少ないため調査に手間がかかり、ネットワーク的にも隔離されている特殊な環境であったため、特有の制約があり苦労しました。 2-1. 順番が重要 廃止作業は、いきなりサーバを止めて消すのではなく、段階的に影響範囲を狭めていくのが安全です。 入口(ユーザアクセス)を別系統へ逃がす 周辺システムとの連携を止める 必要データをバックアップする ネットワーク的に遮断する(FWを閉じるなど) サービスを停止する リソースを削除する 2-2. 遮断の段階 今回一番意識したのは「一発で消さない」ことです。 例えばネットワーク遮断やサーバ停止は、どちらも使えなくするという意味では同じですが、復旧可能性と確認のしやすさが違います。 ここでいう「疎通だけ止める」は、FW(ファイアウォール)を閉じて外部から到達できない状態にする、といった対応です。メリットは、サーバ上のプロセスやデータを触らずに入口だけ塞げるため、切り戻しが必要になってもFWを戻すだけで復旧できる点です。 まず疎通だけ止める(FWを閉じるなどのネットワーク遮断) しばらく様子を見る 何も起きなければ削除に進む という順で進めると、万が一のときの切り戻しコストが下がります。 2-3. 「様子見」を挟む 古いシステムは依存関係がドキュメントに残っていないことが多く、停止後に思いがけない影響が出る可能性があります。 そのため、停止と削除の間に時間を置き、アラートや問い合わせなどの遅れて出る症状を拾えるようにし、切り戻しの手間とリスクを最小化しながら進めて行きました。 3. 実際の対応 システム廃止は、次の手順で進めました。 # 作業 1 新システムへのリダイレクト設定 2,3 周辺システムとの連携停止 4 バックアップ 5 FW遮断(疎通停止) 6 システム停止 7 リソース削除 3-1. リダイレクト設定 まずはじめに、旧システムへのアクセスを、新システムへリダイレクトする対応を行っていました。 事前に経路となるリンクを差し替える対応を行って頂いたのですが、念の為旧システムに来るアクセスを新システムにリダイレクトするようにしました。 Ansibleを用いて該当サーバで稼働するhttpdに設定を反映し、URLに付随するクエリパラメータにより遷移先が異なるため、動作確認は 3 パターン実施しました。 パターン 遷移先 用途 1 サービス終了ページ サービス終了済みの案内 2 新システム 新システムへのリダイレクト その他 新システム 例外処理 3-2. ファイル連携停止 関連システムとファイルの送受信でデータをやり取りしているバッチを停止しました。 この作業では他部署のシステムとの連携を解除するので、業務担当者との綿密な確認が必須でした。 手順としては、 先方でファイルの取り込みを停止 廃止するシステムでファイルの出力を停止 先方でファイルの出力を停止 廃止するシステムでファイルの取り込みを停止 の流れで行いました。 関連システムによってpull/pushの向きが違うなどの差がありましたが、基本的には取り込み処理停止→送信停止、の流れで行えば双方でのアラートを予防することができます。 3-3. 通信停止 上記とは別のシステムからのREST APIを使用した通信を切る対応を実施した。 こちらは部署内管理のものだったので構成の確認は比較的簡単にできたものの、開発経験がないPHPだったのと、ソースコードをコピペで作った関係で該当システムに無関係な記述が多量に含まれていて、必要な部分を抽出するのに手間取ってしまいました。 3-4. バックアップ 続いて関連情報のバックアップをしました。 バックアップ対象は色々考えられますが、今回は実際に動作していたソースコード(微妙にgitHub上のものと異なる)、他システムと授受していたファイル群、関連するログ、データベースのダンプ、実体をバックアップしました。 今回はサーバ上でバックアップ用ファイルを作成した後にAWS CLIを用いてバックアップを行いました。 なんとサーバ時刻の時刻がズレておりAWS CLI の認証が通らなかったため、作業前に sudo date -s で時刻合わせを行いました。普通であればNTPサーバの設定を行うところですが該当システムはもうすぐ廃棄するものであるため、dateコマンドで簡易的に時間をあわせるのみにしました。 # ソースコード tar czvf ./source.tar.gz {path_to_source} source # ログ tar czvf ./logs.tar.gz -C {path_to_log} logs # MySQLダンプ mysqldump {connection_info} > dump.sql # MySQL実体 # 実際にはmariadb sudo tar czvf ./mysql.tar.gz -C {path_to_mysql} mysqldb # httpdログ tar czvf ./httpd_logs.tar.gz -C /var/log httpd # dryrun で転送対象を確認してから本転送 aws s3 cp ./{backup_dir}/ s3://{bucket_name}/{backup_dir} \\ --recursive --dryrun --profile {profile_name} aws s3 cp ./{backup_dir}/ s3://{bucket_name}/{backup_dir} \\ --recursive --profile {profile_name} 3-5. FWを閉じる 続いてFWを閉じる対応をしました。 対象の環境に設定されているFWの設定を無効化することで、システムの停止をすることなく外部からの通信を遮断することができ、外部から見ると停止しているのと同じ状況を発生させることができます。 この対応を停止、削除の前に挟むことで切り戻しのコストを小さくしつつ、停止状況の再現ができました。 3-6. systemd unit停止 続いて本格的に停止を行っていきます。 停止対象は順番に、 zabbix-agent: システム状態の監視を行っている 事前にzabbixサーバから該当システムの監視を停止しておく必要があります。 keepalived: 主系/従系の切り替え用 httpd: php動作用 mariadb: phpのデータ保存に使用 です。 システム廃棄のための停止なので切り戻すことはありませんが、万が一のために順番を考えてsystemd unitを停止していきます。 停止したあと一日おいて問題が発生しないか様子見をします。 # 停止する順番を守る systemctl disable --now zabbix-agent.service systemctl disable --now keepalived.service systemctl disable --now httpd.service systemctl disable --now mariadb.service 3-7. リソース削除 最後にシステムを稼働させていたリソースを削除し、環境を完全に廃棄しました。 4. まとめ・振り返り 今回のシステム廃止では、単に停止するだけでなく、依存関係の切り離し、データ保全、利用者影響の最小化まで含めて段取り良く進める必要があり、想像以上に調整と確認が多い作業でした。 特に、構築年代が古く資料が乏しいことに加え、ネットワーク的に隔離された特殊環境だったため、調査と作業手順の確立に時間がかかりました。 システム停止に伴うトラブルを最小化するために詳細に調査を重ねた結果、想定外の対応範囲が増加し続け、当初の目標スケジュールを大幅に超過してしまいました。 また、周辺システム連携停止では、双方でアラートや業務影響が出ないように業務担当者と手順とタイミングを丁寧に合わせる必要があり、関係者調整の重要性を強く実感しました。 今回のレガシーシステムの調査、外部システム担当者との調整などの経験を本配属後の業務に活かしていきます。

動画

該当するコンテンツが見つかりませんでした

書籍