元CloudFormationユーザーがTerraform、Pulumiどっち使ってみたいか

この記事は[Skillnote Advent Calendar 2024](https://qiita.com/advent-calendar/2024/skillnote)の13日目の記事です。


## はじめに
こんにちは！2024年11月より株式会社Skillnoteで主にSRE領域を担当している平山です。
前職では客先常駐の形態で金融系の基盤をAWSで構築しておりました。

そこではAWSのみだったため、CloudFormationを約1年半ほど使用しておりましたが、現職ではAWSだけでなくDatadogやSnowflakeなども使用しているため、
IaCツールとしてマルチベンダーに対応しているTerraformとPulumiの使用感を確認したいと思います。

## この記事でわかること
- Terraform、Pulumiそれぞれの特徴
- 両IaCツールのチュートリアル的なコード
- CFnユーザーからの両IaCツールの使用感想

## Terraform、Pulumiの特徴
複数ありますが、大きな特徴の違いとしては書ける言語の違いになります。
| |言語 |
|---|---|
|Teraform  |HCL(独自の言語)  |
|Pulumi  |好きなプログラミング言語  |

その他の特徴は以下のブログやPulumi公式の比較が参考になります。
- [IaC シリーズ: プロジェクトに最適な IaC ツールはこちら](https://tech-blog.jtp.co.jp/entry/2023/08/23/200750)
- [Terraform vs. Pulumi](https://www.pulumi.com/docs/iac/concepts/vs/terraform/)

## 検証内容
### 構成図
100万回見たね、これって感じのサーバーレスな構成図で試してみます。
![serverless.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288531/cb8450c8-ed3a-0d1f-0f64-83107bc54823.png)

### 実行環境
セキュリティの考慮をなくすため、Cloud9で実施していきます。
※デフォルトの権限では実行できないため、以下のようにCloud9のAMTCをOFFにして適切なIAMロールをアタッチする必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288531/cfe41a5c-c0e1-962d-cc59-702f686eafe5.png)


## Terraformでの実装
適当な作業用フォルダを作成&移動して以下を実施していく
```bash:
# インストール
# https://developer.hashicorp.com/terraform/install#linux
sudo yum install -y yum-utils shadow-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo yum -y install terraform
# インストールできたか確認
terraform --version
```
```terraform:
# ワークスペースを初期化
terraform init
```
Terraform実行のための資材を以下の通り配置
```bash:
ll | awk '{print $9}'
provider.tf
apigw.tf
lambda.tf
lambda_function.zip
```
各tfファイルは以下の通り作成
※lambda用のpythonコードは本筋と関係ないため割愛。単にメッセージをリターンするだけです。
<details><summary>tfファイル</summary>

```terraform:provider.tf
provider "aws" {
  region = "ap-northeast-1"
}
```
```terraform:lambda.tf
## Lambda関数
resource "aws_lambda_function" "lambda" {
  function_name = "tf_lambda_function"
  runtime       = "python3.13"
  handler       = "lambda_function.lambda_handler"

  filename = "lambda_function.zip"

  role = aws_iam_role.lambda_role.arn
}

## Lambdaロール
resource "aws_iam_role" "lambda_role" {
  name = "lambda_tf_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action = "sts:AssumeRole",
        Effect = "Allow",
        Principal = {
          Service = "lambda.amazonaws.com"
        },
      },
    ],
  })
}
```
```terraform:apigw.tf
## APIGateway
resource "aws_api_gateway_rest_api" "api" {
  name        = "terrafor_API"
  description = "terraform API"
}

resource "aws_api_gateway_resource" "api_resource" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  parent_id   = aws_api_gateway_rest_api.api.root_resource_id
  path_part   = "terraform"
}

resource "aws_api_gateway_method" "api_method" {
  rest_api_id   = aws_api_gateway_rest_api.api.id
  resource_id   = aws_api_gateway_resource.api.id
  http_method   = "GET"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "backend" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  resource_id = aws_api_gateway_resource.api_resource.id
  http_method = aws_api_gateway_method.api_method.http_method

  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.lambda.invoke_arn
}

resource "aws_api_gateway_deployment" "api_stage" {
  depends_on = [aws_api_gateway_integration.backend]

  rest_api_id = aws_api_gateway_rest_api.api.id
  stage_name  = "test"
}

resource "aws_lambda_permission" "api_gateway" {
  statement_id  = "AllowAPIGatewayInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.lambda.function_name
  principal     = "apigateway.amazonaws.com"

  source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*/*"
}
```

</details>
準備ができたので以下のterraformコマンドで実行

```terraform
# テンプレートの整形
terraform fmt
# 変更内容の確認だけのコマンド
terraform plan
# 変更内容の確認と適用
terraform apply
```
作ったAPIへリクエストを投げてみたらレスポンス帰ってきましたね！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288531/9e32fe49-8b0f-6106-2dfa-8686895ed3f3.png)


## Pulumiでの実装
こちらでも適当な作業用フォルダを作成&移動して以下を実施していく
```bash
# インストール
curl -fsSL https://get.pulumi.com | sh
# インストールできたか確認
pulumi version
# pythonでawsリソースを定義できるように環境を設定
# puumiの使用にはアカウント所持が必須の為、初回実行時はここでPulumiのurlが表示され、アカウントの登録が必要になります。
pulumi new aws-python
# 以下の設定ファイルとpythonコードが作成される
ll | awk '{print $9}'
Pulumi.test.yaml
Pulumi.yaml
__main__.py
__pycache__
requirements.txt
venv
```
「\_\_main__.py」にはお試し用のコードが入っており、「pulumi up」ですぐにs3バケットが作成できるような状態になっております。
今回の構築内容になるようにpythonファイルを以下のように上書きしていきます。
<details><summary>__main__.py</summary>

```python:__main__.py
import pulumi
import pulumi_aws as aws

# Define AWS Lambda role
role = aws.iam.Role("lambdaRole",
    assume_role_policy="""
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "sts:AssumeRole",
                "Principal": {
                    "Service": "lambda.amazonaws.com"
                },
                "Effect": "Allow",
                "Sid": ""
            }
        ]
    }
    """
)

# Attach AWSLambdaBasicExecutionRole policy to the role
aws.iam.RolePolicyAttachment("lambdaRolePolicyAttachment",
    role=role.id,
    policy_arn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
)

# Create the Lambda function
lambda_function = aws.lambda_.Function("pulumi_lambda_function",
    runtime="python3.8",
    name="pulumi_lambda_function",
    role=role.arn,
    handler="lambda_function.lambda_handler",
    code=pulumi.FileArchive("./lambda_function.zip")
)

# Create an API Gateway
api = aws.apigateway.RestApi("api", name="pulumi_api")

# Create a resource under the API Gateway
resource = aws.apigateway.Resource("api-resource",
    rest_api=api.id,
    parent_id=api.root_resource_id,
    path_part="pulumi"
)

# Create a method for the resource
method = aws.apigateway.Method("api-method",
    rest_api=api.id,
    resource_id=resource.id,
    http_method="GET",
    authorization="NONE"
)

# Create an integration for the Lambda function
integration = aws.apigateway.Integration("api-integration",
    rest_api=api.id,
    resource_id=resource.id,
    http_method=method.http_method,
    integration_http_method="POST",
    type="AWS_PROXY",
    uri=lambda_function.invoke_arn
)

# Deploy the API Gateway
deployment = aws.apigateway.Deployment("api-deployment",
    rest_api=api.id,
    stage_name="test"
)

# Grant API Gateway permissions to invoke the Lambda function
permission = aws.lambda_.Permission("api-permission",
    action="lambda:InvokeFunction",
    function=lambda_function.name,
    principal="apigateway.amazonaws.com",
    source_arn=pulumi.Output.concat(api.execution_arn, "/*")
)

# Export the API endpoint
pulumi.export("url", pulumi.Output.concat(deployment.invoke_url, "/pulumi"))
pulumi.export("lambda_function_arn", lambda_function.arn)
```
</details>
最後に以下のpulumiコマンドを実行して、表示される構築内容に問題なければyesを選択して作成する。

```bash
pulumi up
```
こちらも作ったAPIへリクエスト投げてみると、レスポンスが帰ってきました🙌
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288531/6609ba66-2ada-7250-94f2-3c3c13ce6b34.png)

## 使用感
- **Terraform**
    - CloudFormationだとネストされたスタックなどを使用しないと分割したテンプレートを一気に流し込むことができませんでしたが、そのような作り込みが無くても作成できました。
    - HCL言語は今回のような内容を単純に書く分には学習の負担はほとんどありませんでした。
- **Pulumi**
    - Pulumi AIというコード作成支援が無料で使えるが、精度はいまいちでChatGPTに聞いた方がましでした。
    - 自分の環境ではpythonは問題なく作成できましたが、TypeScriptで試そうとしたときに「pulumi up」でタイムアウトとなり作成できませんでした。
    - TerraformやCloudFormationのようにリソース毎の内部的な依存関係はあまり考慮せずにAPIをたたいているようで、初回の「pulumi up」では全てのリソースは作成できず、今回の構成だと3回「pulumi up」を繰り返す必要がありました。
    - ここはTerraformも同様の認識ですが、自動ロールバックがないので途中まで作成されたリソースは残ってしまうので、変更する範囲はなるべく細かくするのが良いと感じました
    - lambdaのような一部のリソースに関してはオプションで名前を指定してあげないとデフォルトではランダム文字列がリソース名のsuffixとして付けられるようでした。
もちろん新たに「pulumi up」する毎に新規リソースが作成されるわけではないですが、冪等性の観点から、デフォルトの動作としては不適切かなと思いました。
    - 変更内容の表示については視覚的にかなり見やすかったなと思いました。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288531/9b9da861-7804-8104-9ce8-edd70ab2e8ac.png)

## 結論
pulumiにも光るものはありましたが、今回の使用感と書籍やネットで参考にできる情報がある等総合的に鑑みてTerraformを使用していきたいなと思いました！
ただ、IaCは設定値の確認のためにドキュメントみながら書くため時間がかかるのが一つのデメリットと時運は感じています。
そのためplumi AIの精度が上がってその部分がほぼ不要になればとても有効なツールだと感じました！
標準でTerraformなどからPulumiへコードを変換する機能も用意されているので今後も注目していきたいです…！！

### 今後について
今回は単純な構成でしか検証していないので、今後は以下の検証記事を記載していこうかなと思います。
- Terraformのベストプラクティスに沿った構築方法やCICDの構成
-  DatadogやSnowflakeのようなAWS以外でのTerraformの構築

## まとめ 
弊社ではsnowflakeやdatadog等AWS以外にも触れるだけでなく、
フロント、バックエンド領域の方とスクラムを組んでいるので様々な領域に携わっていくことができます。
興味のある方はぜひご検討ください！