OpenShift secrets allow you to load private data into your pods and containers without baking them into your container images for all to see and use. This allows you to separate your prave data from your code, and securely host your images in public registries. In this example, we'll be using OpenShift secrets to store a set of API key credentials for an AWS (Amazon Web Services) IAM (Identity and Access Management) account, which we'll be using to send emails with Amazon SES (Simple Email Service).
This guide presumes you've already signed up for an AWS account, and run through the verification process for the sender and recipient addresses you want to use. If you haven't requested to be let out of the AWS sandbox, you'll still be subject to the same SES sender restrictions as you would be if you were sending email locally.
Here is a modified version of the example sender code from the AWS documentation. It's the just about the most simple implementation possible, and can be run standalone after saving to a file like "ses_sender.go" and running via "go run ses_sender.go". Or it can be modified and integrated into whatever application you have in mind already.
package main import ( "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ses" ) const ( Sender = "[email protected]" Recipient = "[email protected]" Subject = "Golang SES test email" TextBody = "Body of text, your message goes here." CharSet = "UTF-8" ) func main() { sess, err := session.NewSession(&aws.Config{ Region: aws.String("some-aws-region")}, ) svc := ses.New(sess) input := &ses.SendEmailInput{ Destination: &ses.Destination{ CcAddresses: []*string{}, ToAddresses: []*string{ aws.String(Recipient), }, }, Message: &ses.Message{ Body: &ses.Body{ Text: &ses.Content{ Charset: aws.String(CharSet), Data: aws.String(TextBody), }, }, Subject: &ses.Content{ Charset: aws.String(CharSet), Data: aws.String(Subject), }, }, Source: aws.String(Sender), } result, err := svc.SendEmail(input) if err != nil { if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { case ses.ErrCodeMessageRejected: fmt.Println(ses.ErrCodeMessageRejected, aerr.Error()) case ses.ErrCodeMailFromDomainNotVerifiedException: fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error()) case ses.ErrCodeConfigurationSetDoesNotExistException: fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error()) default: fmt.Println(aerr.Error()) } } else { fmt.Println(err.Error()) } return } fmt.Println("Email Sent to address: " + Recipient) fmt.Println(result) }If you wanted to use this practically in a web application, like the one we wrote at Golang Echo Router Example, you can get an idea of how you might include this for use as a simple web contact form with this example. Note that this doesn't cover input validation or anti abuse measures, just parsing the input from the contact form into a string and sending it via SES.
// POST /post-contact func postContact(c echo.Context) error { TextBody := c.FormValue("name") + "\n" + c.FormValue("email") + "\n" + c.FormValue("message") sess, err := session.NewSession(&aws.Config{ Region: aws.String("some-aws-region")}, ) svc := ses.New(sess) input := &ses.SendEmailInput{ Destination: &ses.Destination{ CcAddresses: []*string{}, ToAddresses: []*string{ aws.String(Recipient), }, }, Message: &ses.Message{ Body: &ses.Body{ Text: &ses.Content{ Charset: aws.String(CharSet), Data: aws.String(TextBody), }, }, Subject: &ses.Content{ Charset: aws.String(CharSet), Data: aws.String(Subject), }, }, Source: aws.String(Sender), } result, err := svc.SendEmail(input) if err != nil { if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { case ses.ErrCodeMessageRejected: fmt.Println(ses.ErrCodeMessageRejected, aerr.Error()) case ses.ErrCodeMailFromDomainNotVerifiedException: fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error()) case ses.ErrCodeConfigurationSetDoesNotExistException: fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error()) default: fmt.Println(aerr.Error()) } } else { fmt.Println(err.Error()) } } fmt.Println(c.FormValue("name")) fmt.Println(c.FormValue("email")) fmt.Println(c.FormValue("message")) fmt.Println("Email Sent to address: " + Recipient) fmt.Println(result)Either way, both of these examples expect to source their credentials from the default AWS credentials location. Specifically, they look for a plain text file called 'credentials'.at $HOME/.aws or ~/.aws unless specified otherwise. It is possible to read the credentials from elsewhere by setting an environment variable for a shared credentials file, or just by passing the API key and secret access key as variables when you go to call your function, but it's easy enough to accomodate the defaults. Make a new file called "credentials" with the following info in the same format. Keep the "[default]" line as is since AWS will attempt to source credentials from this section first.
[default] aws_access_key_id:Actually loading these credentials into a secret is just as simple. If you haven't already done so, login to the OpenShift cluster with the following command. You can skip this step if you're already logged in:aws_secret_access_key:
oc login https://api.pro-us-east-1.openshift.com --token=If you're already logged in, then this line is all that's needed to create the new secret, which will match the contents of the file named "contact_form_creds":
oc secrets new email-sender-secrets credentials=credentialsTo explain what all is going on here, here's what an export of the created secret looks like:
oc export secret contactform apiVersion: v1 data: credentials: W2RlZmF1bHRdCmF3c19hY2Nlc3Nfa2V5X2lkOiBBS0lBSjdTQ0xHU0dKM7E0WEpBQQphd4Gfc2VjcmV0X2FjY2Vzc19rZXk6IFNjcUJMTlDaGVRVUdtN2l1NUXFwRnU3ZGVpTU9Oa2NDcVZwTW1TMncgCg== kind: Secret metadata: creationTimestamp: null name: contactform type: OpaqueWe have "email-sender-secrets" which is the name of the secret that we'll need to refer to in template's DeployConfig section. The "credentials=credentials" portion means we're making a dictionary key named "credentials" with the contents matching those in a local file, which we also named "credentials". Any of these values can be set to whatever descriptive name you prefer, so if you wanted to you could just as easily create a new secret with keys from multiple sources like:
oc secrets new some-secret-name db_creds=file1.txt users=file2.csv
--- - apiVersion: v1 kind: DeploymentConfig spec: template: spec: containers: volumeMounts: - mountPath: /opt/app-root/src/.aws name: email-sender-secrets volumes: - name: email-sender-secrets secret: secretName: email-sender-secretsAnd finally, here's a full example inline with a template:
--- apiVersion: v1 kind: Template metadata: name: email-sender objects: - apiVersion: v1 kind: ImageStream metadata: labels: template: email-sender name: "${PLAT}-email-sender" spec: tags: - annotations: null from: kind: DockerImage name: "library/${PLAT}-email-sender:latest" importPolicy: {} name: latest - apiVersion: v1 kind: DeploymentConfig metadata: labels: template: email-sender name: email-sender spec: replicas: 1 selector: deploymentconfig: email-sender strategy: resources: {} type: Rolling template: metadata: labels: deploymentconfig: email-sender spec: containers: - env: - name: OO_PAUSE_ON_START value: "false" image: "email-sender/${PLAT}-email-sender:latest" imagePullPolicy: Always name: email-sender resources: {} securityContext: {} terminationMessagePath: /dev/termination-log volumeMounts: - mountPath: /opt/app-root/src/.aws name: email-sender-secrets volumes: - name: email-sender-secrets secret: secretName: email-sender-secrets dnsPolicy: ClusterFirst restartPolicy: Always securityContext: {} terminationGracePeriodSeconds: 30 test: false triggers: - type: ConfigChange - imageChangeParams: automatic: true containerNames: - email-sender from: kind: ImageStreamTag name: "${PLAT}-email-sender:latest" type: ImageChange - apiVersion: v1 kind: Service metadata: labels: template: email-sender name: email-sender spec: selector: deploymentconfig: email-sender sessionAffinity: None type: ClusterIP parameters: - description: Platform name name: PLAT value: rhel7You can use multiple secrets in your DeploymentConfigs the same way, just make sure you have a unique name and secretName for each one. You can also mount multiple secrets into the same mountPath directory, like /secrets or /etc/myconfig/somedir for example.