【Terraform】OCI環境をIaCで構築

Cloud

前回AWS上のTerrafrom実行環境を作成しましたので、その実行環境を利用してOCIにVMをプロビジョニングします。
OCIはAlwaysFreeの範囲が広いため、検証期間をあまり気にしなくていいのがポイント高いですよね。

NW構成

OCIのSubnetを2つに分けて公開範囲を絞り、NSGで通信制御します。
ついでにObjectStrageを作成します。
またPublic SubnetにComputeインスタンスでBastionを作成しますが、OCIにはBastion(要塞)サービスがあり最大3時間セッションを維持できるようですので、本番環境ではOCI Bastionを利用する方がセキュリティは高くなります。

フォルダ構成

├── .env
├── .gitignore
├── .oci
│   └── oci_api_key.pem
├── .ssh
│   ├── id_ed25519
│   └── id_ed25519.pub
├── cloud-init-oci-cli.yaml
├── compute.tf
├── docker-compose.yml
├── instance_principal.tf
├── network.tf
├── nsg.tf
├── objectstorage.tf
├── outputs.tf
├── provider.tf
├── sample.txt
├── terraform.tfvars
└── variables.tf

SSH認証鍵作成、OSアップロード用ファイル作成

Bastion、Webserverに接続するための認証鍵を作成します。
検証目的で面倒なのでBastionとWebserverどちらも同じ秘密鍵を使います。

sh-5.2$ mkdir .ssh
sh-5.2$ sudo ssh-keygen -t ed25519 -f ./.ssh/id_ed25519

API認証でObjectStorageにアップロードする用に sample.txt を作成します。

sh-5.2$ echo "AWS→ObjectStorage" | sudo tee sample.txt

APIキー作成&接続情報入手

APIキー入手

API接続するために最低限必要となる情報は下記5つになります。

  1. Tenancy OCID
  2. User OCID
  3. FingerPrint
  4. API接続用の秘密鍵の場所
  5. Regin

この5つの情報はAPIキーを作成することで入手することができます。
・ユーザー設定 >トークン及びキー >APIキーの追加

OpenSSHなどで作成してアップロードしてもいいですが、今回はGUIで鍵を作成します。
秘密キーのダウンロード、公開キーのダウンロードのそれぞれのボタンを押すことで鍵をダウンロードできます。
・APIキー・ペアの作成 >秘密キーのダウンロード >公開キーのダウンロード >追加

プレビュー画面に表示される情報を利用してアクセスを行います。
メモ帳などにコピーしておきましょう。

コンパートメント作成(任意)

コンパートメントを分割することでリソースやコスト分析ができるようです。
ただし契約単位はテナンシになるので、コンパートメントごとに請求書分離はできず、内部請求を可視化することができるだけのようです。Azureのリソースグループが限りなく近いようですね。
リソースやコスト分析用なので検証用であれば作成しなくても問題ありません。
・アイデンティティ・ドメイン >コンパートメント >コンパートメントの作成

AD Name &オブジェクト・ストレージ・ネームスペース取得

GUIで探すとOCI内部でしか保持していない情報など表示できないものがありますので基本的にOCI CLIで取得したほうが早いです。

oci iam availability-domain list --compartment-id [テナンシID]
oci os ns get 

ファイル作成

構成ファイル作成

確認したAPIキーやAD Nameなどを.envで定義します。
コンパートメントを作成していない場合はcompartmentにtenancyIDを入れることになります。

.env
TF_VAR_tenancy_ocid=ocid1.tenancy.oc1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TF_VAR_user_ocid=ocid1.user.oc1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TF_VAR_fingerprint=xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx
TF_VAR_private_key_path=/root/.oci/oci_api_key.pem
TF_VAR_ssh_public_key_path=/workspace/.ssh/id_ed25519.pub
TF_VAR_compartment_ocid=ocid1.compartment.oc1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TF_VAR_ad_name=XXXX:AP-TOKYO-1-AD-1
TF_VAR_objectstorage_namespace=xxxxxxxxxxxxx

docker定義ファイル作成

docker-compose.yml
version: "3.9"

services:
  terraform:
    image: hashicorp/terraform:1.14
    container_name: terraform
    volumes:
      - .:/workspace
      - ./.oci:/root/.oci:ro
    working_dir: /workspace
    env_file:
      - .env
    tty: true
.gitignore
.env
.terraform/
.oci/
terraform.tfstate*

TFファイル作成

NSGはIngress、Egressに分割し、変数化しているため少しコード量が多くなっています。
WebserverからObject Storageにアクセスするための仕込みを入れているので複雑な構成になってしまいました。

provider.tf
############################
# Provider
############################
terraform {
  required_providers {
    oci = {
      source = "oracle/oci"
    }

    time = {
      source = "hashicorp/time"
    }
  }
}

provider "oci" {
  tenancy_ocid     = var.tenancy_ocid
  user_ocid        = var.user_ocid
  fingerprint      = var.fingerprint
  private_key_path = var.private_key_path
  region           = "ap-tokyo-1"
}

動的グループとIAMポリシーの作成を優先して、Webserver起動を遅らせます。

instance_principal.tf
############################
# ObjectStorageアップロード用Instance Principalの認証設定(Dynamic Group作成)
############################
resource "oci_identity_dynamic_group" "web_dg" {
  name           = "webserver-dg"
  description    = "Dynamic group for webserver instance"
  compartment_id = var.tenancy_ocid

  matching_rule = "ALL {instance.compartment.id = '${var.compartment_ocid}'}"
}


############################
# ObjectStorageアップロード用Instance Principalの認証設定(IAMポリシー作成)
############################
resource "oci_identity_policy" "web_policy" {
  compartment_id = var.tenancy_ocid
  name           = "webserver-objectstorage-policy"
  description    = "Allow webserver to access Object Storage"

  statements = [
    "Allow dynamic-group webserver-dg to read buckets in compartment id ${var.compartment_ocid}",
    "Allow dynamic-group webserver-dg to manage objects in compartment id ${var.compartment_ocid}",
  ]
}

############################
# 起動後直後はバケットと接続ができないのでwebserver作成を少し遅延
############################
resource "time_sleep" "wait_iam" {
  depends_on = [
    oci_identity_dynamic_group.web_dg,
    oci_identity_policy.web_policy
  ]

  create_duration = "60s"
}

Webserverに仕込みを入れる関係でE2.1.Microを入れようとしましたが、スペックが足りずフリーズやdnfのkillが頻発したのでE3.Flexに変更しました。
(OCIのリソース枯渇でA1.FrexやE4.Flexはプロビジョニングできませんでした)

compute.tf
############################
# Compute Image
############################
data "oci_core_images" "oracle_linux" {
  compartment_id           = var.compartment_ocid
  operating_system         = "Oracle Linux"
  operating_system_version = "10"
  shape                    = "VM.Standard.E2.1.Micro"

  sort_by    = "TIMECREATED"
  sort_order = "DESC"
}

data "oci_core_images" "oracle_linux_full" {
  compartment_id             = var.compartment_ocid
  operating_system           = "Oracle Linux"
  operating_system_version   = "10"
  shape                      = "VM.Standard.E3.Flex"

  sort_by    = "TIMECREATED"
  sort_order = "DESC"
}

locals {
  selected_image_id = length(data.oci_core_images.oracle_linux_full.images) > 0 ? data.oci_core_images.oracle_linux_full.images[0].id : null
}
############################
# Compute Instance(Bastion)
############################
resource "oci_core_instance" "bastion" {
  availability_domain = var.ad_name
  compartment_id      = var.compartment_ocid
  shape               = "VM.Standard.E2.1.Micro"
  display_name        = "bastion"

  create_vnic_details {
    subnet_id        = oci_core_subnet.public.id
    assign_public_ip = true
    nsg_ids          = [oci_core_network_security_group.bastion.id]
  }

  metadata = {
    ssh_authorized_keys = file(var.ssh_public_key_path)
    user_data = base64encode(<<-EOF
      #cloud-config
      package_update: false

      bootcmd:
        - fallocate -l 2G /swapfile
        - chmod 600 /swapfile
        - mkswap /swapfile
        - swapon /swapfile
    EOF
    )
  }

  source_details {
    source_type = "image"
    source_id   = data.oci_core_images.oracle_linux.images[0].id
    boot_volume_size_in_gbs = 50
  }
}


############################
# Compute Instance(WebServer)
############################
resource "oci_core_instance" "webserver" {
  availability_domain = var.ad_name
  compartment_id      = var.compartment_ocid
  #shape               = "VM.Standard.E2.1.Micro" #Free
  #shape               = "VM.Standard.A1.Flex"    #Free
  shape                    = "VM.Standard.E3.Flex"  #有料
  #shape               = "VM.Standard.E4.Flex"     #有料

  # Instance Principal接続で動的グループ、ポリシー作成直後だと紐づくのに数十分かかるので動的グループ、ポリシーを先に作成して60s遅延を入れる
  depends_on = [
    time_sleep.wait_iam
  ]

  # E3.Flex用
  shape_config {
    ocpus         = 1
    memory_in_gbs = 4
  }

  instance_options {
    are_legacy_imds_endpoints_disabled = false
  }

  create_vnic_details {
    subnet_id = oci_core_subnet.private.id
    assign_public_ip = false
    nsg_ids   = [oci_core_network_security_group.web.id]
  }

  metadata = {
    ssh_authorized_keys = file(var.ssh_public_key_path)
    user_data = filebase64("cloud-init-oci-cli.yaml")
  }

  source_details {
    source_type = "image"
    source_id   = local.selected_image_id
    boot_volume_size_in_gbs = 50 # E3.Flex用
  }

  display_name = "webserver"

  freeform_tags = {
    role = "webserver"
  }
}
network.tf
############################
# VCN
############################
resource "oci_core_vcn" "main" {
  cidr_block     = "10.0.0.0/16"
  compartment_id = var.compartment_ocid
  display_name   = "prod_vcn"
}

############################
# IGW
############################
resource "oci_core_internet_gateway" "igw" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id
  display_name   = "igw"
}


############################
# NAT GW
############################
resource "oci_core_nat_gateway" "nat" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id
}

############################
# SGW (Object Storage用)
############################
data "oci_core_services" "all" {}

locals {
  object_storage_service_id = one([
    for s in data.oci_core_services.all.services :
    s.id
    if strcontains(s.cidr_block, "objectstorage")
  ])
}

resource "oci_core_service_gateway" "sgw" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id
  display_name   = "sgw"

  services {
    service_id = local.object_storage_service_id
  }
}


############################
# Route Table
############################
resource "oci_core_route_table" "public_rt" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id
  display_name   = "public_rt"

  route_rules {
    destination       = "0.0.0.0/0"
    destination_type  = "CIDR_BLOCK"
    network_entity_id = oci_core_internet_gateway.igw.id
  }
}

locals {
  object_storage = one([
    for s in data.oci_core_services.all.services :
    s
    if strcontains(s.cidr_block, "objectstorage")
  ])
}

resource "oci_core_route_table" "private_rt" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id
  display_name   = "private_rt"

  route_rules {
    destination       = local.object_storage.cidr_block
    destination_type  = "SERVICE_CIDR_BLOCK"
    network_entity_id = oci_core_service_gateway.sgw.id
  }
  route_rules {
    destination       = "0.0.0.0/0"
    destination_type  = "CIDR_BLOCK"
    network_entity_id = oci_core_nat_gateway.nat.id
  }
}


############################
# Subnet
############################
resource "oci_core_subnet" "public" {
  cidr_block                 = "10.0.10.0/24"
  vcn_id                     = oci_core_vcn.main.id
  compartment_id             = var.compartment_ocid
  route_table_id             = oci_core_route_table.public_rt.id
  prohibit_public_ip_on_vnic = false
  display_name               = "public_subnet"
}

resource "oci_core_subnet" "private" {
  cidr_block                 = "10.0.20.0/24"
  vcn_id                     = oci_core_vcn.main.id
  compartment_id             = var.compartment_ocid
  route_table_id             = oci_core_route_table.private_rt.id
  prohibit_public_ip_on_vnic = true
  display_name               = "private_subnet"
}

NSGはBastionのIngress/Egress、WebserverのIngress/Egressで分けています。

nsg.tf
############################
# NSG(Bastion)
############################
resource "oci_core_network_security_group" "bastion" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id
  display_name   = "bastion_nsg"
}

resource "oci_core_network_security_group_security_rule" "bastion_ingress" {
  for_each = {
    for rule in var.bastion_ingress_rules :
    "${rule.protocol}-${rule.min}" => rule
  }

  network_security_group_id = oci_core_network_security_group.bastion.id
  direction                 = "INGRESS"
  protocol                  = each.value.protocol
  source_type               = "CIDR_BLOCK"  # 固定
  source                    = each.value.source

  # TCP
  dynamic "tcp_options" {
    for_each = each.value.protocol == "6" && try(each.value.min, null) != null ? [1] : []
    content {
      destination_port_range {
        min = each.value.min
        max = each.value.max
      }
    }
  }

  # UDP
  dynamic "udp_options" {
    for_each = each.value.protocol == "17" && try(each.value.min, null) != null ? [1] : []
    content {
      destination_port_range {
        min = each.value.min
        max = each.value.max
      }
    }
  }

  # ICMP
  dynamic "icmp_options" {
    for_each = each.value.protocol == "1" ? [1] : []
    content {
      type = try(each.value.icmp_type, null)
      code = try(each.value.icmp_code, null)
    }
  }
}

resource "oci_core_network_security_group_security_rule" "bastion_egress" {
  for_each = {
    for rule in var.bastion_egress_rules :
    "${rule.protocol}-${rule.min}" => rule
  }

  network_security_group_id = oci_core_network_security_group.bastion.id
  direction                 = "EGRESS"
  protocol                  = each.value.protocol
  destination               = each.value.destination
  destination_type          = "CIDR_BLOCK"  # 固定

  # TCP
  dynamic "tcp_options" {
    for_each = each.value.protocol == "6" && try(each.value.min, null) != null ? [1] : []
    content {
      destination_port_range {
        min = each.value.min
        max = each.value.max
      }
    }
  }

  # UDP
  dynamic "udp_options" {
    for_each = each.value.protocol == "17" && try(each.value.min, null) != null ? [1] : []
    content {
      destination_port_range {
        min = each.value.min
        max = each.value.max
      }
    }
  }

  # ICMP
  dynamic "icmp_options" {
    for_each = each.value.protocol == "1" ? [1] : []
    content {
      type = try(each.value.icmp_type, null)
      code = try(each.value.icmp_code, null)
    }
  }
}

############################
# NSG(WebServer)
############################
resource "oci_core_network_security_group" "web" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id
  display_name   = "web_nsg"
}

locals {
  bastion_nsg_id = oci_core_network_security_group.bastion.id
}

resource "oci_core_network_security_group_security_rule" "web_ingress" {
  for_each = {
    for rule in var.web_ingress_rules :
    "${rule.protocol}-${rule.min}" => rule
  }

  network_security_group_id = oci_core_network_security_group.web.id
  direction                 = "INGRESS"
  protocol                  = each.value.protocol
  source_type               = each.value.source_type
  source                    = each.value.source == "bastion" ? local.bastion_nsg_id : each.value.source

  # TCP
  dynamic "tcp_options" {
    for_each = each.value.protocol == "6" && try(each.value.min, null) != null ? [1] : []
    content {
      destination_port_range {
        min = each.value.min
        max = each.value.max
      }
    }
  }

  # UDP
  dynamic "udp_options" {
    for_each = each.value.protocol == "17" && try(each.value.min, null) != null ? [1] : []
    content {
      destination_port_range {
        min = each.value.min
        max = each.value.max
      }
    }
  }

  # ICMP
  dynamic "icmp_options" {
    for_each = each.value.protocol == "1" ? [1] : []
    content {
      type = try(each.value.icmp_type, null)
      code = try(each.value.icmp_code, null)
    }
  }
}

resource "oci_core_network_security_group_security_rule" "web_egress" {
  for_each = {
    for rule in var.web_egress_rules :
    "${rule.protocol}-${rule.min}" => rule
  }

  network_security_group_id = oci_core_network_security_group.web.id
  direction        = "EGRESS"
  protocol         = each.value.protocol
  destination      = each.value.destination
  destination_type = "CIDR_BLOCK"

  # TCP
  dynamic "tcp_options" {
    for_each = each.value.protocol == "6" && try(each.value.min, null) != null ? [1] : []
    content {
      destination_port_range {
        min = each.value.min
        max = each.value.max
      }
    }
  }

  # UDP
  dynamic "udp_options" {
    for_each = each.value.protocol == "17" && try(each.value.min, null) != null ? [1] : []
    content {
      destination_port_range {
        min = each.value.min
        max = each.value.max
      }
    }
  }

  # ICMP
  dynamic "icmp_options" {
    for_each = each.value.protocol == "1" ? [1] : []
    content {
      type = try(each.value.icmp_type, null)
      code = try(each.value.icmp_code, null)
    }
  }
}

実運用で使う場合は sample.txt などの固有名詞は変数化したほうが管理しやすそうですね

objectstorage.tf
############################
# Object Storage
############################
resource "oci_objectstorage_bucket" "prod_bucket" {
  compartment_id = var.compartment_ocid
  namespace      = var.objectstorage_namespace
  name           = "prod-private-bucket"
  access_type    = "NoPublicAccess"
}

resource "oci_objectstorage_object" "prod_upload" {
  namespace = var.objectstorage_namespace
  bucket    = oci_objectstorage_bucket.prod_bucket.name
  object    = "sample.txt"
  source = "sample.txt" # ローカルファイルパス
}
variables.tf
############################
# 変数定義
############################
variable "tenancy_ocid" {}
variable "user_ocid" {}
variable "fingerprint" {}
variable "private_key_path" {}
variable "compartment_ocid" {}
variable "ad_name" {}
variable "objectstorage_namespace" {}
variable "ssh_public_key_path" {}

variable "bastion_ingress_rules" {
  type = list(object({
    protocol = string
    source   = string
    min      = number
    max      = number
  }))
}

variable "bastion_egress_rules" {
  type = list(object({
    protocol    = string
    destination = string
    min         = number
    max         = number
  }))
}

variable "web_ingress_rules" {
  type = list(object({
    protocol    = string
    source_type = string
    source      = string
    min         = number
    max         = number
  }))
}

variable "web_egress_rules" {
  type = list(object({
    protocol    = string
    destination = string
    min         = number
    max         = number
  }))
}
terraform.tfvars
############################
# 変数設定
############################
bastion_ingress_rules = [
  {
    protocol    = "6"
    source_type = "CIDR_BLOCK"
    source      = "0.0.0.0/0"
    min         = 22
    max         = 22
  }
]

bastion_egress_rules = [
  {
    protocol         = "6"
    destination_type = "CIDR_BLOCK"
    destination      = "10.0.20.0/24"
    min              = 22
    max              = 22
  }
]

web_ingress_rules = [
  {
    protocol    = "6"
    source_type = "NETWORK_SECURITY_GROUP"
    source      = "bastion"
    min         = 22
    max         = 22
  },
  {
    protocol    = "6"
    source_type = "CIDR_BLOCK"
    source      = "10.0.10.0/24"
    min         = 80
    max         = 80
  }
]

web_egress_rules = [
  {
    protocol         = "6"
    destination_type = "CIDR_BLOCK"
    destination      = "0.0.0.0/0"
    min              = 443
    max              = 443
  },
  {
    protocol         = "6"
    destination_type = "CIDR_BLOCK"
    destination      = "0.0.0.0/0"
    min              = 80
    max              = 80
  },
  {
    protocol    = "17" # UDP
    destination_type = "CIDR_BLOCK"
    destination = "0.0.0.0/0"
    min = 53
    max = 53
  }
]
outputs.tf
############################
# Output(コンソールにIPを表示)
############################
output "bastion_public_ip" {
  value = oci_core_instance.bastion.public_ip
}

output "web_private_ip" {
  value = oci_core_instance.webserver.private_ip
}

Webserverで実行させるCloud-init設定になります。
WebserverからObjectStorageにOCI CLIを使ってファイルをputします。

cloud-init-oci-cli.yaml
#cloud-config
package_update: false
package_upgrade: false
packages:
  - python3
  - python3-pip
  - unzip
  - curl
  - nginx

bootcmd:
  - fallocate -l 2G /swapfile
  - chmod 600 /swapfile
  - mkswap /swapfile
  - swapon /swapfile

runcmd:
  - |
    cat <<'EOF' > /home/opc/setup_oci.sh
    #!/bin/bash
    # OCI CLIインストール
    curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh -o /tmp/oci_install.sh
    bash /tmp/oci_install.sh --accept-all-defaults --install-dir /home/opc/oci

    # OCI CLIインストール確認
    /home/opc/oci/bin/oci --version

    # OCI CLI永続化
    export PATH=$PATH:/home/opc/oci/bin;
    echo export PATH=\$PATH:/home/opc/oci/bin >> /home/opc/.bashrc

    # Instance Principal 確認
    NAMESPACE=$(/home/opc/oci/bin/oci os ns get --auth instance_principal | jq -r '.data')
    echo "Namespace: $NAMESPACE"

    # UPLOAD用フォルダ作成
    mkdir -p /home/opc/upload

    # アップロード
    BUCKET_NAME=prod-private-bucket
    LOCAL_FILE="/home/opc/upload/sample_private_subnet.txt"
    OBJECT_NAME="sample_private_subnet.txt"
    echo "PrivateCloud->Objectstorage" > $LOCAL_FILE
    chown -R opc:opc /home/opc/upload
    /home/opc/oci/bin/oci os object put --namespace $NAMESPACE --bucket-name $BUCKET_NAME --name $OBJECT_NAME --file $LOCAL_FILE --auth instance_principal
    EOF

  # 実行権限を付与
  - chmod +x /home/opc/setup_oci.sh
  # スクリプト実行
  - /home/opc/setup_oci.sh

  # Nginxインストール
  - dnf module enable -y nginx:1.20
  - dnf install -y nginx
  - systemctl enable nginx
  - systemctl start nginx
  - firewall-cmd --add-service http
  - firewall-cmd --add-service http --permanent

Terraform実行

Terraformの実行はAWS作成するときとaws-vaultを挟まないので、コマンドは少し違いますが内部はdocker composeで動作するため出力結果や確認方法は同じになります。

sh-5.2$ # Terraform初期化
sh-5.2$ docker compose run --rm terraform init
sh-5.2$ # 現在の管理状態と実際のクラウド環境の差分比較
sh-5.2$ docker compose run --rm terraform plan
sh-5.2$ # 設定差分を反映
sh-5.2$ docker compose run --rm terraform apply -auto-approve
sh-5.2$ # compute instanceの再作成
sh-5.2$ docker compose run --rm terraform apply -auto-approve -replace=oci_core_instance.[instance名]
sh-5.2$ # 管理しているリソース一覧を表示
sh-5.2$ docker compose run --rm terraform state list
sh-5.2$ # 管理しているリソースを削除
sh-5.2$ docker compose run --rm terraform destroy -auto-approve

GUI確認

・Computeインスタンス

・ObjectStorage
sample.txt・・・AWS端末からAPIキー経由でアップロード
sample_private_subnet.txt・・・Private Subnet内のComputeからInstance Principal経由でアップロード

通信確認

SSH接続(Bastion → Webserver)

同じ秘密鍵を利用するので鍵をコピーします。

sh-5.2$ sudo cat .ssh/id_ed25519
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END OPENSSH PRIVATE KEY-----
sh-5.2$
sh-5.2$ sudo ssh -i .ssh/id_ed25519 opc@xxx.xxx.xxx.xxx
The authenticity of host 'xxx.xxx.xxx.xxx (xxx.xxx.xxx.xxx)' can't be established.
ED25519 key fingerprint is SHA256:G/pM0M82cxCS6kfzQfUxKhyRk30O0o5ZvSAT+D791Tk.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'xxx.xxx.xxx.xxx' (ED25519) to the list of known hosts.
[opc@bastion ~]$
[opc@bastion ~]$ #秘密鍵を貼り付け、権限変更
[opc@bastion ~]$ sudo vi id_ed25519
[opc@bastion ~]$ sudo chmod 600 id_ed25519
[opc@bastion ~]$
[opc@bastion ~]$ sudo ssh -i id_ed25519 opc@10.0.20.188
The authenticity of host '10.0.20.188 (10.0.20.188)' can't be established.
ED25519 key fingerprint is SHA256:XKLGvvPfGO9HhGc7V4WQl/TcU0ztNJ2tmtcw1b3wPCk.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.0.20.188' (ED25519) to the list of known hosts.
[opc@webserver ~]$

HTTP確認

Nginxがインストール出来ていればNAT Gatewayは問題ありません。

[opc@bastion ~]$ curl -XGET http://10.0.20.188
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
        <head>
                <title>Test Page for the HTTP Server on Oracle Linux</title>
                <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
                <style type="text/css">
                        /*<![CDATA[*/
                        body {
                                background-color: #fff;
                                color: #000;
                                font-size: 0.9em;
                                font-family: sans-serif,helvetica;
                                margin: 0;
                                padding: 0;
                        }
                        ~~省略~~
[opc@bastion ~]$

SSH接続できない時

・インスタンス >OS管理 >Cloud Shell接続の起動
コンソール接続した時と同じ情報が表示できるので接続できないときはコンソールから確認しましょう。
VM.Standard.E2.1.Microの場合、スペックが低いためSSH接続できるようになるまで30分くらいかかります。

最後に

Instance PrincipalでのObjectStorageへのアクセス部分で沼にはまり、1週間程切り分けをしていました。
宣言型で実行順番を気にしない特徴のTerraformですが、クラウド仕様によっては実行順番を制御しなくてはいけないと勉強になりました。
AWSやAzureの表面上しか触っていないので何とも言えないですが、OCIはAPIキーを揃えたり、OCIDでアクセスしたりとTerraformで管理するのは初心者では厳しいなと感じました。