👩‍💻

Terraform以外にもIaCツールはあるんです!

2023/06/10に公開

導入

こんにちは。みなさんはIaCしてますか?今やIaCはクラウドネイティブな開発では当たり前の技術のようになっていますね。私はここ半年間で色々なIaCツール(Terraform以外)を経験したので、その感想や学んだことを備忘録を兼ねてざっくりと羅列します。

IaCのツール比較

私は以下のIaCツールを使用しました。

  • AWS CloudFormation
  • AWS CDK
  • Azure ARMテンプレート
  • Azure Bicep

なぜかIaCの定番であるTerraformを使ったことがないという特殊な人になっています。この原因は、お客様の都合でAWSやAzureの純正のツールを使用したいといった要望があったからです。このように、Terraformが使えない場合のIaCツールの選択肢としては、AWSならCloudFormationかAWS CDK、AzureならARMテンプレートかBicepになります。外部ツールは使わずに各クラウド内のサービスで完結したい!みたいなお客さんだと割と発生する事象かと思います。ただ、やはり自分の周りではTerraformを使っている人が圧倒的に多いので若干疎外感を感じています。😢
寂しいので今後はTerraformも勉強する予定です。

AWS CloudFormation

AWSでIaCを行う上で基本となるツールで、JSONかYAMLでAWSのリソースを記述します。ちなみに、Elastic BeanstalkやCloud9のようなサービスは裏でCloudFormationのテンプレートが使用されています。コード自体は全てJSONかYAMLで記述します。どうでもいいですが、個人的にはJSONよりYAMLの方がかわいいと思います。JSONは波括弧で全て囲うのがなんかめんどくさいオタク感あります。
CloudFormationのテンプレートは以下のような形式になっています。

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "My CloudFormation Stack",
  "Parameters": {},
  "Mappings": {},
  "Conditions": {},
  "Resources": {},
  "Outputs": {}
}
  • AWSTemplateFormatVersion: テンプレートの機能を識別するフォーマットバージョン。
  • Description: テンプレートの説明。
  • Parameters: テンプレート内で参照可能なパラメータ。デフォルト値などを設定可能。
  • Mappings: テンプレート内で参照可能な条件付きのパラメータ。
  • Conditions: 作成するリソースを制御する条件分岐。
  • Resources: テンプレートで作成するリソース群。
  • Outputs: ユーザーに渡すテンプレートの出力値。マネコンやCLIから確認可能。

基本的にはResourcesセクションにひたすら必要なリソースを記述していきます。後ほどまた出てきますが、以下はCIDRブロックが10.0.0.0/16のVPCの例です。

"Resources": {
  "MyVpcF9F0CA6F": {
   "Type": "AWS::EC2::VPC",
   "Properties": {
    "CidrBlock": "10.0.0.0/16",
    "EnableDnsHostnames": true,
    "EnableDnsSupport": true,
    "InstanceTenancy": "default",
    "Tags": [
     {
      "Key": "Name",
      "Value": "VpcStack/MyVpc"
     }
    ]
   }
  },

なお、CloudFormationはリソースの集まりであるスタックという単位でリソースを管理します。一つのテンプレートの中に必要なリソースを全て記述することもできなすが、テンプレートの規模が大きくなると単一のテンプレートでの管理が困難になってくるので、このスタックの分割の仕方が重要になってきます。AWSのBlack Beltには以下のようなスタックの分割例が載っていました。以下の例では各リソースのライフサイクルや依存関係に基づいてスタックの分割が行われています。


インフラ全体のスタック分割


アプリケーションレイヤ内部のスタック分割

引用元:20200826 AWS Black Belt Online Seminar AWS CloudFormation
https://www.slideshare.net/AmazonWebServicesJapan/20200826-aws-black-belt-online-seminar-aws-cloudformation-238501102

CloudFormationをデプロイする際は、直接変更を実施するか、変更セットというものを使用して事前にどのようなリソースが作成されるか確認できる機能があります。変更によってはリソースが中断されたり、再作成される場合もあるので、基本的には変更セットを使用して作成されるリソースや権限周りの変更を前もって確認するのが良いかと思います。

ちなみに、AWS公式のツールではありませんが、Former2というツールを使うことで、既存のリソースをCloudFormationのテンプレートとして書き起こすことが出来ます。

iann0036/former2: Generate CloudFormation / Terraform / Troposphere templates from your existing AWS resources.
https://github.com/iann0036/former2

AWS CDK

プログラミング言語(JavaScript, TypeScript, Python, Java, C# など)でIaCのコードを記述することが出来るフレームワークです。ただ、実際の実装例はTypescriptが大半なので、Typescriptがデファクトスタンダードのようなところがあります。
CDKは記述したコードからCloudFormationのテンプレートを作成するコンパイラのような立ち位置なので、基本的にはCDKを習得したらCloudFormationを直接記述する必要はなくなります。また、生成したテンプレートは単体で使用することもできます。
基本的にCDKはCloudFormationの概念を継承しており、スタックの分割などの開発時に気にしなければいけないポイントはCloudFormationと同様です。ただ、スタックの分割は、やりすぎるとエラーの温床になるので出来るだけ分割しない方が良いです。依存関係がややこしくなればなるほど保守性も落ちるので、当然ではありますね。
CDKにはConstructという概念があり、こちらにはリソースをまとめるような役割があります。ConstructはL1 Construct~L3 Constructまであり、L1~L3に上がるにつれて抽象度が上がり、記述するコードの量は少なくなります。詳しくは以下の記事を参照してください。

AWS CDK の3種類の Construct を使ってデプロイしてみた | DevelopersIO
https://dev.classmethod.jp/articles/aws-cdk-construct-explanation/

CloudFormationと違う点として、圧倒的なコーディングの自由度が挙げられます。当たり前ですがプログラミング言語の文法がそのまま使えるので、for文での複数のインスタンスの作成、if文での条件分岐、共通処理の関数化、インターフェイスでの変数の実装、指定したリソース群でのクラスの作成、作成したクラスの継承、モジュール化、ライブラリの使用、SDKの使用などとてつもなく自由度が高いです。この辺りがCDKの大きな特徴かと思います。

例として次のようなやる気のないアーキテクチャを適当にL2 Constructを使って実装してみます。例なので設定値は割と適当です。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { aws_ec2 as ec2 } from 'aws-cdk-lib';

export class VpcStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Create VPC
    const vpc = new ec2.Vpc(this, 'MyVpc', {
      cidr: '10.0.0.0/16',
      maxAzs: 1,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'PublicSubnet',
          subnetType: ec2.SubnetType.PUBLIC,
        },
      ],
    });

    // UserData for AppServer (setup httpd)
    const userdata = ec2.UserData.forLinux({ shebang: '#!/bin/bash' });
    userdata.addCommands(
      'sudo yum -y install httpd',
      'sudo systemctl enable httpd',
      'sudo systemctl start httpd',
      'echo "<h1>Hello from $(hostname)</h1>" > /var/www/html/index.html',
      'chown apache.apache /var/www/html/index.html',
    );

    // Create EC2 instance
    const instance = new ec2.Instance(this, 'MyInstance', {
      vpc: vpc,
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
      machineImage: new ec2.AmazonLinuxImage(),
      vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
      userData: userdata,
    });
  }
}

以下は、このCDKコードから生成されたCloudFormationのテンプレートです。

{
 "Resources": {
  "MyVpcF9F0CA6F": {
   "Type": "AWS::EC2::VPC",
   "Properties": {
    "CidrBlock": "10.0.0.0/16",
    "EnableDnsHostnames": true,
    "EnableDnsSupport": true,
    "InstanceTenancy": "default",
    "Tags": [
     {
      "Key": "Name",
      "Value": "VpcStack/MyVpc"
     }
    ]
   }
  },
  "MyVpcPublicSubnetSubnet1Subnet60D1320D": {
   "Type": "AWS::EC2::Subnet",
   "Properties": {
    "VpcId": {
     "Ref": "MyVpcF9F0CA6F"
    },
    "AvailabilityZone": {
     "Fn::Select": [
      0,
      {
       "Fn::GetAZs": ""
      }
     ]
    },
    "CidrBlock": "10.0.0.0/24",
    "MapPublicIpOnLaunch": true,
    "Tags": [
     {
      "Key": "aws-cdk:subnet-name",
      "Value": "PublicSubnet"
     },
     {
      "Key": "aws-cdk:subnet-type",
      "Value": "Public"
     },
     {
      "Key": "Name",
      "Value": "VpcStack/MyVpc/PublicSubnetSubnet1"
     }
    ]
   }
  },
  "MyVpcPublicSubnetSubnet1RouteTable00654ADB": {
   "Type": "AWS::EC2::RouteTable",
   "Properties": {
    "VpcId": {
     "Ref": "MyVpcF9F0CA6F"
    },
    "Tags": [
     {
      "Key": "Name",
      "Value": "VpcStack/MyVpc/PublicSubnetSubnet1"
     }
    ]
   }
  },
  "MyVpcPublicSubnetSubnet1RouteTableAssociation2CCE9CDC": {
   "Type": "AWS::EC2::SubnetRouteTableAssociation",
   "Properties": {
    "RouteTableId": {
     "Ref": "MyVpcPublicSubnetSubnet1RouteTable00654ADB"
    },
    "SubnetId": {
     "Ref": "MyVpcPublicSubnetSubnet1Subnet60D1320D"
    }
   }
  },
  "MyVpcPublicSubnetSubnet1DefaultRoute2D379878": {
   "Type": "AWS::EC2::Route",
   "Properties": {
    "RouteTableId": {
     "Ref": "MyVpcPublicSubnetSubnet1RouteTable00654ADB"
    },
    "DestinationCidrBlock": "0.0.0.0/0",
    "GatewayId": {
     "Ref": "MyVpcIGW5C4A4F63"
    }
   },
   "DependsOn": [
    "MyVpcVPCGW488ACE0D"
   ]
  },
  "MyVpcIGW5C4A4F63": {
   "Type": "AWS::EC2::InternetGateway",
   "Properties": {
    "Tags": [
     {
      "Key": "Name",
      "Value": "VpcStack/MyVpc"
     }
    ]
   }
  },
  "MyVpcVPCGW488ACE0D": {
   "Type": "AWS::EC2::VPCGatewayAttachment",
   "Properties": {
    "VpcId": {
     "Ref": "MyVpcF9F0CA6F"
    },
    "InternetGatewayId": {
     "Ref": "MyVpcIGW5C4A4F63"
    }
   }
  },
  "MyInstanceInstanceSecurityGroup3E7A7DD1": {
   "Type": "AWS::EC2::SecurityGroup",
   "Properties": {
    "GroupDescription": "VpcStack/MyInstance/InstanceSecurityGroup",
    "SecurityGroupEgress": [
     {
      "CidrIp": "0.0.0.0/0",
      "Description": "Allow all outbound traffic by default",
      "IpProtocol": "-1"
     }
    ],
    "Tags": [
     {
      "Key": "Name",
      "Value": "VpcStack/MyInstance"
     }
    ],
    "VpcId": {
     "Ref": "MyVpcF9F0CA6F"
    }
   }
  },
  "MyInstanceInstanceRole1C4D4747": {
   "Type": "AWS::IAM::Role",
   "Properties": {
    "AssumeRolePolicyDocument": {
     "Statement": [
      {
       "Action": "sts:AssumeRole",
       "Effect": "Allow",
       "Principal": {
        "Service": "ec2.amazonaws.com"
       }
      }
     ],
     "Version": "2012-10-17"
    },
    "Tags": [
     {
      "Key": "Name",
      "Value": "VpcStack/MyInstance"
     }
    ]
   }
  },
  "MyInstanceInstanceProfile2784C631": {
   "Type": "AWS::IAM::InstanceProfile",
   "Properties": {
    "Roles": [
     {
      "Ref": "MyInstanceInstanceRole1C4D4747"
     }
    ]
   }
  },
  "MyInstanceA12EC128": {
   "Type": "AWS::EC2::Instance",
   "Properties": {
    "AvailabilityZone": {
     "Fn::Select": [
      0,
      {
       "Fn::GetAZs": ""
      }
     ]
    },
    "IamInstanceProfile": {
     "Ref": "MyInstanceInstanceProfile2784C631"
    },
    "ImageId": {
     "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter"
    },
    "InstanceType": "t2.micro",
    "SecurityGroupIds": [
     {
      "Fn::GetAtt": [
       "MyInstanceInstanceSecurityGroup3E7A7DD1",
       "GroupId"
      ]
     }
    ],
    "SubnetId": {
     "Ref": "MyVpcPublicSubnetSubnet1Subnet60D1320D"
    },
    "Tags": [
     {
      "Key": "Name",
      "Value": "VpcStack/MyInstance"
     }
    ],
    "UserData": {
     "Fn::Base64": "#!/bin/bash\nsudo yum -y install httpd\nsudo systemctl enable httpd\nsudo systemctl start httpd\necho \"<h1>Hello from $(hostname)</h1>\" > /var/www/html/index.html\nchown apache.apache /var/www/html/index.html"
    }
   },
   "DependsOn": [
    "MyInstanceInstanceRole1C4D4747"
   ]
  }
 }

記述量が圧倒的に違いますね。
また、CloudFormationのテンプレートをよく見ると実際には裏でルートテーブルやインターネットゲートウェイ、セキュリティグループなど様々なリソースが自動的に作られていることがわかります。このようにCDKは抽象化されており少ないコード量で記述が可能な分、裏で”いい感じ”に作られるリソースにどのようなものが含まれているかを把握しておかなければ、自分の意図したものと異なるリソースが作成される可能性があります。ただ、基本的にはベストプラクティスに沿ったデフォルト値が入ったリソースが作られるので、そこまで気にする必要はありません。細かい設定が必要な場合は、抽象度の低いConstructを一部使用するといったことも可能です。

Azure ARMテンプレート

AzureでIaCを実現するために提供されている機能で、Azure上のリソースをJSON形式のテンプレートを使用して管理することが可能です。ARM(Azure Resource Manager)は名前の通りAzureのリソースを管理する仕組みで、Azure上のリソースは裏で全てこのARMテンプレートが使用されています。

ARMテンプレートは以下のような形式になっています。

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "variables": {},
    "resources": [],
    "output": []
}
  • $schema: テンプレートのバージョンが記述されているJSONスキーマファイルの場所。
  • contentVersion: テンプレートのバージョン。
  • parameters: テンプレート内で使用するパラメータ。
  • variables: テンプレート内で使用する変数。パラメータを組み合わせて使用することが多い。
  • resources: テンプレートで作成するリソース。
  • output: テンプレートの出力値。

ARMテンプレートもCloudFormationと同じように、基本的にはresourcesセクションにひたすら必要なリソースを定義していきます。後ほどまた出てきますが、以下はVNETの例です。

"resources": [
    {
      "type": "Microsoft.Network/virtualNetworks",
      "apiVersion": "2021-05-01",
      "name": "MyVnet",
      "location": "[parameters('location')]",
      "properties": {
        "addressSpace": {
          "addressPrefixes": [
            "10.0.0.0/16"
          ]
        }
      }
    },

基本的にはCloudFormationと同じような構文ですね。ARMテンプレートにはCloudFormationのスタックのような概念はありませんが、CloudFormationと同様にテンプレートを分割し、ネストさせることができます。
ちなみにARMテンプレートは一から作成する必要はなく、以下のような形でテンプレートを用意することが出来ます。

1.Azureポータルから既存のリソースの構成をエクスポート
2.Azureポータルでリソースのパラメータを指定して、"確認及び作成"の段階でエクスポート
3.Azureが公式に提供しているサンプルテンプレートを使用する

1番目の方法では既存のリソースをリバースエンジニアリング的に全てテンプレートで出力できるのですが、Azureポータル上でリソースを作成する際には出てこないデフォルト値など全て出力され、とてつもなくコードの可読性が低いのでおすすめしません。既存の構成から全く変えずにバックアップとして取っておくような使い方ならありかもしれません。
2番目の方法では、基本的にAzureポータル上で指定したパラメータしか出力されないので、1番目の方法よりは使いやすいです。ただ、一度作成した後にポータル上で行う設定値などは手動で追加する必要があります。個人的には、2番目の方法でテンプレート準備し、足りない部分は公式のテンプレートを参考にして補完する方法が良いかと思います。

Azure Bicep

JSONは人間が読み書きするものじゃねえ、という人のために作られたツールです。基本的に記述する要素はARMテンプレートと同じなので、ARMテンプレートを使用した経験のある人は比較的移行が簡単かと思います。また、BicepファイルをビルドすることでARMテンプレートが生成されるようになっているのですが、ARMテンプレートからBicepファイルへデコンパイルすることも可能です。Microsoft公式としてはARMテンプレート→Bicepへのデコンパイルはベストエフォートらしいのですが、実際にやってみたところ全体のコードの数%も直すことなくデコンパイルできたので、既にARMテンプレートを使用している方はとりあえずBicepにデコンパイルしてみることをおすすめします。

ARMテンプレートと比べ構文が完結となっており、JSONにありがちなネストしまっくて波括弧の片割れの数が一致しなくてエラーが出たり、リソース間の依存関係を記述し忘れてエラーが出るといった悩みから解放されます。また、Bicepではローカルにある他のBicepファイルやARMテンプレートをモジュールとして呼び出すことが可能です。ARMテンプレートを分割する場合はテンプレートをストレージアカウントのBLOBやGitHubなどのリポジトリに保存し、HTTPやHTTPSとしてダウンロードできるURIとして指定する必要があるので、これは楽ですね。CloudFormationもそうですが、いちいちテンプレートを全てストレージにアップロードして、URIを指定する必要があるのが面倒くさい仕様ですね。
ARMテンプレートとの差異は下記のドキュメントに詳しく書いてあります。

Azure Resource Manager テンプレートの構文を JSON と Bicep で比較する - Azure Resource Manager | Microsoft Learn
https://learn.microsoft.com/ja-jp/azure/azure-resource-manager/bicep/compare-template-syntax

ARMテンプレートとも共通して言えることですが、やはりTerraformやCloudFormationなどに比べると知名度が低いため、実装例やブログなどの参考資料が非常に少なく、Microsoftの読みづらい公式ドキュメントを参照しなければいけません。個人的にはこれがかなり苦痛でした。

AWSと同様に次のようなやる気のないアーキテクチャを実装します。

param location string = 'eastus'

resource myVnet 'Microsoft.Network/virtualNetworks@2021-05-01' = {
  name: 'MyVnet'
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.0.0.0/16'
      ]
    }
  }
}

resource mySubnet 'Microsoft.Network/virtualNetworks/subnets@2021-05-01' = {
  parent: myVnet
  name: 'MySubnet'
  properties: {
    addressPrefix: '10.0.0.0/24'
  }
}

resource myPublicIP 'Microsoft.Network/publicIPAddresses@2021-05-01' = {
  name: 'MyPublicIP'
  location: location
  properties: {
    publicIPAllocationMethod: 'Dynamic'
  }
}

resource myNIC 'Microsoft.Network/networkInterfaces@2021-05-01' = {
  name: 'MyNIC'
  location: location
  properties: {
    ipConfigurations: [
      {
        name: 'MyNICConfig'
        properties: {
          subnet: {
            id: mySubnet.id
          }
          publicIPAddress: {
            id: myPublicIP.id
          }
        }
      }
    ]
  }
}

resource myVM 'Microsoft.Compute/virtualMachines@2021-04-01' = {
  name: 'MyVM'
  location: location
  properties: {
    hardwareProfile: {
      vmSize: 'Standard_B1s'
    }
    storageProfile: {
      osDisk: {
        createOption: 'FromImage'
      }
      imageReference: {
        publisher: 'Canonical'
        offer: 'UbuntuServer'
        sku: '18.04-LTS'
        version: 'latest'
      }
    }
    osProfile: {
      computerName: 'MyVM'
      adminUsername: 'adminuser'
      adminPassword: 'Password123!'
    }
    networkProfile: {
      networkInterfaces: [
        {
          id: myNIC.id
        }
      ]
    }
  }
}

以下は、このBicepコードから生成されたARMテンプレートです。

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "metadata": {
    "_generator": {
      "name": "bicep",
      "version": "0.15.31.15270",
      "templateHash": "13184148899573276172"
    }
  },
  "parameters": {
    "location": {
      "type": "string",
      "defaultValue": "eastus"
    }
  },
  "resources": [
    {
      "type": "Microsoft.Network/virtualNetworks",
      "apiVersion": "2021-05-01",
      "name": "MyVnet",
      "location": "[parameters('location')]",
      "properties": {
        "addressSpace": {
          "addressPrefixes": [
            "10.0.0.0/16"
          ]
        }
      }
    },
    {
      "type": "Microsoft.Network/virtualNetworks/subnets",
      "apiVersion": "2021-05-01",
      "name": "[format('{0}/{1}', 'MyVnet', 'MySubnet')]",
      "properties": {
        "addressPrefix": "10.0.0.0/24"
      },
      "dependsOn": [
        "[resourceId('Microsoft.Network/virtualNetworks', 'MyVnet')]"
      ]
    },
    {
      "type": "Microsoft.Network/publicIPAddresses",
      "apiVersion": "2021-05-01",
      "name": "MyPublicIP",
      "location": "[parameters('location')]",
      "properties": {
        "publicIPAllocationMethod": "Dynamic"
      }
    },
    {
      "type": "Microsoft.Network/networkInterfaces",
      "apiVersion": "2021-05-01",
      "name": "MyNIC",
      "location": "[parameters('location')]",
      "properties": {
        "ipConfigurations": [
          {
            "name": "MyNICConfig",
            "properties": {
              "subnet": {
                "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'MyVnet', 'MySubnet')]"
              },
              "publicIPAddress": {
                "id": "[resourceId('Microsoft.Network/publicIPAddresses', 'MyPublicIP')]"
              }
            }
          }
        ]
      },
      "dependsOn": [
        "[resourceId('Microsoft.Network/publicIPAddresses', 'MyPublicIP')]",
        "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'MyVnet', 'MySubnet')]"
      ]
    },
    {
      "type": "Microsoft.Compute/virtualMachines",
      "apiVersion": "2021-04-01",
      "name": "MyVM",
      "location": "[parameters('location')]",
      "properties": {
        "hardwareProfile": {
          "vmSize": "Standard_B1s"
        },
        "storageProfile": {
          "osDisk": {
            "createOption": "FromImage"
          },
          "imageReference": {
            "publisher": "Canonical",
            "offer": "UbuntuServer",
            "sku": "18.04-LTS",
            "version": "latest"
          }
        },
        "osProfile": {
          "computerName": "MyVM",
          "adminUsername": "adminuser",
          "adminPassword": "Password123!"
        },
        "networkProfile": {
          "networkInterfaces": [
            {
              "id": "[resourceId('Microsoft.Network/networkInterfaces', 'MyNIC')]"
            }
          ]
        }
      },
      "dependsOn": [
        "[resourceId('Microsoft.Network/networkInterfaces', 'MyNIC')]"
      ]
    }
  ]

ARMテンプレートはJSONのためBicepよりもネストが多かったり、依存関係を明示的に指示してあげる必要があるため、Bicepよりも冗長なコードになっています。ただ、この二つにはCDKとCloudFormationほどの差はないので、あくまでBicepはARMテンプレートの無駄な記述を省いたものだと思ったらいいかもしれません。
ちなみに、Resourceの部分を見てもらえばわかる通り、ARMテンプレートやBicepはリソースごとに使用するAPIのバージョンを指定しなければならず、バージョンによって指定できる項目や表記の仕方が若干変わってきます。個人的にはこれが非常に面倒くさかったです。

Terraform(おまけ)

Terraformはまだほとんど勉強していないですが、この記事のサンプルコードレベルだったらすぐ作れそうだったので書いてみます。
上で紹介したAzureの構成を実装してみます。

# Configure Azure Provider
provider "azurerm" {
  features {}
}

# Set default location
variable "location" {
  default = "eastus"
}

# Create Resource Group
resource "azurerm_resource_group" "my_resource_group" {
  name     = "my-resource-group"
  location = var.location
}

# Create Virtual Network
resource "azurerm_virtual_network" "my_vnet" {
  name                = "my-vnet"
  resource_group_name = azurerm_resource_group.my_resource_group.name
  address_space       = ["10.0.0.0/16"]
  location            = var.location
}

# Create Subnet
resource "azurerm_subnet" "public_subnet" {
  name                 = "public-subnet"
  resource_group_name  = azurerm_resource_group.my_resource_group.name
  virtual_network_name = azurerm_virtual_network.my_vnet.name
  address_prefixes     = ["10.0.0.0/24"]
}

# Create Public IP Address
resource "azurerm_public_ip" "my_public_ip" {
  name                = "my-public-ip"
  resource_group_name = azurerm_resource_group.my_resource_group.name
  allocation_method   = "Dynamic"
  location            = var.location
}

# Create Network Interface
resource "azurerm_network_interface" "my_nic" {
  name                = "my-nic"
  resource_group_name = azurerm_resource_group.my_resource_group.name

  ip_configuration {
    name                          = "my-nic-config"
    subnet_id                     = azurerm_subnet.public_subnet.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.my_public_ip.id
  }

  location = var.location
}

# Create Virtual Machine
resource "azurerm_linux_virtual_machine" "my_vm" {
  name                = "my-vm"
  resource_group_name = azurerm_resource_group.my_resource_group.name
  location            = var.location
  size                = "Standard_B1s"
  admin_username      = "adminuser"
  network_interface_ids = [azurerm_network_interface.my_nic.id]
  admin_password = "Password123!"

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  
}

今まで紹介してきたIaCツールの中で最も構文が簡潔ですね。本当に必要最低限の要素だけ定義すればよい感じです。
構文はBicepに似てますね。というより、Bicepの方がTerraformに寄せていっていますね。なら最初からTerraformでいいのでは?となりますが。笑

結局どれを使えばいいの?

IaCはコードといえどアプリ開発者が行うビジネスロジックの実装などとは異なり、基本的にはJSONやYAMLでパラメータとその値の対を羅列するだけなので、普段コードを書かないインフラエンジニアの方でもとっつきやすいと思います。
CDKは全てコードで記述できる分かなり自由度は高いのですが、その分学習コストは一番高いように感じました。クラスの継承やインターフェイスの実装といった概念が出て来るので、オブジェクト指向言語を使用したことのない人だと少しとっつきにくいかと思います。アプリケーション開発者が使用することで本領を発揮する気がします。
Terraformは必要最小限の要素しか記述する必要がないので非常に簡潔で、感動しました。ARMテンプレートやBicepにあるようなAPI versionなど本質的には開発する上で必要のない情報を見なくていいのは、可読性の面に関しても非常にアドバンテージがあるように思います。この辺りがTerraformが広く使われている所以なのかもしれないですね。
このようにIaCツールには様々なものがあり、使用するものによって記述量(=開発工数)も学習コストも全く異なります。また、参照する記事や実装例の豊富さも開発スピードにもろに関わってきます。AWSの場合は、コードの実装例や、エラーの対処法が書いてあるブログなどが結構出てくるのですが、Azureの場合はエラーが出ても基本的にMicrosoftの公式ドキュメントしか出てこないので、トライアンドエラーで解決するしかありません。
そのため、IaCツールを実際のプロジェクトに適用する際には各々の特性の違いを適切に把握しておく必要があるかと思います。

JSON&YAML or プログラミング言語

JSONやYAMLで全部書くのは中々ハードな作業です。必要な設定値も多くなるので、自然と行数も多くなります。また、ARMテンプレートの場合はリソースの依存関係なども細かく指定しなければいけないので大変です。
JSONやYAMLでのIaCというのは、コンパイラが存在する前の機械語でコーディングするようなものです。なので、TerraformやBicepのようなドメイン固有言語での記述が可能なツールがある場合、そちらを使用するのが自然と思います。少々学習コストはかかるかもしれませんが、長期的な目線で見ると後者を使用する方がコードの記述や運用面でも楽になるかと思います。
また、最近はCDKに加えてPulumiというIaCツールも出てきているようです。こちらもCDKと同じようにプログラミング言語を用いてIaCのコードを記述できるものですが、CDKと異なる点はTerraformのようにマルチクラウドに対応してる点です。また、Terraformの場合はHCL(HashiCorp Configuration Language)という独自の言語を覚える必要がありますが、Pulumiはマルチクラウド対応+プログラミング言語での記述なので、TerraformとCDKのいいとこどりのようなツールです。このようにプログラミング言語でのIaCが本格化すれば、インフラ担当者にもコーディングスキルが求められるようになるかもしれません。実際、CDKを使用している現場ではインフラエンジニアがバックエンドエンジニアを兼任しているケースもあるようなので、時間の問題かもしれません。

PulumiはIaCの革命児になれるか
https://zenn.dev/yuta28/articles/pulumi-ai-revolutionary

Terraformは素晴らしい

と、ここまで色々書きましたが、やはりTerraformは素晴らしいと思います。AWSでもAzureでもGCPでも何でもデプロイできるのは有能ですね。世界にはエスペラント語というものが存在します。世界中のあらゆる人が簡単に学ぶことができ、英語に成り代わる世界のリングワ・フランカ(共通語)として作られた人工言語です。Terraformはこのラテン語のようにクラウド界におけるIaCのリングワ・フランカと言えると思います。なので、迷った場合はぜひTerraformを使いましょう!

Discussion