よ〜んです、今度こそTerraformに入門します。

今回の流れ

こんな感じの流れで進めていきます。

  1. AWS CDKでデプロイする
  2. AWS CDKでremovalPolicyを当てる
  3. cf2tfで変換
  4. terraform管理下へ移動
    1. terraform validate
    2. terraform import
    3. terraform plan
  5. terraformでapply(デプロイ)

AWS CDKでデプロイ

前回の記事cf2tfを使ってうまくいきませんでしたが、懲りずに使っていきます。

今回は以下の構成を作成していきます。

architecture-beta
  service apigw(logos:aws-api-gateway)[API Gateway]
  service sqs(logos:aws-sqs)[SQS]
  service lambda(logos:aws-lambda)[Lambda]

  apigw:R -- L:sqs
  sqs:R -- L:lambda

デプロイが完了し、動作も確認できました。

AWS CDKでremovalPolicyを当てる

removalPolicyを各リソースに設定し、再度デプロイします。

Stack TerraformHandsonCdkStack
Resources
[-] AWS::ApiGateway::Deployment ApiGateway/Deployment ApiGatewayDeploymentA26796E894ec103d13e6ff7fa9f0b2d7210d7a88 destroy
[+] AWS::ApiGateway::Deployment ApiGateway/Deployment ApiGatewayDeploymentA26796E88dbee6e416fd69eb402670e32290cd3a
[~] AWS::SQS::Queue MessageQueue MessageQueue7A3BF959
 ├─ [~] DeletionPolicy
 │   ├─ [-] Delete
 │   └─ [+] Retain
 └─ [~] UpdateReplacePolicy
     ├─ [-] Delete
     └─ [+] Retain
[~] AWS::Lambda::Function ProcessorLambda ProcessorLambda71A929CE
 ├─ [+] DeletionPolicy
 │   └─ Retain
 └─ [+] UpdateReplacePolicy
     └─ Retain
[~] AWS::ApiGateway::RestApi ApiGateway ApiGateway11E7F47B
 ├─ [+] DeletionPolicy
 │   └─ Retain
 └─ [+] UpdateReplacePolicy
     └─ Retain
[~] AWS::ApiGateway::Stage ApiGateway/DeploymentStage.prod ApiGatewayDeploymentStageprod1C6D5CD6
 └─ [~] DeploymentId
     └─ [~] .Ref:
         ├─ [-] ApiGatewayDeploymentA26796E894ec103d13e6ff7fa9f0b2d7210d7a88
         └─ [+] ApiGatewayDeploymentA26796E88dbee6e416fd69eb402670e32290cd3a
[~] AWS::ApiGateway::Resource ApiGateway/Default/messages ApiGatewaymessagesC321BE78
 ├─ [+] DeletionPolicy
 │   └─ Retain
 └─ [+] UpdateReplacePolicy
     └─ Retain
[~] AWS::IAM::Role ApiGatewayRole ApiGatewayRoleD2518903
 ├─ [+] DeletionPolicy
 │   └─ Retain
 └─ [+] UpdateReplacePolicy
     └─ Retain

npx cdk diffするとこんな感じになりました。

cf2tfで変換

mkdir terraform-handson-tf

適当にterraformのディレクトリを切ります。

cf2tf terraform-handson-cdk/cdk.out/TerraformHandsonCdkStack.template.json -o terraform-handson-tf

ちょっとでも楽したいので、cf2tfを使ってざっくり変換させます。

ls terraform-handson-tf

data.tf         output.tf       variable.tf
locals.tf       resource.tf

このようなファイルが出力されました。

terraformに入門する

Manage infrastructure | Terraform | HashiCorp Developerを参考にしました。

validateする

terraform initして、terraform validateします。

Error: Incorrect attribute value type
Inappropriate value for attribute "assume_role_policy": string required, but have object.

IAM Roleのスキーマでエラーが出ていたので修正します。

- policy = {
+ policy = jsonencode({
    Statement = [
      {
        Action = [
          "sqs:ChangeMessageVisibility",
          "sqs:DeleteMessage",
          "sqs:GetQueueAttributes",
          "sqs:GetQueueUrl",
          "sqs:ReceiveMessage"
        ]
        Effect = "Allow"
        Resource = aws_sqs_queue.message_queue7_a3_bf959.arn
      }
    ]
    Version = "2012-10-17"
-  }    
+  })
`````mermaid
architecture-beta
  service apigw(logos:aws-api-gateway)[API Gateway]
  service sqs(logos:aws-sqs)[SQS]
  service lambda(logos:aws-lambda)[Lambda]

  apigw:R -- L:sqs
  sqs:R -- L:lambda

stringではなく、jsonencode()object渡しします。

Error: Missing required argument
The argument "function_name" is required, but no definition was found.

terraform の aws_lambda_function では、function_name が必須だそうです。

CloudFormationのリソースタブからLambdaのfunction nameを引っ張ってきて、設定します。

Error: Incorrect attribute value type

code_sha256 = {
    S3Bucket = "${S3_Bucket}
    S3Key = "${S3_Key}
}

Inappropriate value for attribute "code_sha256": string required, but have object.

AWS CDKの場合、Lambdaで使用するコードをS3からいい感じに処理してくれますが、terraformへの変換にあたり、archive_fileを使う方式に変更しました。

他にもAWS CDKのMetadataの情報が入っていたりするので、これらは削除しておきます。

terraform validate

Success! The configuration is valid.

validateが完了しました。(ここまで1時間)

import する

ここからimport blockを使ってインポートしていきます。

Plan: 13 to add, 0 to change, 0 to destroy.

まず初めにterraform planをした結果、全て、will be createdの状態となりました。

resource.tfの上から順番にインポートしていきます。

基本的にはCloudFormationのリソースタブの物理IDを入力すばよかったのですが、違うパターンも多かったので以下に載せておきます。

API Gateway Stage

Error: Unexpected format of ID ("prod"), expected REST-API-ID/STAGE-NAME

API Gateway resource

Error: Unexpected format of ID ("messages"), expected REST-API-ID/RESOURCE-ID

API Gateway method

Error: Unexpected format of ID ("${rest_api_id}|${resource_id}|POST"), expected REST-API-ID/RESOURCE-ID/HTTP-METHOD

IAM Role inline policy

- resource "aws_iam_policy" "api_gateway_role_default_policy_c1776_bbe" {
+ resource "aws_iam_role_policy" "api_gateway_role_default_policy_c1776_bbe" {
+ role = aws_iam_role.api_gateway_role_d2518903.id
  policy = jsonencode({
    Statement = [
      {
        Action = [
          "sqs:GetQueueAttributes",
          "sqs:GetQueueUrl",
          "sqs:SendMessage"
        ]
        Effect = "Allow"
        Resource = aws_sqs_queue.message_queue7_a3_bf959.arn
      }
    ]
    Version = "2012-10-17"
  })
  name = "ApiGatewayRoleDefaultPolicyC1776BBE"
  // CF Property(Roles) = [
  //   aws_iam_role.api_gateway_role_d2518903.arn
  // ]
}

IAM Role の inline policyはaws_iam_policyからaws_iam_role_policyにして対応しました。

とりあえず、全てのresouceをimportしました。

Plan: 12 to import, 4 to add, 1 to change, 3 to destroy.

planする

細かいパラメーターの調整をしていきます。

cf2tfの仕様で、rest_api_idのキーにrest_apiarnを渡してしまうようです。それによって変更が起きていたので、修正していきます。

Plan: 12 to import, 1 to add, 2 to change, 0 to destroy.

ここまで減らせましたが、警告が出ているので治します。

Warning: Deprecated attribute

on output.tf line 2, in output "api_gateway_endpoint5_aa8_ec3_a":
2:   value = join("", ["https://", aws_api_gateway_rest_api.api_gateway11_e7_f47_b.arn, ".execute-api.", data.aws_region.current.name, ".", data.aws_partition.current.dns_suffix, "/", aws_api_gateway_stage.api_gateway_deployment_stageprod1_c6_d5_cd6.arn, "/"])

The attribute "name" is deprecated. Refer to the provider documentation for details.

data.aws_region.current.namedata.aws_region.current.regionに変更します。

Warning: Argument is deprecated

with aws_iam_role.processor_lambda_service_role_e9_f4_ab56, on resource.tf line 28, in resource "aws_iam_role" "processor_lambda_service_role_e9_f4_ab56":

28:   managed_policy_arns = [
29:     join("", ["arn:", data.aws_partition.current.partition, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"])
30:   ]
 
managed_policy_arns is deprecated. Use the aws_iam_role_policy_attachment resource instead. If Terraform should exclusively manage all managed policy attachments (the current behavior of this argument), use the aws_iam_role_policy_attachments_exclusive resource as well.
resource "aws_iam_role_policy_attachment" "processor_lambda_basic_execution" {
  role       = aws_iam_role.processor_lambda_service_role_e9_f4_ab56.name
  policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

マネージドポリシーには、aws_iam_role_policy_attachmentを使用するそうです。

次に、CF Propertyを削除していきます。

-  // CF Property(Roles) = [
-  //   aws_iam_role.processor_lambda_service_role_e9_f4_ab56.arn
-  // ]

上記のような、既に設定したものは削除します。

resource "aws_api_gateway_method" "api_gatewaymessages_postcebe9881" {
  authorization        = "NONE"
  http_method          = "POST"
  authorization_scopes = []
  request_models       = {}
  request_parameters   = {}
  resource_id          = aws_api_gateway_resource.api_gatewaymessages_c321_be78.id
  rest_api_id          = aws_api_gateway_rest_api.api_gateway11_e7_f47_b.id
  // CF Property(Integration) = {
  //   Credentials = aws_iam_role.api_gateway_role_d2518903.arn
  //   IntegrationHttpMethod = "POST"
  //   IntegrationResponses = [
  //     {
  //       ResponseTemplates = {
  //         application/json = "{"message": "Message sent to queue"}"
  //       }
  //       StatusCode = "200"
  //     }
  //   ]
  //   RequestParameters = {
  //     integration.request.header.Content-Type = "'application/x-www-form-urlencoded'"
  //   }
  //   RequestTemplates = {
  //     application/json = "Action=SendMessage&MessageBody=$input.body"
  //   }
  //   Type = "AWS"
  //   Uri = join("", ["arn:", data.aws_partition.current.partition, ":apigateway:", data.aws_region.current.name, ":sqs:path/", data.aws_caller_identity.current.account_id, "/", aws_sqs_queue.message_queue7_a3_bf959.name])
  // }
  // CF Property(MethodResponses) = [
  //   {
  //     StatusCode = "200"
  //   }
  // ]
}

API Gatewayの統合の処理がうまくいってなさそうなので、aws_api_gateway_integrationで書き直します。

resource "aws_api_gateway_integration" "sqs_api_integration" {
  rest_api_id = aws_api_gateway_rest_api.api_gateway11_e7_f47_b.id
  resource_id = aws_api_gateway_resource.api_gatewaymessages_c321_be78.id
  http_method = aws_api_gateway_method.api_gatewaymessages_postcebe9881.http_method
  type = "AWS"
  request_parameters = {
    "integration.request.header.Content-Type" = "'application/x-www-form-urlencoded'"
  }
  request_templates = {
    "application/json" = "Action=SendMessage&MessageBody=$input.body"
  }
  uri = join("", ["arn:", data.aws_partition.current.partition, ":apigateway:", data.aws_region.current.region, ":sqs:path/", data.aws_caller_identity.current.account_id, "/", aws_sqs_queue.message_queue7_a3_bf959.name])
  credentials = aws_iam_role.api_gateway_role_d2518903.arn
  cache_key_parameters = []
  integration_http_method = "POST"
}

integrationのresponseはaws_api_gateway_integration_responseで書き直します。

resource "aws_api_gateway_integration_response" "sqs_api_integration_response" {
  rest_api_id = aws_api_gateway_rest_api.api_gateway11_e7_f47_b.id
  resource_id = aws_api_gateway_resource.api_gatewaymessages_c321_be78.id
  http_method = aws_api_gateway_method.api_gatewaymessages_postcebe9881.http_method
  status_code = aws_api_gateway_method_response.response_200.status_code
  response_templates = {
    "application/json" = "{\"message\": \"Message sent to queue\"}"
  }
}

メソッドのレスポンスもaws_api_gateway_method_responseで書き直します。

resource "aws_api_gateway_method_response" "response_200" {
  http_method = aws_api_gateway_method.api_gatewaymessages_postcebe9881.http_method
  resource_id = aws_api_gateway_resource.api_gatewaymessages_c321_be78.id
  rest_api_id = aws_api_gateway_rest_api.api_gateway11_e7_f47_b.id
  status_code = "200"
}

これらはAWS CDKのリソースとしては定義されていませんでしたが、無理やりインポートしてみます。

Plan: 15 to import, 1 to add, 0 to change, 0 to destroy.

terraform applyする

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

terraform applyできました。

npx cdk driftしてみます。

Stack TerraformHandsonCdkStack
Modified Resources
[~] AWS::ApiGateway::Stage ApiGateway/DeploymentStage.prod ApiGatewayDeploymentStageprod1C6D5CD6
 └─ [~] /DeploymentId
     ├─ [-] 1hi0t5
     └─ [+] 9m147t

1 resource has drifted from their expected configuration

API GatewayのデプロイIDのみが変更された状態になっております。

今考えればAPI Gatewayのデプロイもimportすればよかったなと思っています

まとめ

AWSCDKで作ったAPI Gateway / SQS / Lambda構成を、cf2tfでterraformへ移行する流れを検証した。

cf2tfはCloudFormationの論理IDとterraformのresource idが対応するため、importは非常にやりやすかったです。cf2tfプロジェクトを色々お手伝いしようかなと思いました。

terraformの話でいくと、terraformの宣言的な書き方や依存関係を意識しなくていい点も良く、CDKからterraformへの移行感覚を掴めました。

近いうちにもうちょっと複雑な構成にチャレンジしてみようと思います。