terraformをゼロ知識から使ってみた


# terraformを使ってみた

terraformって名前しか聞いたことない人やそもそも何かわからない人向けに、自分の備忘録を兼ねて書いていきます。
参考にした記事などを、各章ごとに載せているので、環境が異なる人などはそちらを参考にしてください。

今回使用したコードはgithubに載せているので、そちらを参考にしてください。

https://github.com/soranjiro/terraform_practice

## terraformとは

> terraformは、インフラストラクチャをコードとして管理するためのツールです。これにより、インフラストラクチャの構成をコードとして定義し、バージョン管理や自動化が可能になります。
terraformを使用すると、インフラストラクチャの変更を簡単に追跡し、再現可能な環境を作成できます。
terraformは、AWS、Azure、Google Cloud Platformなどのクラウドプロバイダーや、VMware、OpenStackなどのオンプレミス環境に対応しています。

簡単にいうと、インフラをコードで管理するためのツールです。

## terraformのインストール

今回はmacOSでのインストールを行います。

記事はこちらを参考にしました。
ただし、terraformnをダウンロードするのに、バージョン管理してくれる`tfenv`を使います。

:::note
tfenvはpyenvのようなものです。
tfenvを使うことで、terraformのバージョンを簡単に切り替えることができます。
:::


```bash
# brewでtfenvをインストール
brew install tfenv

# tfenvでterraformをインストール
tfenv install latest

# tfenvでterraformのバージョンを指定
tfenv use latest

# terafomのバージョンを確認
terraform -v
```

:::note
参考にした記事

https://qiita.com/kamatama_41/items/ba59a070d8389aab7694

https://zenn.dev/erueru_tech/articles/f755f345447131

:::

## terraformを使ってみる

早速terraformを使ってみます。

はじめに`terraform init`を実行する必要があるのですが、その前に`tfstate`という設定ファイルを作成する必要があります。
`tfstate`は、terraformが管理するインフラストラクチャの状態を保存するファイルです。

そのために、tfstateを保存するためのS3バケットを作成します。

### S3バケットの作成
まず、AWSのコンソールにログインします。

AWSのコンソールからS3を開き、バケットを作成します。
バケット名は`terraform-practice-tfstate-soranjiro`とします。
バケットのリージョンは`ap-northeast-1`を選択します。

これでtfstateを保存するためのS3バケットが作成されました。

次に、このバケットにtfstateを保存するように、terraformの設定を行います。

:::note
aws cliを使ってS3バケットを作成することもできます。
```bash
aws s3api create-bucket --bucket terraform-practice-tfstate-soranjiro --region ap-northeast-1 --create-bucket-configuration LocationConstraint=ap-northeast-1
```
:::

:::warn
S3バケットの名前はグローバルで一意である必要があります。
同じ名前のバケットが存在する場合は、別の名前を指定してください。
ユーザ名やプロジェクト名を入れると良いでしょう。
:::

### terraformの設定

terraformの設定ファイルを作成します。
ファイル名はなんでも良いです。
直下に置かれた`.tf`ファイルを全て読み込むので、`main.tf`や`terraform.tf`などの名前をつけることが多いです。

```terraform
terraform {
  required_version = ">= 0.12"
  backend "s3" {
    bucket = "terraform-practice-tfstate"
    key    = "terraform.tfstate"
    region = "ap-northeast-1"
  }
}

provider "aws" {
  region = "ap-northeast-1"
}
```

:::note
参考にした記事

https://developer.hashicorp.com/terraform/language/backend/s3

:::

これで、terraformの設定が完了しました。

### terraformの初期化
次に、terraformを初期化します。

```bash
terraform init
```
これで、terraformの初期化が完了しました。


:::warn
awsの認証情報がないと以下のようなエラーが出ます。
```bash
╷
│ Error: No valid credential sources found
│
│ Please see https://www.terraform.io/docs/language/settings/backends/s3.html
│ for more information about providing credentials.
│
│ Error: failed to refresh cached credentials, no EC2 IMDS role found, operation error ec2imds: GetMetadata, exceeded maximum number of attempts, 3, request send failed, Get
│ "http://169.254.169.254/latest/meta-data/iam/security-credentials/": dial tcp 169.254.169.254:80: connect: no route to host
│
╵
```

この場合は、aws cliの認証情報を設定する必要があります。
```bash
brew install awscli
aws configure
```
:::


:::note
参考にした記事

https://qiita.com/soranjiro/items/eb6855b55041fd6154aa

https://reffect.co.jp/aws/aws-cli

https://zenn.dev/akkie1030/articles/aws-cli-setup-tutorial

https://zenn.dev/fairy_penguin/articles/03750343c64167

:::


`.terraform`という隠しフォルダが作成されて`terraform.tfstate`というファイルたちが作成されていたら成功です。

## lambdaの作成
せっかくなので、lambda関数を作成してみます。
lambda関数は、AWSのサーバーレスコンピューティングサービスです。
簡単にいうと、コードを書くだけで環境とかはAWSが用意してくれるサービスです。

ディレクトリ構成は以下のようにします。

```bash
sample
├── lambda		    # Lambda関数をまとめたディレクトリ
│   └── hello-world         # TerraformのLambdaモジュール
│       ├── sample
│       │   ├── go.mod
│       │   ├── go.sum
│       │   └── main.go
│       └── main.tf
├── main.tf
└── provider.tf
```

:::note
最終的には、以下のようにしたいですが、一回で`terraform apply`ができなくなってしまうので一旦上に書いたようにします。
```bash
├─ env
│  ├─ dev
│  ├─ prod
│  │  └─ lambda
│  └─ stg
└─ modules
    └─ lambda
```
:::

<details>
<summary>main.tf</summary>

```terraform
# main.tf
module "hello_world" {
  source = "./lambda/hello-world"
}
```
</details>

<details>
<summary>provider.tf</summary>

```terraform
# provider.tf
terraform {
  required_version = ">= 0.12"
  backend "s3" {
    bucket = "terraform-practice-tfstate-soranjiro"
    key    = "terraform.tfstate"
    region = "ap-northeast-1"
  }
}

provider "aws" {
  region = "ap-northeast-1"
}
```
</details>


<details>
<summary>hello-world/main.tf</summary>

```terraform
# lambda/hello-world/main.tf
##################################
# Lambdaに付与するロール            #
##################################
data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "iam_for_lambda" {
  name               = "iam_for_lambda"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}


##################################
# Lambda                         #
##################################

resource "terraform_data" "default" {
  triggers_replace = {
    always_run = timestamp()
  }

  provisioner "local-exec" {
    command = "cd ${path.module}/sample/ && GOOS=linux GOARCH=amd64 go build -o ./build/bootstrap main.go"
  }
}

data "archive_file" "lambda" {
  type        = "zip"
  source_file = "${path.module}/sample/build/bootstrap"
  output_path = "${path.module}/sample/go.zip"

  depends_on = [ terraform_data.default ]
}

resource "aws_lambda_function" "hellow_world" {
  filename      = "${path.module}/sample/go.zip"
  function_name = "hello-world"
  role          = aws_iam_role.iam_for_lambda.arn
  handler       = "sample"

  source_code_hash = data.archive_file.lambda.output_base64sha256

  runtime = "provided.al2023"
}
```

</details>

<details>
<summary>hello-world/sample/main.go</summary>

```go
// lambda/hello-world/sample/main.go
package main

import (
    "context"
    "fmt"

    "github.com/aws/aws-lambda-go/lambda"
)

type MyEvent struct {
    Name string `json:"name"`
}

func HandleRequest(ctx context.Context, name MyEvent) (string, error) {
    return fmt.Sprintf("Hello %s!", name.Name), nil
}

func main() {
    lambda.Start(HandleRequest)
}
```
</details>

```bash
cd lambda/hello-world
go mod init lambda/hello-world
go mod tidy
```
これで、lambda関数の準備ができました。

## lambda関数のデプロイ
次に、lambda関数をデプロイします。

```bash
terraform init
terraform apply
# yesと入力
```

**Apply complete!**とでたら成功です。
もし、権限エラーで失敗している場合は、IAMロールの権限を確認してください。
`PowerUserAccess`と`IAMFullAccess`を付与したら（ほぼできないことないので当たり前ですが）できると思います。
自分に必要な権限だけを付与するように不必要な権限は削除していきましょう！


:::note
参考にした記事

https://zenn.dev/asahigamieru/articles/ab40b758ff70bc

https://techblog.forgevision.com/entry/Terraform/directory

:::

## lambda関数の確認
実際に作成されたかを確認してみましょう。

### AWSコンソールから確認
AWSのコンソールから作成されたことが確認できると思います。
lambdaのコンソールから、関数を選択して、テストを実行してみましょう。
```json
{
  "name": "soranjiro"
}
```
実行結果は以下のようになります。

```json
{
  "statusCode": 200,
  "body": "\"Hello soranjiro!\""
}
```

### aws cliから確認
ついでに、aws cliで確認してみましょう。

```bash
# 実行可能なlambda関数の一覧を取得
aws lambda list-functions --region ap-northeast-1
```

これで、`hello-world`という関数が作成されていることが確認できると思います。
次に、lambda関数を実行してみましょう。

```bash
# lambda関数を実行
aws lambda invoke \
  --function-name hello-world \
  --cli-binary-format raw-in-base64-out \
  --payload '{"name": "World"}' \
  response.json
```

このコマンドの内容は、
- `--function-name`: 実行するlambda関数の名前
- `--cli-binary-format`: aws cliのバージョン2以降で必要なオプション
- `--payload`: lambda関数に渡す引数
  - json形式で、`name`というキーに`World`という値を渡しています。
  - キーを変更することで、別のレスポンスを得ることができます。
- `response.json`: 実行結果を保存するファイル名

実行結果は、ターミナルに出力され、bodyの部分（lambdaからのレスポンス）は`response.json`に保存されます。
```json
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
```

`response.json`の中身は以下のようになります。
```json
"Hello World!"
```

## lambda関数の削除
terraformで作成したリソースは、terraform destroyコマンドで削除することができます。
```bash
terraform destroy
# yesと入力
```

`Destroy complete! Resources: 3 destroyed.`と表示されれば成功です。

以下のように、lambda関数が削除されていることを確認できます。
```bash
% aws lambda list-functions --region ap-northeast-1
{
    "Functions": []
}
```