Marco Braga
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # TODO: title of blog > TODO: content of blog post --- <!-- DRAFT/The following steps are scratch/sections based in the original infra onboarding guide (desired state to reorder the OCI steps described in the bottom of this page) https://docs.providers.openshift.org/platform-external/installing/ --> ## Section 1. Setup OpenShift configuration ### Create the install-config.yaml ### Create the manifests #### Customize manifest for CCM (optional) #### Patch the Infrastructure Object #### Create MachineConfig for Kubelet Provider ID ### Create ignition files ## Section 2. Create Infrastructure resources ### Identity ### Network ### DNS ### Load Balancers ### Create compute nodes #### Upload the RHCOS image #### Bootstrap #### Control Plane #### Compute/workers ## Section 3. Deploy Cloud Controller Manager (CCM) ## Review the installation --- <!-- DRAFT/The following steps are copyed 'as-is' from the Infrastructure Onboarding Guide --> # Use case of installing a cluster with external platform type in Oracle Cloud Infrastructure This use case provides details of how to deploy an OpenShift cluster using external platform type in Oracle Cloud Infrastructure (OCI), deploying providers' Cloud Controller Manager (CCM). The guide derives from ["Installing a cluster on any platform"](https://docs.openshift.com/container-platform/4.13/installing/installing_platform_agnostic/installing-platform-agnostic.html) documentation, adapted to the external platform type. The steps provide low-level details to customize Oracle's components like CCM. This guide is organized into three sections: - Section 1: Create infrastructure resources (Network, DNS, and Load Balancer), mostly required before OCI CCM configuration. - Section 2: Create OpenShift configurations with customized resources (OCI CCM) - Section 3: Create the compute nodes and review the installation If you are exploring how to customize the OpenShift platform external type with CCM, without deploying the whole cluster creation in OCI, feel free to jump to `Section 2`. Section 1 and 3 are mostly OCI-specific, and it is valuable for readers exploring in detail the OCI manual deployment. !!! tip "Automation options" The goal of this document is to provide details of the platform external type, without focusing on the infrastructure automation. The tool used to provision the resources described in this guide is the Oracle Cloud CLI. Alternatively, the automation can be achieved using official [Ansible](https://docs.oracle.com/en-us/iaas/tools/oci-ansible-collection/4.25.0/index.html) or [Terraform](https://registry.terraform.io/providers/oracle/oci/latest/docs) modules. !!! danger "Unsupported Document" This guide is created only for Red Hat partners or providers aiming to extend external components in OpenShift, and should not be used as an official or supported OpenShift installation method. Please review the product documentation to get the supported path. Table of Contents - [Prerequisites](#prerequisites) - [Section 1. Create Infrastructure resources](#section-1-create-infrastructure-resources) - [Identity](#identity) - [Network](#network) - [DNS](#dns) - [Load Balancer](#load-balancer) - [Section 2. Preparing the installation](#section-2-preparing-the-installation) - [Create install-config.yaml](#create-the-installer-configuration) - [Create manifests](#create-manifests) - [Create manifests for CCM](#create-manifests-for-oci-cloud-controller-manager) - [Create custom manifests for Kubelet](#create-custom-manifests-for-kubelet) - [Create ignition files](#create-ignition-files) - [Section 3. Create the cluster](#section-3-create-the-cluster) - [Cluster nodes](#cluster-nodes) - [Upload the RHCOS image](#upload-the-rhcos-image) - [Bootstrap](#bootstrap) - [Control Plane](#control-plane) - [Compute](#computeworkers) - [Review the installation](#review-the-installation) ## Prerequisites ### Clients #### OpenShift clients Download the OpenShift CLI and installer: - [Navigate to the release controller](https://openshift-release.apps.ci.l2s4.p1.openshiftapps.com/#4-dev-preview) and choose the release image: !!! tip "Credentials" The [Red Hat Cloud credential (Pull secret)](https://console.redhat.com/openshift/install/metal/agent-based) is required to pull from the repository `quay.io/openshift-release-dev/ocp-release`. Alternatively, you can provide the option `-a /path/to/pull-secret.json`. The examples in this document export the path of the pull secret to the environment variable `PULL_SECRET_FILE`. !!! warning "Supported OpenShift versions" The Platform External is available in OpenShift 4.14+. - Extract the tools (clients): ```sh oc adm release extract -a $PULL_SECRET_FILE \ --tools "quay.io/openshift-release-dev/ocp-release:4.14.0-rc.7-x86_64" ``` - Extract the tarball files: ```sh tar xvfz openshift-client-*.tar.gz tar xvfz openshift-install-*.tar.gz ``` Move the binaries `openshift-install` and `oc` to any directory exported in the `$PATH`. #### OCI Command Line Interface The OCI CLI is used in this guide to create infrastructure resources in the OCI. - [Install the CLI](https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/cliinstall.htm#InstallingCLI__linux_and_unix): ```sh python3.9 -m venv ./venv-oci && source ./venv-oci/bin/activate pip install oci-cli ``` - [Setup the user](https://docs.oracle.com/en-us/iaas/tools/oci-ansible-collection/4.25.0/guides/authentication.html#api-key-authentication) (Using [Console](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm#two)) #### Utilities - Download [jq](https://jqlang.github.io/jq/download/): used to filter the results returned by CLI - Download [yq](https://github.com/mikefarah/yq/releases/tag/v4.34.1): used to patch the `yaml` manifests. ~~~bash wget -O yq "https://github.com/mikefarah/yq/releases/download/v4.34.1/yq_linux_amd64" chmod u+x yq ~~~ - Download [butane](https://github.com/coreos/butane): used to create MachineConfig files. ~~~bash wget -O butane "https://github.com/coreos/butane/releases/download/v0.18.0/butane-x86_64-unknown-linux-gnu" chmod u+x butane ~~~ ### Setup the Provider Account A user with administrator access was used to create the OpenShift cluster described in this use case. The cluster was created in a dedicated compartment in Oracle Cloud Infrastructure, it allows the creation of custom policies for components like Cloud Controller Manager. The following steps describe how to create an compartment from any nested level, and create predefined tags used to apply policies to the compartment: - Set the compartment id variables: ```sh # A new compartment will be created as a child of this: PARENT_COMPARTMENT_ID="<ocid1.compartment.oc1...>" # Cluster Name CLUSTER_NAME="ocp-oci-demo" # DNS information BASE_DOMAIN=example.com DNS_COMPARTMENT_ID="<ocid1.compartment.oc1...>" ``` - Create cluster compartment - child of `${PARENT_COMPARTMENT_ID}`: ```sh COMPARTMENT_NAME_OPENSHIFT="$CLUSTER_NAME" COMPARTMENT_ID_OPENSHIFT=$(oci iam compartment create \ --compartment-id "$PARENT_COMPARTMENT_ID" \ --description "$COMPARTMENT_NAME_OPENSHIFT compartment" \ --name "$COMPARTMENT_NAME_OPENSHIFT" \ --wait-for-state ACTIVE \ --query data.id --raw-output) ``` ## Section 1. Create Infrastructure resources ### Identity There are two methods to provide authentication to Cloud Controller Manager to access the Cloud API: - User - Instance Principals The steps described in this document are using [Instance Principals](https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm). Instance principals require extra steps to grant permissions to the Instances to access the APIs. The steps below describe how to create the namespace tags, used in the Dynamic Group rule filtering only the Control Plane nodes to take actions defined in the compartment's Policy. Steps: - Create managed tags: ```sh TAG_NAMESPACE_ID=$(oci iam tag-namespace create \ --compartment-id "${COMPARTMENT_ID_OPENSHIFT}" \ --description "Cluster Name" \ --name "$CLUSTER_NAME" \ --wait-for-state ACTIVE \ --query data.id --raw-output) oci iam tag create \ --description "OpenShift Node Role" \ --name "role" \ --tag-namespace-id "$TAG_NAMESPACE_ID" \ --validator '{"validatorType":"ENUM","values":["master","worker"]}' ``` - Create Dynamic Group with name `demo-${CLUSTER_NAME}-controlplane` with the following rule: ```sh DYNAMIC_GROUP_NAME="${CLUSTER_NAME}-controlplane" oci iam dynamic-group create \ --name "${DYNAMIC_GROUP_NAME}" \ --description "Control Plane nodes for ${CLUSTER_NAME}" \ --matching-rule "Any {instance.compartment.id='$COMPARTMENT_ID_OPENSHIFT', tag.${CLUSTER_NAME}.role.value='master'}" \ --wait-for-state ACTIVE ``` - Create a policy allowing the Dynamic Group `$DYNAMIC_GROUP_NAME` access resources in the cluster compartment (`$COMPARTMENT_NAME_OPENSHIFT`): ```sh POLICY_NAME="${CLUSTER_NAME}-cloud-controller-manager" oci iam policy create --name $POLICY_NAME \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --description "Allow Cloud Controller Manager in OpenShift access Cloud Resources" \ --statements "[ \"Allow dynamic-group $DYNAMIC_GROUP_NAME to manage volume-family in compartment $COMPARTMENT_NAME_OPENSHIFT\", \"Allow dynamic-group $DYNAMIC_GROUP_NAME to manage instance-family in compartment $COMPARTMENT_NAME_OPENSHIFT\", \"Allow dynamic-group $DYNAMIC_GROUP_NAME to manage security-lists in compartment $COMPARTMENT_NAME_OPENSHIFT\", \"Allow dynamic-group $DYNAMIC_GROUP_NAME to use virtual-network-family in compartment $COMPARTMENT_NAME_OPENSHIFT\", \"Allow dynamic-group $DYNAMIC_GROUP_NAME to manage load-balancers in compartment $COMPARTMENT_NAME_OPENSHIFT\"]" ``` !!! tip "Helper" OCI CLI documentation for [`oci iam policy create`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.29.1/oci_cli_docs/cmdref/iam/policy/create.html) OCI Console path: `Menu > Identity & Security > Policies > (Select the Compartment 'openshift') > Create Policy > Name=openshift-oci-cloud-controller-manager` ### Network The OCI VCN (Virtual Cloud Network) must be created using the [Networking requirements for user-provisioned infrastructure](https://docs.openshift.com/container-platform/4.13/installing/installing_platform_agnostic/installing-platform-agnostic.html#installation-network-user-infra_installing-platform-agnostic). !!! tip "Info" The resource name provided in this guide is not a standard but follows a similar naming convention created by the installer in the supported cloud providers. The names will also be used in future sections to discover resources. Create the VCN and dependencies with the following configuration: | Resource | Name | Attributes | Note | | -- | -- | -- | -- | | VCN | `${CLUSTER_NAME}-vcn` | CIDR 10.0.0.0/16 | | | Subnet | `${CLUSTER_NAME}-net-public` | 10.0.0.0/20 | Regional,Resolve DNS (pub) | | Subnet | `${CLUSTER_NAME}-net-private` | 10.0.128.0/20 | Regional,Resolve DNS (priv) | | Internet Gateway | `${CLUSTER_NAME}-igw` | -- | Attached to public route table | | NAT Gateway | `${CLUSTER_NAME}-natgw` | -- | Attached to private route table | | Route Table | `${CLUSTER_NAME}-rtb-public` | `0/0` to `igw` | -- | | Route Table | `${CLUSTER_NAME}-rtb-private` | `0/0` to `natgw` | -- | | NSG | `${CLUSTER_NAME}-nsg-nlb` | -- | Attached to Load Balancer | | NSG | `${CLUSTER_NAME}-nsg-controlplane` | -- | Attached to Control Plane nodes | | NSG | `${CLUSTER_NAME}-nsg-compute` | -- | Attached to Compute nodes | Steps: - VCN - Security List (need?) - IGW - NGW - Route Table: Private - Route Table: Public - Subnets - NSG ```sh # Base doc for network service # https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.30.2/oci_cli_docs/cmdref/network.html # VCN ## https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.30.2/oci_cli_docs/cmdref/network/vcn/create.html VCN_ID=$(oci network vcn create \ --compartment-id "${COMPARTMENT_ID_OPENSHIFT}" \ --display-name "${CLUSTER_NAME}-vcn" \ --cidr-block "10.0.0.0/20" \ --dns-label "ocp" \ --wait-for-state AVAILABLE \ --query data.id --raw-output) # IGW ## https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.30.2/oci_cli_docs/cmdref/network/internet-gateway/create.html IGW_ID=$(oci network internet-gateway create \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --display-name "${CLUSTER_NAME}-igw" \ --is-enabled true \ --wait-for-state AVAILABLE \ --vcn-id $VCN_ID \ --query data.id --raw-output) # NAT Gateway ## https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.30.2/oci_cli_docs/cmdref/network/nat-gateway/create.html NGW_ID=$(oci network nat-gateway create \ --compartment-id ${COMPARTMENT_ID_OPENSHIFT} \ --display-name "${CLUSTER_NAME}-natgw" \ --vcn-id $VCN_ID \ --wait-for-state AVAILABLE \ --query data.id --raw-output) # Route Table: Public ## https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.30.2/oci_cli_docs/cmdref/network/route-table/create.html RTB_PUB_ID=$(oci network route-table create \ --compartment-id ${COMPARTMENT_ID_OPENSHIFT} \ --vcn-id $VCN_ID \ --display-name "${CLUSTER_NAME}-rtb-public" \ --route-rules "[{\"cidrBlock\":\"0.0.0.0/0\",\"networkEntityId\":\"$IGW_ID\"}]" \ --wait-for-state AVAILABLE \ --query data.id --raw-output) # Route Table: Private RTB_PVT_ID=$(oci network route-table create \ --compartment-id ${COMPARTMENT_ID_OPENSHIFT} \ --vcn-id $VCN_ID \ --display-name "${CLUSTER_NAME}-rtb-private" \ --route-rules "[{\"cidrBlock\":\"0.0.0.0/0\",\"networkEntityId\":\"$NGW_ID\"}]" \ --wait-for-state AVAILABLE \ --query data.id --raw-output) # Subnet Public (regional) # https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.30.2/oci_cli_docs/cmdref/network/subnet/create.html SUBNET_ID_PUBLIC=$(oci network subnet create \ --compartment-id ${COMPARTMENT_ID_OPENSHIFT} \ --vcn-id $VCN_ID \ --display-name "${CLUSTER_NAME}-net-public" \ --dns-label "pub" \ --cidr-block "10.0.0.0/21" \ --route-table-id $RTB_PUB_ID \ --wait-for-state AVAILABLE \ --query data.id --raw-output) # Subnet Private (regional) SUBNET_ID_PRIVATE=$(oci network subnet create \ --compartment-id ${COMPARTMENT_ID_OPENSHIFT} \ --vcn-id $VCN_ID \ --display-name "${CLUSTER_NAME}-net-private" \ --dns-label "priv" \ --cidr-block "10.0.8.0/21" \ --route-table-id $RTB_PVT_ID \ --prohibit-internet-ingress true \ --prohibit-public-ip-on-vnic true \ --wait-for-state AVAILABLE \ --query data.id --raw-output) # NSGs (empty to allow be referenced in the rules) ## NSG Control Plane ## https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.30.2/oci_cli_docs/cmdref/network/nsg/create.html NSG_ID_CPL=$(oci network nsg create \ --compartment-id ${COMPARTMENT_ID_OPENSHIFT} \ --vcn-id $VCN_ID \ --display-name "${CLUSTER_NAME}-nsg-controlplane" \ --wait-for-state AVAILABLE \ --query data.id --raw-output) ## NSG Compute/workers NSG_ID_CMP=$(oci network nsg create \ --compartment-id ${COMPARTMENT_ID_OPENSHIFT} \ --vcn-id $VCN_ID \ --display-name "${CLUSTER_NAME}-nsg-compute" \ --wait-for-state AVAILABLE \ --query data.id --raw-output) ## NSG Load Balancers NSG_ID_NLB=$(oci network nsg create \ --compartment-id ${COMPARTMENT_ID_OPENSHIFT} \ --vcn-id $VCN_ID \ --display-name "${CLUSTER_NAME}-nsg-nlb" \ --wait-for-state AVAILABLE \ --query data.id --raw-output) # NSG Rules: Control Plane NSG ## https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.30.2/oci_cli_docs/cmdref/network/nsg/rules/add.html # oci network NSG rules add --generate-param-json-input security-rules cat <<EOF > ./oci-vcn-nsg-rule-nodes.json [ { "description": "allow all outbound traffic", "protocol": "all", "destination": "0.0.0.0/0", "destination-type": "CIDR_BLOCK", "direction": "EGRESS", "is-stateless": false }, { "description": "All from control plane NSG", "direction": "INGRESS", "is-stateless": false, "protocol": "all", "source": "$NSG_ID_CPL", "source-type": "NETWORK_SECURITY_GROUP" }, { "description": "All from control plane NSG", "direction": "INGRESS", "is-stateless": false, "protocol": "all", "source": "$NSG_ID_CMP", "source-type": "NETWORK_SECURITY_GROUP" }, { "description": "All from control plane NSG", "direction": "INGRESS", "is-stateless": false, "protocol": "all", "source": "$NSG_ID_NLB", "source-type": "NETWORK_SECURITY_GROUP" }, { "description": "allow ssh to nodes", "direction": "INGRESS", "is-stateless": false, "protocol": "6", "source": "0.0.0.0/0", "source-type": "CIDR_BLOCK", "tcp-options": { "destination-port-range": { "max": 22, "min": 22 } } } ] EOF oci network nsg rules add \ --nsg-id "${NSG_ID_CPL}" \ --security-rules file://oci-vcn-nsg-rule-nodes.json oci network nsg rules add \ --nsg-id "${NSG_ID_CMP}" \ --security-rules file://oci-vcn-nsg-rule-nodes.json # NSG Security rules for NSG cat <<EOF > ./oci-vcn-nsg-rule-nlb.json [ { "description": "allow Kube API", "direction": "INGRESS", "is-stateless": false, "source-type": "CIDR_BLOCK", "protocol": "6", "source": "0.0.0.0/0", "tcp-options": { "destination-port-range": { "max": 6443, "min": 6443 }} }, { "description": "allow Kube API to Control Plane", "destination": "$NSG_ID_CPL", "destination-type": "NETWORK_SECURITY_GROUP", "direction": "EGRESS", "is-stateless": false, "protocol": "6", "tcp-options":{"destination-port-range":{ "max": 6443, "min": 6443 }} }, { "description": "allow MCS listener from control plane pool", "direction": "INGRESS", "is-stateless": false, "protocol": "6", "source": "$NSG_ID_CPL", "source-type": "NETWORK_SECURITY_GROUP", "tcp-options": {"destination-port-range":{ "max": 22623, "min": 22623 }} }, { "description": "allow MCS listener from compute pool", "direction": "INGRESS", "is-stateless": false, "protocol": "6", "source": "$NSG_ID_CMP", "source-type": "NETWORK_SECURITY_GROUP", "tcp-options": {"destination-port-range": { "max": 22623, "min": 22623 }} }, { "description": "allow MCS listener access the Control Plane backends", "destination": "$NSG_ID_CPL", "destination-type": "NETWORK_SECURITY_GROUP", "direction": "EGRESS", "is-stateless": false, "protocol": "6", "tcp-options": {"destination-port-range": { "max": 22623, "min": 22623 }} }, { "description": "allow listener for Ingress HTTP", "direction": "INGRESS", "is-stateless": false, "source-type": "CIDR_BLOCK", "protocol": "6", "source": "0.0.0.0/0", "tcp-options": {"destination-port-range": { "max": 80, "min": 80 }} }, { "description": "allow listener for Ingress HTTPS", "direction": "INGRESS", "is-stateless": false, "source-type": "CIDR_BLOCK", "protocol": "6", "source": "0.0.0.0/0", "tcp-options": {"destination-port-range": { "max": 443, "min": 443 }} }, { "description": "allow backend access the Compute pool for HTTP", "destination": "$NSG_ID_CMP", "destination-type": "NETWORK_SECURITY_GROUP", "direction": "EGRESS", "is-stateless": false, "protocol": "6", "tcp-options": {"destination-port-range": { "max": 80, "min": 80 }} }, { "description": "allow backend access the Compute pool for HTTPS", "destination": "$NSG_ID_CMP", "destination-type": "NETWORK_SECURITY_GROUP", "direction": "EGRESS", "is-stateless": false, "protocol": "6", "tcp-options": {"destination-port-range": { "max": 443, "min": 443 }} } ] EOF oci network nsg rules add \ --nsg-id "${NSG_ID_NLB}" \ --security-rules file://oci-vcn-nsg-rule-nlb.json ``` ### Load Balancer Steps to create the OCI Network Load Balancer (NLB) to the cluster. A single NLB is created with listeners to Kubernetes API Server, Machine Config Server (MCS) and Ingress for HTTP and HTTPS. The MCS is the only one with internal access. The following resources will be created in the NLB: - Backend Sets (BSet): | BSet Name | Port | Health Check (Proto/Path/Interval/Timeout) | | -- | -- | -- | | `${CLUSTER_NAME}-api` | TCP/6443 | HTTPS`/readyz`/10/3 | | `${CLUSTER_NAME}-mcs` | TCP/22623 | HTTPS`/healthz`/10/3 | | `${CLUSTER_NAME}-http` | TCP/80 | TCP/80/10/3 | | `${CLUSTER_NAME}-https` | TCP/443 | TCP/443/10/3 | - Listeners: | Name | Port | BSet Name | | -- | -- | -- | | `${CLUSTER_NAME}-api` | TCP/6443 | `${CLUSTER_NAME}-api` | | `${CLUSTER_NAME}-mcs` | TCP/22623 | `${CLUSTER_NAME}-mcs` | | `${CLUSTER_NAME}-http` | TCP/80 | `${CLUSTER_NAME}-http` | | `${CLUSTER_NAME}-https` | TCP/443 | `${CLUSTER_NAME}-https` | Steps: - Get the Public Subnet ID - Get Security Group ID - Create NLB - Create BackendSets - Create Listeners ```sh # NLB base: https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.30.2/oci_cli_docs/cmdref/nlb.html # Create BackendSets ## Kubernetes API Server (KAS): api ## Machine Config Server (MCS): mcs ## Ingress HTTP ## Ingress HTTPS cat <<EOF > ./oci-nlb-backends.json { "${CLUSTER_NAME}-api": { "health-checker": { "interval-in-millis": 10000, "port": 6443, "protocol": "HTTPS", "retries": 3, "return-code": 200, "timeout-in-millis": 3000, "url-path": "/readyz" }, "ip-version": "IPV4", "is-preserve-source": false, "name": "${CLUSTER_NAME}-api", "policy": "FIVE_TUPLE" }, "${CLUSTER_NAME}-mcs": { "health-checker": { "interval-in-millis": 10000, "port": 22623, "protocol": "HTTPS", "retries": 3, "return-code": 200, "timeout-in-millis": 3000, "url-path": "/healthz" }, "ip-version": "IPV4", "is-preserve-source": false, "name": "${CLUSTER_NAME}-mcs", "policy": "FIVE_TUPLE" }, "${CLUSTER_NAME}-ingress-http": { "health-checker": { "interval-in-millis": 10000, "port": 80, "protocol": "TCP", "retries": 3, "timeout-in-millis": 3000 }, "ip-version": "IPV4", "is-preserve-source": false, "name": "${CLUSTER_NAME}-ingress-http", "policy": "FIVE_TUPLE" }, "${CLUSTER_NAME}-ingress-https": { "health-checker": { "interval-in-millis": 10000, "port": 443, "protocol": "TCP", "retries": 3, "timeout-in-millis": 3000 }, "ip-version": "IPV4", "is-preserve-source": false, "name": "${CLUSTER_NAME}-ingress-https", "policy": "FIVE_TUPLE" } } EOF cat <<EOF > ./oci-nlb-listeners.json { "${CLUSTER_NAME}-api": { "default-backend-set-name": "${CLUSTER_NAME}-api", "ip-version": "IPV4", "name": "${CLUSTER_NAME}-api", "port": 6443, "protocol": "TCP" }, "${CLUSTER_NAME}-mcs": { "default-backend-set-name": "${CLUSTER_NAME}-mcs", "ip-version": "IPV4", "name": "${CLUSTER_NAME}-mcs", "port": 22623, "protocol": "TCP" }, "${CLUSTER_NAME}-ingress-http": { "default-backend-set-name": "${CLUSTER_NAME}-ingress-http", "ip-version": "IPV4", "name": "${CLUSTER_NAME}-ingress-http", "port": 80, "protocol": "TCP" }, "${CLUSTER_NAME}-ingress-https": { "default-backend-set-name": "${CLUSTER_NAME}-ingress-https", "ip-version": "IPV4", "name": "${CLUSTER_NAME}-ingress-https", "port": 443, "protocol": "TCP" } } EOF # NLB create # https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.30.2/oci_cli_docs/cmdref/nlb/network-load-balancer/create.html NLB_ID=$(oci nlb network-load-balancer create \ --compartment-id ${COMPARTMENT_ID_OPENSHIFT} \ --display-name "${CLUSTER_NAME}-nlb" \ --subnet-id "${SUBNET_ID_PUBLIC}" \ --backend-sets file://oci-nlb-backends.json \ --listeners file://oci-nlb-listeners.json \ --network-security-group-ids "[\"$NSG_ID_NLB\"]" \ --is-private false \ --nlb-ip-version "IPV4" \ --wait-for-state ACCEPTED \ --query data.id --raw-output) ``` ### DNS Steps to create the resource records pointing to the API address (public and private), and to the default router. The following DNS records will be created: | Domain | Record | Value | | -- | -- | -- | | `${CLUSTER_NAME}`.`${BASE_DOMAIN}` | api | Public IP Address or DNS for the Load Balancer | | `${CLUSTER_NAME}`.`${BASE_DOMAIN}` | api-int | Private IP Address or DNS for the Load Balancer | | `${CLUSTER_NAME}`.`${BASE_DOMAIN}` | *.apps | Public IP Address or DNS for the Load Balancer | !!! tip "Helper" It's not required to have a publicly accessible API and DNS domain, alternatively, you can use a bastion host to access the private API endpoint. Steps: - Get Public IP for LB - Get Private IP for LB - Create records ```sh # NLB IPs ## https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.30.2/oci_cli_docs/cmdref/nlb/network-load-balancer/list.html ## Public NLB_IP_PUBLIC=$(oci nlb network-load-balancer list \ --compartment-id ${COMPARTMENT_ID_OPENSHIFT} \ --display-name "${CLUSTER_NAME}-nlb" \ | jq -r '.data.items[0]["ip-addresses"][] | select(.["is-public"]==true) | .["ip-address"]') ## Private NLB_IP_PRIVATE=$(oci nlb network-load-balancer list \ --compartment-id ${COMPARTMENT_ID_OPENSHIFT} \ --display-name "${CLUSTER_NAME}-nlb" \ | jq -r '.data.items[0]["ip-addresses"][] | select(.["is-public"]==false) | .["ip-address"]') # DNS record ## Assuming the zone already exists and is in DNS_COMPARTMENT_ID DNS_RECORD_APIINT="api-int.${CLUSTER_NAME}.${BASE_DOMAIN}" oci dns record rrset patch \ --compartment-id ${DNS_COMPARTMENT_ID} \ --domain "${DNS_RECORD_APIINT}" \ --rtype "A" \ --zone-name-or-id "${BASE_DOMAIN}" \ --scope GLOBAL \ --items "[{ \"domain\": \"${DNS_RECORD_APIINT}\", \"rdata\": \"${NLB_IP_PRIVATE}\", \"rtype\": \"A\", \"ttl\": 300 }]" DNS_RECORD_APIEXT="api.${CLUSTER_NAME}.${BASE_DOMAIN}" oci dns record rrset patch \ --compartment-id ${DNS_COMPARTMENT_ID} \ --domain "${DNS_RECORD_APIEXT}" \ --rtype "A" \ --zone-name-or-id "${BASE_DOMAIN}" \ --scope GLOBAL \ --items "[{ \"domain\": \"${DNS_RECORD_APIEXT}\", \"rdata\": \"${NLB_IP_PUBLIC}\", \"rtype\": \"A\", \"ttl\": 300 }]" DNS_RECORD_APPS="*.apps.${CLUSTER_NAME}.${BASE_DOMAIN}" oci dns record rrset patch \ --compartment-id ${DNS_COMPARTMENT_ID} \ --domain "${DNS_RECORD_APPS}" \ --rtype "A" \ --zone-name-or-id "${BASE_DOMAIN}" \ --scope GLOBAL \ --items "[{ \"domain\": \"${DNS_RECORD_APPS}\", \"rdata\": \"${NLB_IP_PUBLIC}\", \"rtype\": \"A\", \"ttl\": 300 }]" ``` ## Section 2. Preparing the installation This section describes how to set up OpenShift to customize the manifests used in the installation. ### Create the installer configuration Modify and export the variables used to build the `install-config.yaml` and the later steps: ```sh INSTALL_DIR=./install-dir mkdir -p $INSTALL_DIR SSH_PUB_KEY_FILE="${HOME}/.ssh/bundle.pub" PULL_SECRET_FILE="${HOME}/.openshift/pull-secret-latest.json" ``` #### Create install-config.yaml Create the `install-config.yaml` setting the platform type to `external`: ```sh cat <<EOF > ${INSTALL_DIR}/install-config.yaml apiVersion: v1 baseDomain: ${BASE_DOMAIN} metadata: name: "${CLUSTER_NAME}" platform: external: platformName: oci publish: External pullSecret: > $(cat ${PULL_SECRET_FILE}) sshKey: | $(cat ${SSH_PUB_KEY_FILE}) EOF ``` ### Create manifests ```sh openshift-install create manifests --dir $INSTALL_DIR ``` #### Create manifests for OCI Cloud Controller Manager The steps in this section describe how to customize the OpenShift installation providing the Cloud Controller Manager manifests to be added in the bootstrap process. !!! warning "Info" This guide is based on the OCI CCM v1.26.0. You must read the [project documentation](https://github.com/oracle/oci-cloud-controller-manager) for more information. Steps: - Create the namespace manifest: !!! danger "Important" Red Hat does not recommend creating resources in namespaces prefixed with `kube-*` and `openshift-*`. The custom namespace manifest must be created, and then deployment manifests must be adapted to use the custom namespace. See [the documentation](https://docs.openshift.com/container-platform/4.13/applications/projects/working-with-projects.html) for more information. ```sh OCI_CCM_NAMESPACE=oci-cloud-controller-manager cat <<EOF > ${INSTALL_DIR}/manifests/oci-00-ccm-namespace.yaml apiVersion: v1 kind: Namespace metadata: name: $OCI_CCM_NAMESPACE annotations: workload.openshift.io/allowed: management include.release.openshift.io/self-managed-high-availability: "true" labels: "pod-security.kubernetes.io/enforce": "privileged" "pod-security.kubernetes.io/audit": "privileged" "pod-security.kubernetes.io/warn": "privileged" "security.openshift.io/scc.podSecurityLabelSync": "false" "openshift.io/run-level": "0" "pod-security.kubernetes.io/enforce-version": "v1.24" EOF ``` <!-- !!! danger "TODO" - The Pod Admission Security must be reviewed aiming to use other than `privileged` - Set [critical pod annotation](https://kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/#rescheduler-guaranteed-scheduling-of-critical-add-ons) in this namespace. --> - Export the variables used to create the OCI CCM Cloud Config: ```sh OCI_CLUSTER_REGION=us-sanjose-1 # Review the defined vars cat <<EOF>/dev/stdout OCI_CLUSTER_REGION=$OCI_CLUSTER_REGION VCN_ID=$VCN_ID SUBNET_ID_PUBLIC=$SUBNET_ID_PUBLIC EOF ``` - Create the OCI CCM configuration as a secret stored in the install directory: ```sh cat <<EOF > ./oci-secret-cloud-provider.yaml auth: region: $OCI_CLUSTER_REGION useInstancePrincipals: true compartment: $COMPARTMENT_ID_OPENSHIFT vcn: $VCN_ID loadBalancer: securityListManagementMode: None subnet1: $SUBNET_ID_PUBLIC EOF cat <<EOF > ${INSTALL_DIR}/manifests/oci-01-ccm-00-secret.yaml --- apiVersion: v1 kind: Secret metadata: name: oci-cloud-controller-manager namespace: $OCI_CCM_NAMESPACE data: cloud-provider.yaml: $(base64 -w0 < ./oci-secret-cloud-provider.yaml) EOF ``` <!-- !!! warning "Question" - Is it possible to use NSG instead of SecList in Load Balancer? --> - Download manifests from [OCI CCM's Github](https://github.com/oracle/oci-cloud-controller-manager) and save it in the directory `${INSTALL_DIR}/manifests`: ```sh CCM_RELEASE=v1.26.0 wget https://github.com/oracle/oci-cloud-controller-manager/releases/download/${CCM_RELEASE}/oci-cloud-controller-manager-rbac.yaml -O oci-cloud-controller-manager-rbac.yaml wget https://github.com/oracle/oci-cloud-controller-manager/releases/download/${CCM_RELEASE}/oci-cloud-controller-manager.yaml -O oci-cloud-controller-manager.yaml ``` - Patch the RBAC file setting the correct namespace in the `ServiceAccount`: ```sh ./yq ". | select(.kind==\"ServiceAccount\").metadata.namespace=\"$OCI_CCM_NAMESPACE\"" oci-cloud-controller-manager-rbac.yaml > ./oci-cloud-controller-manager-rbac_patched.yaml ``` - Patch the RBAC file setting the correct namespace in the `ServiceAccount`: ```sh cat << EOF > ./oci-ccm-rbac_patch_crb-subject.yaml - kind: ServiceAccount name: cloud-controller-manager namespace: $OCI_CCM_NAMESPACE EOF ./yq eval-all -i ". | select(.kind==\"ClusterRoleBinding\").subjects *= load(\"oci-ccm-rbac_patch_crb-subject.yaml\")" ./oci-cloud-controller-manager-rbac_patched.yaml ``` - Split the RBAC manifest file: ```sh ./yq -s '"./oci-01-ccm-01-rbac_" + $index' ./oci-cloud-controller-manager-rbac_patched.yaml &&\ mv -v ./oci-01-ccm-01-rbac_*.yml ${INSTALL_DIR}/manifests/ ``` - Patch the CCM DaemonSet manifest setting the namespace, append the tolerations, and add env vars for the kube API URL used in OpenShift: <!-- TODO: create the expression to merge both paths into a single yq statement. --> ```sh cat <<EOF > ./oci-cloud-controller-manager-ds_patch1.yaml metadata: namespace: $OCI_CCM_NAMESPACE spec: template: spec: tolerations: - key: node.kubernetes.io/not-ready operator: Exists effect: NoSchedule EOF # Create the containers' env patch cat <<EOF > ./oci-cloud-controller-manager-ds_patch2.yaml spec: template: spec: containers: - env: - name: KUBERNETES_PORT value: "tcp://api-int.$CLUSTER_NAME.$BASE_DOMAIN:6443" - name: KUBERNETES_PORT_443_TCP value: "tcp://api-int.$CLUSTER_NAME.$BASE_DOMAIN:6443" - name: KUBERNETES_PORT_443_TCP_ADDR value: "api-int.$CLUSTER_NAME.$BASE_DOMAIN" - name: KUBERNETES_PORT_443_TCP_PORT value: "6443" - name: KUBERNETES_PORT_443_TCP_PROTO value: "tcp" - name: KUBERNETES_SERVICE_HOST value: "api-int.$CLUSTER_NAME.$BASE_DOMAIN" - name: KUBERNETES_SERVICE_PORT value: "6443" - name: KUBERNETES_SERVICE_PORT_HTTPS value: "6443" EOF # Merge required objects for the pod's template spec ./yq eval-all '. as $item ireduce ({}; . *+ $item)' oci-cloud-controller-manager.yaml oci-cloud-controller-manager-ds_patch1.yaml > oci-cloud-controller-manager-ds_patched1.yaml # Merge required objects for the pod's containers spec ./yq eval-all '.spec.template.spec.containers[] as $item ireduce ({}; . *+ $item)' oci-cloud-controller-manager-ds_patched1.yaml ./oci-cloud-controller-manager-ds_patch2.yaml > ./oci-cloud-controller-manager-ds_patched2.yaml # merge patches to ${INSTALL_DIR}/manifests/oci-01-ccm-02-daemonset.yaml ./yq eval-all '.spec.template.spec.containers[] *= load("./oci-cloud-controller-manager-ds_patched2.yaml")' oci-cloud-controller-manager-ds_patched1.yaml > ${INSTALL_DIR}/manifests/oci-01-ccm-02-daemonset.yaml ``` The following CCM manifest files must be created in the installation `manifests/` directory: ```sh $ tree $INSTALL_DIR/manifests/ [...] ├── oci-00-ccm-namespace.yaml ├── oci-01-ccm-00-secret.yaml ├── oci-01-ccm-01-rbac_0.yml ├── oci-01-ccm-01-rbac_1.yml ├── oci-01-ccm-01-rbac_2.yml ├── oci-01-ccm-02-daemonset.yaml [...] ``` #### Create custom manifests for Kubelet The Kubelet parameter `providerID` is the unique identifier of the instance in OCI. It must be set before the node is initialized by CCM using a custom MachineConfig. The Provider ID must be set dynamically for each node. The steps below describe how to create a MachineConfig object to create a systemd unit to create a kubelet configuration discovering the Provider ID in OCI by querying the [Instance Metadata Service (IMDS)](https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/gettingmetadata.htm). Steps: - Create the butane files for master and worker configurations: ```sh function create_machineconfig_kubelet() { local node_role=$1 cat << EOF > ./mc-kubelet-$node_role.bu variant: openshift version: 4.13.0 metadata: name: 00-$node_role-kubelet-providerid labels: machineconfiguration.openshift.io/role: $node_role storage: files: - mode: 0755 path: "/usr/local/bin/kubelet-providerid" contents: inline: | #!/bin/bash set -e -o pipefail NODECONF=/etc/systemd/system/kubelet.service.d/20-providerid.conf if [ -e "\${NODECONF}" ]; then echo "Not replacing existing \${NODECONF}" exit 0 fi PROVIDERID=\$(curl -H "Authorization: Bearer Oracle" -sL http://169.254.169.254/opc/v2/instance/ | jq -r .id); cat > "\${NODECONF}" <<EOF [Service] Environment="KUBELET_PROVIDERID=\${PROVIDERID}" EOF systemd: units: - name: kubelet-providerid.service enabled: true contents: | [Unit] Description=Fetch kubelet provider id from Metadata After=NetworkManager-wait-online.service Before=kubelet.service [Service] ExecStart=/usr/local/bin/kubelet-providerid Type=oneshot [Install] WantedBy=network-online.target EOF } create_machineconfig_kubelet "master" create_machineconfig_kubelet "worker" ``` - Process the butane files to `MachineConfig` objects: ```sh function process_butane() { local src_file=$1; shift local dest_file=$1 ./butane $src_file -o $dest_file } process_butane "./mc-kubelet-master.bu" "${INSTALL_DIR}/openshift/99_openshift-machineconfig_00-master-kubelet-providerid.yaml" process_butane "./mc-kubelet-worker.bu" "${INSTALL_DIR}/openshift/99_openshift-machineconfig_00-worker-kubelet-providerid.yaml" ``` The MachineConfig files must exist: ```sh ls ${INSTALL_DIR}/openshift/99_openshift-machineconfig_00-*-kubelet-providerid.yaml ``` ### Create ignition files Once the manifests are placed, you can create the cluster ignition configurations: ~~~bash openshift-install create ignition-configs --dir $INSTALL_DIR ~~~ The ignition files must be generated in the install directory (files with extension `*.ign`): ```text $ tree $INSTALL_DIR /path/to/install-dir ├── auth │ ├── kubeadmin-password │ └── kubeconfig ├── bootstrap.ign ├── master.ign ├── metadata.json └── worker.ign ``` ## Section 3. Create the cluster The first part of this section describes how to create the compute nodes and dependencies, once the instances are provisioned, the bootstrap will initialize the control plane, and then when control plane nodes join the cluster, are initialized by CCM, and the control plane workloads scheduled, the bootstrap will be completed. The second part describes how to approve the CSR for worker nodes, and to review the cluster installation. ### Cluster nodes Every node role uses different ignition files. The following table shows which ignition file is required for each node role: | Node Name | Ignition file | Fetch source | | -- | -- | -- | | bootstrap | `${PWD}/user-data-bootstrap.json` | Preauthenticated URL | | control planes nodes (pool) | `${INSTALL_DIR}/master.json` | Internal Load Balancer (MCS) | | compute nodes (pool) | `${INSTALL_DIR}/worker.json` | Internal Load Balancer (MCS) | Run the following commands to populate the values for the environment variables required to create instances: - `IMAGE_ID`: Custom RHCOS image previously uploaded. - `SUBNET_ID_PUBLIC`: Public regional subnet used in bootstrap. - `SUBNET_ID_PRIVATE`: Private regional subnet used to create control plane and compute nodes. - `NSG_ID_CPL`: Network Security Group ID used in Control Planes ```sh # Gather subnet IDs SUBNET_ID_PUBLIC=$(oci network subnet list --compartment-id $COMPARTMENT_ID_OPENSHIFT \ | jq -r '.data[] | select(.["display-name"] | endswith("public")).id') SUBNET_ID_PRIVATE=$(oci network subnet list --compartment-id $COMPARTMENT_ID_OPENSHIFT \ | jq -r '.data[] | select(.["display-name"] | endswith("private")).id') # Gather the Network Security group for the control plane NSG_ID_CPL=$(oci network nsg list -c $COMPARTMENT_ID_OPENSHIFT \ | jq -r '.data[] | select(.["display-name"] | endswith("controlplane")).id') NSG_ID_CMP=$(oci network nsg list -c $COMPARTMENT_ID_OPENSHIFT \ | jq -r '.data[] | select(.["display-name"] | endswith("compute")).id') ``` !!! warning "Check if required variables have values before proceeding" ``` cat <<EOF>/dev/stdout COMPARTMENT_ID_OPENSHIFT=$COMPARTMENT_ID_OPENSHIFT SUBNET_ID_PUBLIC=$SUBNET_ID_PUBLIC SUBNET_ID_PRIVATE=$SUBNET_ID_PRIVATE NSG_ID_CPL=$NSG_ID_CPL EOF ``` !!! tip "Helper - OCI CLI documentation" - [`oci compute image list`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.29.1/oci_cli_docs/cmdref/compute/image/list.html) - [`oci network subnet list`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.29.1/oci_cli_docs/cmdref/network/subnet/list.html) - [`oci network nsg list`](#) #### Upload the RHCOS image The image used in this guide is QCOW2. The `openshift-install` command provides the option `coreos print-stream-json` to show all the available artifacts. The steps below describe how to download the image, upload it to an OCI bucket, and then create a custom image. - Get the image name to be used in later steps: ```sh IMAGE_NAME=$(basename $(openshift-install coreos print-stream-json | jq -r '.architectures["x86_64"].artifacts["openstack"].formats["qcow2.gz"].disk.location')) ``` - Download the `QCOW2` image: ```sh wget $(openshift-install coreos print-stream-json | jq -r '.architectures["x86_64"].artifacts["openstack"].formats["qcow2.gz"].disk.location') ``` - Create the bucket: ```sh BUCKET_NAME="${CLUSTER_NAME}-infra" oci os bucket create --name $BUCKET_NAME --compartment-id $COMPARTMENT_ID_OPENSHIFT ``` !!! tip "Helper - OCI CLI documentation" - [`oci os bucket create`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.29.1/oci_cli_docs/cmdref/os/bucket/create.html) OCI Console path: `Menu > Storage > Buckets > (Choose the Compartment `openshift`) > Create Bucket` - Upload the image to OCI Bucket: ```sh oci os object put -bn $BUCKET_NAME --name images/${IMAGE_NAME} --file ${IMAGE_NAME} ``` !!! tip "Helper - OCI CLI documentation" - [`oci os object put`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.29.1/oci_cli_docs/cmdref/os/object/put.html) OCI Console path: `Menu > Storage > Buckets > (Choose the Compartment `openshift`) > (Choose the Bucket `openshift-infra`) > Objects > Upload` - Import to the Instance Image service: ```sh STORAGE_NAMESPACE=$(oci os ns get | jq -r .data) oci compute image import from-object -bn $BUCKET_NAME --name images/${IMAGE_NAME} \ --compartment-id $COMPARTMENT_ID_OPENSHIFT -ns $STORAGE_NAMESPACE \ --display-name ${IMAGE_NAME} --launch-mode "PARAVIRTUALIZED" \ --source-image-type "QCOW2" # Gather the Custom Compute image for RHCOS IMAGE_ID=$(oci compute image list --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --display-name $IMAGE_NAME | jq -r '.data[0].id') ``` !!! tip "Helper" OCI CLI documentation for [`oci compute image import`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.29.1/oci_cli_docs/cmdref/compute/image/import/from-object.html) OCI CLI documentation for [`oci os ns get`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.29.1/oci_cli_docs/cmdref/os/ns/get.html) #### Bootstrap The bootstrap node is responsible for creating the temporary control plane and serve the ignition files to other nodes through the MCS. The OCI user data has a size limitation that prevents to use of the bootstrap ignition file directly when launching the node. A new ignition file will be created replacing it with a remote URL fetching from the temporary Bucket Object URL. Once the bootstrap instance is created, it must be attached to the load balancer in the Backend Sets of Kubernetes API Server and Machine Config Server. Steps: - Upload the `bootstrap.ign` to the infrastructure bucket ```sh oci os object put -bn $BUCKET_NAME --name bootstrap-${CLUSTER_NAME}.ign \ --file $INSTALL_DIR/bootstrap.ign ``` !!! tip "Helper" OCI CLI documentation for [`oci os object put`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.29.1/oci_cli_docs/cmdref/os/object/put.html) - Generate the pre-authenticated request to generate a unique URL used by the bootstrap to access the ignition file stored in the OCI object store: !!! warning "Attention" The bucket object URL will expire in one hour if you are planning to create the bootstrap later, please adjust the `$EXPIRES_TIME`. The install certificates expire 24 hours after the ignition files have been created, consider regenerating it if the ignitions are older than that. ```sh EXPIRES_TIME=$(date -d '+1 hour' --rfc-3339=seconds) IGN_BOOTSTRAP_URL=$(oci os preauth-request create --name bootstrap-${CLUSTER_NAME} \ -bn $BUCKET_NAME -on bootstrap-${CLUSTER_NAME}.ign \ --access-type ObjectRead --time-expires "$EXPIRES_TIME" \ | jq -r '.data["full-path"]') ``` !!! tip "Helper" OCI CLI documentation for [`oci os preauth-request create`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.29.0/oci_cli_docs/cmdref/os/preauth-request/create.html) The generated URL for the ignition file `bootstrap.ign` must be available in the `$IGN_BOOTSTRAP_URL`. - Create the ignition file to boot the bootstrap node, pointing to the remote ignition source: ```sh cat <<EOF > ./user-data-bootstrap.json { "ignition": { "config": { "replace": { "source": "${IGN_BOOTSTRAP_URL}" } }, "version": "3.1.0" } } EOF ``` - Launch the instance for the bootstrap: ```sh AVAILABILITY_DOMAIN="gzqB:US-SANJOSE-1-AD-1" INSTANCE_SHAPE="VM.Standard.E4.Flex" oci compute instance launch \ --hostname-label "bootstrap" \ --display-name "bootstrap" \ --availability-domain "$AVAILABILITY_DOMAIN" \ --fault-domain "FAULT-DOMAIN-1" \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --subnet-id $SUBNET_ID_PUBLIC \ --nsg-ids "[\"$NSG_ID_CPL\"]" \ --shape "$INSTANCE_SHAPE" \ --shape-config "{\"memoryInGBs\":16.0,\"ocpus\":8.0}" \ --source-details "{\"bootVolumeSizeInGBs\":120,\"bootVolumeVpusPerGB\":60,\"imageId\":\"${IMAGE_ID}\",\"sourceType\":\"image\"}" \ --agent-config '{"areAllPluginsDisabled": true}' \ --assign-public-ip True \ --user-data-file "./user-data-bootstrap.json" \ --defined-tags "{\"$CLUSTER_NAME\":{\"role\":\"master\"}}" ``` !!! tip "Helper - OCI CLI documentation" - [`oci compute instance launch`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.29.1/oci_cli_docs/cmdref/compute/instance/launch.html) - [`oci compute shape list`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.29.1/oci_cli_docs/cmdref/compute/shape/list.html) !!! tip "Follow the bootstrap process" You can SSH to the node and follow the bootstrap process: `journalctl -b -f -u release-image.service -u bootkube.service` - Discover the load balancer's backend sets and the bootstrap instance IDs: ```sh BES_API_NAME=$(oci nlb backend-set list --network-load-balancer-id $NLB_ID | jq -r '.data.items[] | select(.name | endswith("api")).name') BES_MCS_NAME=$(oci nlb backend-set list --network-load-balancer-id $NLB_ID | jq -r '.data.items[] | select(.name | endswith("mcs")).name') INSTANCE_ID_BOOTSTRAP=$(oci compute instance list -c $COMPARTMENT_ID_OPENSHIFT | jq -r '.data[] | select((.["display-name"]=="bootstrap") and (.["lifecycle-state"]=="RUNNING")).id') test -z $INSTANCE_ID_BOOTSTRAP && echo "ERR: Bootstrap Instance ID not found=[$INSTANCE_ID_BOOTSTRAP]. Try again." ``` !!! tip "Helper - OCI CLI documentation" - [`oci nlb network-load-balancer list`]() - [`oci nlb backend-set list`]() - [`oci compute instance list`]() - Attach the bootstrap instance to the "API" backend set: ```sh # oci nlb backend-set update --generate-param-json-input backends cat <<EOF > ./nlb-bset-backends-api.json [ { "isBackup": false, "isDrain": false, "isOffline": false, "name": "${INSTANCE_ID_BOOTSTRAP}:6443", "port": 6443, "targetId": "${INSTANCE_ID_BOOTSTRAP}" } ] EOF # Update API Backend Set oci nlb backend-set update --force \ --backend-set-name $BES_API_NAME \ --network-load-balancer-id $NLB_ID \ --backends file://nlb-bset-backends-api.json \ --wait-for-state SUCCEEDED ``` !!! tip "Helper - OCI CLI documentation" - [`oci nlb backend-set update`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.29.1/oci_cli_docs/cmdref/nlb/backend-set/update.html) - Attach the bootstrap instance to the "MCS" backend set: ```sh cat <<EOF > ./nlb-bset-backends-mcs.json [ { "isBackup": false, "isDrain": false, "isOffline": false, "name": "${INSTANCE_ID_BOOTSTRAP}:22623", "port": 22623, "targetId": "${INSTANCE_ID_BOOTSTRAP}" } ] EOF oci nlb backend-set update --force \ --backend-set-name $BES_MCS_NAME \ --network-load-balancer-id $NLB_ID \ --backends file://nlb-bset-backends-mcs.json \ --wait-for-state SUCCEEDED ``` #### Control Plane Three control plane instances will be created. The instances is created using [Instance Pool](https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/creatinginstancepool.htm), which will automatically inherit the same configuration and attach to the required listeners: API and MCS. - Creating the instance configuration required by the instance pool: ```sh INSTANCE_CONFIG_CONTROLPLANE="${CLUSTER_NAME}-controlplane" # To generate all the options: # oci compute-management instance-configuration create --generate-param-json-input instance-details cat <<EOF > ./instance-config-details-controlplanes.json { "instanceType": "compute", "launchDetails": { "agentConfig": {"areAllPluginsDisabled": true}, "compartmentId": "$COMPARTMENT_ID_OPENSHIFT", "createVnicDetails": { "assignPrivateDnsRecord": true, "assignPublicIp": false, "nsgIds": ["$NSG_ID_CPL"], "subnetId": "$SUBNET_ID_PRIVATE" }, "definedTags": { "$CLUSTER_NAME": { "role": "master" } }, "displayName": "${CLUSTER_NAME}-controlplane", "launchMode": "PARAVIRTUALIZED", "metadata": {"user_data": "$(base64 -w0 < $INSTALL_DIR/master.ign)"}, "shape": "$INSTANCE_SHAPE", "shapeConfig": {"memoryInGBs":16.0,"ocpus":8.0}, "sourceDetails": {"bootVolumeSizeInGBs":120,"bootVolumeVpusPerGB":60,"imageId":"${IMAGE_ID}","sourceType":"image"} } } EOF oci compute-management instance-configuration create \ --display-name "$INSTANCE_CONFIG_CONTROLPLANE" \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --instance-details file://instance-config-details-controlplanes.json ``` - Creating the instance pool: ```sh INSTANCE_POOL_CONTROLPLANE="${CLUSTER_NAME}-controlplane" INSTANCE_CONFIG_ID_CPL=$(oci compute-management instance-configuration list \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ | jq -r ".data[] | select(.[\"display-name\"] | startswith(\"$INSTANCE_CONFIG_CONTROLPLANE\")).id") # # oci compute-management instance-pool create --generate-param-json-input load-balancers cat <<EOF > ./instance-pool-loadbalancers-cpl.json [ { "backendSetName": "$BES_API_NAME", "loadBalancerId": "$NLB_ID", "port": 6443, "vnicSelection": "PrimaryVnic" }, { "backendSetName": "$BES_MCS_NAME", "loadBalancerId": "$NLB_ID", "port": 22623, "vnicSelection": "PrimaryVnic" } ] EOF # oci compute-management instance-pool create --generate-param-json-input placement-configurations cat <<EOF > ./instance-pool-placement.json [ { "availabilityDomain": "$AVAILABILITY_DOMAIN", "faultDomains": ["FAULT-DOMAIN-1","FAULT-DOMAIN-2","FAULT-DOMAIN-3"], "primarySubnetId": "$SUBNET_ID_PRIVATE", } ] EOF oci compute-management instance-pool create \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --instance-configuration-id "$INSTANCE_CONFIG_ID_CPL" \ --size 0 \ --display-name "$INSTANCE_POOL_CONTROLPLANE" \ --placement-configurations "file://instance-pool-placement.json" \ --load-balancers file://instance-pool-loadbalancers-cpl.json ``` !!! tip "Helper - OCI CLI documentation" - [`oci compute-management instance-pool create`](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.35.0/oci_cli_docs/cmdref/compute-management/instance-pool/create.html) - Scale up (alternatively the `--size` can be adjusted when creating the Instance Pool): ```sh INSTANCE_POOL_ID_CPL=$(oci compute-management instance-pool list \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ | jq -r ".data[] | select( (.[\"display-name\"]==\"$INSTANCE_POOL_CONTROLPLANE\") and (.[\"lifecycle-state\"]==\"RUNNING\") ).id") oci compute-management instance-pool update --instance-pool-id $INSTANCE_POOL_ID_CPL --size 3 ``` !!! tip "Helper - OCI CLI documentation" - [`oci compute-management instance-pool update `](https://docs.oracle.com/en-us/iaas/tools/oci-cli/3.35.0/oci_cli_docs/cmdref/compute-management/instance-pool/update.html) #### Compute/workers - Creating the instance configuration: ```sh INSTANCE_CONFIG_COMPUTE="${CLUSTER_NAME}-compute" # oci compute-management instance-configuration create --generate-param-json-input instance-details cat <<EOF > ./instance-config-details-compute.json { "instanceType": "compute", "launchDetails": { "agentConfig": {"areAllPluginsDisabled": true}, "compartmentId": "$COMPARTMENT_ID_OPENSHIFT", "createVnicDetails": { "assignPrivateDnsRecord": true, "assignPublicIp": false, "nsgIds": ["$NSG_ID_CMP"], "subnetId": "$SUBNET_ID_PRIVATE" }, "definedTags": { "$CLUSTER_NAME": { "role": "worker" } }, "displayName": "${CLUSTER_NAME}-worker", "launchMode": "PARAVIRTUALIZED", "metadata": {"user_data": "$(base64 -w0 < $INSTALL_DIR/worker.ign)"}, "shape": "$INSTANCE_SHAPE", "shapeConfig": {"memoryInGBs":16.0,"ocpus":8.0}, "sourceDetails": {"bootVolumeSizeInGBs":120,"bootVolumeVpusPerGB":20,"imageId":"${IMAGE_ID}","sourceType":"image"} } } EOF oci compute-management instance-configuration create \ --display-name "$INSTANCE_CONFIG_COMPUTE" \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --instance-details file://instance-config-details-compute.json ``` - Creating the instance pool: ```sh INSTANCE_POOL_COMPUTE="${CLUSTER_NAME}-compute" INSTANCE_CONFIG_ID_CMP=$(oci compute-management instance-configuration list \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ | jq -r ".data[] | select(.[\"display-name\"] | startswith(\"$INSTANCE_CONFIG_COMPUTE\")).id") BES_HTTP_NAME=$(oci nlb backend-set list --network-load-balancer-id $NLB_ID \ | jq -r '.data.items[] | select(.name | endswith("http")).name') BES_HTTPS_NAME=$(oci nlb backend-set list --network-load-balancer-id $NLB_ID \ | jq -r '.data.items[] | select(.name | endswith("https")).name') # # oci compute-management instance-pool create --generate-param-json-input load-balancers cat <<EOF > ./instance-pool-loadbalancers-cmp.json [ { "backendSetName": "$BES_HTTP_NAME", "loadBalancerId": "$NLB_ID", "port": 80, "vnicSelection": "PrimaryVnic" }, { "backendSetName": "$BES_HTTPS_NAME", "loadBalancerId": "$NLB_ID", "port": 443, "vnicSelection": "PrimaryVnic" } ] EOF oci compute-management instance-pool create \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --instance-configuration-id "$INSTANCE_CONFIG_ID_CMP" \ --size 0 \ --display-name "$INSTANCE_POOL_COMPUTE" \ --placement-configurations "[{\"availabilityDomain\":\"$AVAILABILITY_DOMAIN\",\"faultDomains\":[\"FAULT-DOMAIN-1\",\"FAULT-DOMAIN-2\",\"FAULT-DOMAIN-3\"],\"primarySubnetId\":\"$SUBNET_ID_PRIVATE\"}]" \ --load-balancers file://instance-pool-loadbalancers-cmp.json ``` - Scale up the compute nodes: ```sh INSTANCE_POOL_ID_CMP=$(oci compute-management instance-pool list \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ | jq -r ".data[] | select( (.[\"display-name\"]==\"$INSTANCE_POOL_COMPUTE\") and (.[\"lifecycle-state\"]==\"RUNNING\") ).id") oci compute-management instance-pool update --instance-pool-id $INSTANCE_POOL_ID_CMP --size 2 ``` ### Review the installation Export the kubeconfig: ```sh export KUBECONFIG=$INSTALL_DIR/auth/kubeconfig ``` #### OCI Cloud Controller Manager - Check if the CCM pods have been started and nodes initialized: ```sh oc logs -f daemonset.apps/oci-cloud-controller-manager -n oci-cloud-controller-manager ``` Example output: ``` I0816 04:22:12.019529 1 node_controller.go:484] Successfully initialized node inst-rdlw6-demo-oci-003-controlplane.priv.ocp.oraclevcn.com with cloud provider ``` - Check if the nodes have been initialized: ```sh oc get nodes ``` - Check if the controllers are running for each master: ```sh oc get all -n oci-cloud-controller-manager ``` #### Approve certificates for compute nodes When you add machines to a cluster, two pending certificate signing requests (CSRs) are generated for each machine that you added. You must confirm that these CSRs are approved or, if necessary, approve them yourself. The client requests must be approved first, followed by the server requests. Check the pending certificates using `oc get csr -w`, then approve those by running: ```sh oc adm certificate approve $(oc get csr -o json | jq -r '.items[] | select(.status.certificate == null).metadata.name') ``` Observe the nodes joining in the cluster by running: `oc get nodes -w`. #### Wait for Bootstrap to complete Check if you can remove the bootstrap instance when the control plane nodes have been up and running correctly. You can check by running the following command: ```sh openshift-install --dir $INSTALL_DIR wait-for bootstrap-complete ``` Example output: ```text INFO It is now safe to remove the bootstrap resources INFO Time elapsed: 1s ``` #### Check installation complete It is also possible to wait for the installation to complete by using the `openshift-install` binary: ```sh openshift-install --dir $INSTALL_DIR wait-for install-complete ``` Example output: ```text $ openshift-install --dir $INSTALL_DIR wait-for install-complete INFO Waiting up to 40m0s (until 6:17PM -03) for the cluster at https://api.oci-ext00.mydomain.com:6443 to initialize... INFO Checking to see if there is a route at openshift-console/console... INFO Install complete! INFO To access the cluster as the system:admin user when using 'oc', run 'export KUBECONFIG=/home/me/oci/oci-ext00/auth/kubeconfig' INFO Access the OpenShift web-console here: https://console-openshift-console.apps.oci-ext00.mydomain INFO Login to the console with user: "kubeadmin", and password: "[super secret]" INFO Time elapsed: 2s ``` Alternatively, you can watch the cluster operators to follow the installation process: ```sh watch -n5 oc get clusteroperators ``` The cluster will be ready to use once the operators are stabilized. If you have issues, you can start exploring the [Throubleshooting Installations page](https://docs.openshift.com/container-platform/4.13/support/troubleshooting/troubleshooting-installations.html). ## Destroy the cluster This section provides a single script to clean up the resources created by this user guide. Run the following command to delete the resource considering the dependencies: ```sh # Compute ## Clean up instances oci compute instance terminate --force \ --instance-id $INSTANCE_ID_BOOTSTRAP oci compute-management instance-pool terminate --force \ --instance-pool-id $INSTANCE_POOL_ID_CMP \ --wait-for-state TERMINATED oci compute-management instance-configuration delete --force \ --instance-configuration-id $INSTANCE_CONFIG_ID_CMP oci compute-management instance-pool terminate --force \ --instance-pool-id $INSTANCE_POOL_ID_CPL \ --wait-for-state TERMINATED oci compute-management instance-configuration delete --force \ --instance-configuration-id $INSTANCE_CONFIG_ID_CPL ## Custom image oci compute image delete --force --image-id ${IMAGE_ID} # IAM ## Remove policy oci iam policy delete --force \ --policy-id $(oci iam policy list \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --name $POLICY_NAME | jq -r .data[0].id) \ --wait-for-state DELETED ## Remove dynamic group oci iam dynamic-group delete --force \ --dynamic-group-id $(oci iam dynamic-group list \ --name $DYNAMIC_GROUP_NAME | jq -r .data[0].id) \ --wait-for-state DELETED ## Remove tag namespace and key oci iam tag-namespace retire --tag-namespace-id $TAG_NAMESPACE_ID oci iam tag-namespace cascade-delete \ --tag-namespace-id $TAG_NAMESPACE_ID \ --wait-for-state SUCCEEDED ## Bucket for RES_ID in $(oci os preauth-request list --bucket-name "$BUCKET_NAME" | jq -r .data[].id); do echo "Deleting Preauth request $RES_ID" oci os preauth-request delete --force \ --bucket-name "$BUCKET_NAME" \ --par-id "${RES_ID}"; done oci os object delete --force \ --bucket-name "$BUCKET_NAME" \ --object-name "images/${IMAGE_NAME}" oci os object delete --force \ --bucket-name "$BUCKET_NAME" \ --object-name "bootstrap-${CLUSTER_NAME}.ign" oci os bucket delete --force \ --bucket-name "$BUCKET_NAME" # Load Balancer oci nlb network-load-balancer delete --force \ --network-load-balancer-id $NLB_ID \ --wait-for-state SUCCEEDED # Network and dependencies for RES_ID in $(oci network subnet list \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --vcn-id $VCN_ID | jq -r .data[].id); do echo "Deleting Subnet $RES_ID" oci network subnet delete --force \ --subnet-id $RES_ID \ --wait-for-state TERMINATED; done for RES_ID in $(oci network nsg list \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --vcn-id $VCN_ID | jq -r .data[].id); do echo "Deleting NSG $RES_ID" oci network nsg delete --force \ --nsg-id $RES_ID \ --wait-for-state TERMINATED; done for RES_ID in $(oci network security-list list \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --vcn-id $VCN_ID \ | jq -r '.data[] | select(.["display-name"] | startswith("Default") | not).id'); do echo "Deleting SecList $RES_ID" oci network security-list delete --force \ --security-list-id $RES_ID \ --wait-for-state TERMINATED; done oci network route-table delete --force \ --wait-for-state TERMINATED \ --rt-id $(oci network route-table list \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --vcn-id $VCN_ID \ | jq -r '.data[] | select(.["display-name"] | endswith("rtb-public")).id') oci network route-table delete --force \ --wait-for-state TERMINATED \ --rt-id $(oci network route-table list \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --vcn-id $VCN_ID \ | jq -r '.data[] | select(.["display-name"] | endswith("rtb-private")).id') for RES_ID in $(oci network nat-gateway list \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --vcn-id $VCN_ID | jq -r .data[].id); do echo "Deleting NATGW $RES_ID" oci network nat-gateway delete --force \ --nat-gateway-id $RES_ID \ --wait-for-state TERMINATED; done for RES_ID in $(oci network internet-gateway list \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --vcn-id $VCN_ID | jq -r .data[].id); do echo "Deleting IGW $RES_ID" oci network internet-gateway delete --force \ --ig-id $RES_ID \ --wait-for-state TERMINATED; done oci network vcn delete --force \ --vcn-id $VCN_ID \ --wait-for-state TERMINATED # Compartment oci iam compartment delete --force \ --compartment-id $COMPARTMENT_ID_OPENSHIFT \ --wait-for-state SUCCEEDED ``` ## Summary This guide walked through an OpenShift deployment on Oracle Cloud Infrastructure, a non-integrated provider, using the feature Platform External introduced in 4.14. The feature allows an initial integration with OCI without needing to change the OpenShift code base. It will also open the possibility to quickly deploy cloud provider components natively, like CSI drivers, which mostly require extra setup with CCM. ## Next steps - [Validating an installation](https://docs.openshift.com/container-platform/4.13/installing/validating-an-installation.html#validating-an-installation) - [Running conformance tests in non-integrated providers](./conformance-tests-opct.md)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully