AWSLab

CloudFormationのEC2にElastic IPを後付けする【AMI再解決でインスタンスが消える罠】

スポンサーラベル
スタック更新でEC2が作り直される仕組み。LatestAmiIdはSSMパラメータ参照型のため更新のたびに再解決され、ImageIdが変わってReplacement=Trueになる。AmiIdOverrideで稼働中のAMIを固定すれば、変更はEipとEipAssociationの追加2件のみになる AWS

当サイトはアフィリエイト広告を利用しています。

CloudFormationで構築したEC2は、既定のままだと停止・起動のたびにパブリックIPが変わります。検証環境なら我慢もできますが、URLをブックマークしたりセキュリティグループの調整を繰り返したりしていると、さすがに面倒になってきます。

そこでCloudFormationでAWS上にDify環境を自動構築するで作った検証環境に、Elastic IP(EIP)を後から足すことにしました。テンプレートに10行ほど追加するだけの、ごく単純な作業のはずでした。

ところが変更セットを確認したところ、触ってもいないEC2インスタンスにReplacement=Trueが出ました。そのまま適用していれば、インスタンスが作り直されてEBS上のデータが全部消えていたところです。本記事では、この罠の原因と回避策を実際の出力とあわせて解説します。

1. EIPの定義自体は10行で済む

まずは素直にテンプレートへ追加します。必要なのはAWS::EC2::EIPAWS::EC2::EIPAssociationの2つです。

  # パブリック IP を固定する。
  # 自動割り当ての IP はインスタンスを停止・起動するたびに変わるため、
  # そのたびに URL が変わり、ブックマークも使えなくなる。
  Eip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-eip

  EipAssociation:
    Type: AWS::EC2::EIPAssociation
    Properties:
      AllocationId: !GetAtt Eip.AllocationId
      InstanceId: !Ref Instance

Domain: vpcはVPC用のEIPを確保する指定です。EIPAssociationで、確保したEIPを既存のインスタンスに紐付けます。あわせてOutputsの参照も!Sub http://${Eip}/のようにEIP側へ寄せておくと、意図が明確になります。

2. 変更セットに「置き換え」が出た

適用前に必ず変更セットで影響を確認します。ここで想定外の出力が出ました。

=== 変更内容 ===
  Add      EipAssociation   AWS::EC2::EIPAssociation Replacement=None
  Add      Eip              AWS::EC2::EIP            Replacement=None
  Modify   Instance         AWS::EC2::Instance       Replacement=True
           └ ImageId (再作成: Always)

EIPの追加2件は想定どおりですが、3行目が問題です。InstanceReplacement=True、しかも理由がImageIdの「再作成: Always」になっています。

Replacement=Conditionalなら、ルートボリュームがEBSの場合は再起動で済みます。しかしImageIdの変更は無条件でインスタンスの置き換えを引き起こします。置き換えられれば、EBS上で動いていたデータベースの中身は残りません。

テンプレートのInstanceリソースには一行も触れていないのに、なぜImageIdが変わるのでしょうか。変更セットの読み方や典型的なエラーについてはCloudFormationエラーの原因と対応メモにも整理しています。

3. 原因はSSMパラメータ参照型のAMI

先に全体像を示します。左が今回踏んだ流れ、右がこの記事で採る回避策です。

スタック更新でEC2が作り直される仕組み。LatestAmiIdはSSMパラメータ参照型のため更新のたびに再解決され、ImageIdが変わってReplacement=Trueになる。AmiIdOverrideで稼働中のAMIを固定すれば、変更はEipとEipAssociationの追加2件のみになる

テンプレートではAMIを次のように指定していました。最新のAmazon Linux 2023を自動で拾うための、よく知られた書き方です。

  LatestAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64

この型はスタック更新のたびにCloudFormationがSSMパラメータを解決し直します。 AWSが新しいAMIを公開していれば、当然そのIDに変わります。

実際に確認すると、稼働中インスタンスのAMIとパラメータの現在値がずれていました。

# 稼働中インスタンスが使っている AMI
aws ec2 describe-instances --instance-ids i-xxxxxxxxxxxxxxxxx \
  --query "Reservations[0].Instances[0].ImageId" --output text
# => ami-016923362cc95896d

# SSM パラメータの現在値
aws ssm get-parameter \
  --name "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64" \
  --query "Parameter.{Value:Value,Version:Version,Modified:LastModifiedDate}"
{
    "Value": "ami-0f7e90d3283d2e250",
    "Version": 184,
    "Modified": "2026-08-18T07:09:11+09:00"
}

構築したのが8月16日、AMIが更新されたのが8月18日。わずか2日の差で、EIPを足すだけの更新がインスタンスの作り直しに化けていました。

3.1 UsePreviousValueでは防げない

変更セットを作る際、パラメータにはUsePreviousValueを指定していました。それでも再解決は起きます。

UsePreviousValueが引き継ぐのは「パラメータに与えた値」、つまりSSMのパス文字列のほうであって、解決後のAMI IDではありません。 パスが同じでも、参照先の中身が変われば結果は変わります。「前回の値を使う指定をしているから安全」と考えるのは誤りです。

3.2 バージョン固定も使えなかった

SSMパラメータはパス:バージョンの形式で特定バージョンを参照できます。それなら以前のAMIに固定できるのでは、と考えて試しました。

aws ssm get-parameter \
  --name "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64:183"
An error occurred (ParameterVersionNotFound): Systems Manager could not find
version 183 of /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64.

履歴の取得もAccessDeniedExceptionCannot access/update data for the restricted parameter)で拒否されます。

AWSが提供するパブリックパラメータは、利用者側から過去バージョンを参照できません。 バージョン固定という逃げ道は塞がっていました。

4. AmiIdOverrideを足して固定する

回避策として、AMIを明示的に上書きできるパラメータをテンプレートに追加しました。空なら従来どおりSSMの最新を使い、値が入っていればそちらを優先します。

  AmiIdOverride:
    Type: String
    Default: ""
    Description: >
      使用する AMI を明示的に固定したいときに指定する。空なら LatestAmiId を使う。
      稼働中の環境を更新するときは、ここに現在の AMI ID を渡して固定する。

Conditions:
  # AmiIdOverride が空でなければ、そちらを優先する
  UseAmiOverride: !Not [!Equals [!Ref AmiIdOverride, ""]]

Instanceリソース側はFn::Ifで切り替えます。

      ImageId: !If [UseAmiOverride, !Ref AmiIdOverride, !Ref LatestAmiId]

新規構築のときは空のままで最新AMIを拾い、稼働中の環境を更新するときだけ固定するという使い分けができます。既存の書き方を壊さずに済むのが利点です。CloudFormationでEC2そのものを定義する基本形についてはAWSのCloudFormationでIaCを実践(4) – ec2で解説しています。

なお、同じ「更新のたびに作り直される」問題はUserDataでも起きます。こちらはReplacement: Conditional(EBSルートなら再起動のみ)で済みますが、そもそもUserDataは初回起動時にしか実行されないため、稼働中の環境に設定を反映する目的では使えません。UserDataの基本的な使い方はAWS CloudFormationでEC2にUserdataでパッケージをインストールするにまとめています。

適用スクリプト側では、稼働中インスタンスのAMIを自動で取得して渡すようにしました。手で控えておく必要がなくなります。

# 稼働中インスタンスが今使っている AMI をそのまま渡す
inst = cfn.describe_stack_resource(StackName=STACK, LogicalResourceId="Instance")
iid = inst["StackResourceDetail"]["PhysicalResourceId"]
ami = ec2.describe_instances(InstanceIds=[iid])["Reservations"][0]["Instances"][0]["ImageId"]
params.append({"ParameterKey": "AmiIdOverride", "ParameterValue": ami})

5. 再度の変更セットと適用

AMIを固定したうえで、もう一度変更セットを作ります。

AMI を ami-016923362cc95896d に固定しました
=== 変更内容 ===
  Add      EipAssociation   AWS::EC2::EIPAssociation Replacement=None
  Add      Eip              AWS::EC2::EIP            Replacement=None

Modify Instanceが消え、追加2件だけになりました。 この状態なら安全です。適用すると1分ほどで完了しました。

=== Outputs ===
  ElasticIp              203.0.113.25
  InstanceId             i-xxxxxxxxxxxxxxxxx
  DifyUrl                http://203.0.113.25/install

インスタンスが本当に無傷かを確認します。

aws ec2 describe-instances --instance-ids i-xxxxxxxxxxxxxxxxx \
  --query "Reservations[0].Instances[0].{State:State.Name,PublicIp:PublicIpAddress,LaunchTime:LaunchTime,Ami:ImageId}"
{
    "State": "running",
    "PublicIp": "203.0.113.25",
    "LaunchTime": "2026-08-16T04:55:43+00:00",
    "Ami": "ami-016923362cc95896d"
}

LaunchTimeが構築時のまま、AMIも変わらず、パブリックIPだけがEIPに入れ替わっています。再起動すら発生せず、コンテナは動いたままIPが固定できました。

6. EIPの課金で誤解しやすい点

EIPは「持っているだけで課金される」と覚えている方が多いのですが、正確には条件があります。

状態課金
実行中のインスタンスに関連付けている無料
どこにも関連付けていない約$0.005/時(月$3.6程度)
インスタンスを停止している課金される

見落としやすいのが3行目です。インスタンスを停止すると、EIPは「実行中のインスタンスに関連付いていない」状態になり課金対象になります。 「使わない時間はEC2を止めて節約する」運用の場合、止めている間はEBSに加えてEIPの料金も乗ります。長期間止めるなら、スタックごと削除したほうが安く済みます。

なお、EIPをCloudFormationの管理下に置いておけば、スタック削除時に自動で解放されます。手動で確保したEIPは解放し忘れると課金が続くため、IaCで管理する意味はここにもあります。

7. まとめ

稼働中のEC2にElastic IPを後付けする際の要点を整理します。

  • EIPの追加自体はAWS::EC2::EIPAWS::EC2::EIPAssociationの10行程度で済む
  • ただしAWS::SSM::Parameter::Value<AWS::EC2::Image::Id>型のAMIは、更新のたびに再解決される
  • UsePreviousValueが引き継ぐのはSSMのパス文字列であり、解決後のAMI IDではない
  • AWSのパブリックパラメータは過去バージョンを参照できないため、バージョン固定では逃げられない
  • AmiIdOverrideのようなパラメータとFn::Ifを足して、稼働中の環境ではAMIを明示的に固定する
  • EIPはインスタンス停止中も課金される。関連付けている=無料、ではない

一番の教訓は、変更セットは必ず出して中身を読むことに尽きます。「EIPを足すだけ」という認識でupdate-stackを直接叩いていたら、データを失ってから気づくことになっていました。

CloudFormationでは「テンプレートを変えていないリソースは変わらない」とは限りません。外部を参照するパラメータを使っている限り、こちらが何もしなくても解決結果は動きます。

本記事の内容は東京リージョンで実際に構築した環境に適用して確認したものです。Dify環境の構築についてはCloudFormationでAWS上にDify環境を自動構築する、ファイルストレージの外部化についてはDifyのファイルストレージをS3にするで解説しています。

AWSを効率的に学習する方法

私が効率的にAWSを学習するために実施した方法は以下の通りです。
 ①最初に書籍(ハンズオンができる)を購入、座学でAWSの基礎を学習
 ②AWS資格試験を取得ための学習

※私の場合は①と②を合わせて2か月でソリューションアーキテクトを取得できました。

①AWS基礎学習

最初に購入した書籍は「Amazon Web Services 基礎からのネットワーク&サーバー構築」です。

Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂4版

この本では、AWSの基本サービスを利用したハンズオンを通じて、AWSの基礎を学習することができます。

また、タイトル通りAWSのネットワークやインフラに関しても網羅しているため、もともとインフラ系の技術者ではない人たちにとっても分かりやすい内容だと思います。

とりあえずAWS上にサーバーを設定して開発を行うための準備までするには最良の一冊です。

Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂4版

②AWS資格取得

AWSの基礎をある程度学習することができたら、次はAWS資格を取得しましょう。まずはAWSソリューションアーキテクトを目指しましょう。

資格勉強のための問題集をひたすら解きながら、AWSの知識を積み重ねて習得していきましょう!

苦行ではありますがこの問題集を3周ほどすればAWS用語や構成に関しても習得できているはずです。

AWS認定ソリューションアーキテクト-アソシエイト問題集

あわせて読みたい

コメント

タイトルとURLをコピーしました