Setup in the past Succeeded in Local and Containerized Environment (e.g., ECS) but Failed in Lambda Functions
In the past we have defined an gmail service using kotlin in this article.
This method fails when we use a snapStarted
-function running a spring boot because
-
By default the
tokens-prod/StoredCredential
that we put inproject-root/
orresources/
of the project will be clone to/var/task
of the lambda function, however; -
/var/task
is only readable but not writable Unfortunatelly every time we submit our credential, google-related sdk will make an adjustment to ourtokens-prod/StoredCredential
, meaning that/var/task
must be awritable
directory, leading to a failure to launch the gmail service due to non-writability.
Workaround
Good news is that for every lambda function the /tmp
folder is writable, and we can move our credential into that folder before launching the gmail-authflow
.
Step 1. Locate your credential to resources folder
Let's place our credential here:
Next let's define the file path by classpath:directory-name
Step 2. Setup resource loader
Let's create a BeanConfig
class to manage our beans:
and let's define
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.io.DefaultResourceLoader import org.springframework.core.io.ResourceLoader @Configuration class BeanConfig { @Bean fun resourceLoader(): ResourceLoader { return DefaultResourceLoader() } }
Step 3. Adjust getCredentials
method in previous article
getCredentials
method in previous articleWe have mentioned this article at the beginning (which includes all the implementation of our GmailService
), now let's adjust getCredentials
by the highlighted lines:
@Service class GmailServiceImpl( @Value("\${gmail.error-receiver}") private val errorReceiver: String, @Value("\${gmail.credential-path}") private val credentialPath: String, @Value("\${stage.env}") private val env: String, @Value("\${gmail.email-sender}") private val emailSender: String, @Value("\${gmail.token-path}") private val tokenPath: String, @Qualifier("resourceLoader") private val resourceLoader: ResourceLoader, private val environment: Environment, ) : EmailService { private val httpTransport = GoogleNetHttpTransport.newTrustedTransport() private val jsonFactory = GsonFactory.getDefaultInstance() @Throws(IOException::class) private fun getCredentials(httpTransport: NetHttpTransport, jsonFactory: GsonFactory): Credential { // Load client secrets. val isLambdaEnvironment = environment.getProperty("IS_LAMBDA") == "true" val inputStream: InputStream = resourceLoader.getResource(credentialPath).inputStream ?: throw FileNotFoundException("Resource not found: $credentialPath") val clientSecrets = GoogleClientSecrets.load(jsonFactory, InputStreamReader(inputStream)) val resourceTokenPath = resourceLoader.getResource(tokenPath) val flow = when { !isLambdaEnvironment -> standardAuthFlow(httpTransport, jsonFactory, clientSecrets, resourceTokenPath) else -> lambdaAuthflow(resourceTokenPath, httpTransport, jsonFactory, clientSecrets) } val receiver = LocalServerReceiver.Builder().setPort(8888).build() val credential: Credential = AuthorizationCodeInstalledApp(flow, receiver).authorize("user") // returns an authorized Credential object. return credential } private fun lambdaAuthflow( resourceTokenPath: Resource, httpTransport: NetHttpTransport, jsonFactory: GsonFactory, clientSecrets: GoogleClientSecrets?, ): GoogleAuthorizationCodeFlow? { val tmpTokenDir = "/tmp/tokens-prod" val tempDir = File(tmpTokenDir) tempDir.mkdirs() resourceTokenPath.file.listFiles()?.forEach { file -> val tempFile = File(tempDir, file.name) file.inputStream().use { inputStream -> FileOutputStream(tempFile).use { outputStream -> inputStream.copyTo(outputStream) } } } return GoogleAuthorizationCodeFlow.Builder( httpTransport, jsonFactory, clientSecrets, setOf(GmailScopes.GMAIL_SEND) ) .setDataStoreFactory(FileDataStoreFactory(tempDir.toPath().toFile())) .setAccessType("offline") .build() } private fun standardAuthFlow( httpTransport: NetHttpTransport, jsonFactory: GsonFactory, clientSecrets: GoogleClientSecrets?, resourceTokenPath: Resource, ): GoogleAuthorizationCodeFlow? = GoogleAuthorizationCodeFlow.Builder( httpTransport, jsonFactory, clientSecrets, setOf(GmailScopes.GMAIL_SEND) ) .setDataStoreFactory(FileDataStoreFactory(Paths.get(resourceTokenPath.uri).toFile())) .setAccessType("offline") .build() override fun sendEmail(subject: String, bodyText: String, toEmail: String?): Message? { val service = Gmail.Builder(httpTransport, jsonFactory, getCredentials(httpTransport, jsonFactory)) .setApplicationName("payment") .build() val props = Properties() val session: Session = Session.getDefaultInstance(props, null) val email: MimeMessage = MimeMessage(session) email.setFrom(InternetAddress(emailSender)) email.addRecipient( javax.mail.Message.RecipientType.TO, InternetAddress(toEmail ?: errorReceiver) ) email.subject = subject email.setContent(bodyText, "text/html; charset=utf-8") val buffer = ByteArrayOutputStream() email.writeTo(buffer) val rawMessageBytes = buffer.toByteArray() val encodedEmail = Base64.encodeBase64URLSafeString(rawMessageBytes) var message = Message() message.setRaw(encodedEmail) try { // Create send message message = service.users().messages().send("me", message).execute() println("Message id: " + message.id) println(message.toPrettyString()) return message } catch (e: GoogleJsonResponseException) { val error = e.details if (error.code == 403) { println("Unable to create draft: " + e.details) throw Exception("403 not found") } else { throw e } } } }
What we have done:
-
When env variable
IS_LAMBDA
is found and equal to"true"
, we use thelambdaAuthflow
, which clones everything in/var/task/tokens-prod
into/tmp/tokens-prod
and trigger theauth-flow
as before. -
Otherwise we go back to
standardAuthFlow
(which is the code copied from documentation)
Apart from highlighted content, the rest remains the same.