🌤️

AWSで時間を無駄にしないためのN個のTips

2023/12/06に公開

はじめに

2023年も終盤ですね🌅
今年の業務はAWSから始まりAWSで終わりました。実績としては年内3つのWebアプリケーションをAWS環境にリリースしました。

実務レベルでBtoCのアプリケーションを運用するにあたり、セキュリティやオペレーション連携、オンプレとの相互通信、監査対応など、沢山の課題を解決しました。
(お力添え頂いた方々ありがとうございます!)
この経験で得た知見をまとめていきます。

また、こちらも同時に投稿させていただきました。
合わせてご覧いただけますと幸いです🙇‍♂

https://zenn.dev/ishiyama/articles/d82d4b8a583a79

自己紹介(Context)

私はWeb基盤を提供している企業でWebアプリケーションエンジニアをしています。
社内モダナイゼーションの一環で、自社インフラの実行環境からAWS環境にクラウドリフトしました。そのときの経験をもとに作成しています。

目次

Tips ネットワーク編

通信が失敗する原因の切り分けに迷う

初めの頃はリソース間通信がうまくいかない理由がどこにあるのか、当たりを付けるのに苦労しました。
AWSのアクセス制御は基本的にはホワイトリスト形式で、アクセス制御全てをパスすることで初めて通信可能な状態になります。

補足
制御の種類 対象
ルートテーブル サブネット
ACL サブネット
セキュリティグループ AWSリソース
IAMロール リソースまたはアカウント

ルートテーブルはネットワークの経路を定義するものであり、セキュリティグループはリソースごとに許可(または拒否)するIOを定義するもの。IAMロールはリソースに対し、AWSリソースの読み取りまたは変更を許可(または拒否)する権限設定になります。

ここあたりの整理がうまくできず、セキュリティグループで許可したのにうまく通信ができなかったり、経路設定しているのにデータの読み取りができないなどの問題が起きました。

また、このときの原因はアプリケーション層のログでは詳細が拾えないことも多く、どこに問題があるか切り分けがしずらいです。

闇雲に設定するのでなく、俯瞰的にネットワーク構成を考えてから設定することで、このような事態を避けることができそうです。

サブネットは5つのIPアドレスが予約されている

サブネットを作成するとAWS側で5つのIPアドレスが自動的に予約されます。
そのためCIDRを/28で切った場合の使用可能なプライベートIPは、16個ではなく11個です。
冗長構成にしつつ、小さな粒度でサブネットを作成すると、以外と早くIPアドレスの枯渇問題に直面します。
VPCの再構築は大変な手間なので、VPCのIPレンジは余裕を持って設計しておくのが良さそうです。

VPCエンドポイントの通信はAWSネットワーク内で完結する

インターフェース型のVPCエンドポイントを設置した場合、インターネットは経由せず、AWSのネットワーク内で完結します。

そのためNATゲートウェイを使用するよりも低遅延でセキュアになります。

その通信、知らずにインターネット経由してませんか…?

プライベートサブネットからインターネットアクセスが必要な場合にNATゲートウェイを設置することが多いと思います。そのためプライベートサブネットからも、知らずにAWSリソースともインターネットを経由していることがあります。
セキュリティ要件が厳格なプロジェクトは気を使ったほうが良さそうです。

VPCエンドポイントは設置しているだけでコストがかかる

VPCエンドポイントはインターフェース型とゲートウェイ型があり、インターフェース型は設置しているだけで時間単位で$0.01の料金が発生します。月単位では$7以上になります。

セキュアで低遅延だからといって、とりあえず設置しておくとその分コストが嵩みます。
たくさん通信するのであれば、NATゲートウェイよりVPCエンドポイントの方が安いと言われているので、使用する要件に対して、費用対効果があっているか見極める必要がありそうです。

EICを使うとプライベートサブネットのEC2に直接SSH接続ができる

EC2 Instance Connect(EIC)を使用すると、プライベートサブネットに配置しているEC2に直接SSHすることが可能です。
パブリックサブネットを立てる必要がなく、経路の設定も不要なので手軽つセキュアです。
また、EICを使用すると、EC2にAWS管理コンソールから直接接続することができます。 AWSの管理画面上で全て完結できるのはとても良い体験です。

SSHのキーペアは…?

EICはIAMベースでのアクセス制御になります。
EC2を接続するときに、EICが裏側で使い捨てのSSH公開鍵/秘密鍵を自動で作成し、使ってくれているそうです。

AmazonLinuxはインターネットを媒介せずyumを使用できる

AmazonLinuxのインスタンスはプライベートサブネットにあったとしても、yumに限りAWSのネットワーク内でパッケージの更新およびインストールを完結させることが可能です。
ただし、S3ゲートウェイ型のVPCエンドポイントを設置しておく必要はあります。

https://repost.aws/ja/knowledge-center/ec2-al1-al2-update-yum-without-internet

なるべくACMで発行したSSL証明書を使用したい

AWS Certificate Manager(ACM)で発行した証明書は、AWSのリソースにセットされている & DNSサーバーに指定のCNAMEレコードがセットされている限り、無料かつ自動で証明書の更新が行われます。つまり、運用コスト0です。

補足

また、DNSサーバーはRoute53である必要はないそうです。

既に第三者の証明書局(CA)から証明書を取得している場合や、ACMが発行した証明書では満たされないアプリケーション固有の要件がある場合はしょうがないですが、なるべくACMで発行したSSL証明書を使用することをお勧めします。

Hostsは意外と役に立つ

何かと敬遠されがちなHostsですが、ネットワークを構築しているときはとても役に立ちます。

例えば、エンドユーザー → 別プロバイダー → AWSのような構成になっている場合、別プロバイダーを挟まずにAWSに直接アクセスしたい場合はHostsで指定のドメインにAWS側IPを設定することで別プロバイダーを挟まず、直接AWSと通信することが可能です。

私が実務でHostsを使ったのは…

私の場合は、SSL証明書の更新が正しく行われているか、ブラウザを通して確認する必要があったので、この方法を使いました。
(このときはグローバルアクセラレーターによりAWS側のIPを固定していました。)

なを、ネットワークエンジニアの方に教えてもらったのですが、digでも同じことができます。

curl -X GET -I -v --connect-to hogehoge.com::172.217.175.78: hogehoge.com/

踏み台サーバーを設置する前にSSMとEICの違いを抑える

EC2を設置してSSHポートフォワードを使い、AWS上のデータベースに接続する。
よくある構成だと思いますが、Session Manager(SSM)とEC2 Instance Connect(EIC)どちらを使用するかは要件に合わせて選ぶ必要がありそうです。

以下、ポイントを抜粋

  • 1VPCにつき1つのEICエンドポイントしか作れない
  • そのためEICはAZ障害に弱い
  • EICのコストはデータ転送量のみ
  • SSMはVPCエンドポイントの設置コストが掛る
  • EICの最大セッションは1時間のみ
  • SSMの最大セッションは24時間

以下の記事がとても参考になりました。

https://dev.classmethod.jp/articles/compare-eic-endpoint-and-session-manager/

DirectConnectやVPNを使うと…

ローカルからAWSのリソース(例えばRDS)に接続したいとき。
踏み台サーバーを設置するのも良いですが、DirectConnectやVPNでネットワークを構成している場合、VPC内のリソースにIP指定で直接通信することが可能です。

Tips アプリケーション編

アプリケーション層の情報を信用してはいけない

例えば X-Forwarded-For(XFF)という、エンドユーザーのIPアドレスを保持するために使用されるHTTPヘッダーがあります。

XFFはHTTPヘッダーに入ってくる情報であり、L7であるアプリケーション層の情報です。

クラウドフロント(や別のプロバイダー)を前段に挟む場合、送信元IPアドレスはクラウドフロントのエッジロケーションのIPアドレスになり、エンドユーザーのIPアドレスはXFFに格納されます。

アプリケーション層の情報はFiddlerなど使うと容易に改ざん可能なのでXFFを信用してアクセス制御をしてはいけません。

補足

例として Application Load Balancer(ALB)にはHTTPヘッダーを見てルーティングする機能があります。

公開前のアプリケーションを制限したく、特定のIPアドレスのみを許可するためにALBのルーティング機能を使ったとします。ですがここで見ているXFFはアプリケーション層の情報です。

アプリケーション層の情報は書き換えが容易で、特別な知識がなくとも、curlやネットワークキャプチャツールなどを使うと誰でも改ざんすることが可能です。

IP制限を入れる場合は、クラウドフロントを経由しない別のルートを作成し、セキュリティグループでIP制限を行うという方法が安全そうです。

また、セキュリティグループで、判定に使われるのは送信元のIPアドレスであり、XFFではないので覚えておくと役に立ちそうです。

コンテナに状態を持つのはアンチパターン

スティッキーセッションといって、IPアドレスでリクエストを固定する方法があります。
私の環境では、ログインセッションを持っているサーバーに対してリクエストを固定する目的で使用されていました。

これが前提になっている場合、スケールアウトしたとしてもリクエストが均等に慣らされず、スケールメリットを得られません。

現在はコンテナを使ったオートスケールが主流です。その場合、N個のコンテナにリクエストを均等に分散できるよう、なるべくステートレスなWebアプリケーションを目指しましょう。

スティッキーセッションの止め方は…

では、スティッキーセッションを止めるにはどうしたら良いかというと、コンテナ内のメモリ領域などにリクエスト特有の状態を保持しないステートレスなアプリケーションを作るようにします。

例えばログインセッションがあります。
従来のセッション管理は、ログインが成功したらWebサーバーにユーザーのセッション情報を保持し、そのセッション情報が存在しているうちはログイン状態とするという仕組みでした。

しかし、N個のサーバーに対してそれを行う場合、次回のリクエストが前回のWebサーバーに行われるとは限らないので、ログイン状態が維持できなくなります。

これを回避する方法は

  1. ElastiCasheなどを使用してセッション情報をコンテナから外出しする
  2. JWTなどのトークン形式の認証にする

などがあります。

リソース同時接続時の排他制御を考慮する

例えば、トークン生成に使用されるプライベートキーなど、N個のコンテナから共通で使われるものをS3や、KMSなどに外出しすることがあります。

そのとき、その生成をアプリケーション側から行う場合は排他制御が適切に行われているか確認する必要があります。

私が踏んだ地雷…

私は.NETのWebアプリケーションで、SSMの Parameter Store にアプリケーション側から暗号鍵を定期的にローテーションしてくれるAWS公式のライブラリを使用していました。
ですが、そのライブラリの排他制御が弱かったようで、同時に暗号鍵を作りにいってしまい、バグった経験があります。

https://github.com/aws/aws-ssm-data-protection-provider-for-aspnet/issues/14

ロードバランサーをSSLの終端にするとHSTSヘッダーが付与されない

HTTP Strict Transport Security(HSTS)はブラウザとWebサーバー間の通信をHTTPSのみに制限するレスポンスヘッダーです。

通常のHTTP(平文)→ HTTPS(暗号済)へのリダイレクトと異なり、HTTPのURLをリクエストされてもHTTPを一切経由せず、ブラウザ上で強制的にHTTPSのURLへリクエストさせます。

Strict-Transport-Security: max-age=<expire-time>

私の環境ではECSの前段にALBを配置し、ALBをSSLの終端としていました。

これはAWSのパブリックなアプリケーションでよく見る構成ですが、HSTSは「HTTPSの経路でのみレスポンスヘッダーにHSTSを付与する」という仕様が前提にあります。
これにより、ALBをSSLの終端にするとHSTSのヘッダーが付与されないという現象がおこりました。

これを回避するには…

これを回避するために、アプリケーション側に
このリクエストはHTTPでリクエストされているように見えるけど、それはALBをSSLの終端としているからで、エンドユーザーとの通信自体はHTTPSで行われている
ということをアプリケーションに教える必要があります。

ALBはバックエンドに対して以下のリクエストヘッダーを付与します。

  • X-Forwarded-For(LBが受け取ったIP)
  • X-Forwarded-Proto(LBとの接続に使われたプロトコル)
  • X-Forwarded-Port(LBとの接続に使われたポート)

元の通信がHTTPSかは「X-Forwarded-Proto」を見ればわかるので、これの値が正しいプロトコルであるとアプリケーションに教えてあげましょう。

機密情報を扱う場合はマネージドサービスで

データベースの認証情報や、APIキーなどの機密情報はSecretManagerや、SSM Parameter Storeから取得するようにしましょう。

機密情報をアプリケーション内部に保持している場合があります。
例えばデータベースの接続文字列などは慣習的に設定ファイルに持っている人も多いのではないでしょうか。

RDSとSecretManagerの場合…

RDSのSecretManager統合機能を使うと、RDS側で設定を行ってくれるそうです。キーローテーションの運用などが手軽になりそうです。

https://dev.classmethod.jp/articles/aws-rds-integration-aws-secrets-manager/

機密情報はECSのvalueFromで環境変数に展開する

ECSのタスクから機密情報を使用したいとき、タスク定義のvalueFromフィールドにパラメーターのARMを指定することでタスク起動時に環境変数に展開することが可能です。
使用できるパラメーターストアはSecretManagerか SSM Parameter Storeです。

環境変数に実値を登録するとECSの定義からその登録値が覗けてしまいますが、valueFromを使うことで、情報の隠蔽が可能となります。

タスクロールとタスク実行ロール

ECSにはタスクロールとタスク実行ロールの2種類が設定ができます。
valueFromを使用する場合、そのパラメーターストアへのアクセス権を付与するのは、タスクの起動に使われる「タスク実行ロール」の方になります。
タスク実行中の権限である「タスクロール」にアクセス権を付与する必要がないので、万が一コンテナがのっとられたり、インジェクション攻撃をくらっても、パラメーターストアへのアクセスはIAMロールにより防ぐことができます。

Tips CICD/開発環境編

CodeDeployのBlueGreenデプロイメントが神

CodeDeployの機能でBlueGreenデプロイメントがあります。

ロードバランサーのリスナーを本番用と検証用とを用意し、まずは検証用ポートに向けてデプロイを行います。
検証用のリスナーには社内IPなどでリクエストを制限し、検証が完了したら本番リスナーと検証リスナーの入れ替えを行いリリース完了とします。

CodeDeployを使用しない場合、このようなデプロイの仕組みを自前で用意するのは大変です。
AWSを使用するならぜひ使いたい機能です。

マルチアカウント構成で消耗しないために

マルチアカウント構成で開発をする場合、都度アカウントの切り替えが発生します。
それが嵩んでいくと、アカウントの切り替えに要する時間と、業務の割り込みが入ったときに今どのアカウントで作業していたかなど、情報整理が負担になってきます。

マルチアカウント構成はAWSも推奨している方法なので、ぜひ慣れたいところ。
よい対応方法あれば教えてください🙏

https://aws.amazon.com/jp/blogs/startup/multi-accounts-and-control-tower/

私の実務で使っていた環境は…

私の環境では

  • CICDアカウント
  • 開発アカウント
  • ステージングアカウント
  • 本番アカウント

の4アカウントを切り替えながら使用しています。
対応策としては、マルチブラウザやシークレットモード、Chromeで複数プロファイルを使用するなどがありますが、どれもイマイチという所感です。

CodeCommitがきつい

普段GitHubやGitLabを使用しているエンジニアからすると、機能やGUIの操作性に不満を感じると思います。

使用するメリットとしては「AWSのネットワーク内で完結する」「AWSのリソースと連携しやすい」などです。

前Tipsとの合わせになりますが、リポジトリにGitHub、CICDにGitHub Actions、実行環境にAWSあたりがよく取られる構成でしょうか。(マルチアカウント疲れも低減されそうです。)

Tips IoC編

まずは管理コンソールから、問題なければTerraformに落とし込む

私の環境では、IaCであるTerraform使ってAWSリソースを管理していました。
変更をApplyするにはCICD環境を使い、該当のブランチにPushすることで、CodePipleを動かす必要があります。
パイプラインはPull→Plan→Applyの順番を守るため、小さな変更でも環境反映まで時間が多少の待ち時間が発生します。

無闇に変更をPushしていると、時間がいくらあっても足りません。
横着せず、小さな変更でもまずは管理コンソールで変更を加え、問題なければTerraformの実装に落とし込むというフローを守ると、結果、時間の節約になったなと感じます。

Terraformを使うことでAWS環境の横展開が容易に

繰り返しになりますが、私の環境ではIaCであるTerraform使ってAWSリソースを管理していました。

私は今回、3つのWebアプリケーションをAWSにリリースする必要がありました。
最初のアプリケーションの環境構築には私の経験不足や、社内とのコンセンサスなどで数ヶ月程時間を要しました。
ですが、AWS環境をプログラムコードで管理していたため、2回目以降の環境構築は速攻終わりました。(そのアプリケーションに合わせたチューニングはもちろんします。)

また、開発・ステージング・本番といくつかの環境に分けて段階的にアプリケーションを配置していると思いますが、それぞれの環境毎にAWS構成に差異がないことをプログラムコードが補償してくれます。

さらにセキュアでDRY…

さらに、他チームへの横展開ではAWSアカウントの権限を渡さなくとも、プログラムコード渡すだけで済むので安全です。

皆さんの環境でも、ベース部分で使いまわせる箇所は沢山あると思います。今まではIaCの導入を考えなかった方も、この機会に検討してみてはいかがでしょうか。

Terraformを使用することでプログラム開発のツールが使える

AWSリソース管理をプログラムで行うことで、普段プログラム開発で使用しているツールやフローをそのまま流用することができます。
AWSのリソース管理を使い慣れているVScodeやエディタの拡張機能を使用して管理できるのはとても良い体験でした。

まだまだあるぞメリット…

実際に使用して感じたメリットを列挙します。

メリット

  • Gitによるバージョン管理ができる
    • ブランチ運用できる
    • 変更履歴を追える
    • リソースの差分が確認できる
  • ペアプログラミングで開発できる
  • コードレビューが行える
  • コードベースでナレッジを共有できる
  • エディターの補助機能が使える
  • ChatGPTや GitHub Copilot などのAI補助が受けられる
  • クラウドリソースの変更がCICDのフローに乗せられる
  • ヒューマンエラーが減る

などがあげられます。
今回構築するにあたり、特に「AI補助が受けられる点」「コードレビューができる点」「コードベースでナレッジが可能である点」 にとても助けられました。

ステートファイルで機密情報がダダ漏れ

ステートファイルとは、Terraformで使用される、現在のクラウドの状態を記録しておき、変更を追跡するためのファイルです。
もし、Terraformのコード内で機密情報を定義した場合、このファイルを覗くことで全てを見ることができてしまいます。

Terraform内で機密情報を定義しない方法…

私が取り扱った例を3つ。

  1. ECSの環境変数で機密情報はvalueFromを使用してタスク起動時に展開する。
  2. RDSのSecretManager統合機能を使う。
  3. ignore_changesでパスワードなどを指定し、特定の機密情報をTerraformの監視下から除外したうえで、手動で変更する。
lifecycle {
    ignore_changes = [master_password]
}

Tips その他

アーキテクチャ図の書き方に正解はない

インターネットで記事をを見ているとAWSアーキテクチャ図によってアイコンが違っていたり、ネットワークの色が違ったり、リソースが処略されていたり…
一体どの書き方がデファクトスタンダードなのかと思いました。ですが、AWS側は「アーキテクチャ図に正解はない」と言っています。

https://aws.amazon.com/jp/builders-flash/202204/way-to-draw-architecture/?awsf.filter-name=*all

図を残すことは思いやり…

構成図を残すことは、その後を開発する人からすると構成のアウトラインを知ることができ、とても有用です。そしてその図はあなたが伝えたいことを伝えやすい粒度にすれば良いのです。

構成図を書くときはDraw.ioというツールをおすすめします。VScodeの拡張機能でも提供されており、私のプロジェクトではアーキテクチャ図もソース管理の一環としてリポジトリにプッシュするようにしていました。

ECSタスクではタグの伝搬設定を明示的に行う

プロダクトごとにどれだけのコストが掛かっているか確認する方法として、コスト配分タグを各種リソースに設定するのが一般的だと思います。
ですが、ECSタスクは何も設定せずに起動してもタグが付与されません。この状態ではECSタスクがコスト配分の集計から漏れてしまいます。

管理コンソールから設定する場合は、タスクの実行で明示的に「タグの伝搬元:タスク定義」を選択する必要があります。

terraformの場合…
resource "aws_ecs_service" "service" {
  // 略
  propagate_tags  = "SERVICE"
}

このように設定することで、ECSタスクがECSサービスから受け継がれ、コスト集計に含めることが可能になります。

https://dev.classmethod.jp/articles/ecs-using-tags/

まとめ

AWS環境構築を通して、日頃、Webアプリケーションがどれだけの技術に支えられていたかということを再認識することができました。

当記事は普段Webアプリケーションエンジニアをしている私がAWSの環境構築をするにあたり、構築当初から知っておきたかった知見をまとめました。
普段からAWSを扱っているエンジニアからしたら幼稚な内容も多かったと思います。
それでもこのTipsのどれか1つでもヒットして「しなくて良い苦労」を未然に防ぐお力になれたら幸いです。

長文になりましたが、最後まで見ていただきありがとうございました!

Discussion