สำหรับนักพัฒนาคนไหนที่มีการใช้ 3rd Party Service ที่ต้องกำหนด API Key ไว้ในโปรเจค วันนี้เจ้าของบล็อกจะมานำเสนอ Gradle Plugin จากทาง Google ที่ชื่อว่า Secrets Gradle Pluing for Android กันครับ

บทความที่เกี่ยวข้อง

โดยปกติที่มีการใช้งาน 3rd Party Service สำหรับแอปบนแอนดรอยด์ ก็จะต้องมีการกำหนด API Key ไว้ในโปรเจคไม่ว่าจะกำหนดผ่านโค้ด Java/Kotlin หรือกำหนดไว้ใน Android Manifest แบบนี้

<!-- AndroidManifest.xml -->
<manifest ...>
    <application ...>
        <!-- ... -->
        <meta-data
            android:name="com.example.sdk.service"
            android:value="<API_KEY>" />
    </application>
</manifest>

ซึ่ง API Key ในลักษณะแบบนี้มักจะเป็น Public API Key ที่สามารถเปิดเผยให้คนอื่นเห็นได้ จึงกำหนดไว้ในโค้ด Java/Kotlin หรือ Android Manifest ได้อย่างสบายใจ

ใส่ API Key ไว้ในโปรเจคมันก็สะดวกแหละ แต่...

API Key ติดขึ้นไปใน Version Control

ถ้าโปรเจคมีการใช้งาน Version Control อย่าง Git ก็หมายความว่า API Key เหล่านี้ก็จะติดขึ้นไปด้วยเช่นกัน ซึ่งไม่ใช่เรื่องดีสำหรับบางโปรเจคซักเท่าไร โดยเฉพาะ Open Source Project ที่เปิดให้ใครเข้ามาดูโค้ดก็ได้

โค้ดเกี่ยวกับ API Key ที่อาจจะติดขึ้นไปในแอปบน Production

ในบาง 3rd Party Service มีการแยก API Key สำหรับตอนพัฒนาแอป (Development) และตอนที่ใช้งานจริง (Production) จึงมีเงื่อนไขว่าถ้าเป็น Debug Build ก็ให้ใช้ Development Key และถ้าเป็น Release Build ก็ใช้ Production Key แทน

สมมติว่าเจ้าของบล็อกเขียนโค้ดในลักษณะแบบนี้เพื่อแยก API Key ทั้งสองตาม Build Type โดยเช็คจาก BuildConfig แบบนี้

if(BuildConfig.DEBUG) {
    Sdk.initialize("<DEVELOPMENT_KEY>")
} else {
    Sdk.initialize("<PRODUCTION_KEY>")
}

ซึ่งตอน Release Build จะได้ค่า BuildConfig.DEBUG เป็น false เสมอ ทำให้โค้ดที่ว่าจะถูกลบให้โดยอัตโนมัติและเหลือแค่นี้แทน

Sdk.initialize("<PRODUCTION_KEY>")

แต่ถ้าเรียกใช้คำสั่งดังกล่าวผ่านการสร้าง Function แทนก็จะติดว่าปัญหาว่าโค้ดเหล่านี้ก็จะติดไปในไฟล์ APK ด้วยกันทั้งหมด

fun isDebug() = BuildConfig.DEBUG

if(isDebug()) {
    Sdk.initialize("<DEVELOPMENT_KEY>")
} else {
    Sdk.initialize("<PRODUCTION_KEY>")
}

ซึ่งโค้ดแบบนี้ไม่ใช่เรื่องดีซักเท่าไร เพราะแทนที่จะมีแค่ Production Key ติดขึ้นไป กลับมี Development Key ติดขึ้นไปด้วย

แยก API Key ตาม Build Type หรือ Product Flavor

สำหรับแอปที่มีหลาย Build Type หรือ Product Flavor และต้องการแยก API Key ออกจากกันอย่างสิ้นเชิง ก็อาจจะใช้ Build Variant เข้ามาช่วยจัดการแบบนี้แทน

<module>
└── src
    ├── development
    │   └── kotlin
    │       └── com
    │           └── akexorcist
    │               └── sleepingforless
    │                   └── config
    │                       └── ApiKeys.kt
    ├── production
    │   └── kotlin
    │       └── com
    │           └── akexorcist
    │               └── sleepingforless
    │                   └── config
    │                       └── ApiKeys.kt
    └── staging
        └── kotlin
            └── com
                └── akexorcist
                    └── sleepingforless
                        └── config
                            └── ApiKeys.kt

แต่วิธีแบบนี้ก็จะมี Human Error เกิดขึ้นได้ เช่น มีเหตุการณ์ที่จำเป็นต้องทำ API Key Rotation แล้วนักพัฒนาเผลอเปลี่ยนแค่บน  development ซึ่งตอนทดสอบก็จะไม่เจอปัญหาอะไร เพราะใช้งานได้ปกติเนื่องจากเป็น Flavor ที่ใช้ในระหว่างพัฒนาแอป แต่จะเจอปัญหาอีกทีตอนที่ Publish ขึ้น Google Play เนื่องจากลืมเปลี่ยน API Key ใน production ด้วย

ซึ่งปัญหาแบบนี้เกิดขึ้นได้ไม่ยาก เนื่องจากนักพัฒนาอาจจะไม่ทันสังเกตว่าไฟล์ ApiKeys.kt มีการแยก Build Variant ไว้นั่นเอง โดยเฉพาะอย่างยิ่งตอนที่แสดงหน้าต่าง Project แบบ Android ที่จะมองไม่เห็น Product Flavor อื่น เพราะ Android Studio ซ่อนไว้ให้โดยอัตโนมัติและแสดงเฉพาะ Product Flavor ตามที่เราเลือกไว้เท่านั้น

อีกหนึ่งทางเลือกก็คือการใช้ Secrets Gradle Plugin for Android นั่นเอง

ถึงแม้ว่าวิธีจัดการกับ API Key ด้วยวิธีที่พูดไปก่อนหน้านี้จะไม่ได้มีข้อเสียที่ร้ายแรง ตราบใดนักพัฒนารู้และเข้าใจวิธีจัดการอย่างถูกต้อง ดังนั้น Secrets Gradle Plugin ที่เจ้าของบล็อกหยิบมาแนะนำจึงไม่ใช่ Best Practice หรือวิธีที่ดีที่สุด แต่เป็นแค่หนึ่งในทางเลือกสำหรับการจัดการกับ API Key เท่านั้น

GitHub - google/secrets-gradle-plugin: A Gradle plugin for providing your secrets to your Android project.
A Gradle plugin for providing your secrets to your Android project. - google/secrets-gradle-plugin

โดยหลักการทำงานของ Secrets Gradle Plugin จะเป็นแบบนี้

  • เก็บ Variable สำหรับ API Key หรือ Secret ใด ๆ ในรูปของ Properties File
  • Variable จะถูกแปลงเป็นข้อมูลเพื่อใช้ใน Android Manifest หรือรวมไว้ใน BuildConfig เพื่อให้เรียกใช้งานผ่านโค้ด Java/Kotlin ได้
  • Key ที่เก็บไว้ใน Secrets Gradle Plugin จะถูกรวมไว้ในไฟล์ APK อยู่เหมือนเดิม ไม่ได้ซ่อน API Key ออกจากโปรเจคหรือเข้ารหัสให้กับ API Key แต่อย่างใด (อันนี้สำคัญมาก)
  • local.properties จะเป็น Default Properties File ของ Secrets Gradle Plugin เนื่องจากเป็น Properties File ที่จะอยู่ใน .gitignore อยู่แล้ว ซึ่งเป็นที่มาของ Secrets Gradle Plugin ที่ทำให้ API Key ไม่ติดขึ้นไปอยู่บน Version Control (ถ้าจะเปลี่ยนเป็นไฟล์อื่นที่ไม่ใช่ local.properties ก็ต้องเพิ่มไฟล์นั้นเข้าไปใน .gitignore เอง)

วิธีการใช้งาน Secrets Gradle Plugin for Android

เพิ่ม Secrets Gradle Plugin เข้าไปในโปรเจค

เนื่องจากเป็น Gradle Plugin จึงต้องประกาศไว้ใน build.gradle.kts ที่ Project-level แบบนี้

// build.gradle.kts (Project)
plugins {
    id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "<latest_version>" apply false
}

และถ้ายังใช้เป็น Groovy (build.gradle) อยู่ ก็ให้ประกาศแบบนี้แทน

// build.gradle (Project)
plugins {
    id "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" version "<latest_version>" apply false
}
จะเห็นว่า Plugin ID ของ Secret Gradle Plugin เป็นหนึ่งใน Gradle Plugin ที่สร้างขึ้นมาเพื่อใช้ใน Google Maps SDK for Android นั่นเอง

สำหรับ Module ใดที่ต้องการใช้ API Key จาก Secrets Gradle Plugin ก็ให้ประกาศ Plugin ตัวนี้ไว้ใน build.gradle.kts ของ Module นั้นได้เลย

// build.gradle.kts (Module)
plugins {
    id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
}

ถ้ายังใช้เป็น Groovy (build.gradle) อยู่ ก็ให้ประกาศแบบนี้แทน

// build.gradle.kts (Module)
plugins {
    id "com.google.android.libraries.mapsplatform.secrets-gradle-plugin"
}

และเนื่องจาก Android Gradle Plugin (AGP) ในเวอร์ชันล่าสุดจะไม่สร้าง BuildConfig ให้โดยอัติโนมัติแล้ว ดังนั้น Module จะต้องตั้งค่าเพื่อให้ Gradle สร้างไฟล์ BuildConfig ด้วย

// build.gradle.kts (Module)
android {
    buildFeatures {
        buildConfig = true
    }
}
BuildConfig จะถูกสร้างตอน assemble ใน Gradle Task เท่านั้น ดังนั้นถ้าเจอปัญหาว่าในโค้ดหา BuildConfig ไม่เจอ ให้ลอง Rebuild Project ซักครั้งก่อน (และต้อง Build ผ่านด้วยนะ)

สร้าง Variable เพื่อเก็บ API Key หรือ Secret ใด ๆ

ถ้าไม่ได้กำหนดค่าใด ๆ เพิ่มเติม Secrets Gradle Plugin จะใช้ข้อมูลที่อยู่ใน local.properties โดยอัตโนมัติ ดังนั้นนักพัฒนาจึงสามารถประกาศค่าที่ต้องการไว้ในไฟล์ดังกล่าวได้เลย

// local.properties
GOOGLE_MAPS_KEY=1234567890

การทำ API Key หรือ Secret ไปใช้งาน

ในกรณีที่ใช้งานกับโค้ด Java/Kotlin ผ่าน BuildConfig นักพัฒนาจะต้องสั่ง Gradle Task เพื่อให้สร้าง BuildConfig ใหม่เท่านั้น ถึงจะเห็น Variable ที่เพิ่งจะประกาศเพิ่มเข้าไป

โดยปกติ local.properties จะมีการประกาศ sdk.dir ไว้ แต่เนื่องจากอยู่ใน Default Ignore List ของ Secrets Gradle Plugin ด้วย จึงทำให้ค่าดังกล่าวไม่แสดงใน BuildConfig

และถ้าต้องการใช้ใน Android Manifest ก็ให้ประกาศในรูปแบบนี้แทน

${VARIABLE_NAME}

ยกตัวอย่างเช่น

// AndroidManifest.xml
<meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="${GOOGLE_MAPS_KEY}" />

แล้ว Secrets Gradle Plugin จะใส่ค่าจาก Variable ดังกล่าวให้ตอน Build Time เอง

กำหนดค่าเพิ่มเติมสำหรับ Secrets Gradle Plugin

การกำหนดค่าดังกล่าวสามารถทำได้ทั้งใน Project-level และ Module-level โดยขึ้นอยู่กับว่าต้องการกำหนดค่าอะไร

แต่ถ้าต้องการกำหนดค่าไว้ใน Project-level ก็อย่าลืมเพิ่ม Plugin ไว้ที่ Project-level ด้วยนะ เนื่องจากตัวอย่างก่อนหน้านี้จะเป็นการเพิ่ม Plugin ไว้แค่ใน Module ที่ต้องการใช้งานเท่านั้น

// build.gradle.kts (Project)
// Before
plugins {
    id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "<latest_version>" apply false
}

// After
plugins {
    id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "<latest_version>"
}
แค่ลบ apply false ออกน่ะแหละ

เปลี่ยน Properties File

ในกรณีที่ไม่ต้องการใช้ local.properties และอยากใช้ Properties File อื่นแทน ถ้าไม่มีการแยกตาม Build Type หรือ Product Flavor ก็ให้กำหนดไว้ใน Project-level เลยก็ได้

แต่ถ้าต้องการแยก Build Type หรือ Product Flavor ก็ให้ข้ามไปดูหัวข้อถัดไปได้เลย
// build.gradle.kts (Project)
secrets {
    propertiesFileName = "key.properties"
}

จากเดิมที่ Secrets Gradle Plugin จะดึง Variable จาก local.properties ก็จะเปลี่ยนไปใช้จาก key.properties แทน

และที่สำคัญ อย่าลืมเพิ่มไฟล์ดังกล่าวไว้ใน .gitignore เพื่อไม่ให้ติดขึ้นไปอยู่บน Version Control ด้วยนะ (เหตุผลหลักของการใช้ Secrets Gradle Plugin เลยนะ)

แยก Properties File ตาม Build Type หรือ Product Flavor

ในกรณีที่มีการแยก API Key ตาม Build Type หรือ Product Flavor ก็ควรจะแยก Properties File ออกจากการอย่างชัดเจน เพื่อให้จัดการในภายหลังได้ง่าย

โดยนักพัฒนาสามารถกำหนด propertiesFileName แยกตาม Build Type หรือ Product Flavor ใน Module ที่เรียกใช้งาน Secrets Gradle Plugin ได้เลย

// build.gradle.kts (Module)
android {
    productFlavors {
        create("development") {
            secrets {
                propertiesFileName = "key.development.properties"
            }
        }
        create("staging") {
            secrets {
                propertiesFileName = "key.staging.properties"
            }
        }
        create("production") {
            secrets {
                propertiesFileName = "key.production.properties"
            }
        }
}

Default Properties File สำหรับ Default Value

Secrets Gradle Plugin รองรับการกำหนด Default Value สำหรับบาง Variable ที่ไม่ต้องการประกาศไว้ในทุก Properties File ด้วยนะ

โดยนักพัฒนาสามารถสร้าง Default Properties File (ยกตัวอย่างเช่น key.default.properties และกำหนด Default Value สำหรับ Variable ที่ต้องการได้ตามใจชอบ แล้วกำหนดค่าให้กับ Secrets Gradle Plugin ไว้ใน Project-level ได้เลย

// build.gradle.kts (Project)
secrets {
    defaultPropertiesFileName = "key.default.properties"
}

เพิ่ม Variable บางตัวเข้าไปใน Ignore List

ถ้า Properties File ถูกนำไปใช้อย่างอื่นด้วย และไม่ต้องการให้บาง Variable ถูก Secrets Gradle Plugin เรียกใช้งานได้ ก็ให้เพิ่ม Variable ดังกล่าวไว้ใน Ignore List แบบนี้

// build.gradle.kts (Project)
secrets {
    ignoreList += listOf(
        "ndk.*",
        "KEYSTORE_PATH",
    )
}
ถ้าใช้ = แทน += กับ local.properties ก็อย่าลืมเพิ่ม sdk.dir ด้วยนะ

จะเห็นว่านักพัฒนาสามารถกำหนดเป็นชื่อ Variable โดยตรงได้เลย หรือจะใช้ Regular Expression ก็ได้เช่นกัน

และการกำหนด Ignore List ไว้ใน Project-level จะเหมาะสมกว่า เพราะโดยปกตินักพัฒนาจะต้องการให้ทุก Module เรียกใช้งาน Variable ได้เหมือนกันทั้งหมด มากกว่าการกำหนดแยกกันในแต่ละ Module

สิ่งที่นักพัฒนาต้องทำเพิ่ม

บนระบบ CI/CD

ในเมื่อ Properties File ที่ใช้กับ Secrets Gradle Plugin จะไม่ติดขึ้นไปกับ Version Control เพราะกำหนดไว้ใน .gitignore แล้ว สิ่งที่นักพัฒนาต้องทำเพิ่มก็คือการทำให้ CI/CD เตรียมไฟล์ Properties File เหล่านั้นเพื่อให้ CI/CD ทำการ Build App เป็นไฟล์ APK ได้นั่นเอง

โดย API Key ที่ใช้บน CI/CD จะเก็บไว้ที่ไหนก็ได้แล้วแต่ Infrastructure ของนักพัฒนาเลย ยกตัวอย่างเช่น อาจจะเก็บไว้ใน GitHub Secrets หรือ Vault ของ HashiCorp เป็นต้น เพื่อให้ CI/CD สามารถเข้าถึงข้อมูลดังกล่าวได้เท่านั้น แล้วเอาข้อมูลดังกล่าวมาสร้างเป็น Properties File ที่มีชื่อเดียวกันกับที่กำหนดไว้ใน Secrets Gradle Plugin

สำหรับทีมพัฒนา

ถ้ามีนักพัฒนาเพียงคนเดียวก็คงไม่มีปัญหาอะไร แต่ถ้าเป็นทีมที่มีหลายคนก็ควรจะมีรายละเอียดสำหรับขั้นตอนในการ Setup Project สำหรับนักพัฒนาที่ Clone Project ใหม่ด้วย เพราะนักพัฒนาทุกคนจะต้องเพิ่ม Variable สำหรับ API Key หรือ Secrets ใด ๆ เข้าไปใน Proprties File ในเครื่องตัวเองให้ครบตามที่กำหนดไว้ก่อนจะเริ่มพัฒนาแอปนั่นเอง

สรุป

Secrets Gradle Plugin เป็น Plugin ที่เหมาะกับทีมพัฒนาที่ต้องการแยก API Key หรือ Secrets ใด ๆ ออกตาม Environment (Build Type หรือ Product Flavor) ด้วยเหตุผลทางด้านความปลอดภัยในการทำงานระดับองค์กรหรือบริษัท

ยกตัวอย่างเช่น ต้องการให้เครื่องของนักพัฒนาถือแค่ API Key สำหรับตอนพัฒนาแอปเท่านั้น และให้ CI/CD ถือ API Key ที่จะใช้ใน Production เพื่อป้องกัน API Key ที่หลุดออกไปสู่ภายนอกโดยไม่จำเป็น (ซึ่งใน ณ ที่นี้คือ API Key สำหรับตอนพัฒนาแอปที่เป็นคนละตัวกับ Production)

โดยจุดประสงค์ดั้งเดิมของ Secrets Gradle Plugin คือสร้างขึ้นมาเป็นทางเลือกให้นักพัฒนาใช้ร่วมกับ Google Maps SDK for Android นั่นเอง แต่ถ้าอยากจะกำหนด API Key ของ Google Maps SDK ไว้ในโปรเจคโดยตรง ก็สามารถทำได้เช่นกัน ไม่ได้บังคับว่าจะต้องใช้ Secrets Gradle Plugin เสมอไป

ด้วยเหตุนี้ Secrets Gradle Plugin จึงเป็นตัวช่วยในการแยก API Key หรือ Secret ใด ๆ ที่มีการเรียกใช้งานภายในแอปและไม่ต้องการให้ติดขึ้นไปอยู่บน Version Control เท่านั้น ไม่ได้ช่วยซ่อนหรือเข้ารหัสให้กับข้อมูลดังกล่าวแต่อย่างใด เพราะวิธีซ่อนข้อมูลที่เป็นความลับที่ดีที่สุดในฝั่งแอนดรอยด์ ก็คือการไม่ใส่ข้อมูลดังกล่าวไว้ในโปรเจคตั้งแต่แรกนั่นเอง

และนั่นก็หมายความว่านักพัฒนาไม่จำเป็นต้องใช้งาน Secrets Gradle Plugin ก็ได้ ตราบใดที่มั่นใจว่าสามารถจัดการได้อย่างถูกต้องและปลอดภัยมากพออยู่แล้ว

แหล่งข้อมูลอ้างอิง