Mac: Terraform と Terragrunt と Ansible でハローワールド

Mac: Terraform と Terragrunt と Ansible でハローワールド

0. Terraform と Terragrunt と Ansible をインストールする #

Homebrew でインストールします。

brew tap hashicorp/tap
brew install hashicorp/tap/terraform

brew install terragrunt

brew install ansible

terraformterragruntansible コマンドを実行してインストールできたことを確認します。

terragrunt --version
> terragrunt version 0.86.2

terraform --version
> Terraform v1.13.1

ansible --version
> ansible [core 2.19.2]

1. プロビジョニング定義を書いてみる #

以下の要件を満たすように作成します。

  • ap-northeast-1 リージョンの既存の subnet の中に Amazon Linux の t3.micro インスタンスを起動します。
  • EC2 インスタンス起動時の処理(ユーザデータ)で Ansible をインストール、実行します。
  • Ansible で Go をインストールし、Go のスクリプトをビルドします。Systemd のサービスとして Go のバイナリを登録します。
    • 補足:ユーザデータはインスタンス初回起動時にしか実行されません。インスタンス再起動時にもスクリプトが実行されるようにするためには Systemd のサービスとして登録します。
  • Go のバイナリが実行されると “Hello, World!” と書かれたテキストファイル hello.txt を作成します。
  • 起動した EC2 インスタンスに Session Manager(SSM)で接続できるようにします。(https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html

なお、AWS CLI では SSO(Single Sign-On)を利用して AWS にログインすることを前提としています。

ディレクトリ構成 #

my-provisioning/
├ .gitignore
├ my_userdata.sh
├ site.yml
├ main.go
├ terragrunt.hcl
└ main.tf

.gitignore #

.terragrunt-cache/
.terragrunt-stack/

my_userdata.sh #

ユーザデータスクリプトです。EC2 インスタンスの起動時に実行されるよう、後述の Terraform(main.tf) で指定しています。

https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html

Ansible をインストールし、Ansible を実行するのに必要なファイルを用意したうえで後述の site.yml の内容を実行します。

#!/bin/bash
set -eux

yum update -y

# Install Ansible
amazon-linux-extras enable ansible2
yum install -y ansible

# Create directory if not exists
mkdir -p /opt/my-app

# Create site.yml file based on the input
cat <<'EOF' > /opt/my-app/site.yml
${my_playbook}
EOF

# Create main.go file based on the input
cat <<'EOF' > /opt/my-app/main.go
${my_go}
EOF
chmod 0644 /opt/my-app/main.go

# Execute Ansible Playbook
ansible-playbook -i "localhost," -c local /opt/my-app/site.yml

site.yml #

Ansible の Playbook です。Go をインストールし、Go のスクリプトを実行します。

- hosts: localhost
  connection: local

  become: true
  tasks:
    - name: Install Go
      yum:
        name: golang
        state: present

    - name: Initialize go.mod
      command: /usr/bin/env bash -lc 'cd /opt/my-app && [ -f go.mod ] || go mod init my-app'
      args:
        chdir: /opt/my-app

    - name: Resolve modules
      command: /usr/bin/env bash -lc 'cd /opt/my-app && go mod tidy'
      args:
        chdir: /opt/my-app

    - name: Build Go
      command: /usr/bin/env bash -lc 'cd /opt/my-app && CGO_ENABLED=0 go build -ldflags "-s -w" -o /usr/local/bin/my-app main.go'
      args:
        chdir: /opt/my-app

    - name: Create systemd unit for executing my-app on boot
      copy:
        dest: /etc/systemd/system/my-app.service
        mode: "0644"
        content: |
          [Unit]
          Description=My App
          After=network-online.target
          Wants=network-online.target

          [Service]
          Type=simple
          ExecStart=/usr/local/bin/my-app
          WorkingDirectory=/opt/my-app
          Restart=on-failure
          RestartSec=5s
          Environment=GOWORK=off
          Environment=GOMODCACHE=/opt/my-app/go-mod-cache

          [Install]
          WantedBy=multi-user.target

    - name: Reload systemd
      systemd:
        daemon_reload: true

    - name: Enable and start my-app
      systemd:
        name: my-app.service
        enabled: true
        state: started

main.go #

Go のスクリプトです。“Hello, World!” と書かれたテキストファイル hello.txt を作成します。

package main

import (
 "fmt"
 "os"
 "path/filepath"
)

func main() {
 content := "Hello, World!"

 dir := "/opt/my-app/"
 fileName := "hello.txt"
 filePath := filepath.Join(dir, fileName)

 err := os.WriteFile(filePath, []byte(content), 0644)
 if err != nil {
  fmt.Println("作成に失敗しました:", err)
  return
 }

 fmt.Printf("作成しました: %s\n", filePath)
}

terragrunt.hcl #

Terragrunt の設定ファイルです。ステートファイルの保存先や Terraform を実行する際の引数(inputs)などを指定しています。

locals {
  aws_profile = "my_profile" # 自分の環境に合わせて書き換える - AWS SSO でログインする際のプロファイル名

  aws_remote_state_s3_region      = "ap-northeast-1"
  aws_remote_state_s3_bucket_name = "my-remote-state-bucket"
  aws_remote_state_s3_file_key    = "terraform.tfstate"

  aws_ec2_region        = "ap-northeast-1"
  aws_ec2_vpc_id        = "vpc-xxxxxxxxxxxxxxxxx" # 自分の環境に合わせて書き換える - ap-northeast-1 内の既存の VPC ID
  aws_ec2_subnet_id     = "subnet-xxxxxxxxxxxxxxxxx" # 自分の環境に合わせて書き換える - ap-northeast-1 内の既存の subnet ID
  aws_ec2_ami_id        = "ami-0228232d282f16465" # Amazon Linux 2023 x86_64
  aws_ec2_instance_type = "t3.micro"
  aws_ec2_name          = "my-ec2"
}

terraform {
  source = "./"

  extra_arguments "aws_profile" {
    commands = [
      "init",
      "apply",
      "refresh",
      "import",
      "plan",
      "taint",
      "untaint"
    ]

    env_vars = {
      AWS_PROFILE = "${local.aws_profile}"
    }
  }
}

remote_state {
  backend = "s3"

  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }

  config = {
    encrypt = true
    region  = local.aws_remote_state_s3_region
    bucket  = local.aws_remote_state_s3_bucket_name
    key     = local.aws_remote_state_s3_file_key
  }
}

inputs = {
  ec2_region            = local.aws_ec2_region
  ec2_vpc_id            = local.aws_ec2_vpc_id
  ec2_subnet_id         = local.aws_ec2_subnet_id
  ec2_ami_id            = local.aws_ec2_ami_id
  ec2_instance_type     = local.aws_ec2_instance_type
  ec2_name              = local.aws_ec2_name
}

main.tf #

Terraform の定義ファイルです。variable で Terragrunt の inputs で指定した値を受け取り処理を行います。

# ------------------------------------------------------------------------------
# Terraform Configuration
# ------------------------------------------------------------------------------
terraform {
  required_version = ">= 0.13"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# ------------------------------------------------------------------------------
# Variables
# ------------------------------------------------------------------------------
variable "ec2_region" {
  type = string
}

variable "ec2_vpc_id" {
  type = string
}

variable "ec2_subnet_id" {
  type = string
}

variable "ec2_ami_id" {
  type = string
}

variable "ec2_instance_type" {
  type = string
}

variable "ec2_name" {
  type = string
}

# ------------------------------------------------------------------------------
# Provider Configuration
# ------------------------------------------------------------------------------
provider "aws" {
  region = var.ec2_region
}

# ------------------------------------------------------------------------------
# Resources - IAM
# ------------------------------------------------------------------------------
resource "aws_iam_role" "this" {
  name = "my-ssm-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "this" {
  role       = aws_iam_role.this.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "this" {
  name = "my-ssm-profile"
  role = aws_iam_role.this.name
}

# ------------------------------------------------------------------------------
# Resources - Security Group
# ------------------------------------------------------------------------------
resource "aws_security_group" "this" {
  name        = "my-egress-security-group"
  description = "Security group for allow any outbound traffic"
  vpc_id      = var.ec2_vpc_id

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

  tags = {
    Name = "my-egress-security-group"
  }
}

# ------------------------------------------------------------------------------
# Resources - EC2
# ------------------------------------------------------------------------------
resource "aws_instance" "this" {
  ami           = var.ec2_ami_id
  instance_type = var.ec2_instance_type

  subnet_id              = var.ec2_subnet_id
  vpc_security_group_ids = [aws_security_group.this.id]

  associate_public_ip_address = false

  iam_instance_profile = aws_iam_instance_profile.this.name

  tags = {
    Name = var.ec2_name
  }

  user_data = templatefile("${path.module}/my_userdata.sh", {
    my_playbook = file("${path.module}/site.yml")
    my_go       = file("${path.module}/main.go")
  })

  user_data_replace_on_change = true
}

2. プロビジョニングを実行してみる #

aws sso login するプロファイル名(my_profile 部分)は自身の環境に合わせて書き換えてください。

プロビジョニングの実行 #

aws sso login --profile my_profile

terragrunt plan

terragrunt apply -auto-approve

デプロビジョニングの実行 #

aws sso login --profile my_profile

export AWS_PROFILE=my_profile
export AWS_SDK_LOAD_CONFIG=1

terragrunt destroy