Terraformなんもわからん部~DockerからTerraformを使おう~

# Terraformなんもわからん
Terraformがなんもわかりません。理解するためにおててを動かしていきます。
目標は、terraform経由でAWSのサービスを利用し、最終的にはデータ基盤を自作することです。

# Terraformとは？
クラウド上のリソースをコードベースで構築できるもので、コードをもとに環境構築を可能にするもの。
これにより、いろいろと便利になる。
らしいです。研修で教わりました！
gpt4oに聞いてみた

>🌱 Terraformの特徴
>・__Infrastructure as Code(IaC)__：
>サーバー、ネットワーク、DBなどのインフラ構成を、コード（通常は.tfファイル）で定義できます。
>
>・__クラウドを横断して使える__：
>AWS、Azure、Google Cloud など、複数のクラウドサービスに対応。
>
>・__宣言的な構文__：
>「こういう状態にしたい」と宣言するだけで、Terraformが足りない部分だけを自動で構築・変更してくれます。
>
>・__プラン機能__：
>実行前に「何がどう変わるのか」を事前に確認できるので、安全にインフラを更新できます。

だそうです。色々と便利ですね！

## Terraform、何が分からないの？
シンプルに言語に慣れてなくてよくわかりません。
なので、慣れる意味でまず色々とみていきます。
まずはいつもの通りサンプルを生成させます。

### AWS VPCコード
まずはvpcを生成するコードをこんな感じで生み出してもらいました。
```terraform
# awsを使うための基本的な設定
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Configure the AWS Provider
# 使うAWSのリージョンとプロファイルの設定
provider "aws" {
  region  = "ap-northeast-1"
  profile = "default"
}

# Create a VPC
# 実際のvpcを作るための部分
resource "aws_vpc" "sample_vpc" {
  cidr_block = "10.0.0.0/16" # IPアドレスを、10.0.0.0 ~ 10.0.255.255の中から適当に決める 10.0.0.0はプライベート範囲
  tags = {
    Name = "sample_vpc"
    Environment = "development"
    Project = "sample_project"  
  }
}
```
こんな感じで、基本的にはresorceとresorce名、そして作成時の名前を入れて、そのあとは各種設定を無限に入れる、という感じで成り立っていました。
なるほどね。言語としては、まあ見ればわかる感じになっているので、とても分かりやすい。

### VPC作成を複数ファイルに分割して頑張ろう
先ほどのコードをgpt4oに投げて、改善案を出してもらったところ、いろいろと分割してきたので、それぞれ見ていきます。

```terraform:main.tf
resource "aws_vpc" "dev_sample_vpc" {
  cidr_block = var.dev_vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true
  # DNS解決を使えて便利・・・？

  tags = {
    Name        = var.dev_vpc_name # 変数の受け取りは別ファイルでも同階層ならこれで受けられるらしい
    Environment = "development"
    Project     = "sample_project"
  }
}
```

```terraform:variables.tf
# 変数をファイルとして分離することで、後々楽になる
variable "dev_vpc_cidr" {
  default = "10.0.0.0/16"
}

variable "dev_vpc_name" {
  default = "sample_vpc"
}
```

```terraform:provider.tf
# AWSを使うための初期設定部分
provider "aws" {
  region  = "ap-northeast-1"
  profile = "default"
}
```

こんな感じで、機能ごとに分割することで、後々管理しやすくなるらしいです。楽だね！

### 本格的？にいろいろ作る
ファイルを見る最後として、awsのec2(サーバー、というか要するにパソコン本体)を立てて、そこに安全につなげるような構成を依頼しました。
具体的には、VPC内部にEC2入りのプライベートサブネット、natゲートウェイを置くパブリックサブネットを立てて、そこに安全にアクセスできるよう、インターネットゲートウェイとnatゲートウェイを作る、という感じにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806752/2f4e69b6-eaf8-4f43-9918-1a6d540df6ce.png)
GPTに生成させた構成図です。

いやサブネットって何？？？？？
聞いてみました！
>🔷 VPC（Virtual Private Cloud）とは？
>AWS上の仮想的なネットワーク空間。
>企業やアプリケーションごとに独立したネットワークを持てる感じ。
>例えば：
>IPレンジ（CIDR）を自分で決められる（例：10.0.0.0/16）
>ルートテーブルやセキュリティグループを持つ
>中にサブネットやインスタンス、NAT、IGWなどを作っていく
>
>🧠 イメージ：
>VPCは「会社の敷地全体」みたいなもの。
>その中に建物（サブネット）がある。

>🔶 サブネット（Subnet）とは？
>VPC内を分割した小さなネットワーク単位。
>実際にEC2などのリソースが配置される場所。
>サブネットごとに
>パブリック or プライベートかを決められる（IGW/NATの有無で）利用するAZ（アベイラビリティゾーン）を指定できる

>🧠 イメージ：
>サブネットは「敷地内の建物」や「部屋」みたいなもの。
>たとえば：
>A棟（パブリックサブネット）→ インターネットと直結
>B棟（プライベートサブネット）→ インターネットには直接出れない（NAT経由）

ということらしいです。VPCがネットワーク全体で、サブネットはその中に作る、実際にリソースを配置する場所のようです。
なるほどね
マンションとマンションの部屋、家具みたいな感じの関係性っぽい
さらに、サブネットにはプライベートとパブリックがあるけど、パブリックは外部からアクセス可能、プライベートは内部から外部へのアクセスのみ可能らしい。

>🌐 パブリックサブネットとプライベートサブネットの違い
>🔷 パブリックサブネット（Public Subnet）
>- インターネットと直接通信ができるサブネット
>- EC2に**グローバルIP（パブリックIP）**を付けると、外部からアクセス可能になる
>- Internet Gateway（IGW）経由で通信できるルートがある
>🧠 例：
>- Webサーバー（外からアクセスしたい）
>- 踏み台サーバー（Bastion）
>
>🔶 プライベートサブネット（Private Subnet）
>- インターネットに直接アクセスできないサブネット
>- 外部と通信するにはNAT Gatewayなどの中継が必要
>- よって、外部から直接アクセスされにくくセキュア
>
>🧠 例：
>- アプリサーバーやDBサーバー（外から直接アクセスさせたくないもの）
>- バックエンド処理系のEC2など

あとさらっとルートテーブルとかいうものを出してきた。
多分アクセスされたときにどこにつなぐかのテーブルっぽい。
パブリックなら、0.0.0.0/0をインターネットゲートウェイに、プライベートなら0.0.0.0/0をNATゲートウェイに投げるらしい。

0.0.0.0/0って何？　→　すべてのIPアドレス、を意味するらしく、マッチするものがない場合にこれになるらしい。VPC内部で見つからなかったら外部に出す、という感じらしい。へー
インターネットゲートウェイもNATゲートウェイも、これを見る限りただ接続元と接続先のIPアドレスに応じて振り分けてるだけっぽい。
```plaintext
[プライベートEC2] → 0.0.0.0/0 → NAT GW → IGW → インターネット（出れる）
[パブリックEC2]  ← 0.0.0.0/0 ← IGW ← インターネット（入れる）
```
って感じ。ほーん。
何となくわかったような気もする。実際のファイルを見る

### 本格的に作る(ファイル観察編)
gpt4oに作らせたので、それをまとめてみていく。
まずはprovider設定。さっきと同じ
```terraform:provider.tf
provider "aws" {
  region = "ap-northeast-1"
}
```
次にVPCとインターネットゲートウェイの設定
```terraform:vpc.tf
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "main-vpc"
  }
}

# インターネットゲートウェイ(プライベートサブネットと外部をつなぐ扉)
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "main-igw"
  }
}
```
次にサブネットの設定
```terraform:subnets.tf
# publicサブネットの作成
# サブネット自体は共通で、外部からアクセス可能にするかどうかの違い
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true
  availability_zone       = "ap-northeast-1a"

  tags = {
    Name = "public-subnet"
  }
}

# プライベートサブネットの作成
# サブネット自体は共通で、外部アクセスを設定していない
resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "private-subnet"
  }
}

```
次に、natゲートウェイ(プライベートサブネットへの通信を制限するためのゲート)
```terraform:nat.tf
resource "aws_eip" "nat_eip" {
  domain = "vpc"
}

resource "aws_nat_gateway" "nat" {
  allocation_id = aws_eip.nat_eip.id
  subnet_id     = aws_subnet.public.id # 外部アクセス可能にするためにpublicサブネットにおく

  tags = {
    Name = "nat-gateway"
  }

  depends_on = [aws_internet_gateway.igw]
}

```
次に、ルートテーブルの設定　ルートテーブル自体の設定と、サブネットに紐づける設定の二つが必要
```terraform:routes.tf
# まず、インターネットゲートウェイのルートテーブル設定
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "public-rt"
  }
}

# パブリックサブネットに対して、どのルートテーブルを使うかを紐づけ
resource "aws_route_table_association" "public_assoc" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

#　プライベートサブネットのルートテーブル設定
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat.id
  }

  tags = {
    Name = "private-rt"
  }
}

# プライベートサブネットに対して、使うルートテーブルを設定
resource "aws_route_table_association" "private_assoc" {
  subnet_id      = aws_subnet.private.id
  route_table_id = aws_route_table.private.id
}

```
次に、セキュリティグループの設定
いやそもそも何のこと？聞いてみました！

>AWSのファイアウォール的な機能で、インスタンス（EC2など）の通信を制御するルールのかたまり。
>
>- インバウンド（受信）：外から入ってくる通信を許可
>- アウトバウンド（送信）：インスタンスから外へ出ていく通信を許可
>
>📌 ポイント：
>- 許可するルールだけを書く（denyは書かない）
>- デフォルトではすべて拒否されてる
>- インスタンスにアタッチする

ここでいじることで、特定サーバーからだけアクセスできるようにしたり、逆にすべてからアクセスできるようにしたりできるらしい。
基本は全部拒否らしい。なる

```terraform:security.tf
resource "aws_security_group" "ec2_sg" {
  name        = "ec2-sg"
  description = "Allow SSH and HTTP"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["YOUR_IP/32"] # 自分のIPアドレスに置き換えてください
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "ec2-sg"
  }
}

```
最後に、ec2の設定
```terraform:ec2.tf
resource "aws_instance" "web" {
  ami                         = "ami-0f9c61b5a562a16af" # Amazon Linux 2のAMI ID（東京リージョン）
  instance_type               = "t2.micro"
  subnet_id                   = aws_subnet.private.id
  vpc_security_group_ids      = [aws_security_group.ec2_sg.id]
  associate_public_ip_address = false

  tags = {
    Name = "web-server"
  }
}

```

こんな感じらしいです。
いやー量が多い！！！！！でも構成としては割とこれでシンプルっぽいです。アクセス方法もSSHなのでわかりやすいかなと思います。
こっからは実際に一部コードを実行していきます。
(今回、初期設定にめちゃくちゃ苦労したので軽めです。)

# おててを使って遊ぶ(はんずおん！)
おててを動かします

## まずはterraformとAWS CLIの入った環境を作る！
今回は、terraformを使いたいわけですが、パソコンがwindowsだとWSL2経由となっていろいろとめんどくさいようです。
ですので、コンテナ環境にterraformとAWS CLIを入れていこうと思います。

まず、ファイル構成はこんな感じ
```
DOCKER_TEST4
│  .env # AWSのパスとか入れるとこ
│  docker-compose.yml
└─src
        dockerfile
```
ファイルごとにそれぞれ見ていきます。
まずは.env
```.env
AWS_ACCESS_KEY_ID=secret1
AWS_SECRET_ACCESS_KEY=secret2
DEFAULT_REGION_NAME=ap-northeast-1
DEFAULT_OUTPUT_FORMAT=json
```
ここに、ローカルにはおきたいけどリポジトリとかには乗せたくない環境変数を入れてます。
次に、dockerfile
```dockerfile:dockerfile
# ubuntu ベースでtfenvを使ってterraformをインストールする
# tfenvはterraformのバージョン管理ツール
FROM ubuntu:latest

# install curl and unzip
RUN apt-get update && apt-get install -y curl unzip
RUN apt-get install -y git
RUN apt-get install -y make
RUN apt-get install -y jq

# tfenvのインストールとterraformのinstall
RUN git clone --depth=1 https://github.com/tfutils/tfenv.git ~/.tfenv
RUN echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bash_profile
RUN /bin/bash -c "source ~/.bash_profile" \
    && /bin/bash -c "~/.tfenv/bin/tfenv install latest" \
    && /bin/bash -c "~/.tfenv/bin/tfenv use latest"

# AWS CLIのinstall
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
RUN unzip -u awscliv2.zip
RUN ./aws/install
RUN rm -rf awscliv2.zip

# コンテナ起動時に、pathを通すためのもの
ENTRYPOINT ["/bin/bash", "-c" , "source ~/.bash_profile && /bin/bash"]
```
最後に、docker-compose.yml
```yaml:docker-compose
# version: "3.8"

services:
  terraform:# コンテナ起動時の名前？
    build: ./src
    container_name: terraform_AWS
    env_file: # 環境変数の読み込み(build時のみ)
      - ./.env
    volumes: # バインドマウント
      - ./src:/AWS
    working_dir: /AWS
    command: ["/bin/bash", "-c" , "source ~/.bash_profile && /bin/bash"]
    # 初回起動時のpath通し
```
これで、docker-compose.ymlのあるディレクトリで起動することで、terraformの入ったコンテナを起動できる
```bash
docker-compose build
docker-compose run terraform
>>> root@user_id:/AWS# 
```
この状態では、きちんとterraformが導入されていることが分かる。
```bash
root@user_id:/AWS# terraform -v
Terraform v1.11.4
on linux_amd64
```

## 次に、AWS CLIを使える状態にする！
次に、AWS CLIをちゃんと使える状態にするように作業していきます。
今回、私のアカウントは二段階認証がオンになっている関係上、パスワードとIDだけではうまく動いてくれません。
ですので、一時的なセッションキーを用いる必要があります。
設定方法については、こちらに記載がありましたが、awsコンソール上の、自分のアカウント明→セキュリティ認証情報→多要素認証(MFA)→識別子の部分のarn:aws:iam::~を使います。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-files.html#cli-configure-files-where

https://qiita.com/takeITeasy7/items/32a9411caa472caea1ad#powershell%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89

適当にやる場合は、以下のコマンドをまず打ちます
```
aws configure
```
すると、アクセスキー、シークレットアクセスキー、リージョン、出力形式について聞かれるので、適当に入力してEnterを押します。
すると、~/.awsにconfigとcredentialというファイルが生成されます。
これらには、プロファイルごとの情報が記録されるようになっていますので、無理やりvimなどで編集することで、AWSへの接続が可能になります。
うまくいけば、以下のコマンドがエラーなく走るはずです
```
aws s3 ls --profile <決めたユーザー名>
```
しかしながら、エンジニアとしては可能な限り自動化したいわけです。
なので、必要情報(先述したMFAの認証情報(arn:aws:iam::~))を.envに突っ込んだうえで、MFAのコードだけ入れたらアクセスできるようにしました。
まず、ファイル構成は以下の通りです。
```tree
C:.
│  .dockerignore # .env, dockerfileを登録
│  .env # IAMのMFA認証情報を追加
│  docker-compose.yml
│
└─src
    │  app.py
    │  aws_settings.sh # セッションキー作成の自動化スクリプト
    │  dockerfile
    │  streamlit_app.py
    │  test.ipynb
    │
    ├─environment
    └─module
```
次に、aws_settings.shです。以下の通りです。
```bash:aws_settings.sh
#!/bin/bash

# 必要な環境変数を確認
if [[ -z "$AWS_ACCESS_KEY_ID" || -z "$AWS_SECRET_ACCESS_KEY" || -z "$AWS_IAM" ]]; then
  echo "環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_MFA_ARN を設定してください。"
  exit 1
fi

# アクセス用環境変数の設定
# .envファイルにあるものを念のため追加
export AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID"
export AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY"
export AWS_IAM="$AWS_IAM"
export AWS_DEFAULT_REGION="$DEFAULT_REGION_NAME" 

# ワンタイムパスワードを入力
read -p "MFAコードを入力してください: " MFA_CODE

# MFAコードの入力確認
if [[ -z "$MFA_CODE" ]]; then
  echo "MFAコードが入力されていません。"
  exit 1
fi

# AWSセッションキーを取得
SESSION_JSON=$(aws sts get-session-token \
  --serial-number "$AWS_IAM" \
  --token-code "$MFA_CODE" \
  --output json --region ap-northeast-1 \
  --duration-seconds 129600)

if [[ $? -ne 0 ]]; then
  echo "AWSセッションキーの取得に失敗しました。MFAコードを確認してください。"
  exit 1
fi

# セッションキーを抽出
AWS_ACCESS_KEY_ID=$(echo "$SESSION_JSON" | jq -r '.Credentials.AccessKeyId')
AWS_SECRET_ACCESS_KEY=$(echo "$SESSION_JSON" | jq -r '.Credentials.SecretAccessKey')
AWS_SESSION_TOKEN=$(echo "$SESSION_JSON" | jq -r '.Credentials.SessionToken')

# ~/.aws/credentials に登録
mkdir -p ~/.aws
cat > ~/.aws/credentials <<EOL
[default]
aws_access_key_id = $AWS_ACCESS_KEY_ID
aws_secret_access_key = $AWS_SECRET_ACCESS_KEY
aws_session_token = $AWS_SESSION_TOKEN
EOL

echo "AWSセッションキーを ~/.aws/credentials に登録しました。"

# ~/.aws/config にリージョンを登録
cat > ~/.aws/config <<EOL
[default]
region = ap-northeast-1
output = json
EOL
echo "AWSリージョンを ~/.aws/config に登録しました。"
```

これによって、aws cliが一時セッションの形式で使えるようになる。
使用する際は、profileとしてdefaultを明示する。
例えばこんな感じ
```bash
bash aws_settings.sh
# MFAコード入力

aws s3 ls --profile default
```
これによって、アクセスができるようになった。公式資料はこんな感じ。

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_temp_use-resources.html

これで、コンテナ上でterraformとaws cliが使えるようになりました。

## まずはVPCを立ててみようの階
まずは、簡単にVPCだけ立ててみようと思います。
ec2もサブネットもない、本当にVPCだけです。
ファイルとしてはこんな感じ
```terraform:main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    } # awsを使うための初期設定
  }
}

# Configure the AWS Provider
# まずはawsの基本設定を追加
# ここではregionとprofileを設定
provider "aws" {
  region  = "ap-northeast-1"
  profile = "default"
}

# Create a VPC
# resouce リソースの種類 そのリソースの名前
resource "aws_vpc" "sample_vpc" {
  cidr_block = "10.0.0.0/16" 
  # IPv4でのIPの指定の仕方
  # ほぼ16bit分の機器、2**16乗個の機器をつなげる
  tags = {
    Name = "sample_vpc"
    Environment = "development"
    Project = "sample_project"  
  } # aws console上でのtag設定
}
```
この状態で、terraformを使って起動から削除まで行えます。
```bash
cd ~/your_workspace # main.tfのおいてある階層に移動
terraform init # 初期化
terraform validate # コード自体の検証を実施
terraform apply # 実際にリソースを起動できる
terraform destroy # 今回のterraform経由で作成したリソースをまとめて爆破
```
以上！終わり！VPCを立てて爆破しました。
リソースの立て方自体はこれですべてです。

# まとめ
terraformの言語体系とか、実際に構成を作るときのコードを見ながら、クラウド用語を把握しつつ、最後に簡単な設定で実際に起動してみました。
terraformの利点として、リソースを一括で管理できるので、インスタンスの消し忘れからの料金爆上げの未来を避けることが出来ます。(最後はdestroyするだけ)
AWS CLIの初期設定で苦戦した関係で、今回はほとんどハンズオン出来てないので、もうちょっとここら辺を自分で深堀していけたらと思います。