CloudFormationのコード作成後に確認したいポイント

2024.01.31

はじめに

こんにちは!入社5ヶ月目の和田響です。
先日初めてCloudFormationを使用して、AWSリソースを構築しました。

この記事ではCloudFormationのコード作成時に気をつけたいポイントについて、先輩エンジニアとのレビュー内容を元に「最低限これは気にして!」という内容をまとめていきます。
駆け出しエンジニアの方の参考になれば幸いです。

先に3行まとめ

以下の質問に理由を持ってYesと答えられるようなコードにしましょうという内容です。

  • 設計通りに構築できますか?
  • 機密情報がハードコーディングされていませんか?
  • 他の環境やプロジェクトでの再利用が容易ですか?

チェックポイント

設計通りに構築できているか?

言わずもがな、設計書通りに構築できているかは一番大事なポイントです。
詳細設計書やパラメータシートなどと実機を見比べて、意図した通りに構築できているかチェックします。

設計段階での注意にはなりますが、Amazon S3 バケットなどのグローバルリソースの名前の競合(検証環境と本番環境で同一名など)が起きるとリソースが適切に構築できないので注意しましょう。

機密情報がコーディングされていないか?

パスワードなど

パスワードやAPIキーなどの機密情報がハードコーディングされていないかを確認します。
以下の例ではRDSのパスワードmyhardcodedpasswordをハードコーディングしています。

Resources:
  MyDBInstance:
    Type: 'AWS::RDS::DBInstance'
    Properties:
      DBName: 'MyDatabase'
      AllocatedStorage: '20'
      DBInstanceClass: 'db.t2.micro'
      Engine: 'mysql'
      MasterUsername: 'admin'
      MasterUserPassword: 'myhardcodedpassword' # ハードコーディングされたパスワード
      BackupRetentionPeriod: '3'

万が一このコードが漏洩した場合、悪意のある第三者にパスワードが知られ不正アクセスのリスクが生じます。
そのようなリスクを回避するためにParametersで定義したパスワードを参照するようにし、Parameters入力の際はNoEcho: 'true'に設定することで作業者以外にパスワードを見られるリスクを軽減します。
(パスワードの参照はParameter StoreやSecrets Managerの利用も検討すべきですが、設計時の観点のため本記事では深掘りしません。)

Parameters:
  DBPassword:
    Description: 'The database admin account password'
    Type: 'String'
    NoEcho: 'true' # パスワードの値を出力やログに表示しない

Resources:
  MyDBInstance:
    Type: 'AWS::RDS::DBInstance'
    Properties:
      DBName: 'MyDatabase'
      AllocatedStorage: '20'
      DBInstanceClass: 'db.t2.micro'
      Engine: 'mysql'
      MasterUsername: 'admin'
      MasterUserPassword: !Ref DBPassword # パラメータからパスワードを参照
      BackupRetentionPeriod: '3'

CloudFormationのセキュリティ対策としては「万が一コードが流出しても不正にアクセスされないか?」を基準に検討すると良いでしょう。

アカウント固有の情報

AWSアカウントIDのようなAWSのアカウント固有の情報も、パスワードと同様の理由でハードコーディングが推奨されません。
以下の例では、EC2インスタンスを起動する権限を持つIAMポリシーのResource部分に、リージョン情報(us-east-1)と、特定のAWSアカウントID(123456789012)がハードコーディングされています。

Resources:
  MyIAMPolicy:
    Type: 'AWS::IAM::Policy'
    Properties:
      PolicyName: 'MyEC2AccessPolicy'
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Action: 'ec2:StartInstances'
            Resource: 'arn:aws:ec2:us-east-1:123456789012:instance/*' # アカウントIDがハードコーディングされている

${AWS::AccountId}${AWS::Region}といったAWS固有のパラメータ参照を使用することで、アカウント固有の情報をハードコーディングすることなくリソースを動的に作成できます。

Resources:
  MyIAMPolicy:
    Type: 'AWS::IAM::Policy'
    Properties:
      PolicyName: 'MyEC2AccessPolicy'
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Action: 'ec2:StartInstances'
            Resource: !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*' # アカウントIDとリージョンが動的に参照される

設計段階の話のため本記事とは直接関係はありませんが、S3バケットのようなグローバルリソースでは名前の重複を防ぐために、リソース名にAWSアカウントIDを入れることがしばしばあります。
そのような場合もAWS固有のパラメータ参照を使用することで、検証環境と本番環境で同じソースコードで違う名前のリソースを構築できます。

再利用できるか?

上記の2つからはやや優先順位は落ちますが、同じ構成の他プロジェクトや、同一プロジェクトの異なる環境において再利用しやすいかどうかも重要な観点です。
以下の例ではwadahibikiというプレフィックスで各リソースを構築していますが、いずれもハードコーディングされているため、同じ構成の違うプロジェクトで再利用する際は全て書き換えなくてはなりません。

Resources:
  MyAppS3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: 'wadahibiki-static-assets' # プレフィックスがハードコーディングされている

  MyAppDBInstance:
    Type: 'AWS::RDS::DBInstance'
    Properties:
      # ... その他のプロパティ ...
      DBInstanceIdentifier: 'wadahibiki-database' # プレフィックスがハードコーディングされている

以下のようにParametersでプレフィクスを指定することで、他プロジェクトで使い回す場合の修正範囲を狭めることができます。(以下例ではDefault: 'wadahibiki'部分を修正するのみ)

Parameters:
  ProjectPrefix:
    Description: Prefix for resource names to ensure uniqueness
    Type: String
    Default: 'wadahibiki'

Resources:
  MyAppS3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub '${ProjectPrefix}-static-assets' # プレフィックスがパラメータで定義されている

  MyAppDBInstance:
    Type: 'AWS::RDS::DBInstance'
    Properties:
      # ... その他のプロパティ ...
      DBInstanceIdentifier: !Sub '${ProjectPrefix}-database' # プレフィックスがパラメータで定義されている

同じような例ですが、以下のように環境名を${EnvironmentName}で定義することで同じプロジェクト内の各環境での使い回しが容易になります。

Parameters:
  ProjectName:
    Description: The name of the project to use as a prefix.
    Type: String
    Default: 'wadahibiki'

  EnvironmentName:
    Description: The name of the environment (e.g., dev, staging, prod).
    Type: String
    AllowedValues:
      - dev
      - staging
      - prod

Resources:
  MyAppS3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub '${ProjectName}-${EnvironmentName}-static-assets' # プロジェクト名と環境名の組み合わせ

  MyAppDBInstance:
    Type: 'AWS::RDS::DBInstance'
    Properties:
      # ... その他のプロパティ ...
      DBInstanceIdentifier: !Sub '${ProjectName}-${EnvironmentName}-database' # プロジェクト名と環境名の組み合わせ

Outputs:
  S3BucketName:
    Description: "Name of the S3 bucket"
    Value: !GetAtt MyAppS3Bucket.BucketName

  DBInstanceIdentifier:
    Description: "Identifier of the DB instance"
    Value: !Ref MyAppDBInstance

まとめ

長くなりましたが、まとめると最低限以下の3つの問いにYESで答えられるようなコードを書きましょうということです。

  • 設計通りに構築できますか?
  • 機密情報がハードコーディングされていませんか?
  • 他の環境やプロジェクトでの再利用が容易ですか?

最後に

今回は、CloudFormationのコード作成時に気をつけたいポイントについて自戒の意味も込めて書いてみました。
どなたかの参考になれば幸いです。