概要
- 先日、興味深い記事がバズってました
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 に登録すればいいです