AWS

CloudFormationデプロイ失敗を防ぐ〜見落としがちなリスクと対策〜

sho

はじめに

西藤です。

クラウドインフラの構築や変更をコードで表現・管理する手法をInfrastructure as Code (IaC)と呼びます。
その中でもAWSにおいては、AWSネイティブなツールとしてAWS CloudFormationがあります。
本ブログの読者の多くの方も規模の大小はあれど、何らかの形でCloudFormationの利用経験があるのではないかと思います。

では、その変更内容のレビューはどうしていますか?
テンプレートのファイルを編集して、そのコードをチームでレビューし、問題なければデプロイするという流れでしょうか。

果たして、コード内容を見るレビューだけで十分でしょうか?

今回は、CloudFormationを使用したインフラ変更時のレビューについて、掘り下げます。

記事の前提

このブログ記事では次のような観点を掘り下げていきます。

  • IaCコードレビューの課題
  • CloudFormation「変更セット」の活用と課題
  • cfn-lintによる静的解析
  • 自動化による品質向上
  • taskcatを使った実環境テスト

これらを通じて、CloudFormationを使用したインフラ変更におけるより良いプラクティスを考察したいと思います。

また、サンプルとして次のような既存インフラがあって、それに変更を加えていくシナリオを考えてみましょう。

  • VPC内にプライベートサブネット2つ、パブリックサブネット2つ
  • EC2インスタンス1つがパブリックサブネットに設置されておりApacheのウェブサーバーが稼働している
  • インターネットからEC2インスタンスのIPアドレスへアクセスできる

テンプレートのコードは次のような形です。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'VPC with public and private subnet, and EC2 instance for web hosting'

Resources:
  # VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: WebApp-VPC

  # Internet Gateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: WebApp-IGW

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # Public Subnet
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: Public-Subnet

  # Private Subnet
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: ap-northeast-1a
      Tags:
        - Key: Name
          Value: Private-Subnet

  # Route Tables
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: Public-Route-Table

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: Private-Route-Table

  # Routes
  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  # Route Table Associations
  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

  # Security Group for Web Server
  WebServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for web server
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
          Description: HTTP access
      Tags:
        - Key: Name
          Value: WebServer-SG

  # EC2 Instance
  WebServerInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0f95ad36d6d54ceba  # Amazon Linux 2023 AMI (ap-northeast-1)
      InstanceType: t3.small
      SubnetId: !Ref PublicSubnet
      SecurityGroupIds:
        - !Ref WebServerSecurityGroup
      UserData:
        Fn::Base64: |
          #!/bin/bash
          dnf update -y
          dnf install -y httpd
          systemctl start httpd
          systemctl enable httpd
          echo "<h1>Hello from Web Server</h1>" > /var/www/html/index.html
      Tags:
        - Key: Name
          Value: WebServer

Outputs:
  VPCId:
    Description: VPC ID
    Value: !Ref VPC

  WebServerPublicIP:
    Description: Public IP address of the web server
    Value: !GetAtt WebServerInstance.PublicIp

  WebServerURL:
    Description: URL of the web server
    Value: !Sub "http://${WebServerInstance.PublicIp}"

AMIのIDを動的に取得していないですとか、単一のアベイラビリティーゾーン(AZ)でサブネットを構築してしまっているなどの課題はありますが、あくまでサンプルとしてご覧ください。

では、この既存インフラに変更を加えるとき、安全に行うにはどのようにすればいいか確認していきましょう。

1. IaCコードレビューの課題

さて、CloudFormationで管理されているインフラの変更をレビューするときに具体的にどのようなステップを踏むでしょうか?

たとえば、次のような流れが考えられます。

  1. 担当者がテンプレートファイルを編集
  2. その編集内容をGitリポジトリにPushし、コード管理システム上にてPull Requestを作成
  3. レビュアーがPull Requestの差分を確認
  4. 問題がなければPull Requestを承認・Merge(修正が必要な際は担当者が指摘をして、修正が確認できたら承認・Merge)
  5. 承認されたコードでデプロイ

図に表すと次のとおりです。

コードベースがあるとき、Pull Requestの差分を確認してコードレビューを行うのは一般的です。
しかし、CloudFormationのコードのレビューでわかるのは「コード上の変更点」だけです。
もちろん、たとえば

  • 「今取り組んでいるタスクってEC2インスタンスのサイズ変更することじゃなかったっけ?これだと台数の変更になっている」

という点をレビュアーが指摘してくれる可能性はあるので一定の効果はあります。
しかし、「どのようなインフラ変更が起きるのか」をコードの変更点から推測することになります。

これだと、「人の目でコードの変更内容を見た分には問題なさそうだったが、実際にデプロイするときにエラーになってしまう。想定していなかったインフラの変更が入る」
ということが起きるリスクが残ります。

そういったリスクに対応するために、次に紹介するような仕組みを加えて、インフラ変更時の安全性を高めていきましょう。

2. CloudFormation「変更セット」の活用

コードレビューでは見えないインフラ差分を事前に可視化する方法として、CloudFormationには「変更セット」という仕組みがあります。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html

これはCloudFormationのテンプレートデプロイ時にどのようなインフラ変更が行われるかを、実際のインフラと比較した上で表示してくれる機能です。
これをどのように活用できるか確認してみましょう。

たとえば、冒頭のインフラ例に対して、

  • EC2インスタンスのインスタンスタイプをt3.micro から t3.small に変更する
  • AMIが古くなっているので最新のものに差し替える

というケースを考えてみましょう。

YAMLコード内においては

sample-infrastructure.yaml

# <略>
  # EC2 Instance
  WebServerInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0f95ad36d6d54ceba  # Amazon Linux 2023 AMI (ap-northeast-1)
      InstanceType: t3.micro
      SubnetId: !Ref PublicSubnet
      SecurityGroupIds:
        - !Ref WebServerSecurityGroup
      UserData:
        Fn::Base64: |
          #!/bin/bash
          dnf update -y
          dnf install -y httpd
          systemctl start httpd
          systemctl enable httpd
          echo "<h1>Hello from Web Server</h1>" > /var/www/html/index.html
      Tags:
        - Key: Name
          Value: WebServer
# <略>

のような記述になっていたとします。
そこから
InstanceType: t3.micro と書かれている記述を InstanceType: t3.smallに更新したコードに変更します。

sample-infrastructure-v2.yaml:

# <略>
  # EC2 Instance
  WebServerInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-07faa35bbd2230d90  # Amazon Linux 2023 AMI (ap-northeast-1)
      InstanceType: t3.small
      SubnetId: !Ref PublicSubnet
      SecurityGroupIds:
        - !Ref WebServerSecurityGroup
      UserData:
        Fn::Base64: |
          #!/bin/bash
          dnf update -y
          dnf install -y httpd
          systemctl start httpd
          systemctl enable httpd
          echo "<h1>Hello from Web Server</h1>" > /var/www/html/index.html
      Tags:
        - Key: Name
          Value: WebServer
# <略>

というコードになります。diffを見ると次のような形です。

% diff sample-infrastructure.yaml sample-infrastructure-v2.yaml                      
114,115c114,115
<       ImageId: ami-0f95ad36d6d54ceba  # Amazon Linux 2023 AMI (ap-northeast-1)
<       InstanceType: t3.micro
---
>       ImageId: ami-07faa35bbd2230d90  # Amazon Linux 2023 AMI (ap-northeast-1)
>       InstanceType: t3.small

このコードを使って、既存のCloudFormation Stackに対して、変更セット作成を進めます。

修正版のYAMLファイルをアップロードして手順を進める

「送信」を押して、変更セットの作成開始
画面のメモ表示にもあるとおり「変更セット」の作成を行うこと自体は既存のStackに影響を与えません。「送信」を押した後の画面で実際のインフラ変更を行うかを判断することができるので、安心して手順を進めましょう。

変更セットの作成が完了すると、既存のインフラにどのような変更が見込まれるかの表示画面に変わります。

上記の例を見ると、EC2インスタンスにReplaceAndDeleteが赤字で表示されています。
今回の例で、AMIのID変更を行おうとしたことが理由ではあるのですが、これはリソースの削除を伴う形での置き換えが行われることを表しています。

たとえば、EC2インスタンスで言うと、EC2インスタンス内におけるデータ(ログ・アプリケーションデータ・アプリケーション自身)の保全を行なっていないと、このインフラ変更が原因でそれらのデータが失われることを意味します。

関係者が、EC2のインフラ変更時の挙動をあらかじめ理解していれば、コードだけを見たレビュー時点でもこれに気がつけるかもしれませんが、人力では限界があります。

このような「変更セット」を使って、仕組み的にインフラ変更時のリスクを検出し、その上でインフラ変更の意思決定することを基本動作とできると良いでしょう。

フロー図に表すと次のような具合です。

3. 「変更セット」で検出できない事象

さて、「変更セット」を使うことでインフラ変更時の挙動を確認できました。

しかし、残念ながらこれだけでは網羅しきれないシチュエーションがあります。
次の例を見てください。

sample-infrastructure-v3.yaml:

# <略>
  # EC2 Instance
  WebServerInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0f95ad36d6d54ceba  # Amazon Linux 2023 AMI (ap-northeast-1)
      InstanceType: t3.smal # t3.smallではなく、t3.smal
      SubnetId: !Ref PublicSubnet
# <略>

インスタンスタイプとして t3.smallを指定しようとしたところスペルミスで t3.smalと記載してしまっているケースを考えます。

この状態で「変更セット」を作成し実行するとどうなるでしょうか。

作成画面は次のようになりました。

「変更セット」自体はエラーなく作成できてしまいました。

変更箇所の詳細を見ても、smal(※smallではない)になると表示されていますが、この時点ではエラーになりません。

さらにこのまま実行するとどうなるでしょうか。試してみます。

実行自体は開始できました。

しかし、最終的に The following supplied instance types do not exist: [t3.smal](t3.smalというインスタンスタイプは存在しない)というエラーとともに失敗し、Rollbackとなりました。

つまり、インスタンスタイプという個別の属性の内訳自体はCloudFormationの「変更セット」機能では捕捉してくれないのです。

Pull Requestレビューと「変更セット」の確認を経ても、デプロイ時にエラーとなるリスクは残るというわけです。

では、どう対策できるのか?

次に紹介する仕組みを使って、そういったデプロイエラーのリスクを軽減していきましょう。

4. cfn-lintによる静的解析

cfn-lintは https://github.com/aws-cloudformation/cfn-lint にて公開されているCloudFormationのlinterです。

CloudFormationのテンプレートコードを精査して、エラーを発生させる可能性のある構文エラーがないかを検査できます。

このツールを使ってどのような活用ができるかを紹介します。

コマンドラインとして利用

cfn-lintはCLIツールとして提供されており、テンプレートファイルに対してチェックを行うコマンドが使えます。

上記のような t3.smalと書いてしまっているYAMLファイルに対してチェックコマンドを実行すると次のような画面になります。

% cfn-lint sample-infrastructure-v3.yaml 

# <略>

E3030 't3.smal' is not one of ['a1.2xlarge', 'a1.4xlarge', 'a1.large', 'a1.medium', 'a1.metal', 'a1.xlarge', 'c1.medium', # <略>

t3.smalが有効なインスタンスタイプではないということを警告しています。(末尾を省略してしまっているのですが、実際にはすべてのインスタンスタイプが列挙されていて、そのいずれにも合致していない。という旨の表示になっていました。)

このように、cfn-lintを併用することで、CloudFormationの機能だけでは検出できないエラーも事前に防止できます。

IDEの拡張機能として利用

また、VS CodeのようなIDEの拡張機能としても提供されています。インストールした上で該当のコード箇所を見てみますと次のような表示になります。

編集中のコード上に波線表示で、エラーが指摘されています。

これを使えば、導入のハードルも低く、視覚的にもわかりやすい形で誤りに気がつくことができますね。

5. 自動化による品質向上

さて、上記にはCloudFormationの「変更セット」機能とcfn-lintの例を挙げました。

それぞれの機能を組み合わせていくことで、万全なチェックを行えますが、欠かさずにそれらの機能を使わないといけません。
往々にして、こういったチェックの仕組みから漏れてしまった時に、トラブルというのは発生するものです。
人の意識に頼らず、自動でチェックが働く仕組みが重要です。

ここでは、これらの仕組みを自動的に使うようにする設定を紹介します。

5.1 CloudFormation Git同期機能

CloudFormationにGit同期機能というものがあります。

https://aws.amazon.com/jp/about-aws/whats-new/2023/11/aws-cloudformation-git-management-stacks/

従来はテンプレートファイルがgit管理されていても、それをAWSアカウント内にデプロイする際には専用のコマンドラインやAPIを組み合わせた仕組みを作って、デプロイする必要がありました。

2023年にリリースされたこの機能により、各Gitソリューションと連携してGitリポジトリ内のコード変更と直接連動してインフラ変更を進めることができるようになっています。

そして、さらに

https://aws.amazon.com/about-aws/whats-new/2024/09/aws-cloudformation-git-sync-supports-pull-request-workflows/

のように、Pull Request上のコメントの形で通知できるようになりました。

AWS CodeConnectionsでの接続設定、CloudFormationでのGit同期の有効化設定・・・など必要な設定はいくつかあるのですが、そういった設定のハンズオンはここでは割愛して、どのようなことが実現できるかを紹介します。

前提として、GitHubリポジトリと連携済みのCloudFormation Stackがあります。

この状態で、CloudFormationテンプレートを変更し、GitリポジトリにPushします。(変更内容は最初の例と同じEC2インスタンスをt3.microからt3.smallへ変更します)

すると、Pushしてからしばらくして自動的にPull Request上にコメントが追加されました。
今回の場合は1つのリソースにて更新があったという旨のコメントがついています。

コメント内のClick here to view change detailsをクリックすると変更セットの内訳が表示されます。

リソースごとの情報を一覧で表示する仕組みなので仕方ないのですが、横に長い表示になるため、左右にスクロールして確認します。

重要なところで言うと、そのリソースが削除を伴わず変更されるのかなどがわかるAttributeChangeTypeなどを見ると良いでしょう。
(この例だと、Modifyとあり、リソースの削除は発生しないということがわかります。)

変更セットの内容が自動的にPull Requestに投稿されるため、コード差分だけでなく実際のインフラ変更内容もPull Request上で確認できます。これによりレビューの精度と安全性が高まります。

そして、レビュー完了後、Pull RequestをMergeすることでデプロイも自動的に行われるので、細かい手間も削減されます。
フロー図に表すと次のような形です。

自動化される部分も増えて、合理的になっています。

5.2 Git Hookでcfn-lintを自動実行

cfn-lintは
CLIとして提供されているので、ファイルの更新を行った後に必要に応じて手動でコマンドを実行してチェックするのは手間です。

git hookを仕込みcfn-lintコマンドを呼び出すようにすることで、エラーを内包したコードがcommitされないように防ぐことが可能です。
以下にはそのスクリプトの例を紹介します。

.git/hooks/pre-commit:

#!/bin/bash

# CloudFormation Lint Pre-commit Hook
# このフックは、コミット前にCloudFormationテンプレートをcfn-lintで検証します

echo "Running cfn-lint on CloudFormation templates..."

# コミット対象のファイルを取得
files=$(git diff --cached --name-only --diff-filter=ACM | while read file; do
  # ファイル拡張子をチェック(yaml, yml, json)
  if [[ "$file" =~ \.(yaml|yml|json)$ ]]; then
    # ファイルの内容にAWSTemplateFormatVersionが含まれているかチェック
    if grep -q "AWSTemplateFormatVersion" "$file" 2>/dev/null; then
      echo "$file"
    fi
  fi
done)

if [ -z "$files" ]; then
    echo "No CloudFormation template files to check."
    exit 0
fi

# cfn-lintが利用可能かチェック
if ! command -v cfn-lint &> /dev/null; then
    echo "Error: cfn-lint is not installed or not in PATH"
    echo "Please install cfn-lint: brew install cfn-lint"
    exit 1
fi

# 各ファイルに対してcfn-lintを実行
exit_code=0
for file in $files; do
    echo "Checking $file..."

    # cfn-lintを実行
    if ! cfn-lint "$file" -i W ; then
        echo "❌ cfn-lint failed for $file"
        exit_code=1
    else
        echo "✅ $file passed cfn-lint validation"
    fi
done

if [ $exit_code -ne 0 ]; then
    echo ""
    echo "❌ Pre-commit hook failed!"
    echo "Please fix the CloudFormation template errors before committing."
    echo "You can run 'cfn-lint <filename>' manually to see detailed errors."
else
    echo ""
    echo "✅ All CloudFormation templates passed validation!"
fi

exit $exit_code

内容としては

  • pre-commitとして、git commitが行われようとした時点でスクリプト呼び出し
  • gitの変更内容にCloudFormationテンプレートファイルが含まれているか確認
  • cfn-lintコマンドが利用可能か確認(できない場合はセットアップを促す)
  • cfn-lintコマンドが利用可能だった場合は、コマンド実行しエラーがあれば、そこでコミット失敗となるようにする(Warningは判定から除外)
  • エラーがなければ、git commitを許可する

という内容です。

この設定を入れた上で、誤ったコードを含んだ状態でgit commitしようとした時の表示は次のとおりです。

% git commit -m "smal"
Running cfn-lint on CloudFormation templates...
Checking sample-infrastructure.yaml...
E3030 't3.smal' is not one of ['a1.2xlarge', 'a1.4xlarge', 'a1.large', 'a1.medium', 'a1.metal', 'a1.xlarge', 'c1.medium' 
# <略>
sample-infrastructure.yaml:114:7

❌ cfn-lint failed for sample-infrastructure.yaml

❌ Pre-commit hook failed!
Please fix the CloudFormation template errors before committing.
You can run 'cfn-lint <filename>' manually to see detailed errors.

インスタンスタイプとして、t3.smalと入れた内容でgit commitしようとしたら、エラーを検出し、git commitに失敗します。
これにより、コード編集のたびにcfn-lintを叩く必要がなく、git commitしようとした段階で自動的にエラーが含まれるのを防いでくれます。

6. taskcatによる実環境テスト

ここまでCloudFormationの「変更セット」機能とcfn-lintの活用方法を紹介しました。
この組み合わせで、デプロイ時のエラーになるリスクはかなり軽減できると思いますが、CloudFormation「変更セット」のドキュメントには次のような気になる記述があります。

変更セットでは、CloudFormation によるスタックの更新が正常に行われるかどうかはわかりません。例えば、アカウントクォータを超過する、更新をサポートしていないリソースを更新しようとしている、リソースの変更に必要な許可が足りていないなど、スタックの更新が失敗する原因になる可能性があるものを、変更セットでは確認しません。更新が失敗した場合、CloudFormation では元の状態にリソースをロールバックするように試みます。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html

あれ、一見するとここまでの内容と矛盾しそうです。
「変更セット」やcfn-lintは、そもそもコードの記述内容が原因でエラーとなることを防ぐ仕組みだと言えます。ただし、それだけではアカウントクオータの制約は検出できないわけです。
結局、デプロイしようとしている変更に対して、そのリソース以外の要因で失敗する可能性は残る。実際にAWSアカウント内にデプロイしてみないとわからない。というエラーのリスクは残るわけでそこがポイントとなります。

そこで検討したいのがtaskcatです。

これはCloudFormationテンプレートを使って、実際に1回デプロイを行い、デプロイが完遂できるかどうかを確認することで、そのテンプレートがエラーなくデプロイできるものになっているかをチェックできるツールです。
(チェックが終わったらデプロイ済みのリソースの削除も自動的におこわれます。)

実際にデプロイを行うことで上記のようなアカウントクオータの制約などのような「実際にデプロイしてはじめて気が付くポイント」を洗い出すことができます。

デプロイ時のParameter設定もファイルに定義する形で設定できるので、さまざまなシチュエーションを試し、「このテンプレートをAWS環境に実際にデプロイしてもエラーなく実行できるのか」を確認できるので、より安心してデプロイできるようにする仕組みとして使えます。

詳細は末尾の参考資料欄を見ていただくほか、以前、このツールを使った仕組みについて書いた記事もあるのでそちらをご参考いただければと思います。

過去の記事
Cloud9・Codeサービス・Taskcatで実現するCloudFormationテンプレート開発環境を作る
Cloud9・Codeサービス・Taskcatで実現するCloudFormationテンプレート開発環境を作る

まとめ

以上、CloudFormationにおけるインフラ変更を安全に実行するための方法を紹介しました。内容が長くなってしまったので、改めてまとめると次のとおりです。

  • CloudFormationで管理されているインフラ変更にはテンプレートのコード変更だけでなく「変更セット」を使った確認を行うことで、コードレビューだけでは予防できないデプロイ時のエラーを防ぐ
  • 「変更セット」を使った確認だけではカバーされないエラーがあるので、cfn-lintで補う
  • 「変更セット」もcfn-lintも自動化された仕組みを作ることでもれなくチェックが行われる体制を構築するべき
  • taskcatを使ってCloudFormationテンプレートのデプロイが完遂できるかを事前に確認することで、デプロイ時のエラーのリスクをさらに減らす

こういった工夫を加えながら、CloudFormationでのインフラ管理をより安全なものにしたい人にとっての参考になれば幸いです。

今回はデプロイ時のエラー回避に焦点を当てたため、セキュリティリスクの静的解析やベストプラクティスとの比較は対象外としました。
冒頭のサンプルコードもセキュリティ的な観点からするとまだ多くの課題が残ったコードになっているはずです。

これらの点については、別の機会に掘り下げたいと思います。

参考資料

AUTHOR
sho
sho
記事URLをコピーしました