CDK - How to use Opensearch Domain MasterUserPassword within userdata - opensearch

In the Opensearch L2 construct, if you add fine grained access controls, a Secret in Secrets Manager will be created for you (accessible by the masterUserPassword).
I want to use this generated password within a CloudformationInit later on, but not sure how to.
from aws_cdk import aws_ec2 as ec2
from aws_cdk import aws_iam as iam
from aws_cdk import aws_opensearchservice as opensearch
from aws_cdk import aws_s3 as s3
class OpensearchStack(Stack):
def __init__(
self,
scope: Construct,
construct_id: str,
**kwargs,
) -> None:
super().__init__(scope, construct_id, **kwargs)
vpc = ec2.Vpc(self, "generatorVpc", max_azs=2)
bucket = s3.Bucket(self, "My Bucket")
domain = opensearch.Domain(self,"OpensearchDomain",
version=opensearch.EngineVersion.OPENSEARCH_1_3,
vpc=vpc,
fine_grained_access_control=opensearch.AdvancedSecurityOptions(
master_user_name="osadmin",
),
)
instance = ec2.Instance(self, "Instance",
vpc=vpc,
instance_type=ec2.InstanceType.of(
instance_class=ec2.InstanceClass.M5,
instance_size=ec2.InstanceSize.LARGE,
),
machine_image=ec2.MachineImage.latest_amazon_linux(
generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
),
init=ec2.CloudFormationInit.from_elements(
ec2.InitFile.from_string(
file_name="/home/ec2-user/logstash-8.4.0/config/my_conf.conf",
owner="ec2-user",
mode="00755",
content=f"""input {{
s3 {{
bucket => "{bucket.bucket_name}"
region => "{self.region}"
}}
}}
output {{
opensearch {{
hosts => ["{domain.domain_endpoint}:443"]
user => "{domain.master_user_password.secrets_manager("What secret id do I put here?", json_field="username")}"
password => "{domain.master_user_password.secrets_manager("What secret id do I put here?", json_field="password")}"
ecs_compatibility => disabled
}}
}}
""",
)
)
)
Since SecretValue doesn't have a secretId property, I'm not sure how I can determine the Secret ID/Arn of the masterUserPassword.
Is there a better way to get the generated credentials inside my logstash config?

The username value is easy, as you are explicitly setting it as osadmin. To get the password reference, call to_string method on the Domain's master_user_password attribute, which is a SecretValue:
domain.master_user_password.to_string()
In the synthesized template, this gets turned into a CloudFormation dynamic reference to the secret's password. The actual password is not known to the template. It will be resolved cloud-side at deploy time.
The SecretsValue.secrets_manager static method also synthesizes the same dynamic reference. However, you can't use it. The method requires the secret ID, which is not exposed if the Domain construct generates the secret for you.

I ended up adding commands to the CloudFormationInit to pull the OS Credentials from Secrets Manager and did a find and replace which worked
from aws_cdk import aws_ec2 as ec2
from aws_cdk import aws_opensearchservice as opensearch
from aws_cdk import aws_s3 as s3
from aws_cdk import aws_secretsmanager as secretsmanager
from aws_cdk import Stack
from constructs import Construct
class OpensearchStack(Stack):
def __init__(
self,
scope: Construct,
construct_id: str,
**kwargs,
) -> None:
super().__init__(scope, construct_id, **kwargs)
vpc = ec2.Vpc(self, "generatorVpc", max_azs=2)
bucket = s3.Bucket(self, "My Bucket")
domain = opensearch.Domain(self,"OpensearchDomain",
version=opensearch.EngineVersion.OPENSEARCH_1_3,
vpc=vpc,
fine_grained_access_control=opensearch.AdvancedSecurityOptions(
master_user_name="osadmin",
),
)
# Get the domain secret
domain_secret: secretsmanager.Secret = domain.node.find_child("MasterUser")
instance = ec2.Instance(self, "Instance",
vpc=vpc,
instance_type=ec2.InstanceType.of(
instance_class=ec2.InstanceClass.M5,
instance_size=ec2.InstanceSize.LARGE,
),
machine_image=ec2.MachineImage.latest_amazon_linux(
generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
),
init=ec2.CloudFormationInit.from_elements(
ec2.InitFile.from_string(
file_name="/home/ec2-user/logstash-8.4.0/config/my_conf.conf",
owner="ec2-user",
mode="00755",
content=f"""input {{
s3 {{
bucket => "{bucket.bucket_name}"
region => "{self.region}"
}}
}}
output {{
opensearch {{
hosts => ["{domain.domain_endpoint}:443"]
user => "REPLACE_WITH_USERNAME"
password => "REPLACE_WITH_PASSWORD"
ecs_compatibility => disabled
}}
}}
""",
),
ec2.InitPackage.yum("jq"), # install jq
ec2.InitCommand.shell_command(
shell_command=(
f"aws configure set region {self.region} && "
# save secret value to variable
f"OS_SECRET=$(aws secretsmanager get-secret-value --secret-id {domain_secret.secret_arn} "
"--query SecretString) && "
# Pull values from json string
"OS_USER=$(echo $OS_SECRET | jq -r '. | fromjson | .username') && "
"OS_PASS=$(echo $OS_SECRET | jq -r '. | fromjson | .password') && "
# Find and replace
"sed -i \"s/REPLACE_WITH_USERNAME/$OS_USER/g\" /home/ec2-user/logstash-8.4.0/config/my_conf.conf && "
"sed -i \"s/REPLACE_WITH_PASSWORD/$OS_PASS/g\" /home/ec2-user/logstash-8.4.0/config/my_conf.conf"
),
),
)
)
# Don't forget to grant the instance read access to the secret
domain_secret.grant_read(instance.role)

Related

Referencing a loop object

i am currently checking out tanka + jsonnet. But evertime i think i understand it... sth. new irritates me. Can somebody help me understand how to do a loop-reference? (Or general better solution?)
Trying to create multiple deployments with a corresponding configmapVolumeMount and i am not sure how to reference to the according configmap object here?
(using a configVolumeMount it works since it refers to the name, not the object).
deployment: [
deploy.new(
name='demo-' + instance.name,
],
)
+ deploy.configMapVolumeMount('config-' + instance.name, '/config.yml', k.core.v1.volumeMount.withSubPath('config.yml'))
for instance in $._config.demo.instances
],
configMap: [
configMap.new('config-' + instance.name, {
'config.yml': (importstr 'files/config.yml') % {
name: instance.name,
....
},
}),
for instance in $._config.demo.instances
]
regards
Great to read that you're making progress with tanka, it's an awesome tool (once you learned how to ride it heh).
Find below a possible answer, see inline comments in the code, in particular how we ab-use tanka layout flexibility, to "populate" deploys: [...] array with jsonnet objects containing each paired deploy+configMap.
config.jsonnet
{
demo: {
instances: ['foo', 'bar'],
image: 'nginx', // just as example
},
}
main.jsonnet
local config = import 'config.jsonnet';
local k = import 'github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet';
{
local deployment = k.apps.v1.deployment,
local configMap = k.core.v1.configMap,
_config:: import 'config.jsonnet',
// my_deploy(name) will return name-d deploy+configMap object
my_deploy(name):: {
local this = self,
deployment:
deployment.new(
name='deploy-%s' % name,
replicas=1,
containers=[
k.core.v1.container.new('demo-%s' % name, $._config.demo.image),
],
)
+ deployment.configMapVolumeMount(
this.configMap,
'/config.yml',
k.core.v1.volumeMount.withSubPath('config.yml')
),
configMap:
configMap.new('config-%s' % name)
+ configMap.withData({
// NB: replacing `importstr 'files/config.yml';` by
// a simple YAML multi-line string, just for the sake of having
// a simple yet complete/usable example.
'config.yml': |||
name: %(name)s
other: value
||| % { name: name }, //
}),
},
// Tanka is pretty flexible with the "layout" of the Kubernetes objects
// in the Environment (can be arrays, objects, etc), below using an array
// for simplicity (built via a loop/comprehension)
deploys: [$.my_deploy(name) for name in $._config.demo.instances],
}
output
$ tk init
[...]
## NOTE: using https://kind.sigs.k8s.io/ local Kubernetes cluster
$ tk env set --server-from-context kind-kind environments/default
[... save main.jsonnet, config.jsonnet to ./environments/default/]
$ tk apply --dry-run=server environments/default
[...]
configmap/config-bar created (server dry run)
configmap/config-foo created (server dry run)
deployment.apps/deploy-bar created (server dry run)
deployment.apps/deploy-foo created (server dry run)

az cli/bicep targeted/single module deploys?

Is there an az/bicep equivalent to terraform apply -target=module.my_app.module.something?
Given a root bicep file:
module app '../../../projects/my/application/app.bicep' = {
name: 'app'
}
module test '../../../projects/my/application/test.bicep' = {
name: 'test'
}
module sample '../../../projects/my/application/sample.bicep' = {
name: 'sample'
params {
p1: 'p1'
}
}
Can I provision just the sample module somehow?
I could do something like: az deployment sub create --template-file ../../../projects/my/application/sample.bicep -l germanywestcentral
But this is not really the same thing, because this bypasses the params passed from the root module (which provides env separations) down to the actual module.
The command you have:
az deployment sub create --template-file ../../../projects/my/application/sample.bicep -l germanywestcentral will work just fine, you just pass the parameters you would normally pass to root.bicep that are needed by that module (e.g. p1)
If you have params that are created/manipulated in root.bicep you'd have to decide how you marshal those values manually.

AttributeError: 'tuple' object has no attribute 'authorize' - GCP Create Service Account with Workload Identity Federation

I am trying to create a service account using Python in GCP. This works fine when i've set env var GOOGLE_APPLICATION_CREDENTIALS to a JSON credentials file, and used the following code:
GoogleCredentials.get_application_default()
However the following code fails in CI - Github Actions using Workload Identity Federation:
import google
import googleapiclient.discovery
import os
from util import get_service_name
environment = os.getenv('ENVIRONMENT')
def create_service_account(requested_project_id):
project_id = requested_project_id
credentials = google.auth.default()
service = googleapiclient.discovery.build(
'iam', 'v1', credentials=credentials)
service_account_name = f'svc-{get_service_name()}'
service_accounts = service.projects().serviceAccounts().list(
name='projects/' + project_id).execute()
service_account_exists = False
for account in service_accounts['accounts']:
if (service_account_name in account['name']):
service_account_exists = True
service_account = account
break
if (service_account_exists == False):
service_account = service.projects().serviceAccounts().create(
name='projects/' + project_id,
body={
'accountId': service_account_name,
'serviceAccount': {
'displayName': service_account_name
}
}).execute()
print(f'{"Already Exists" if service_account_exists else "Created"} service account: ' + service_account['email'])
return service_account
Fails with the error:
File "/opt/hostedtoolcache/Python/3.9.0/x64/lib/python3.9/site-packages/googleapiclient/_helpers.py", line 131, in positional_wrapper
return wrapped(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.9.0/x64/lib/python3.9/site-packages/googleapiclient/discovery.py", line 298, in build
service = build_from_document( File "/opt/hostedtoolcache/Python/3.9.0/x64/lib/python3.9/site-packages/googleapiclient/_helpers.py", line 131, in positional_wrapper
return wrapped(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.9.0/x64/lib/python3.9/site-packages/googleapiclient/discovery.py", line 600, in build_from_document
http = _auth.authorized_http(credentials) File "/opt/hostedtoolcache/Python/3.9.0/x64/lib/python3.9/site-packages/googleapiclient/_auth.py", line 119, in authorized_http
return credentials.authorize(build_http()) AttributeError: 'tuple' object has no attribute 'authorize'
I am using the following Github Action to authenticate with Google
- name: Authenticate to Google Cloud To Create Service Account
uses: google-github-actions/auth#v0.4.3
with:
workload_identity_provider: 'projects/xxx/locations/global/workloadIdentityPools/github-actions-identity-pool/providers/github-provider'
service_account: 'svc-iam-creator-dev#acme-dev-tooling.iam.gserviceaccount.com'
Can anyone help?
You have two problems. This line of code is failing:
credentials = google.auth.default()
Problem 1 - Generate an Google OAuth Access Token
Change the GitHub Actions Step to:
- name: Authenticate to Google Cloud To Create Service Account
uses: google-github-actions/auth#v0.4.3
with:
token_format: 'access_token' # Your python code needs an access token
access_token_lifetime: '300s' # make this value small but long enough to complete the job
workload_identity_provider: 'projects/xxx/locations/global/workloadIdentityPools/github-actions-identity-pool/providers/github-provider'
service_account: 'svc-iam-creator-dev#acme-dev-tooling.iam.gserviceaccount.com'
Problem 2 - Creating Credentials
This line will not work because the credentials are not available from ADC (Application Default Credentials).
credentials = google.auth.default()
Pass the access token generated by Workload Identity Federation to your program from from the GitHub Actions output:
${{ steps.auth.outputs.access_token }}
Create the credentials from the access token:
credentials = google.oauth2.credentials.Credentials(access_token)
service = googleapiclient.discovery.build('iam', 'v1', credentials=credentials)

How to inject secrets to ecs task definitions using aws-cdk

I'm trying to add secrets to a task definition but can't find a way to specify which key to use from the key/value in the secret.
secrets = {
"DBUSER": ecs.Secret.from_secrets_manager(
sm.Secret.from_secret_complete_arn(
self, 'secret-dbuser',
'arn:aws:secretsmanager:eu-west-1:accountid:secret:secret-name').secret_value_from_json('DBUSER')
)
}
container: ecs.ContainerDefinition = task_definition.add_container(
"reports",
image=ecs.RepositoryImage.from_ecr_repository(
ecr.Repository.from_repository_name(self, "container", "container"), tag=image_tag,
),
memory_limit_mib=2048, logging=ecs.LogDriver.aws_logs(stream_prefix="container-"),
secrets=secrets
)
secret_value_from_json returns a SecretValue which isn't what I need.
I've also tried using from_secret_manager with filed='DBUSER' but that gives me an error like this
Invalid request provided: Create TaskDefinition: The Systems Manager parameter name specifie
d for secret DBUSER is invalid. The parameter name can be up to 2048 characters and include the following letters and symbols: a
-zA-Z0-9_.-, (Service: AmazonECS; Status Code: 400; Error Code: ClientException; Request ID
If the secret is in the same account/region, you should be able to do:
secrets = {
"DBUSER": ecs.Secret.from_secrets_manager(
# import the secret by its name
sm.Secret.from_secret_name_v2(self, 'secret-dbuser', '<secret-name-here>'),
# specify the specific field
'DBUSER'
)
}
container: ecs.ContainerDefinition = task_definition.add_container(
"reports",
image=ecs.RepositoryImage.from_ecr_repository(
ecr.Repository.from_repository_name(self, "container", "container"), tag=image_tag,
),
memory_limit_mib=2048, logging=ecs.LogDriver.aws_logs(stream_prefix="container-"),
secrets=secrets
)
ecs.Secret.from_secrets_manager() expects an ISecret and a field.
See also https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_ecs/Secret.html#aws_cdk.aws_ecs.Secret.from_secrets_manager

Accessing GOOGLE_APPLICATION_CREDENTIALS in a kubernetes operator

I am adding a task to an airflow dag as follows:
examples_task = KubernetesPodOperator(
task_id='examples_generation',
dag=dag,
namespace='test',
image='test_amazon_image',
name='pipe-labelled-examples-generation-tf-record-operator',
env={
'GOOGLE_APPLICATION_CREDENTIALS': Variable.get('google_cloud_credentials')
},
arguments=[
"--assets_path", Variable.get('assets_path'),
"--folder_source", Variable.get('folder_source'),
"--folder_destination", Variable.get('folder_destination'),
"--gcs_folder_destination", Variable.get('gcs_folder_destination'),
"--aws_region", Variable.get('aws_region'),
"--s3_endpoint", Variable.get('s3_endpoint')
],
get_logs=True)
I thought I could paste the service account json file as a variable and call it but this doesn't work and airflow/google documentation isn't clear. How do you do this?
Solutions to port the json into an argument
examples_task = KubernetesPodOperator(
task_id='examples_generation',
dag=dag,
namespace='test',
image='test_amazon_image',
name='pipe-labelled-examples-generation-tf-record-operator',
arguments=[
"--folder_source", Variable.get('folder_source'),
"--folder_destination", Variable.get('folder_destination'),
"--gcs_folder_destination", Variable.get('gcs_folder_destination'),
"--aws_region", Variable.get('aws_region'),
"--s3_endpoint", Variable.get('s3_endpoint')
"--gcs_credentials", Variable.get('google_cloud_credentials')
],
get_logs=True)
then in the cli set
import json
from google.cloud import storage
from google.oauth2 import service_account
credentials = service_account.Credentials.from_service_account_info(json.loads(gcs_credentials))
client = storage.Client(project='project_id', credentials=credentials)