Terhubung dari Cloud Functions

Halaman ini berisi informasi dan contoh untuk menghubungkan ke instance Cloud SQL dari layanan yang berjalan di Cloud Functions.

Untuk petunjuk langkah demi langkah pada cara menjalankan aplikasi web sampel Cloud Functions yang terhubung ke Cloud SQL, lihat panduan memulai untuk menghubungkan dari Cloud Functions.

Cloud SQL adalah layanan database yang terkelola sepenuhnya yang membantu Anda menyiapkan, memelihara, mengelola, dan mengatur database relasional Anda di cloud.

Cloud Functions adalah solusi komputasi ringan bagi developer untuk membuat fungsi mandiri, tujuan tunggal yang merespons peristiwa Cloud tanpa perlu mengelola lingkungan server atau runtime.

Siapkan instance Cloud SQL

  1. Aktifkan Cloud SQL Admin API di project Google Cloud yang menjadi sumber koneksi, jika Anda belum melakukannya:

    Mengaktifkan API

  2. Buat Cloud SQL untuk instance SQL Server. Sebaiknya pilih lokasi instance Cloud SQL di region yang sama dengan layanan Cloud Run Anda untuk mendapatkan latensi yang lebih baik, menghindari biaya jaringan, dan mengurangi risiko kegagalan lintas region.

    Secara default, Cloud SQL menetapkan alamat IP publik ke instance baru. Anda juga memiliki opsi untuk menetapkan alamat IP pribadi. Untuk mengetahui informasi selengkapnya tentang opsi konektivitas untuk keduanya, lihat halaman Menghubungkan Ringkasan.

Mengonfigurasi Cloud Functions

Langkah-langkah untuk mengonfigurasi Cloud Functions bergantung pada jenis alamat IP yang Anda tetapkan ke instance Cloud SQL.

IP Publik (default)

Cloud Functions mendukung koneksi ke Cloud SQL untuk SQL Server melalui IP publik menggunakan konektor Go, Java, dan Python.

Untuk mengonfigurasi Cloud Functions guna mengaktifkan koneksi ke instance Cloud SQL:
  • Pastikan instance yang dibuat di atas memiliki alamat IP publik. Anda dapat mengonfirmasi hal ini di halaman Ringkasan untuk instance di Konsol Google Cloud. Jika Anda perlu menambahkan alamat IP publik, lihat Konfigurasi IP publik.
  • Dapatkan INSTANCE_CONNECTION_NAME instance. Nilai ini tersedia:
    • Pada halaman Ringkasan untuk instance, di Konsol Google Cloud, atau
    • Dengan menjalankan perintah berikut: gcloud sql instances describe [INSTANCE_NAME]
  • Konfigurasi akun layanan untuk fungsi Anda. Jika akun layanan yang memberi otorisasi merupakan project yang berbeda dengan instance Cloud SQL, aktifkan Cloud SQL Admin API, lalu tambahkan izin IAM yang tercantum di bawah, pada kedua project tersebut. Pastikan bahwa akun layanan memiliki peran dan izin Cloud SQL yang sesuai untuk terhubung ke Cloud SQL.
    • Untuk terhubung ke Cloud SQL, akun layanan memerlukan salah satu peran IAM berikut:
      • Cloud SQL Client (pilihan)
      • Cloud SQL Editor
      • Cloud SQL Admin
      Atau, Anda dapat menetapkan izin IAM berikut secara manual:
      • cloudsql.instances.connect
      • cloudsql.instances.get
  • Jika Anda menggunakan Cloud Functions (generasi ke 2) dan bukan Cloud Functions (generasi ke 1), hal berikut ini diperlukan (lihat juga Konfigurasi Cloud Run):
    1. Pertama-tama, deploy fungsi Anda.
      Saat Anda pertama kali mulai membuat Cloud Function di konsol Google Cloud, layanan Cloud Run yang mendasarinya belum dibuat. Anda tidak dapat mengonfigurasi koneksi Cloud SQL hingga layanan tersebut dibuat (dengan men-deploy Cloud Function).
    2. Di konsol Google Cloud, di bagian kanan atas pada halaman Detail Function, di bawah Didukung oleh Cloud Run, klik link untuk mengakses layanan Cloud Run yang mendasarinya.
    3. Pada halaman Detail layanan Cloud Run, pilih tab Edit and deploy revisi baru.
    4. Ikuti langkah-langkah standar (seperti dalam kasus perubahan konfigurasi apa pun) guna menyetel konfigurasi baru untuk koneksi Cloud SQL.
      Tindakan ini akan membuat revisi Cloud Run baru, dan revisi berikutnya akan secara otomatis menerima koneksi Cloud SQL ini, kecuali jika Anda mengubahnya secara eksplisit.

IP Pribadi

Jika akun layanan yang memberi otorisasi merupakan project yang berbeda dengan project yang berisi instance Cloud SQL, lakukan hal berikut:

  • Di kedua project, aktifkan Cloud SQL Admin API.
  • Untuk akun layanan dalam project yang berisi instance Cloud SQL, tambahkan izin IAM.
Konektor Akses VPC Serverless menggunakan alamat IP pribadi untuk menangani komunikasi ke jaringan VPC Anda. Untuk terhubung langsung dengan alamat IP pribadi, Anda harus melakukan hal berikut:
  1. Pastikan instance Cloud SQL yang dibuat sebelumnya memiliki alamat IP pribadi. Jika Anda perlu menambahkannya, lihat Mengonfigurasi IP pribadi untuk mengetahui petunjuknya.
  2. Buat konektor Akses VPC Serverless di jaringan VPC yang sama dengan instance Cloud SQL Anda. Perhatikan kondisi berikut:
    • Kecuali jika Anda menggunakan VPC Bersama, konektor Anda harus berada di project dan region yang sama dengan resource yang menggunakannya, tetapi konektor dapat mengirimkan traffic ke resource di region yang berbeda.
    • Akses VPC serverless mendukung komunikasi ke jaringan VPC yang terhubung menggunakan Cloud VPN dan Peering Jaringan VPC.
    • Akses VPC Serverless tidak mendukung jaringan lama.
  3. Mengonfigurasi Cloud Functions untuk menggunakan konektor.
  4. Hubungkan menggunakan alamat IP pribadi dan port 1433 instance Anda.

Menghubungkan ke Cloud SQL

Setelah Anda mengonfigurasi Cloud Functions, Anda dapat terhubung ke instance Cloud SQL Anda.

IP Publik (default)

Untuk jalur IP publik, Cloud Functions menyediakan enkripsi dan terhubung menggunakan konektor Cloud SQL.

Terhubung dengan konektor Cloud SQL

Konektor Cloud SQL adalah library khusus bahasa yang menyediakan enkripsi dan otorisasi berbasis IAM saat terhubung ke instance Cloud SQL.

Python

Untuk menemukan cuplikan ini dalam konteks aplikasi web, lihat README pada GitHub.

import os

from google.cloud.sql.connector import Connector, IPTypes
import pytds

import sqlalchemy

def connect_with_connector() -> sqlalchemy.engine.base.Engine:
    """
    Initializes a connection pool for a Cloud SQL instance of SQL Server.

    Uses the Cloud SQL Python Connector package.
    """
    # Note: Saving credentials in environment variables is convenient, but not
    # secure - consider a more secure solution such as
    # Cloud Secret Manager (https://cloud.google.com/secret-manager) to help
    # keep secrets safe.

    instance_connection_name = os.environ[
        "INSTANCE_CONNECTION_NAME"
    ]  # e.g. 'project:region:instance'
    db_user = os.environ.get("DB_USER", "")  # e.g. 'my-db-user'
    db_pass = os.environ["DB_PASS"]  # e.g. 'my-db-password'
    db_name = os.environ["DB_NAME"]  # e.g. 'my-database'

    ip_type = IPTypes.PRIVATE if os.environ.get("PRIVATE_IP") else IPTypes.PUBLIC

    connector = Connector(ip_type)

    connect_args = {}
    # If your SQL Server instance requires SSL, you need to download the CA
    # certificate for your instance and include cafile={path to downloaded
    # certificate} and validate_host=False. This is a workaround for a known issue.
    if os.environ.get("DB_ROOT_CERT"):  # e.g. '/path/to/my/server-ca.pem'
        connect_args = {
            "cafile": os.environ["DB_ROOT_CERT"],
            "validate_host": False,
        }

    def getconn() -> pytds.Connection:
        conn = connector.connect(
            instance_connection_name,
            "pytds",
            user=db_user,
            password=db_pass,
            db=db_name,
            **connect_args
        )
        return conn

    pool = sqlalchemy.create_engine(
        "mssql+pytds://",
        creator=getconn,
        # ...
    )
    return pool

Java

Untuk menemukan cuplikan ini dalam konteks aplikasi web, lihat README pada GitHub.

Catatan:

  • CLOUD_SQL_CONNECTION_NAME harus direpresentasikan sebagai <MY-PROJECT>:<INSTANCE-REGION>:<INSTANCE-NAME>
  • Lihat persyaratan versi factory soket JDBC untuk file pom.xml di sini .


import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;

public class ConnectorConnectionPoolFactory extends ConnectionPoolFactory {

  // Note: Saving credentials in environment variables is convenient, but not
  // secure - consider a more secure solution such as
  // Cloud Secret Manager (https://cloud.google.com/secret-manager) to help
  // keep secrets safe.
  private static final String INSTANCE_CONNECTION_NAME =
      System.getenv("INSTANCE_CONNECTION_NAME");
  private static final String DB_USER = System.getenv("DB_USER");
  private static final String DB_PASS = System.getenv("DB_PASS");
  private static final String DB_NAME = System.getenv("DB_NAME");

  public static DataSource createConnectionPool() {
    // The configuration object specifies behaviors for the connection pool.
    HikariConfig config = new HikariConfig();

    // The following is equivalent to setting the config options below:
    // jdbc:sqlserver://;user=<DB_USER>;password=<DB_PASS>;databaseName=<DB_NAME>;
    // socketFactoryClass=com.google.cloud.sql.sqlserver.SocketFactory;
    // socketFactoryConstructorArg=<INSTANCE_CONNECTION_NAME>

    // See the link below for more info on building a JDBC URL for the Cloud SQL JDBC Socket Factory
    // https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory#creating-the-jdbc-url

    // Configure which instance and what database user to connect with.
    config
        .setDataSourceClassName("com.microsoft.sqlserver.jdbc.SQLServerDataSource");
    config.setUsername(DB_USER); // e.g. "root", "sqlserver"
    config.setPassword(DB_PASS); // e.g. "my-password"
    config.addDataSourceProperty("databaseName", DB_NAME);

    config.addDataSourceProperty("socketFactoryClass",
        "com.google.cloud.sql.sqlserver.SocketFactory");
    config.addDataSourceProperty("socketFactoryConstructorArg", INSTANCE_CONNECTION_NAME);

    // The Java Connector provides SSL encryption, so it should be disabled
    // at the driver level.
    config.addDataSourceProperty("encrypt", "false");

    // ... Specify additional connection properties here.
    // ...

    // Initialize the connection pool using the configuration object.
    return new HikariDataSource(config);
  }
}

Go

Untuk menemukan cuplikan ini dalam konteks aplikasi web, lihat README pada GitHub.

package cloudsql

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"net"
	"os"

	"cloud.google.com/go/cloudsqlconn"
	mssql "github.com/denisenkom/go-mssqldb"
)

type csqlDialer struct {
	dialer     *cloudsqlconn.Dialer
	connName   string
	usePrivate bool
}

// DialContext adheres to the mssql.Dialer interface.
func (c *csqlDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
	var opts []cloudsqlconn.DialOption
	if c.usePrivate {
		opts = append(opts, cloudsqlconn.WithPrivateIP())
	}
	return c.dialer.Dial(ctx, c.connName, opts...)
}

func connectWithConnector() (*sql.DB, error) {
	mustGetenv := func(k string) string {
		v := os.Getenv(k)
		if v == "" {
			log.Fatalf("Fatal Error in connect_connector.go: %s environment variable not set.\n", k)
		}
		return v
	}
	// Note: Saving credentials in environment variables is convenient, but not
	// secure - consider a more secure solution such as
	// Cloud Secret Manager (https://cloud.google.com/secret-manager) to help
	// keep secrets safe.
	var (
		dbUser                 = mustGetenv("DB_USER")                  // e.g. 'my-db-user'
		dbPwd                  = mustGetenv("DB_PASS")                  // e.g. 'my-db-password'
		dbName                 = mustGetenv("DB_NAME")                  // e.g. 'my-database'
		instanceConnectionName = mustGetenv("INSTANCE_CONNECTION_NAME") // e.g. 'project:region:instance'
		usePrivate             = os.Getenv("PRIVATE_IP")
	)

	dbURI := fmt.Sprintf("user id=%s;password=%s;database=%s;", dbUser, dbPwd, dbName)
	c, err := mssql.NewConnector(dbURI)
	if err != nil {
		return nil, fmt.Errorf("mssql.NewConnector: %w", err)
	}
	dialer, err := cloudsqlconn.NewDialer(context.Background())
	if err != nil {
		return nil, fmt.Errorf("cloudsqlconn.NewDailer: %w", err)
	}
	c.Dialer = &csqlDialer{
		dialer:     dialer,
		connName:   instanceConnectionName,
		usePrivate: usePrivate != "",
	}

	dbPool := sql.OpenDB(c)
	if err != nil {
		return nil, fmt.Errorf("sql.Open: %w", err)
	}
	return dbPool, nil
}

IP Pribadi

Untuk jalur IP pribadi, aplikasi Anda terhubung langsung ke instance melalui jaringan VPC. Metode ini menggunakan TCP untuk terhubung langsung ke instance Cloud SQL tanpa menggunakan Proxy Auth Cloud SQL.

Terhubung dengan TCP

Hubungkan menggunakan alamat IP pribadi instance Cloud SQL Anda sebagai host dan port 1433.

Python

Untuk menemukan cuplikan ini dalam konteks aplikasi web, lihat README pada GitHub.

import os

import sqlalchemy

def connect_tcp_socket() -> sqlalchemy.engine.base.Engine:
    """Initializes a TCP connection pool for a Cloud SQL instance of SQL Server."""
    # Note: Saving credentials in environment variables is convenient, but not
    # secure - consider a more secure solution such as
    # Cloud Secret Manager (https://cloud.google.com/secret-manager) to help
    # keep secrets safe.
    db_host = os.environ[
        "INSTANCE_HOST"
    ]  # e.g. '127.0.0.1' ('172.17.0.1' if deployed to GAE Flex)
    db_user = os.environ["DB_USER"]  # e.g. 'my-db-user'
    db_pass = os.environ["DB_PASS"]  # e.g. 'my-db-password'
    db_name = os.environ["DB_NAME"]  # e.g. 'my-database'
    db_port = os.environ["DB_PORT"]  # e.g. 1433

    pool = sqlalchemy.create_engine(
        # Equivalent URL:
        # mssql+pytds://<db_user>:<db_pass>@<db_host>:<db_port>/<db_name>
        sqlalchemy.engine.url.URL.create(
            drivername="mssql+pytds",
            username=db_user,
            password=db_pass,
            database=db_name,
            host=db_host,
            port=db_port,
        ),
        # ...
    )

    return pool

Java

Untuk menemukan cuplikan ini dalam konteks aplikasi web, lihat README pada GitHub.

Catatan:

  • CLOUD-SQL-CONNECTION-NAME harus ditampilkan sebagai <MY-PROJECT>:<INSTANCE-REGION><INSTANCE-NAME>
  • Menggunakan argumen ipTypes=PRIVATE akan memaksa SocketFactory untuk terhubung dengan IP pribadi yang terkait dengan instance.
  • Lihat persyaratan versi factory soket JDBC untuk file pom.xml di sini .


import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;

public class TcpConnectionPoolFactory extends ConnectionPoolFactory {

  // Note: Saving credentials in environment variables is convenient, but not
  // secure - consider a more secure solution such as
  // Cloud Secret Manager (https://cloud.google.com/secret-manager) to help
  // keep secrets safe.
  private static final String DB_USER = System.getenv("DB_USER");
  private static final String DB_PASS = System.getenv("DB_PASS");
  private static final String DB_NAME = System.getenv("DB_NAME");

  private static final String INSTANCE_HOST = System.getenv("INSTANCE_HOST");
  private static final String DB_PORT = System.getenv("DB_PORT");

  public static DataSource createConnectionPool() {
    // The configuration object specifies behaviors for the connection pool.
    HikariConfig config = new HikariConfig();

    // Configure which instance and what database user to connect with.
    config.setJdbcUrl(
        String.format("jdbc:sqlserver://%s:%s;databaseName=%s", INSTANCE_HOST, DB_PORT, DB_NAME));
    config.setUsername(DB_USER); // e.g. "root", "sqlserver"
    config.setPassword(DB_PASS); // e.g. "my-password"

    // ... Specify additional connection properties here.
    // ...

    // Initialize the connection pool using the configuration object.
    return new HikariDataSource(config);
  }
}

Node.js

Untuk menemukan cuplikan ini dalam konteks aplikasi web, lihat README pada GitHub.

const mssql = require('mssql');

// createTcpPool initializes a TCP connection pool for a Cloud SQL
// instance of SQL Server.
const createTcpPool = async config => {
  // Note: Saving credentials in environment variables is convenient, but not
  // secure - consider a more secure solution such as
  // Cloud Secret Manager (https://cloud.google.com/secret-manager) to help
  // keep secrets safe.
  const dbConfig = {
    server: process.env.INSTANCE_HOST, // e.g. '127.0.0.1'
    port: parseInt(process.env.DB_PORT), // e.g. 1433
    user: process.env.DB_USER, // e.g. 'my-db-user'
    password: process.env.DB_PASS, // e.g. 'my-db-password'
    database: process.env.DB_NAME, // e.g. 'my-database'
    options: {
      trustServerCertificate: true,
    },
    // ... Specify additional properties here.
    ...config,
  };
  // Establish a connection to the database.
  return mssql.connect(dbConfig);
};

Go

Untuk menemukan cuplikan ini dalam konteks aplikasi web, lihat README pada GitHub.

package cloudsql

import (
	"database/sql"
	"fmt"
	"log"
	"os"
	"strings"

	_ "github.com/denisenkom/go-mssqldb"
)

// connectTCPSocket initializes a TCP connection pool for a Cloud SQL
// instance of SQL Server.
func connectTCPSocket() (*sql.DB, error) {
	mustGetenv := func(k string) string {
		v := os.Getenv(k)
		if v == "" {
			log.Fatalf("Fatal Error in connect_tcp.go: %s environment variable not set.\n", k)
		}
		return v
	}
	// Note: Saving credentials in environment variables is convenient, but not
	// secure - consider a more secure solution such as
	// Cloud Secret Manager (https://cloud.google.com/secret-manager) to help
	// keep secrets safe.
	var (
		dbUser    = mustGetenv("DB_USER")       // e.g. 'my-db-user'
		dbPwd     = mustGetenv("DB_PASS")       // e.g. 'my-db-password'
		dbTCPHost = mustGetenv("INSTANCE_HOST") // e.g. '127.0.0.1' ('172.17.0.1' if deployed to GAE Flex)
		dbPort    = mustGetenv("DB_PORT")       // e.g. '1433'
		dbName    = mustGetenv("DB_NAME")       // e.g. 'my-database'
	)

	dbURI := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%s;database=%s;",
		dbTCPHost, dbUser, dbPwd, dbPort, dbName)

	// dbPool is the pool of database connections.
	dbPool, err := sql.Open("sqlserver", dbURI)
	if err != nil {
		return nil, fmt.Errorf("sql.Open: %w", err)
	}

	// ...

	return dbPool, nil
}

PHP

Untuk menemukan cuplikan ini dalam konteks aplikasi web, lihat README pada GitHub.

namespace Google\Cloud\Samples\CloudSQL\SQLServer;

use PDO;
use PDOException;
use RuntimeException;
use TypeError;

class DatabaseTcp
{
    public static function initTcpDatabaseConnection(): PDO
    {
        try {
            // Note: Saving credentials in environment variables is convenient, but not
            // secure - consider a more secure solution such as
            // Cloud Secret Manager (https://cloud.google.com/secret-manager) to help
            // keep secrets safe.
            $username = getenv('DB_USER'); // e.g. 'your_db_user'
            $password = getenv('DB_PASS'); // e.g. 'your_db_password'
            $dbName = getenv('DB_NAME'); // e.g. 'your_db_name'
            $instanceHost = getenv('INSTANCE_HOST'); // e.g. '127.0.0.1' ('172.17.0.1' for GAE Flex)

            // Connect using TCP
            $dsn = sprintf(
                'sqlsrv:server=%s;Database=%s',
                $instanceHost,
                $dbName
            );

            // Connect to the database
            $conn = new PDO(
                $dsn,
                $username,
                $password,
                # ...
            );
        } catch (TypeError $e) {
            throw new RuntimeException(
                sprintf(
                    'Invalid or missing configuration! Make sure you have set ' .
                        '$username, $password, $dbName, and $instanceHost (for TCP mode). ' .
                        'The PHP error was %s',
                    $e->getMessage()
                ),
                $e->getCode(),
                $e
            );
        } catch (PDOException $e) {
            throw new RuntimeException(
                sprintf(
                    'Could not connect to the Cloud SQL Database. Check that ' .
                        'your username and password are correct, that the Cloud SQL ' .
                        'proxy is running, and that the database exists and is ready ' .
                        'for use. For more assistance, refer to %s. The PDO error was %s',
                    'https://cloud.google.com/sql/docs/sqlserver/connect-external-app',
                    $e->getMessage()
                ),
                (int) $e->getCode(),
                $e
            );
        }

        return $conn;
    }
}

Praktik terbaik dan informasi lainnya

Anda dapat menggunakan Proxy Auth Cloud SQL saat menguji aplikasi Anda secara lokal. Lihat panduan memulai untuk penggunaan Proxy Auth Cloud SQL untuk mendapatkan petunjuk terperinci.

Kumpulan Koneksi

Koneksi ke database yang mendasari dapat terputus, baik oleh server database itu sendiri, atau oleh infrastruktur Cloud Functions yang mendasarinya. Kami merekomendasikan untuk menggunakan library klien yang mendukung kumpulan koneksi yang secara otomatis menghubungkan kembali koneksi klien yang rusak. Selain itu, kami merekomendasikan untuk menggunakan kumpulan koneksi yang tercakup secara global untuk meningkatkan kemungkinan bahwa fungsi Anda menggunakan kembali koneksi yang sama untuk pemanggilan fungsi berikutnya, dan menutup koneksi secara alami saat instance dikeluarkan (diperkecil). Untuk contoh yang lebih detail tentang cara menggunakan kumpulan koneksi, lihat Mengelola koneksi database.

Batas Koneksi

Cloud SQL menerapkan batas maksimum pada koneksi serentak, dan batas ini dapat bervariasi tergantung mesin database yang dipilih (lihat Kuota dan Batas Cloud SQL). Sebaiknya gunakan koneksi dengan Cloud Functions, tetapi penting untuk menetapkan jumlah maksimum koneksi ke 1.

Jika memungkinkan, Anda harus berhati-hati untuk hanya melakukan inisialisasi kumpulan koneksi untuk fungsi yang memerlukan akses ke database Anda. Beberapa kumpulan koneksi akan membuat koneksi secara preemtif, yang dapat menggunakan resource berlebih dan mengurangi batas koneksi Anda. Karena alasan ini, sebaiknya gunakan Inisialisasi Lambat untuk menunda pembuatan kumpulan koneksi hingga diperlukan, dan hanya sertakan kumpulan koneksi dalam fungsi yang digunakan.

Untuk contoh yang lebih detail tentang cara membatasi jumlah koneksi, lihat Mengelola koneksi database.

Batas Kuota API

Cloud Functions menyediakan mekanisme yang terhubung menggunakan Proxy Auth Cloud SQL, yang menggunakan Cloud SQL Admin API. Batas kuota API berlaku untuk Proxy Auth Cloud SQL. Kuota Cloud SQL Admin API yang digunakan kira-kira dua kali lipat jumlah instance Cloud SQL yang dikonfigurasi dikali jumlah total fungsi yang di-deploy. Anda dapat menetapkan jumlah maksimum pemanggilan serentak untuk mengubah perkiraan kuota API yang digunakan. Cloud Functions juga memberlakukan batas kapasitas pada jumlah panggilan API yang diizinkan per 100 detik.

Langkah selanjutnya