เลิกใส่ API Key ไว้ในโปรเจคแล้วเปลี่ยนมาใช้ Secrets Gradle Plugin กัน
สำหรับนักพัฒนาคนไหนที่มีการใช้ 3rd Party Service ที่ต้องกำหนด API Key ไว้ในโปรเจค วันนี้เจ้าของบล็อกจะมานำเสนอ Gradle Plugin จากทาง Google ที่ชื่อว่า Secrets Gradle Pluing for Android กันครับ
บทความที่เกี่ยวข้อง
- การใช้งาน 3rd Party Service ด้วย API Key ที่นักพัฒนาแอนดรอยด์ควรรู้
- เลิกใส่ Public API Key ไว้ในโปรเจคแล้วเปลี่ยนมาใช้ Secrets Gradle Plugin กัน [Now Reading]
โดยปกติที่มีการใช้งาน 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 เท่านั้น
โดยหลักการทำงานของ 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 ก็ได้ ตราบใดที่มั่นใจว่าสามารถจัดการได้อย่างถูกต้องและปลอดภัยมากพออยู่แล้ว