4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ORACLE Cloud の「24GB RAM + 4CPU + 200GBストレージのLinuxサーバーを無料で手にいれる方法」を terraform で

Last updated at Posted at 2025-03-22

概要

  • 先日、興味深い記事がバズってました
    https://qiita.com/shota0616/items/d0dfa1f70af073aa9554
  • ORACLE Cloud ( OCI ) では、永年無料で 24GB RAM + 4CPU + 200GBストレージ の Linux サーバが使えるそう。これはぜひ欲しいです
  • わがまま条件
    • 再現性が必要なので terraform で
    • アウトバウンドの パブリック IP は固定したい
    • 認証はキーレスで
  • では、やっていきましょう

ORACLE Cloud 無料枠

美味しい話には裏があるのではないか確認する
https://docs.oracle.com/ja-jp/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm#Details_of_the_Always_Free_Compute_instance__a1_flex

Terraform

OCI での Terraform は、下記のチュートリアルがわかりやすい
https://oracle-japan.github.io/ocitutorials/intermediates/terraform/

認証情報

この後で登場する認証をまとめとく。全部で 3 つ。

terraform 用

  • terraform の認証情報の設定方法は、上記のチュートリアルに書かれてる
  • 事前に手動で、アカウントに紐づく API Key を追加して、鍵とフィンガープリントを取得する

oci コマンド 用

  • VM 起動時に 起動スクリプト ( cloud-init ) で oci コマンド実行する必要がある
  • 鍵を不要にしたいので、identity_dynamic_group を使う
    ( おそらく、Azure の Workload Identity, AWS の sts:AssumeRole みたいなやつぽい )
  • identity_dynamic_group を設定しとけば、VM 上で、oci コマンドが実行可能になる
  • terraform で設定する

ユーザ 用

  • 手動でキーペアを作成
  • terraform で、公開鍵を VM に設定
  • 秘密鍵を使って、VM にログイン

他社クラウドとの違い

  • OCI 用語
    • コンパートメント
      • Azure での リソースグループ のようだ
      • AWS には、その概念はない
  • パブリック IP について
    • パブリック IP を指す URL を、OCI は DNS に公開してくれてない
    • しかし、DNS は有料
    • VM の IP を固定したいので、「予約済パブリック IP」を作成する
    • (AWS と比較して)「予約済パブリック IP」を VM にアタッチすることが、信じられないくらい、面倒臭い
      上記のための手順とスクリプトを ORACLE が公開しており、それを参考にする
    • パブリック IP リソース側で ローカル IP と紐付ける必要がある、マジで意味不明
      ( ローカル IP は VM 起動後じゃないとわからない )

実装

provider.tf

terraform {
  required_providers {
    oci = {
      source = "oracle/oci"
    }
  }
}

provider "oci" {
  region           = var.region
  tenancy_ocid     = var.tenancy_ocid
  user_ocid        = var.user_ocid
  private_key_path = var.private_key_path
  fingerprint      = var.fingerprint
}

variables.tf

variable "region" {
  description = "OCIのリージョン"
  default     = "ap-tokyo-1"
}

variable "tenancy_ocid" {
  description = "OCIのテナンシー OCID"
  type        = string
  default     = " ocid1.tenancy.oc1.. をここに記載 "
}

variable "user_ocid" {
  description = "OCIのユーザー OCID"
  type        = string
  default     = " ocid1.user.oc1.. をここに記載 "
}

variable "fingerprint" {
  description = "APIキーのフィンガープリント"
  type        = string
  default     = " xx:xx:xx:xx:... 的な文字列をここに記載 "
}

variable "private_key_path" {
  description = "terraform 用 秘密鍵 のパス"
  type        = string
  default     = " .pem のパスをここに記載 "
}

variable "ssh_authorized_keys" {
  description = "VM ログイン 用 公開鍵 のパス"
  type        = string
  default     = " .pub のパスをここに記載 "
}

main.tf

locals {
  # 表示名 ポストフィックス
  display_name = "202503"

  # VCN とサブネットの CIDR ブロック
  cidr_block-vcn    = "10.0.0.0/16"
  cidr_block-subnet = "10.0.0.0/24"

  memory_in_gbs           = 24
  ocpus                   = 4
  boot_volume_size_in_gbs = 200
}

locals {
  # VMイメージのOCID
  # 24.04 (OCI CLI インストール スクリプトが未対応)
  # image_ocid = "ocid1.image.oc1.ap-tokyo-1.aaaaaaaax3arbzsjtoqovmp56hfba3q2qyxljbci6ejkuagmkjql6ej3mj6q"
  # 22.04
  image_ocid = "ocid1.image.oc1.ap-tokyo-1.aaaaaaaadcb75lsmgfzxtrirj6l7rb7ecd4ivz7fwzi2py2kk2molifncsia"
}

resource "oci_identity_compartment" "main" {
  compartment_id = var.tenancy_ocid
  name           = "compartment-${local.display_name}"
  description    = "Compartment for development resources"
}

data "oci_identity_availability_domains" "ads" {
  compartment_id = oci_identity_compartment.main.id
}

# VM に権限を付与するための動的グループを作成
resource "oci_identity_dynamic_group" "main" {
  compartment_id = var.tenancy_ocid
  name           = "dynamic_group-${local.display_name}"
  description    = "Dynamic Group for instance principal auth"
  matching_rule  = "ALL {instance.compartment.id = '${oci_identity_compartment.main.id}'}"
}

resource "oci_identity_policy" "main" {
  compartment_id = var.tenancy_ocid
  name           = "policy-${local.display_name}"
  description    = "Allow dynamic group to read VCN resources"

  statements = [
    "Allow dynamic-group ${oci_identity_dynamic_group.main.name} to manage instance-family in tenancy",
    "Allow dynamic-group ${oci_identity_dynamic_group.main.name} to manage virtual-network-family in tenancy",
    "Allow dynamic-group ${oci_identity_dynamic_group.main.name} to manage public-ips in tenancy",
    "Allow dynamic-group ${oci_identity_dynamic_group.main.name} to use subnets in tenancy",
    "Allow dynamic-group ${oci_identity_dynamic_group.main.name} to use vnics in tenancy"
  ]
}

resource "oci_core_vcn" "main" {
  compartment_id = oci_identity_compartment.main.id
  cidr_block     = local.cidr_block-vcn
  display_name   = "vcn-${local.display_name}"
}

resource "oci_core_subnet" "main" {
  compartment_id = oci_identity_compartment.main.id
  vcn_id         = oci_core_vcn.main.id
  display_name   = "subnet-${local.display_name}"

  availability_domain = null
  cidr_block          = local.cidr_block-subnet

  route_table_id    = oci_core_route_table.main.id
  security_list_ids = [oci_core_security_list.main.id]
}

resource "oci_core_route_table" "main" {
  compartment_id = oci_identity_compartment.main.id
  vcn_id         = oci_core_vcn.main.id
  display_name   = "route_table-${local.display_name}"

  route_rules {
    description       = null
    destination       = "0.0.0.0/0"
    destination_type  = "CIDR_BLOCK"
    network_entity_id = oci_core_internet_gateway.main.id
    route_type        = null
  }
}

resource "oci_core_internet_gateway" "main" {
  compartment_id = oci_identity_compartment.main.id
  vcn_id         = oci_core_vcn.main.id
  display_name   = "internet_gateway-${local.display_name}"
}

resource "oci_core_security_list" "main" {
  compartment_id = oci_identity_compartment.main.id
  vcn_id         = oci_core_vcn.main.id
  display_name   = "security_list-${local.display_name}"

  egress_security_rules {
    description = null
    destination = "0.0.0.0/0"
    protocol    = "all"
    stateless   = false
  }
  ingress_security_rules {
    description = null
    protocol    = "1" # ICMP
    source      = local.cidr_block-vcn
    stateless   = false
    icmp_options {
      code = -1
      type = 3
    }
  }
  ingress_security_rules {
    description = null
    protocol    = "1" # ICMP
    source      = "0.0.0.0/0"
    stateless   = false
    icmp_options {
      code = 4
      type = 3
    }
  }
  ingress_security_rules {
    description = null
    protocol    = "6" # TCP
    source      = "0.0.0.0/0"
    stateless   = false
    tcp_options {
      max = 22
      min = 22
    }
  }
}

resource "oci_core_public_ip" "main" {
  compartment_id = oci_identity_compartment.main.id
  lifetime       = "RESERVED"
  display_name   = "public_ip-${local.display_name}"

  # cloud-init で設定する
  private_ip_id = ""
  lifecycle {
    ignore_changes = [private_ip_id]
  }

  # 予約済みのパブリックIPを VM にアタッチするのは OCI では相当面倒臭い
  # https://docs.oracle.com/ja/learn/oci-attach-reserved-ip/index.html#task-1-set-up-a-terraform-script
}

resource "oci_core_instance" "main" {
  compartment_id      = oci_identity_compartment.main.id
  availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name
  display_name        = "instance-${local.display_name}"
  shape               = "VM.Standard.A1.Flex"

  shape_config {
    memory_in_gbs = local.memory_in_gbs
    ocpus         = local.ocpus
  }

  create_vnic_details {
    subnet_id        = oci_core_subnet.main.id
    assign_public_ip = true
  }

  source_details {
    source_type             = "image"
    source_id               = local.image_ocid
    boot_volume_size_in_gbs = local.boot_volume_size_in_gbs
  }

  metadata = {
    ssh_authorized_keys = file(var.ssh_authorized_keys) # SSH公開鍵を指定
    user_data           = base64encode(file("./cloud-init.sh"))
  }

  # We will pass Reserved Public IP OCID and Private Subnet OCID as a freeform tags to the instance
  freeform_tags = {
    publicIP = oci_core_public_ip.main.id
    SubnetId = oci_core_subnet.main.id
  }

  preserve_boot_volume = false
}

cloud-init.sh ( ORACLE が公開しているスクリプト(https://docs.oracle.com/ja/learn/oci-attach-reserved-ip/index.html) ) バグ修正済み

  • VM 新規作成時に、予約済み IP と エフェメラル IP を差し替える
#!/bin/bash
# coding: utf-8
# https://docs.oracle.com/ja/learn/oci-attach-reserved-ip/index.html#introduction
export PATH=$PATH:/root/bin
export OCI_CLI_AUTH=instance_principal

apt install -y jq net-tools
curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh | bash -s -- --accept-all-defaults

function getdetails() {
  # Fetch data using instance principal authentication
    metadata=$(curl -H 'Authorization: Bearer Oracle' http://169.254.169.254/opc/v2/instance)
    instanceid=$(echo $metadata | jq -r '.id')
    compartmentid=$(echo $metadata | jq -r '.compartmentId')
    subnetid=$(echo $metadata | jq -r '.freeformTags.SubnetId')
    publicIp=$(echo $metadata | jq -r '.freeformTags.publicIP')
    primaryvnicid=$(curl -H 'Authorization: Bearer Oracle' http://169.254.169.254/opc/v1/vnics/  | jq -r '.[0].vnicId')
    privateIpId=$(oci network private-ip list --vnic-id $primaryvnicid  | jq -r '.data[].id')
    ephpublicIp=$(oci network vnic get --vnic-id $primaryvnicid | jq -r '.data."public-ip"')
    ephpublicIpId=$( oci network public-ip get --public-ip-address $ephpublicIp | jq -r '.data.id')
    if [[ -z "$subnetid" || "$subnetid" == "" || ${#subnetid} == 0 || -z "$publicIp" || ${#publicIp} == 0 ||  -z "$instanceid" || -z "$compartmentid" || -z "$privateIpId" || -z "$primaryvnicid" || -z "$ephpublicIp" || -z "$ephpublicIpId" ]];
    then
        echo "Missing some details, retrying..."
        return 1
    fi
    return 0
}
# Retry the `getdetails` function thrice
attachVNIC()
{
    attachvnic=$(oci compute instance attach-vnic --instance-id $instanceid --subnet-id $subnetid --wait)
    while [ "$(curl -s http://169.254.169.254/opc/v1/vnics/ | jq 'length')" -lt 2 ]; do sleep 5; done
}
for i in {1..3}; do
  getdetails
  if [[ $? -eq 0 ]]; then
    attachVNIC
    if [[ -n "$attachvnic" ]]; then
        secondaryvnicid=$(echo $attachvnic | jq -r '.data.id')
        echo $secondaryvnicid
        check2VNICs=$(oci compute instance list-vnics --instance-id $instanceid | jq -r '.data[].id')
        echo $check2VNICs
        gatewayIP=$(curl http://169.254.169.254/opc/v1/vnics/ | jq -c '.[] | ( select(.vnicId=="'$secondaryvnicid'" ))' | jq -r '.virtualRouterIp')
        if [[ (-n "$gatewayIP") ]]; then
            echo $gatewayIP
            # wget https://docs.oracle.com/en-us/iaas/Content/Resources/Assets/secondary_vnic_all_configure.sh
            wget https://raw.githubusercontent.com/oracle/terraform-examples/refs/heads/master/examples/oci/connect_vcns_using_multiple_vnics/scripts/secondary_vnic_all_configure.sh
            sudo chmod u+x ./secondary_vnic_all_configure.sh
            sudo ./secondary_vnic_all_configure.sh -c
            iname=$(sudo ./secondary_vnic_all_configure.sh | awk '{print $8}' | awk 'END{print}')
            echo $iname
            sudo route add default gw $gatewayIP dev $iname
            route
            oci os ns get
            delpubIp=$(oci network public-ip delete  --force --public-ip-id $ephpublicIpId --wait-for-state TERMINATED)
            checkIp=$(oci network vnic get --vnic-id $primaryvnicid | jq -r '.data."public-ip"')
            if [[ -z "$checkIp" || "$checkIp" == "null" ]]; then
                echo "Assigning Reserved IP"
                assign=$(oci network public-ip update --force --public-ip-id $publicIp --private-ip-id $privateIpId  --wait-for-state ASSIGNED)
                detach=$(oci compute instance detach-vnic --compartment-id  $compartmentid --vnic-id $secondaryvnicid --force)
                sudo ./secondary_vnic_all_configure.sh -d
                checkIp2=$(oci network vnic get --vnic-id $primaryvnicid | jq -r '.data."public-ip"')
                echo "Reserved IP $checkIp2"
                if [[ -z "$checkIp2" ]]; then
                    oci compute instance update --instance-id $instanceid --freeform-tags '{"errorStatus":"Reserved IP Not Assigned","SubnetId":"'$subnetid'","publicIP":"'$publicIp'"}' --force
                    exit;
                fi
            else
                oci compute instance update --instance-id $instanceid --freeform-tags '{"errorStatus":"Ephemeral IP Not Deleted","SubnetId":"'$subnetid'","publicIP":"'$publicIp'"}' --force
                exit;
            fi
            break;
        else
            oci compute instance update --instance-id $instanceid --freeform-tags '{"errorStatus":"Gateway IP not fetched","SubnetId":"'$subnetid'","publicIP":"'$publicIp'"}' --force
            exit;
        fi
    else
        break;
    fi
  fi
done
if [[ -z "$attachvnic" ]];
then
    echo "Failed"
    oci compute instance update --instance-id $instanceid --freeform-tags '{"errorStatus":"Secondary VNIC not attached","SubnetId":"'$subnetid'","publicIP":"'$publicIp'"}' --force
    exit
fi

checkVNICs2=$(oci compute instance list-vnics --instance-id $instanceid | jq -r '.data | length')

if [[ "$checkVNICs2" > 1 ]];
then
    echo "Detaching Secondary VNIC Failed"
    oci compute instance update --instance-id $instanceid --freeform-tags '{"errorStatus":"Detaching Secondary VNIC Failed","SubnetId":"'$subnetid'","publicIP":"'$publicIp'"}' --force
    exit
fi
oci compute instance update --instance-id $instanceid --freeform-tags '{"SubnetId":"'$subnetid'","publicIP":"'$publicIp'"}' --force

VM ログイン

以下で、つながるはず

ssh ubuntu@<public_ip> -i "VM ログイン時の SSH 公開鍵"

終わりに

  • アウトバウンドの IP を固定したかったので「予約済みパブリック IP」を使用した
  • インバウンドだけの用途で、DNS も持ってるなら、「エフェメラル (自動採番)」の IP を DNS に登録すればいいです
4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?